diff options
Diffstat (limited to 'game/server/cstrike')
96 files changed, 48475 insertions, 0 deletions
diff --git a/game/server/cstrike/bot/cs_bot.cpp b/game/server/cstrike/bot/cs_bot.cpp new file mode 100644 index 0000000..d3d8235 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot.cpp @@ -0,0 +1,1117 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_gamerules.h" +#include "func_breakablesurf.h" +#include "obstacle_pushaway.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( cs_bot, CCSBot ); + +BEGIN_DATADESC( CCSBot ) + +END_DATADESC() + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the number of bots following the given player + */ +int GetBotFollowCount( CCSPlayer *leader ) +{ + 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->IsAlive()) + continue; + + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + if (bot && bot->GetFollowLeader() == leader) + ++count; + } + + return count; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Change movement speed to walking + */ +void CCSBot::Walk( void ) +{ + if (m_mustRunTimer.IsElapsed()) + { + BaseClass::Walk(); + } + else + { + // must run + Run(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if jump was started. + * This is extended from the base jump to disallow jumping when in a crouch area. + */ +bool CCSBot::Jump( bool mustJump ) +{ + // prevent jumping if we're crouched, unless we're in a crouchjump area - jump wins + bool inCrouchJumpArea = (m_lastKnownArea && + (m_lastKnownArea->GetAttributes() & NAV_MESH_CROUCH) && + (m_lastKnownArea->GetAttributes() & NAV_MESH_JUMP)); + + if ( !IsUsingLadder() && IsDucked() && !inCrouchJumpArea ) + { + return false; + } + + return BaseClass::Jump( mustJump ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when injured by something + * NOTE: We dont want to directly call Attack() here, or the bots will have super-human reaction times when injured + */ +int CCSBot::OnTakeDamage( const CTakeDamageInfo &info ) +{ + CBaseEntity *attacker = info.GetInflictor(); + + // getting hurt makes us alert + BecomeAlert(); + StopWaiting(); + + // if we were attacked by a teammate, rebuke + if (attacker->IsPlayer()) + { + CCSPlayer *player = static_cast<CCSPlayer *>( attacker ); + + if (InSameTeam( player ) && !player->IsBot()) + GetChatter()->FriendlyFire(); + } + + if (attacker->IsPlayer() && IsEnemy( attacker )) + { + // Track previous attacker so we don't try to panic multiple times for a shotgun blast + CCSPlayer *lastAttacker = m_attacker; + float lastAttackedTimestamp = m_attackedTimestamp; + + // keep track of our last attacker + m_attacker = reinterpret_cast<CCSPlayer *>( attacker ); + m_attackedTimestamp = gpGlobals->curtime; + + // no longer safe + AdjustSafeTime(); + + if ( !IsSurprised() && (m_attacker != lastAttacker || m_attackedTimestamp != lastAttackedTimestamp) ) + { + CCSPlayer *enemy = static_cast<CCSPlayer *>( attacker ); + + // being hurt by an enemy we can't see causes panic + if (!IsVisible( enemy, CHECK_FOV )) + { + // if not attacking anything, look around to try to find attacker + if (!IsAttacking()) + { + Panic(); + } + else // we are attacking + { + if (!IsEnemyVisible()) + { + // can't see our current enemy, panic to acquire new attacker + Panic(); + } + } + } + } + } + + // extend + return BaseClass::OnTakeDamage( info ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when killed + */ +void CCSBot::Event_Killed( const CTakeDamageInfo &info ) +{ +// PrintIfWatched( "Killed( attacker = %s )\n", STRING(pevAttacker->netname) ); + + GetChatter()->OnDeath(); + + // increase the danger where we died + const float deathDanger = 1.0f; + const float deathDangerRadius = 500.0f; + TheNavMesh->IncreaseDangerNearby( GetTeamNumber(), deathDanger, m_lastKnownArea, GetAbsOrigin(), deathDangerRadius ); + + // end voice feedback + m_voiceEndTimestamp = 0.0f; + + // extend + BaseClass::Event_Killed( info ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if line segment intersects rectagular volume + */ +#define HI_X 0x01 +#define LO_X 0x02 +#define HI_Y 0x04 +#define LO_Y 0x08 +#define HI_Z 0x10 +#define LO_Z 0x20 + +inline bool IsIntersectingBox( const Vector& start, const Vector& end, const Vector& boxMin, const Vector& boxMax ) +{ + unsigned char startFlags = 0; + unsigned char endFlags = 0; + + // classify start point + if (start.x < boxMin.x) + startFlags |= LO_X; + if (start.x > boxMax.x) + startFlags |= HI_X; + + if (start.y < boxMin.y) + startFlags |= LO_Y; + if (start.y > boxMax.y) + startFlags |= HI_Y; + + if (start.z < boxMin.z) + startFlags |= LO_Z; + if (start.z > boxMax.z) + startFlags |= HI_Z; + + // classify end point + if (end.x < boxMin.x) + endFlags |= LO_X; + if (end.x > boxMax.x) + endFlags |= HI_X; + + if (end.y < boxMin.y) + endFlags |= LO_Y; + if (end.y > boxMax.y) + endFlags |= HI_Y; + + if (end.z < boxMin.z) + endFlags |= LO_Z; + if (end.z > boxMax.z) + endFlags |= HI_Z; + + // trivial reject + if (startFlags & endFlags) + return false; + + /// @todo Do exact line/box intersection check + + return true; +} + + +extern void UTIL_DrawBox( Extent *extent, int lifetime, int red, int green, int blue ); + +//-------------------------------------------------------------------------------------------------------------- +/** + * When bot is touched by another entity. + */ +void CCSBot::Touch( CBaseEntity *other ) +{ + // EXTEND + BaseClass::Touch( other ); + + // if we have touched a higher-priority player, make way + /// @todo Need to account for reaction time, etc. + if (other->IsPlayer()) + { + // if we are defusing a bomb, don't move + if (IsDefusingBomb()) + return; + + // if we are on a ladder, don't move + if (IsUsingLadder()) + return; + + CCSPlayer *player = static_cast<CCSPlayer *>( other ); + + // get priority of other player + unsigned int otherPri = TheCSBots()->GetPlayerPriority( player ); + + // get our priority + unsigned int myPri = TheCSBots()->GetPlayerPriority( this ); + + // if our priority is better, don't budge + if (myPri < otherPri) + return; + + // they are higher priority - make way, unless we're already making way for someone more important + if (m_avoid != NULL) + { + unsigned int avoidPri = TheCSBots()->GetPlayerPriority( static_cast<CBasePlayer *>( static_cast<CBaseEntity *>( m_avoid ) ) ); + if (avoidPri < otherPri) + { + // ignore 'other' because we're already avoiding someone better + return; + } + } + + m_avoid = other; + m_avoidTimestamp = gpGlobals->curtime; + } + + // Check for breakables we're actually touching + // If we're not stuck or crouched, we don't care + if ( !m_isStuck && !IsCrouching() && !IsOnLadder() ) + return; + + // See if it's breakable + if ( IsBreakableEntity( other ) ) + { + // it's breakable - try to shoot it. + SetLookAt( "Breakable", other->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are busy doing something important + */ +bool CCSBot::IsBusy( void ) const +{ + if (IsAttacking() || + IsBuying() || + IsDefusingBomb() || + GetTask() == PLANT_BOMB || + GetTask() == RESCUE_HOSTAGES || + IsSniping()) + { + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::BotDeathThink( void ) +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Try to join the given team + */ +void CCSBot::TryToJoinTeam( int team ) +{ + m_desiredTeam = team; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Assign given player as our current enemy to attack + */ +void CCSBot::SetBotEnemy( CCSPlayer *enemy ) +{ + if (m_enemy != enemy) + { + m_enemy = enemy; + m_currentEnemyAcquireTimestamp = gpGlobals->curtime; + + PrintIfWatched( "SetBotEnemy: %s\n", (enemy) ? enemy->GetPlayerName() : "(NULL)" ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * If we are not on the navigation mesh (m_currentArea == NULL), + * move towards last known area. + * Return false if off mesh. + */ +bool CCSBot::StayOnNavMesh( void ) +{ + if (m_currentArea == NULL) + { + // move back onto the area map + + // if we have no lastKnownArea, we probably started off + // of the nav mesh - find the closest nav area and use it + CNavArea *goalArea; + if (!m_currentArea && !m_lastKnownArea) + { + goalArea = TheNavMesh->GetNearestNavArea( GetCentroid( this ) ); + PrintIfWatched( "Started off the nav mesh - moving to closest nav area...\n" ); + } + else + { + goalArea = m_lastKnownArea; + PrintIfWatched( "Getting out of NULL area...\n" ); + } + + if (goalArea) + { + Vector pos; + goalArea->GetClosestPointOnArea( GetCentroid( this ), &pos ); + + // move point into area + Vector to = pos - GetCentroid( this ); + to.NormalizeInPlace(); + + const float stepInDist = 5.0f; // how far to "step into" an area - must be less than min area size + pos = pos + (stepInDist * to); + + MoveTowardsPosition( pos ); + } + + // if we're stuck, try to get un-stuck + // do stuck movements last, so they override normal movement + if (m_isStuck) + Wiggle(); + + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we will do scenario-related tasks + */ +bool CCSBot::IsDoingScenario( void ) const +{ + // if we are deferring to humans, and there is a live human on our team, don't do the scenario + if (cv_bot_defer_to_human.GetBool()) + { + if (UTIL_HumansOnTeam( GetTeamNumber(), IS_ALIVE )) + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we noticed the bomb on the ground or on the radar (for T's only) + */ +bool CCSBot::NoticeLooseBomb( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetScenario() != CCSBotManager::SCENARIO_DEFUSE_BOMB) + return false; + + CBaseEntity *bomb = ctrl->GetLooseBomb(); + + if (bomb) + { + // T's can always see bomb on their radar + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if can see the bomb lying on the ground + */ +bool CCSBot::CanSeeLooseBomb( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetScenario() != CCSBotManager::SCENARIO_DEFUSE_BOMB) + return false; + + CBaseEntity *bomb = ctrl->GetLooseBomb(); + + if (bomb) + { + if (IsVisible( bomb->GetAbsOrigin(), CHECK_FOV )) + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if can see the planted bomb + */ +bool CCSBot::CanSeePlantedBomb( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetScenario() != CCSBotManager::SCENARIO_DEFUSE_BOMB) + return false; + + if (!GetGameState()->IsBombPlanted()) + return false; + + const Vector *bombPos = GetGameState()->GetBombPosition(); + + if (bombPos && IsVisible( *bombPos, CHECK_FOV )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return last enemy that hurt us + */ +CCSPlayer *CCSBot::GetAttacker( void ) const +{ + if (m_attacker && m_attacker->IsAlive()) + return m_attacker; + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Immediately jump off of our ladder, if we're on one + */ +void CCSBot::GetOffLadder( void ) +{ + if (IsUsingLadder()) + { + Jump( MUST_JUMP ); + DestroyPath(); + } +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return time when given spot was last checked + */ +float CCSBot::GetHidingSpotCheckTimestamp( HidingSpot *spot ) const +{ + for( int i=0; i<m_checkedHidingSpotCount; ++i ) + if (m_checkedHidingSpot[i].spot->GetID() == spot->GetID()) + return m_checkedHidingSpot[i].timestamp; + + return -999999.9f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the timestamp of the given spot to now. + * If the spot is not in the set, overwrite the least recently checked spot. + */ +void CCSBot::SetHidingSpotCheckTimestamp( HidingSpot *spot ) +{ + int leastRecent = 0; + float leastRecentTime = gpGlobals->curtime + 1.0f; + + for( int i=0; i<m_checkedHidingSpotCount; ++i ) + { + // if spot is in the set, just update its timestamp + if (m_checkedHidingSpot[i].spot->GetID() == spot->GetID()) + { + m_checkedHidingSpot[i].timestamp = gpGlobals->curtime; + return; + } + + // keep track of least recent spot + if (m_checkedHidingSpot[i].timestamp < leastRecentTime) + { + leastRecentTime = m_checkedHidingSpot[i].timestamp; + leastRecent = i; + } + } + + // if there is room for more spots, append this one + if (m_checkedHidingSpotCount < MAX_CHECKED_SPOTS) + { + m_checkedHidingSpot[ m_checkedHidingSpotCount ].spot = spot; + m_checkedHidingSpot[ m_checkedHidingSpotCount ].timestamp = gpGlobals->curtime; + ++m_checkedHidingSpotCount; + } + else + { + // replace the least recent spot + m_checkedHidingSpot[ leastRecent ].spot = spot; + m_checkedHidingSpot[ leastRecent ].timestamp = gpGlobals->curtime; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Periodic check of hostage count in case we lost some + */ +void CCSBot::UpdateHostageEscortCount( void ) +{ + const float updateInterval = 1.0f; + if (m_hostageEscortCount == 0 || gpGlobals->curtime - m_hostageEscortCountTimestamp < updateInterval) + return; + + m_hostageEscortCountTimestamp = gpGlobals->curtime; + + // recount the hostages in case we lost some + m_hostageEscortCount = 0; + + for( int i=0; i<g_Hostages.Count(); ++i ) + { + CHostage *hostage = g_Hostages[i]; + + // skip dead or rescued hostages + if ( !hostage->IsValid() || !hostage->IsAlive() ) + continue; + + // check if hostage has targeted us, and is following + if ( hostage->IsFollowing( this ) ) + ++m_hostageEscortCount; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are outnumbered by enemies + */ +bool CCSBot::IsOutnumbered( void ) const +{ + return (GetNearbyFriendCount() < GetNearbyEnemyCount()-1) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return number of enemies we are outnumbered by + */ +int CCSBot::OutnumberedCount( void ) const +{ + if (IsOutnumbered()) + return (GetNearbyEnemyCount()-1) - GetNearbyFriendCount(); + + return 0; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest "important" enemy for the given scenario (bomb carrier, VIP, hostage escorter) + */ +CCSPlayer *CCSBot::GetImportantEnemy( bool checkVisibility ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + CCSPlayer *nearEnemy = NULL; + float nearDist = 999999999.9f; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseEntity *entity = UTIL_PlayerByIndex( i ); + + if (entity == NULL) + continue; + +// if (FNullEnt( entity->pev )) +// continue; + +// if (FStrEq( STRING( entity->pev->netname ), "" )) +// continue; + + // is it a player? + if (!entity->IsPlayer()) + continue; + + CCSPlayer *player = static_cast<CCSPlayer *>( entity ); + + // is it alive? + if (!player->IsAlive()) + continue; + + // skip friends + if (InSameTeam( player )) + continue; + + // is it "important" + if (!ctrl->IsImportantPlayer( player )) + continue; + + // is it closest? + Vector d = GetAbsOrigin() - player->GetAbsOrigin(); + float distSq = d.x*d.x + d.y*d.y + d.z*d.z; + if (distSq < nearDist) + { + if (checkVisibility && !IsVisible( player, CHECK_FOV )) + continue; + + nearEnemy = player; + nearDist = distSq; + } + } + + return nearEnemy; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Sets our current disposition + */ +void CCSBot::SetDisposition( DispositionType disposition ) +{ + m_disposition = disposition; + + if (m_disposition != IGNORE_ENEMIES) + m_ignoreEnemiesTimer.Invalidate(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return our current disposition + */ +CCSBot::DispositionType CCSBot::GetDisposition( void ) const +{ + if (!m_ignoreEnemiesTimer.IsElapsed()) + return IGNORE_ENEMIES; + + return m_disposition; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Ignore enemies for a short durationy + */ +void CCSBot::IgnoreEnemies( float duration ) +{ + m_ignoreEnemiesTimer.Start( duration ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Increase morale one step + */ +void CCSBot::IncreaseMorale( void ) +{ + if (m_morale < EXCELLENT) + m_morale = static_cast<MoraleType>( m_morale + 1 ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Decrease morale one step + */ +void CCSBot::DecreaseMorale( void ) +{ + if (m_morale > TERRIBLE) + m_morale = static_cast<MoraleType>( m_morale - 1 ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are acting like a rogue (not listening to teammates, not doing scenario goals) + * @todo Account for morale + */ +bool CCSBot::IsRogue( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + if (!ctrl->AllowRogues()) + return false; + + // periodically re-evaluate our rogue status + if (m_rogueTimer.IsElapsed()) + { + m_rogueTimer.Start( RandomFloat( 10.0f, 30.0f ) ); + + // our chance of going rogue is inversely proportional to our teamwork attribute + const float rogueChance = 100.0f * (1.0f - GetProfile()->GetTeamwork()); + + m_isRogue = (RandomFloat( 0, 100 ) < rogueChance); + } + + return m_isRogue; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are in a hurry + */ +bool CCSBot::IsHurrying( void ) const +{ + if (!m_hurryTimer.IsElapsed()) + return true; + + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + // if the bomb has been planted, we are in a hurry, CT or T (they could be defusing it!) + if (ctrl->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB && ctrl->IsBombPlanted()) + return true; + + // if we are a T and hostages are being rescued, we are in a hurry + if (ctrl->GetScenario() == CCSBotManager::SCENARIO_RESCUE_HOSTAGES && + GetTeamNumber() == TEAM_TERRORIST && + GetGameState()->AreAllHostagesBeingRescued()) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if it is the early, "safe", part of the round + */ +bool CCSBot::IsSafe( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetElapsedRoundTime() < m_safeTime) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if it is well past the early, "safe", part of the round + */ +bool CCSBot::IsWellPastSafe( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetElapsedRoundTime() > 2.0f * m_safeTime) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we were in the safe time last update, but not now + */ +bool CCSBot::IsEndOfSafeTime( void ) const +{ + return m_wasSafe && !IsSafe(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the amount of "safe time" we have left + */ +float CCSBot::GetSafeTimeRemaining( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + return m_safeTime - ctrl->GetElapsedRoundTime(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called when enemy seen to adjust safe time for this round + */ +void CCSBot::AdjustSafeTime( void ) +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + // if we spotted an enemy sooner than we thought possible, adjust our notion of "safe" time + if (ctrl->GetElapsedRoundTime() < m_safeTime) + { + // since right now is not safe, adjust safe time to be a few seconds ago + m_safeTime = ctrl->GetElapsedRoundTime() - 2.0f; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we haven't seen an enemy for "a long time" + */ +bool CCSBot::HasNotSeenEnemyForLongTime( void ) const +{ + const float longTime = 30.0f; + return (GetTimeSinceLastSawEnemy() > longTime); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Pick a random zone and hide near it + */ +bool CCSBot::GuardRandomZone( float range ) +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + const CCSBotManager::Zone *zone = ctrl->GetRandomZone(); + if (zone) + { + CNavArea *rescueArea = ctrl->GetRandomAreaInZone( zone ); + if (rescueArea) + { + Hide( rescueArea, -1.0f, range ); + return true; + } + } + + return false; +} + + + +//-------------------------------------------------------------------------------------------------------------- +class CollectRetreatSpotsFunctor +{ +public: + CollectRetreatSpotsFunctor( CCSBot *me, float range ) + { + m_me = me; + m_count = 0; + m_range = range; + } + + 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 ]; + + if (m_count >= MAX_SPOTS) + break; + + // make sure hiding spot is in range + if (m_range > 0.0f) + if ((spot->GetPosition() - GetCentroid( m_me )).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; + } + + // don't select spot if an enemy can see it + if (UTIL_IsVisibleToTeam( spot->GetPosition() + Vector( 0, 0, HalfHumanHeight ), OtherTeam( m_me->GetTeamNumber() ) )) + continue; + + // don't select spot if it is closest to an enemy + CBasePlayer *owner = UTIL_GetClosestPlayer( spot->GetPosition() ); + if (owner && !m_me->InSameTeam( owner )) + continue; + + m_spot[ m_count++ ] = &spot->GetPosition(); + } + + // if we've filled up, stop searching + if (m_count == MAX_SPOTS) + return false; + + return true; + } + + CCSBot *m_me; + float m_range; + + const Vector *m_spot[ MAX_SPOTS ]; + int m_count; +}; + + +/** + * Do a breadth-first search to find a good retreat spot. + * Don't pick a spot that a Player is currently occupying. + */ +const Vector *FindNearbyRetreatSpot( CCSBot *me, float maxRange ) +{ + CNavArea *area = me->GetLastKnownArea(); + if (area == NULL) + return NULL; + + // collect spots that enemies cannot see + CollectRetreatSpotsFunctor collector( me, maxRange ); + SearchSurroundingAreas( area, GetCentroid( me ), 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_spot[ which ]; +} + +//-------------------------------------------------------------------------------------------------------------- +class FarthestHostage +{ +public: + FarthestHostage( const CCSBot *me ) + { + m_me = me; + m_farRange = -1.0f; + } + + bool operator() ( CHostage *hostage ) + { + if (hostage->IsFollowing( m_me )) + { + float range = (hostage->GetAbsOrigin() - m_me->GetAbsOrigin()).Length(); + if (range > m_farRange) + { + m_farRange = range; + } + } + + return true; + } + + const CCSBot *m_me; + float m_farRange; +}; + +/** + * Return euclidean distance to farthest escorted hostage. + * Return -1 if no hostage is following us. + */ +float CCSBot::GetRangeToFarthestEscortedHostage( void ) const +{ + FarthestHostage away( this ); + + ForEachHostage( away ); + + return away.m_farRange; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return string describing current task + * NOTE: This MUST be kept in sync with the CCSBot::TaskType enum + */ +const char *CCSBot::GetTaskName( void ) const +{ + static const char *name[ NUM_TASKS ] = + { + "SEEK_AND_DESTROY", + "PLANT_BOMB", + "FIND_TICKING_BOMB", + "DEFUSE_BOMB", + "GUARD_TICKING_BOMB", + "GUARD_BOMB_DEFUSER", + "GUARD_LOOSE_BOMB", + "GUARD_BOMB_ZONE", + "GUARD_INITIAL_ENCOUNTER", + "ESCAPE_FROM_BOMB", + "HOLD_POSITION", + "FOLLOW", + "VIP_ESCAPE", + "GUARD_VIP_ESCAPE_ZONE", + "COLLECT_HOSTAGES", + "RESCUE_HOSTAGES", + "GUARD_HOSTAGES", + "GUARD_HOSTAGE_RESCUE_ZONE", + "MOVE_TO_LAST_KNOWN_ENEMY_POSITION", + "MOVE_TO_SNIPER_SPOT", + "SNIPING", + }; + + return name[ (int)GetTask() ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return string describing current disposition + * NOTE: This MUST be kept in sync with the CCSBot::DispositionType enum + */ +const char *CCSBot::GetDispositionName( void ) const +{ + static const char *name[ NUM_DISPOSITIONS ] = + { + "ENGAGE_AND_INVESTIGATE", + "OPPORTUNITY_FIRE", + "SELF_DEFENSE", + "IGNORE_ENEMIES" + }; + + return name[ (int)GetDisposition() ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return string describing current morale + * NOTE: This MUST be kept in sync with the CCSBot::MoraleType enum + */ +const char *CCSBot::GetMoraleName( void ) const +{ + static const char *name[ EXCELLENT - TERRIBLE + 1 ] = + { + "TERRIBLE", + "BAD", + "NEGATIVE", + "NEUTRAL", + "POSITIVE", + "GOOD", + "EXCELLENT" + }; + + return name[ (int)GetMorale() + 3 ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Fill in a CUserCmd with our data + */ +void CCSBot::BuildUserCmd( CUserCmd& cmd, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse ) +{ + Q_memset( &cmd, 0, sizeof( cmd ) ); + if ( !RunMimicCommand( cmd ) ) + { + // Don't walk when ducked - it's painfully slow + if ( m_Local.m_bDucked || m_Local.m_bDucking ) + { + buttons &= ~IN_SPEED; + } + + 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 ); + } +} + +//-------------------------------------------------------------------------------------------------------------- diff --git a/game/server/cstrike/bot/cs_bot.h b/game/server/cstrike/bot/cs_bot.h new file mode 100644 index 0000000..e9530d5 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot.h @@ -0,0 +1,2004 @@ +//========= 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 _CS_BOT_H_ +#define _CS_BOT_H_ + +#include "bot/bot.h" +#include "bot/cs_bot_manager.h" +#include "bot/cs_bot_chatter.h" +#include "cs_gamestate.h" +#include "cs_player.h" +#include "weapon_csbase.h" +#include "cs_nav_pathfind.h" +#include "cs_nav_area.h" + +class CBaseDoor; +class CBasePropDoor; +class CCSBot; +class CPushAwayEnumerator; + +//-------------------------------------------------------------------------------------------------------------- +/** + * For use with player->m_rgpPlayerItems[] + */ +enum InventorySlotType +{ + PRIMARY_WEAPON_SLOT = 1, + PISTOL_SLOT, + KNIFE_SLOT, + GRENADE_SLOT, + C4_SLOT +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * The definition of a bot's behavior state. One or more finite state machines + * using these states implement a bot's behaviors. + */ +class BotState +{ +public: + virtual void OnEnter( CCSBot *bot ) { } ///< when state is entered + virtual void OnUpdate( CCSBot *bot ) { } ///< state behavior + virtual void OnExit( CCSBot *bot ) { } ///< when state exited + virtual const char *GetName( void ) const = 0; ///< return state name +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * The state is invoked when a bot has nothing to do, or has finished what it was doing. + * A bot never stays in this state - it is the main action selection mechanism. + */ +class IdleState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Idle"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is actively searching for an enemy. + */ +class HuntState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Hunt"; } + + void ClearHuntArea( void ) { m_huntArea = NULL; } + +private: + CNavArea *m_huntArea; ///< "far away" area we are moving to +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot has an enemy and is attempting to kill it + */ +class AttackState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Attack"; } + + void SetCrouchAndHold( bool crouch ) { m_crouchAndHold = crouch; } + +protected: + enum DodgeStateType + { + STEADY_ON, + SLIDE_LEFT, + SLIDE_RIGHT, + JUMP, + + NUM_ATTACK_STATES + }; + DodgeStateType m_dodgeState; + float m_nextDodgeStateTimestamp; + + CountdownTimer m_repathTimer; + float m_scopeTimestamp; + + bool m_haveSeenEnemy; ///< false if we haven't yet seen the enemy since we started this attack (told by a friend, etc) + bool m_isEnemyHidden; ///< true we if we have lost line-of-sight to our enemy + float m_reacquireTimestamp; ///< time when we can fire again, after losing enemy behind cover + float m_shieldToggleTimestamp; ///< time to toggle shield deploy state + bool m_shieldForceOpen; ///< if true, open up and shoot even if in danger + + float m_pinnedDownTimestamp; ///< time when we'll consider ourselves "pinned down" by the enemy + + bool m_crouchAndHold; + bool m_didAmbushCheck; + bool m_shouldDodge; + bool m_firstDodge; + + bool m_isCoward; ///< if true, we'll retreat if outnumbered during this fight + CountdownTimer m_retreatTimer; + + void StopAttacking( CCSBot *bot ); + void Dodge( CCSBot *bot ); ///< do dodge behavior +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot has heard an enemy noise and is moving to find out what it was. + */ +class InvestigateNoiseState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "InvestigateNoise"; } + +private: + void AttendCurrentNoise( CCSBot *bot ); ///< move towards currently heard noise + Vector m_checkNoisePosition; ///< the position of the noise we're investigating + CountdownTimer m_minTimer; ///< minimum time we will investigate our current noise +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is buying equipment at the start of a round. + */ +class BuyState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Buy"; } + +private: + bool m_isInitialDelay; + int m_prefRetries; ///< for retrying buying preferred weapon at current index + int m_prefIndex; ///< where are we in our list of preferred weapons + + int m_retries; + bool m_doneBuying; + bool m_buyDefuseKit; + bool m_buyGrenade; + bool m_buyShield; + bool m_buyPistol; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is moving to a potentially far away position in the world. + */ +class MoveToState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "MoveTo"; } + void SetGoalPosition( const Vector &pos ) { m_goalPosition = pos; } + void SetRouteType( RouteType route ) { m_routeType = route; } + +private: + Vector m_goalPosition; ///< goal position of move + RouteType m_routeType; ///< the kind of route to build + bool m_radioedPlan; + bool m_askedForCover; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a Terrorist bot is moving to pick up a dropped bomb. + */ +class FetchBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual const char *GetName( void ) const { return "FetchBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a Terrorist bot is actually planting the bomb. + */ +class PlantBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "PlantBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a CT bot is actually defusing a live bomb. + */ +class DefuseBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "DefuseBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is hiding in a corner. + * NOTE: This state also includes MOVING TO that hiding spot, which may be all the way + * across the map! + */ +class HideState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Hide"; } + + void SetHidingSpot( const Vector &pos ) { m_hidingSpot = pos; } + const Vector &GetHidingSpot( void ) const { return m_hidingSpot; } + + void SetSearchArea( CNavArea *area ) { m_searchFromArea = area; } + void SetSearchRange( float range ) { m_range = range; } + void SetDuration( float time ) { m_duration = time; } + void SetHoldPosition( bool hold ) { m_isHoldingPosition = hold; } + + bool IsAtSpot( void ) const { return m_isAtSpot; } + + float GetHideTime( void ) const + { + if (IsAtSpot()) + { + return m_duration - m_hideTimer.GetRemainingTime(); + } + + return 0.0f; + } + +private: + CNavArea *m_searchFromArea; + float m_range; + + Vector m_hidingSpot; + bool m_isLookingOutward; + bool m_isAtSpot; + float m_duration; + CountdownTimer m_hideTimer; ///< how long to hide + + bool m_isHoldingPosition; + float m_holdPositionTime; ///< how long to hold our position after we hear nearby enemy noise + + bool m_heardEnemy; ///< set to true when we first hear an enemy + float m_firstHeardEnemyTime; ///< when we first heard the enemy + + int m_retry; ///< counter for retrying hiding spot + + Vector m_leaderAnchorPos; ///< the position of our follow leader when we decided to hide + + bool m_isPaused; ///< if true, we have paused in our retreat for a moment + CountdownTimer m_pauseTimer; ///< for stoppping and starting our pauses while we retreat +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is attempting to flee from a bomb that is about to explode. + */ +class EscapeFromBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "EscapeFromBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is following another player. + */ +class FollowState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Follow"; } + + void SetLeader( CCSPlayer *player ) { m_leader = player; } + +private: + CHandle< CCSPlayer > m_leader; ///< the player we are following + Vector m_lastLeaderPos; ///< where the leader was when we computed our follow path + bool m_isStopped; + float m_stoppedTimestamp; + + enum LeaderMotionStateType + { + INVALID, + STOPPED, + WALKING, + RUNNING + }; + LeaderMotionStateType m_leaderMotionState; + IntervalTimer m_leaderMotionStateTime; + + bool m_isSneaking; + float m_lastSawLeaderTime; + CountdownTimer m_repathInterval; + + IntervalTimer m_walkTime; + bool m_isAtWalkSpeed; + + float m_waitTime; + CountdownTimer m_idleTimer; + + void ComputeLeaderMotionState( float leaderSpeed ); +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is actually using another entity (ie: facing towards it and pressing the use key) + */ +class UseEntityState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "UseEntity"; } + + void SetEntity( CBaseEntity *entity ) { m_entity = entity; } + +private: + EHANDLE m_entity; ///< the entity we will use +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is opening a door + */ +class OpenDoorState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "OpenDoor"; } + + void SetDoor( CBaseEntity *door ); + + bool IsDone( void ) const { return m_isDone; } ///< return true if behavior is done + +private: + CHandle< CBaseDoor > m_funcDoor; ///< the func_door we are opening + CHandle< CBasePropDoor > m_propDoor; ///< the prop_door we are opening + bool m_isDone; + CountdownTimer m_timeout; +}; + + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- +/** + * The Counter-strike Bot + */ +class CCSBot : public CBot< CCSPlayer > +{ +public: + DECLARE_CLASS( CCSBot, CBot< CCSPlayer > ); + DECLARE_DATADESC(); + + CCSBot( void ); ///< constructor initializes all values to zero + virtual ~CCSBot(); + virtual bool Initialize( const BotProfile *profile, int team ); ///< (EXTEND) prepare bot for action + + virtual void Spawn( void ); ///< (EXTEND) spawn the bot into the game + virtual void Touch( CBaseEntity *other ); ///< (EXTEND) when touched by another entity + + virtual void Upkeep( void ); ///< lightweight maintenance, invoked frequently + virtual void Update( void ); ///< heavyweight algorithms, invoked less often + virtual void BuildUserCmd( CUserCmd& cmd, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse ); + virtual float GetMoveSpeed( void ); ///< returns current movement speed (for walk/run) + + virtual void Walk( void ); + virtual bool Jump( bool mustJump = false ); ///< returns true if jump was started + + //- behavior properties ------------------------------------------------------------------------------------------ + float GetCombatRange( void ) const; + bool IsRogue( void ) const; ///< return true if we dont listen to teammates or pursue scenario goals + void SetRogue( bool rogue ); + bool IsHurrying( void ) const; ///< return true if we are in a hurry + void Hurry( float duration ); ///< force bot to hurry + bool IsSafe( void ) const; ///< return true if we are in a safe region + bool IsWellPastSafe( void ) const; ///< return true if it is well past the early, "safe", part of the round + bool IsEndOfSafeTime( void ) const; ///< return true if we were in the safe time last update, but not now + float GetSafeTimeRemaining( void ) const; ///< return the amount of "safe time" we have left + float GetSafeTime( void ) const; ///< return what we think the total "safe time" for this map is + virtual void Blind( float holdTime, float fadeTime, float startingAlpha = 255 ); // player blinded by a flashbang + bool IsUnhealthy( void ) const; ///< returns true if bot is low on health + + bool IsAlert( void ) const; ///< return true if bot is in heightened "alert" mode + void BecomeAlert( void ); ///< bot becomes "alert" for immediately nearby enemies + + bool IsSneaking( void ) const; ///< return true if bot is sneaking + void Sneak( float duration ); ///< sneak for given duration + + //- behaviors --------------------------------------------------------------------------------------------------- + void Idle( void ); + + void Hide( CNavArea *searchFromArea = NULL, float duration = -1.0f, float hideRange = 750.0f, bool holdPosition = false ); ///< DEPRECATED: Use TryToHide() instead + #define USE_NEAREST true + bool TryToHide( CNavArea *searchFromArea = NULL, float duration = -1.0f, float hideRange = 750.0f, bool holdPosition = false, bool useNearest = false ); ///< try to hide nearby, return false if cannot + void Hide( const Vector &hidingSpot, float duration = -1.0f, bool holdPosition = false ); ///< move to the given hiding place + bool IsHiding( void ) const; ///< returns true if bot is currently hiding + bool IsAtHidingSpot( void ) const; ///< return true if we are hiding and at our hiding spot + float GetHidingTime( void ) const; ///< return number of seconds we have been at our current hiding spot + + bool MoveToInitialEncounter( void ); ///< move to a hiding spot and wait for initial encounter with enemy team (return false if no spots are available) + + bool TryToRetreat( float maxRange = 1000.0f, float duration = -1.0f ); ///< retreat to a nearby hiding spot, away from enemies + + void Hunt( void ); + bool IsHunting( void ) const; ///< returns true if bot is currently hunting + + void Attack( CCSPlayer *victim ); + void FireWeaponAtEnemy( void ); ///< fire our active weapon towards our current enemy + void StopAttacking( void ); + bool IsAttacking( void ) const; ///< returns true if bot is currently engaging a target + + void MoveTo( const Vector &pos, RouteType route = SAFEST_ROUTE ); ///< move to potentially distant position + bool IsMovingTo( void ) const; ///< return true if we are in the MoveTo state + + void PlantBomb( void ); + + void FetchBomb( void ); ///< bomb has been dropped - go get it + bool NoticeLooseBomb( void ) const; ///< return true if we noticed the bomb on the ground or on radar + bool CanSeeLooseBomb( void ) const; ///< return true if we directly see the loose bomb + + void DefuseBomb( void ); + bool IsDefusingBomb( void ) const; ///< returns true if bot is currently defusing the bomb + bool CanSeePlantedBomb( void ) const; ///< return true if we directly see the planted bomb + + void EscapeFromBomb( void ); + bool IsEscapingFromBomb( void ) const; ///< return true if we are escaping from the bomb + + void RescueHostages( void ); ///< begin process of rescuing hostages + + void UseEntity( CBaseEntity *entity ); ///< use the entity + + void OpenDoor( CBaseEntity *door ); ///< open the door (assumes we are right in front of it) + bool IsOpeningDoor( void ) const; ///< return true if we are in the process of opening a door + + void Buy( void ); ///< enter the buy state + bool IsBuying( void ) const; + + void Panic( void ); ///< look around in panic + bool IsPanicking( void ) const; ///< return true if bot is panicked + void StopPanicking( void ); ///< end our panic + void UpdatePanicLookAround( void ); ///< do panic behavior + + void TryToJoinTeam( int team ); ///< try to join the given team + + void Follow( CCSPlayer *player ); ///< begin following given Player + void ContinueFollowing( void ); ///< continue following our leader after finishing what we were doing + void StopFollowing( void ); ///< stop following + bool IsFollowing( void ) const; ///< return true if we are following someone (not necessarily in the follow state) + CCSPlayer *GetFollowLeader( void ) const; ///< return the leader we are following + float GetFollowDuration( void ) const; ///< return how long we've been following our leader + bool CanAutoFollow( void ) const; ///< return true if we can auto-follow + + bool IsNotMoving( float minDuration = 0.0f ) const; ///< return true if we are currently standing still and have been for minDuration + + void AimAtEnemy( void ); ///< point our weapon towards our enemy + void StopAiming( void ); ///< stop aiming at enemy + bool IsAimingAtEnemy( void ) const; ///< returns true if we are trying to aim at an enemy + + float GetStateTimestamp( void ) const; ///< get time current state was entered + + bool IsDoingScenario( void ) const; ///< return true if we will do scenario-related tasks + + //- scenario / gamestate ----------------------------------------------------------------------------------------- + CSGameState *GetGameState( void ); ///< return an interface to this bot's gamestate + const CSGameState *GetGameState( void ) const; ///< return an interface to this bot's gamestate + + bool IsAtBombsite( void ); ///< return true if we are in a bomb planting zone + bool GuardRandomZone( float range = 500.0f ); ///< pick a random zone and hide near it + + bool IsBusy( void ) const; ///< return true if we are busy doing something important + + //- high-level tasks --------------------------------------------------------------------------------------------- + enum TaskType + { + SEEK_AND_DESTROY, + PLANT_BOMB, + FIND_TICKING_BOMB, + DEFUSE_BOMB, + GUARD_TICKING_BOMB, + GUARD_BOMB_DEFUSER, + GUARD_LOOSE_BOMB, + GUARD_BOMB_ZONE, + GUARD_INITIAL_ENCOUNTER, + ESCAPE_FROM_BOMB, + HOLD_POSITION, + FOLLOW, + VIP_ESCAPE, + GUARD_VIP_ESCAPE_ZONE, + COLLECT_HOSTAGES, + RESCUE_HOSTAGES, + GUARD_HOSTAGES, + GUARD_HOSTAGE_RESCUE_ZONE, + MOVE_TO_LAST_KNOWN_ENEMY_POSITION, + MOVE_TO_SNIPER_SPOT, + SNIPING, + + NUM_TASKS + }; + void SetTask( TaskType task, CBaseEntity *entity = NULL ); ///< set our current "task" + TaskType GetTask( void ) const; + CBaseEntity *GetTaskEntity( void ); + const char *GetTaskName( void ) const; ///< return string describing current task + + //- behavior modifiers ------------------------------------------------------------------------------------------ + enum DispositionType + { + ENGAGE_AND_INVESTIGATE, ///< engage enemies on sight and investigate enemy noises + OPPORTUNITY_FIRE, ///< engage enemies on sight, but only look towards enemy noises, dont investigate + SELF_DEFENSE, ///< only engage if fired on, or very close to enemy + IGNORE_ENEMIES, ///< ignore all enemies - useful for ducking around corners, running away, etc + + NUM_DISPOSITIONS + }; + void SetDisposition( DispositionType disposition ); ///< define how we react to enemies + DispositionType GetDisposition( void ) const; + const char *GetDispositionName( void ) const; ///< return string describing current disposition + + void IgnoreEnemies( float duration ); ///< ignore enemies for a short duration + + enum MoraleType + { + TERRIBLE = -3, + BAD = -2, + NEGATIVE = -1, + NEUTRAL = 0, + POSITIVE = 1, + GOOD = 2, + EXCELLENT = 3, + }; + MoraleType GetMorale( void ) const; + const char *GetMoraleName( void ) const; ///< return string describing current morale + void IncreaseMorale( void ); + void DecreaseMorale( void ); + + void Surprise( float duration ); ///< become "surprised" - can't attack + bool IsSurprised( void ) const; ///< return true if we are "surprised" + + + //- listening for noises ---------------------------------------------------------------------------------------- + bool IsNoiseHeard( void ) const; ///< return true if we have heard a noise + bool HeardInterestingNoise( void ); ///< return true if we heard an enemy noise worth checking in to + void InvestigateNoise( void ); ///< investigate recent enemy noise + bool IsInvestigatingNoise( void ) const; ///< return true if we are investigating a noise + const Vector *GetNoisePosition( void ) const; ///< return position of last heard noise, or NULL if none heard + CNavArea *GetNoiseArea( void ) const; ///< return area where noise was heard + void ForgetNoise( void ); ///< clear the last heard noise + bool CanSeeNoisePosition( void ) const; ///< return true if we directly see where we think the noise came from + float GetNoiseRange( void ) const; ///< return approximate distance to last noise heard + + bool CanHearNearbyEnemyGunfire( float range = -1.0f ) const;///< return true if we hear nearby threatening enemy gunfire within given range (-1 == infinite) + PriorityType GetNoisePriority( void ) const; ///< return priority of last heard noise + + //- radio and chatter-------------------------------------------------------------------------------------------- + void SendRadioMessage( RadioType event ); ///< send a radio message + void SpeakAudio( const char *voiceFilename, float duration, int pitch ); ///< send voice chatter + BotChatterInterface *GetChatter( void ); ///< return an interface to this bot's chatter system + bool RespondToHelpRequest( CCSPlayer *player, Place place, float maxRange = -1.0f ); ///< decide if we should move to help the player, return true if we will + bool IsUsingVoice() const; ///< new-style "voice" chatter gets voice feedback + + + //- enemies ------------------------------------------------------------------------------------------------------ + // BOTPORT: GetEnemy() collides with GetEnemy() in CBaseEntity - need to use different nomenclature + void SetBotEnemy( CCSPlayer *enemy ); ///< set given player as our current enemy + CCSPlayer *GetBotEnemy( void ) const; + int GetNearbyEnemyCount( void ) const; ///< return max number of nearby enemies we've seen recently + unsigned int GetEnemyPlace( void ) const; ///< return location where we see the majority of our enemies + bool CanSeeBomber( void ) const; ///< return true if we can see the bomb carrier + CCSPlayer *GetBomber( void ) const; + + int GetNearbyFriendCount( void ) const; ///< return number of nearby teammates + CCSPlayer *GetClosestVisibleFriend( void ) const; ///< return the closest friend that we can see + CCSPlayer *GetClosestVisibleHumanFriend( void ) const; ///< return the closest human friend that we can see + + bool IsOutnumbered( void ) const; ///< return true if we are outnumbered by enemies + int OutnumberedCount( void ) const; ///< return number of enemies we are outnumbered by + + #define ONLY_VISIBLE_ENEMIES true + CCSPlayer *GetImportantEnemy( bool checkVisibility = false ) const; ///< return the closest "important" enemy for the given scenario (bomb carrier, VIP, hostage escorter) + + void UpdateReactionQueue( void ); ///< update our reaction time queue + CCSPlayer *GetRecognizedEnemy( void ); ///< return the most dangerous threat we are "conscious" of + bool IsRecognizedEnemyReloading( void ); ///< return true if the enemy we are "conscious" of is reloading + bool IsRecognizedEnemyProtectedByShield( void ); ///< return true if the enemy we are "conscious" of is hiding behind a shield + float GetRangeToNearestRecognizedEnemy( void ); ///< return distance to closest enemy we are "conscious" of + + CCSPlayer *GetAttacker( void ) const; ///< return last enemy that hurt us + float GetTimeSinceAttacked( void ) const; ///< return duration since we were last injured by an attacker + float GetFirstSawEnemyTimestamp( void ) const; ///< time since we saw any enemies + float GetLastSawEnemyTimestamp( void ) const; + float GetTimeSinceLastSawEnemy( void ) const; + float GetTimeSinceAcquiredCurrentEnemy( void ) const; + bool HasNotSeenEnemyForLongTime( void ) const; ///< return true if we haven't seen an enemy for "a long time" + const Vector &GetLastKnownEnemyPosition( void ) const; + bool IsEnemyVisible( void ) const; ///< is our current enemy visible + float GetEnemyDeathTimestamp( void ) const; + bool IsFriendInLineOfFire( void ); ///< return true if a friend is in our weapon's way + bool IsAwareOfEnemyDeath( void ) const; ///< return true if we *noticed* that our enemy died + int GetLastVictimID( void ) const; ///< return the ID (entindex) of the last victim we killed, or zero + + bool CanSeeSniper( void ) const; ///< return true if we can see an enemy sniper + bool HasSeenSniperRecently( void ) const; ///< return true if we have seen a sniper recently + + float GetTravelDistanceToPlayer( CCSPlayer *player ) const; ///< return shortest path travel distance to this player + bool DidPlayerJustFireWeapon( const CCSPlayer *player ) const; ///< return true if the given player just fired their weapon + + //- navigation -------------------------------------------------------------------------------------------------- + bool HasPath( void ) const; + void DestroyPath( void ); + + float GetFeetZ( void ) const; ///< return Z of bottom of feet + + enum PathResult + { + PROGRESSING, ///< we are moving along the path + END_OF_PATH, ///< we reached the end of the path + PATH_FAILURE ///< we failed to reach the end of the path + }; + #define NO_SPEED_CHANGE false + PathResult UpdatePathMovement( bool allowSpeedChange = true ); ///< move along our computed path - if allowSpeedChange is true, bot will walk when near goal to ensure accuracy + + //bool AStarSearch( CNavArea *startArea, CNavArea *goalArea ); ///< find shortest path from startArea to goalArea - don't actually buid the path + bool ComputePath( const Vector &goal, RouteType route = SAFEST_ROUTE ); ///< compute path to goal position + bool StayOnNavMesh( void ); + CNavArea *GetLastKnownArea( void ) const; ///< return the last area we know we were inside of + const Vector &GetPathEndpoint( void ) const; ///< return final position of our current path + float GetPathDistanceRemaining( void ) const; ///< return estimated distance left to travel along path + void ResetStuckMonitor( void ); + bool IsAreaVisible( const CNavArea *area ) const; ///< is any portion of the area visible to this bot + const Vector &GetPathPosition( int index ) const; + bool GetSimpleGroundHeightWithFloor( const Vector &pos, float *height, Vector *normal = NULL ); ///< find "simple" ground height, treating current nav area as part of the floor + void BreakablesCheck( void ); + void DoorCheck( void ); ///< Check for any doors along our path that need opening + + virtual void PushawayTouch( CBaseEntity *pOther ); + + Place GetPlace( void ) const; ///< get our current radio chatter place + + bool IsUsingLadder( void ) const; ///< returns true if we are in the process of negotiating a ladder + void GetOffLadder( void ); ///< immediately jump off of our ladder, if we're on one + + void SetGoalEntity( CBaseEntity *entity ); + CBaseEntity *GetGoalEntity( void ); + + bool IsNearJump( void ) const; ///< return true if nearing a jump in the path + float GetApproximateFallDamage( float height ) const; ///< return how much damage will will take from the given fall height + + void ForceRun( float duration ); ///< force the bot to run if it moves for the given duration + virtual bool IsRunning( void ) const; + + void Wait( float duration ); ///< wait where we are for the given duration + bool IsWaiting( void ) const; ///< return true if we are waiting + void StopWaiting( void ); ///< stop waiting + + void Wiggle( void ); ///< random movement, for getting un-stuck + + bool IsFriendInTheWay( const Vector &goalPos ); ///< return true if a friend is between us and the given position + void FeelerReflexAdjustment( Vector *goalPosition ); ///< do reflex avoidance movements if our "feelers" are touched + + bool HasVisitedEnemySpawn( void ) const; ///< return true if we have visited enemy spawn at least once + bool IsAtEnemySpawn( void ) const; ///< return true if we are at the/an enemy spawn right now + + //- looking around ---------------------------------------------------------------------------------------------- + + // BOTPORT: EVIL VILE HACK - why is EyePosition() not const?!?!? + const Vector &EyePositionConst( void ) const; + + void SetLookAngles( float yaw, float pitch ); ///< set our desired look angles + void UpdateLookAngles( void ); ///< move actual view angles towards desired ones + void UpdateLookAround( bool updateNow = false ); ///< update "looking around" mechanism + void InhibitLookAround( float duration ); ///< block all "look at" and "looking around" behavior for given duration - just look ahead + + /// @todo Clean up notion of "forward angle" and "look ahead angle" + void SetForwardAngle( float angle ); ///< define our forward facing + void SetLookAheadAngle( float angle ); ///< define default look ahead angle + + /// look at the given point in space for the given duration (-1 means forever) + void SetLookAt( const char *desc, const Vector &pos, PriorityType pri, float duration = -1.0f, bool clearIfClose = false, float angleTolerance = 5.0f, bool attack = false ); + void ClearLookAt( void ); ///< stop looking at a point in space and just look ahead + bool IsLookingAtSpot( PriorityType pri = PRIORITY_LOW ) const; ///< return true if we are looking at spot with equal or higher priority + bool IsViewMoving( float angleVelThreshold = 1.0f ) const; ///< returns true if bot's view angles are rotating (not still) + bool HasViewBeenSteady( float duration ) const; ///< how long has our view been "steady" (ie: not moving) for given duration + + bool HasLookAtTarget( void ) const; ///< return true if we are in the process of looking at a target + + enum VisiblePartType + { + NONE = 0x00, + GUT = 0x01, + HEAD = 0x02, + LEFT_SIDE = 0x04, ///< the left side of the object from our point of view (not their left side) + RIGHT_SIDE = 0x08, ///< the right side of the object from our point of view (not their right side) + FEET = 0x10 + }; + + #define CHECK_FOV true + bool IsVisible( const Vector &pos, bool testFOV = false, const CBaseEntity *ignore = NULL ) const; ///< return true if we can see the point + bool IsVisible( CCSPlayer *player, bool testFOV = false, unsigned char *visParts = NULL ) const; ///< return true if we can see any part of the player + + bool IsNoticable( const CCSPlayer *player, unsigned char visibleParts ) const; ///< return true if we "notice" given player + + bool IsEnemyPartVisible( VisiblePartType part ) const; ///< if enemy is visible, return the part we see for our current enemy + const Vector &GetPartPosition( CCSPlayer *player, VisiblePartType part ) const; ///< return world space position of given part on player + + float ComputeWeaponSightRange( void ); ///< return line-of-sight distance to obstacle along weapon fire ray + + bool IsAnyVisibleEnemyLookingAtMe( bool testFOV = false ) const;///< return true if any enemy I have LOS to is looking directly at me + + bool IsSignificantlyCloser( const CCSPlayer *testPlayer, const CCSPlayer *referencePlayer ) const; ///< return true if testPlayer is significantly closer than referencePlayer + + //- approach points --------------------------------------------------------------------------------------------- + void ComputeApproachPoints( void ); ///< determine the set of "approach points" representing where the enemy can enter this region + void UpdateApproachPoints( void ); ///< recompute the approach point set if we have moved far enough to invalidate the current ones + void ClearApproachPoints( void ); + void DrawApproachPoints( void ) const; ///< for debugging + float GetHidingSpotCheckTimestamp( HidingSpot *spot ) const; ///< return time when given spot was last checked + void SetHidingSpotCheckTimestamp( HidingSpot *spot ); ///< set the timestamp of the given spot to now + + const CNavArea *GetInitialEncounterArea( void ) const; ///< return area where we think we will first meet the enemy + void SetInitialEncounterArea( const CNavArea *area ); + + //- weapon query and equip -------------------------------------------------------------------------------------- + #define MUST_EQUIP true + void EquipBestWeapon( bool mustEquip = false ); ///< equip the best weapon we are carrying that has ammo + void EquipPistol( void ); ///< equip our pistol + void EquipKnife( void ); ///< equip the knife + + #define DONT_USE_SMOKE_GRENADE true + bool EquipGrenade( bool noSmoke = false ); ///< equip a grenade, return false if we cant + + bool IsUsingKnife( void ) const; ///< returns true if we have knife equipped + bool IsUsingPistol( void ) const; ///< returns true if we have pistol equipped + bool IsUsingGrenade( void ) const; ///< returns true if we have grenade equipped + bool IsUsingSniperRifle( void ) const; ///< returns true if using a "sniper" rifle + bool IsUsing( CSWeaponID weapon ) const; ///< returns true if using the specific weapon + bool IsSniper( void ) const; ///< return true if we have a sniper rifle in our inventory + bool IsSniping( void ) const; ///< return true if we are actively sniping (moving to sniper spot or settled in) + bool IsUsingShotgun( void ) const; ///< returns true if using a shotgun + bool IsUsingMachinegun( void ) const; ///< returns true if using the big 'ol machinegun + void ThrowGrenade( const Vector &target ); ///< begin the process of throwing the grenade + bool IsThrowingGrenade( void ) const; ///< return true if we are in the process of throwing a grenade + bool HasGrenade( void ) const; ///< return true if we have a grenade in our inventory + void AvoidEnemyGrenades( void ); ///< react to enemy grenades we see + bool IsAvoidingGrenade( void ) const; ///< return true if we are in the act of avoiding a grenade + bool DoesActiveWeaponHaveSilencer( void ) const; ///< returns true if we are using a weapon with a removable silencer + bool CanActiveWeaponFire( void ) const; ///< returns true if our current weapon can attack + CWeaponCSBase *GetActiveCSWeapon( void ) const; ///< get our current Counter-Strike weapon + + void GiveWeapon( const char *weaponAlias ); ///< Debug command to give a named weapon + + virtual void PrimaryAttack( void ); ///< presses the fire button, unless we're holding a pistol that can't fire yet (so we can just always call PrimaryAttack()) + + enum ZoomType { NO_ZOOM, LOW_ZOOM, HIGH_ZOOM }; + ZoomType GetZoomLevel( void ); ///< return the current zoom level of our weapon + + bool AdjustZoom( float range ); ///< change our zoom level to be appropriate for the given range + bool IsWaitingForZoom( void ) const; ///< return true if we are reacquiring after our zoom + + bool IsPrimaryWeaponEmpty( void ) const; ///< return true if primary weapon doesn't exist or is totally out of ammo + bool IsPistolEmpty( void ) const; ///< return true if pistol doesn't exist or is totally out of ammo + + int GetHostageEscortCount( void ) const; ///< return the number of hostages following me + void IncreaseHostageEscortCount( void ); + float GetRangeToFarthestEscortedHostage( void ) const; ///< return euclidean distance to farthest escorted hostage + void ResetWaitForHostagePatience( void ); + + //------------------------------------------------------------------------------------ + // Event hooks + // + + /// invoked when injured by something (EXTEND) - returns the amount of damage inflicted + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + /// invoked when killed (EXTEND) + virtual void Event_Killed( const CTakeDamageInfo &info ); + + virtual bool BumpWeapon( CBaseCombatWeapon *pWeapon ); ///< invoked when in contact with a CWeaponBox + + + /// invoked when event occurs in the game (some events have NULL entity) + void OnPlayerFootstep( IGameEvent *event ); + void OnPlayerRadio( IGameEvent *event ); + void OnPlayerDeath( IGameEvent *event ); + void OnPlayerFallDamage( IGameEvent *event ); + + void OnBombPickedUp( IGameEvent *event ); + void OnBombPlanted( IGameEvent *event ); + void OnBombBeep( IGameEvent *event ); + void OnBombDefuseBegin( IGameEvent *event ); + void OnBombDefused( IGameEvent *event ); + void OnBombDefuseAbort( IGameEvent *event ); + void OnBombExploded( IGameEvent *event ); + + void OnRoundEnd( IGameEvent *event ); + void OnRoundStart( IGameEvent *event ); + + void OnDoorMoving( IGameEvent *event ); + + void OnBreakProp( IGameEvent *event ); + void OnBreakBreakable( IGameEvent *event ); + + void OnHostageFollows( IGameEvent *event ); + void OnHostageRescuedAll( IGameEvent *event ); + + void OnWeaponFire( IGameEvent *event ); + void OnWeaponFireOnEmpty( IGameEvent *event ); + void OnWeaponReload( IGameEvent *event ); + void OnWeaponZoom( IGameEvent *event ); + + void OnBulletImpact( IGameEvent *event ); + + void OnHEGrenadeDetonate( IGameEvent *event ); + void OnFlashbangDetonate( IGameEvent *event ); + void OnSmokeGrenadeDetonate( IGameEvent *event ); + void OnGrenadeBounce( IGameEvent *event ); + + void OnNavBlocked( IGameEvent *event ); + + void OnEnteredNavArea( CNavArea *newArea ); ///< invoked when bot enters a nav area + +private: + #define IS_FOOTSTEP true + void OnAudibleEvent( IGameEvent *event, CBasePlayer *player, float range, PriorityType priority, bool isHostile, bool isFootstep = false, const Vector *actualOrigin = NULL ); ///< Checks if the bot can hear the event + +private: + friend class CCSBotManager; + + /// @todo Get rid of these + friend class AttackState; + friend class BuyState; + + // BOTPORT: Remove this vile hack + Vector m_eyePosition; + + void ResetValues( void ); ///< reset internal data to initial state + void BotDeathThink( void ); + + char m_name[64]; ///< copied from STRING(pev->netname) for debugging + void DebugDisplay( void ) const; ///< render bot debug info + + //- behavior properties ------------------------------------------------------------------------------------------ + float m_combatRange; ///< desired distance between us and them during gunplay + mutable bool m_isRogue; ///< if true, the bot is a "rogue" and listens to no-one + mutable CountdownTimer m_rogueTimer; + MoraleType m_morale; ///< our current morale, based on our win/loss history + bool m_diedLastRound; ///< true if we died last round + float m_safeTime; ///< duration at the beginning of the round where we feel "safe" + bool m_wasSafe; ///< true if we were in the safe time last update + void AdjustSafeTime( void ); ///< called when enemy seen to adjust safe time for this round + NavRelativeDirType m_blindMoveDir; ///< which way to move when we're blind + bool m_blindFire; ///< if true, fire weapon while blinded + CountdownTimer m_surpriseTimer; ///< when we were surprised + + bool m_isFollowing; ///< true if we are following someone + CHandle< CCSPlayer > m_leader; ///< the ID of who we are following + float m_followTimestamp; ///< when we started following + float m_allowAutoFollowTime; ///< time when we can auto follow + + CountdownTimer m_hurryTimer; ///< if valid, bot is in a hurry + CountdownTimer m_alertTimer; ///< if valid, bot is alert + CountdownTimer m_sneakTimer; ///< if valid, bot is sneaking + CountdownTimer m_panicTimer; ///< if valid, bot is panicking + + + // instances of each possible behavior state, to avoid dynamic memory allocation during runtime + IdleState m_idleState; + HuntState m_huntState; + AttackState m_attackState; + InvestigateNoiseState m_investigateNoiseState; + BuyState m_buyState; + MoveToState m_moveToState; + FetchBombState m_fetchBombState; + PlantBombState m_plantBombState; + DefuseBombState m_defuseBombState; + HideState m_hideState; + EscapeFromBombState m_escapeFromBombState; + FollowState m_followState; + UseEntityState m_useEntityState; + OpenDoorState m_openDoorState; + + /// @todo Allow multiple simultaneous state machines (look around, etc) + void SetState( BotState *state ); ///< set the current behavior state + BotState *m_state; ///< current behavior state + float m_stateTimestamp; ///< time state was entered + bool m_isAttacking; ///< if true, special Attack state is overriding the state machine + bool m_isOpeningDoor; ///< if true, special OpenDoor state is overriding the state machine + + TaskType m_task; ///< our current task + EHANDLE m_taskEntity; ///< an entity used for our task + + //- navigation --------------------------------------------------------------------------------------------------- + Vector m_goalPosition; + EHANDLE m_goalEntity; + void MoveTowardsPosition( const Vector &pos ); ///< move towards position, independant of view angle + void MoveAwayFromPosition( const Vector &pos ); ///< move away from position, independant of view angle + void StrafeAwayFromPosition( const Vector &pos ); ///< strafe (sidestep) away from position, independant of view angle + void StuckCheck( void ); ///< check if we have become stuck + CCSNavArea *m_currentArea; ///< the nav area we are standing on + CCSNavArea *m_lastKnownArea; ///< the last area we were in + EHANDLE m_avoid; ///< higher priority player we need to make way for + float m_avoidTimestamp; + bool m_isStopping; ///< true if we're trying to stop because we entered a 'stop' nav area + bool m_hasVisitedEnemySpawn; ///< true if we have been at the enemy spawn + IntervalTimer m_stillTimer; ///< how long we have been not moving + + //- path navigation data ---------------------------------------------------------------------------------------- + enum { MAX_PATH_LENGTH = 256 }; + struct ConnectInfo + { + 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 + } + m_path[ MAX_PATH_LENGTH ]; + int m_pathLength; + int m_pathIndex; ///< index of next area on path + float m_areaEnteredTimestamp; + void BuildTrivialPath( const Vector &goal ); ///< build trivial path to goal, assuming we are already in the same area + + CountdownTimer m_repathTimer; ///< must have elapsed before bot can pathfind again + + bool ComputePathPositions( void ); ///< determine actual path positions bot will move between along the path + void SetupLadderMovement( void ); + void SetPathIndex( int index ); ///< set the current index along the path + void DrawPath( void ); + int FindOurPositionOnPath( Vector *close, bool local = false ) const; ///< compute the closest point to our current position on our path + int FindPathPoint( float aheadRange, Vector *point, int *prevIndex = NULL ); ///< compute a point a fixed distance ahead along our path. + bool FindClosestPointOnPath( const Vector &pos, int startIndex, int endIndex, Vector *close ) const; ///< compute closest point on path to given point + bool IsStraightLinePathWalkable( const Vector &goal ) const; ///< test for un-jumpable height change, or unrecoverable fall + void ComputeLadderAngles( float *yaw, float *pitch ); ///< computes ideal yaw/pitch for traversing the current ladder on our path + + mutable CountdownTimer m_avoidFriendTimer; ///< used to throttle how often we check for friends in our path + mutable bool m_isFriendInTheWay; ///< true if a friend is blocking our path + CountdownTimer m_politeTimer; ///< we'll wait for friend to move until this runs out + bool m_isWaitingBehindFriend; ///< true if we are waiting for a friend to move + + #define ONLY_JUMP_DOWN true + bool DiscontinuityJump( float ground, bool onlyJumpDown = false, bool mustJump = false ); ///< check if we need to jump due to height change + + enum LadderNavState + { + APPROACH_ASCENDING_LADDER, ///< prepare to scale a ladder + APPROACH_DESCENDING_LADDER, ///< prepare to go down ladder + FACE_ASCENDING_LADDER, + FACE_DESCENDING_LADDER, + MOUNT_ASCENDING_LADDER, ///< move toward ladder until "on" it + MOUNT_DESCENDING_LADDER, ///< move toward ladder until "on" it + ASCEND_LADDER, ///< go up the ladder + DESCEND_LADDER, ///< go down the ladder + DISMOUNT_ASCENDING_LADDER, ///< get off of the ladder + DISMOUNT_DESCENDING_LADDER, ///< get off of the ladder + MOVE_TO_DESTINATION, ///< dismount ladder and move to destination area + } + m_pathLadderState; + bool m_pathLadderFaceIn; ///< if true, face towards ladder, otherwise face away + const CNavLadder *m_pathLadder; ///< the ladder we need to use to reach the next area + bool UpdateLadderMovement( void ); ///< called by UpdatePathMovement() + NavRelativeDirType m_pathLadderDismountDir; ///< which way to dismount + float m_pathLadderDismountTimestamp; ///< time when dismount started + float m_pathLadderEnd; ///< if ascending, z of top, if descending z of bottom + void ComputeLadderEndpoint( bool ascending ); + float m_pathLadderTimestamp; ///< time when we started using ladder - for timeout check + + CountdownTimer m_mustRunTimer; ///< if nonzero, bot cannot walk + CountdownTimer m_waitTimer; ///< if nonzero, we are waiting where we are + + void UpdateTravelDistanceToAllPlayers( void ); ///< periodically compute shortest path distance to each player + CountdownTimer m_updateTravelDistanceTimer; ///< for throttling travel distance computations + float m_playerTravelDistance[ MAX_PLAYERS ]; ///< current distance from this bot to each player + unsigned char m_travelDistancePhase; ///< a counter for optimizing when to compute travel distance + + //- game scenario mechanisms ------------------------------------------------------------------------------------- + CSGameState m_gameState; ///< our current knowledge about the state of the scenario + + byte m_hostageEscortCount; ///< the number of hostages we're currently escorting + void UpdateHostageEscortCount( void ); ///< periodic check of hostage count in case we lost some + float m_hostageEscortCountTimestamp; + + int m_desiredTeam; ///< the team we want to be on + bool m_hasJoined; ///< true if bot has actually joined the game + + bool m_isWaitingForHostage; + CountdownTimer m_inhibitWaitingForHostageTimer; ///< if active, inhibits us waiting for lagging hostages + CountdownTimer m_waitForHostageTimer; ///< stops us waiting too long + + //- listening mechanism ------------------------------------------------------------------------------------------ + Vector m_noisePosition; ///< position we last heard non-friendly noise + float m_noiseTravelDistance; ///< the travel distance to the noise + float m_noiseTimestamp; ///< when we heard it (can get zeroed) + CNavArea *m_noiseArea; ///< the nav area containing the noise + PriorityType m_noisePriority; ///< priority of currently heard noise + bool UpdateLookAtNoise( void ); ///< return true if we decided to look towards the most recent noise source + CountdownTimer m_noiseBendTimer; ///< for throttling how often we bend our line of sight to the noise location + Vector m_bentNoisePosition; ///< the last computed bent line of sight + bool m_bendNoisePositionValid; + + //- "looking around" mechanism ----------------------------------------------------------------------------------- + float m_lookAroundStateTimestamp; ///< time of next state change + float m_lookAheadAngle; ///< our desired forward look angle + float m_forwardAngle; ///< our current forward facing direction + float m_inhibitLookAroundTimestamp; ///< time when we can look around again + + enum LookAtSpotState + { + NOT_LOOKING_AT_SPOT, ///< not currently looking at a point in space + LOOK_TOWARDS_SPOT, ///< in the process of aiming at m_lookAtSpot + LOOK_AT_SPOT, ///< looking at m_lookAtSpot + NUM_LOOK_AT_SPOT_STATES + } + m_lookAtSpotState; + Vector m_lookAtSpot; ///< the spot we're currently looking at + PriorityType m_lookAtSpotPriority; + float m_lookAtSpotDuration; ///< how long we need to look at the spot + float m_lookAtSpotTimestamp; ///< when we actually began looking at the spot + float m_lookAtSpotAngleTolerance; ///< how exactly we must look at the spot + bool m_lookAtSpotClearIfClose; ///< if true, the look at spot is cleared if it gets close to us + bool m_lookAtSpotAttack; ///< if true, the look at spot should be attacked + const char *m_lookAtDesc; ///< for debugging + void UpdateLookAt( void ); + void UpdatePeripheralVision(); ///< update enounter spot timestamps, etc + float m_peripheralTimestamp; + + enum { MAX_APPROACH_POINTS = 16 }; + struct ApproachPoint + { + Vector m_pos; + CNavArea *m_area; + }; + + ApproachPoint m_approachPoint[ MAX_APPROACH_POINTS ]; + unsigned char m_approachPointCount; + Vector m_approachPointViewPosition; ///< the position used when computing current approachPoint set + + CBaseEntity * FindEntitiesOnPath( float distance, CPushAwayEnumerator *enumerator, bool checkStuck ); + + IntervalTimer m_viewSteadyTimer; ///< how long has our view been "steady" (ie: not moving) + + bool BendLineOfSight( const Vector &eye, const Vector &target, Vector *bend, float angleLimit = 135.0f ) const; ///< "bend" our line of sight until we can see the target point. Return bend point, false if cant bend. + bool FindApproachPointNearestPath( Vector *pos ); ///< find the approach point that is nearest to our current path, ahead of us + bool FindGrenadeTossPathTarget( Vector *pos ); ///< find spot to throw grenade ahead of us and "around the corner" along our path + enum GrenadeTossState + { + NOT_THROWING, ///< not yet throwing + START_THROW, ///< lining up throw + THROW_LINED_UP, ///< pause for a moment when on-line + FINISH_THROW, ///< throwing + }; + GrenadeTossState m_grenadeTossState; + CountdownTimer m_tossGrenadeTimer; ///< timeout timer for grenade tossing + const CNavArea *m_initialEncounterArea; ///< area where we think we will initially encounter the enemy + void LookForGrenadeTargets( void ); ///< look for grenade throw targets and throw our grenade at them + void UpdateGrenadeThrow( void ); ///< process grenade throwing + CountdownTimer m_isAvoidingGrenade; ///< if nonzero we are in the act of avoiding a grenade + + + SpotEncounter *m_spotEncounter; ///< the spots we will encounter as we move thru our current area + float m_spotCheckTimestamp; ///< when to check next encounter spot + + /// @todo Add timestamp for each possible client to hiding spots + enum { MAX_CHECKED_SPOTS = 64 }; + struct HidingSpotCheckInfo + { + HidingSpot *spot; + float timestamp; + } + m_checkedHidingSpot[ MAX_CHECKED_SPOTS ]; + int m_checkedHidingSpotCount; + + //- view angle mechanism ----------------------------------------------------------------------------------------- + float m_lookPitch; ///< our desired look pitch angle + float m_lookPitchVel; + float m_lookYaw; ///< our desired look yaw angle + float m_lookYawVel; + + //- aim angle mechanism ----------------------------------------------------------------------------------------- + Vector m_aimOffset; ///< current error added to victim's position to get actual aim spot + Vector m_aimOffsetGoal; ///< desired aim offset + float m_aimOffsetTimestamp; ///< time of next offset adjustment + float m_aimSpreadTimestamp; ///< time used to determine max spread as it begins to tighten up + void SetAimOffset( float accuracy ); ///< set the current aim offset + void UpdateAimOffset( void ); ///< wiggle aim error based on m_accuracy + Vector m_aimSpot; ///< the spot we are currently aiming to fire at + + struct PartInfo + { + Vector m_headPos; ///< current head position + Vector m_gutPos; ///< current gut position + Vector m_feetPos; ///< current feet position + Vector m_leftSidePos; ///< current left side position + Vector m_rightSidePos; ///< current right side position + int m_validFrame; ///< frame of last computation (for lazy evaluation) + }; + static PartInfo m_partInfo[ MAX_PLAYERS ]; ///< part positions for each player + void ComputePartPositions( CCSPlayer *player ); ///< compute part positions from bone location + + //- attack state data -------------------------------------------------------------------------------------------- + DispositionType m_disposition; ///< how we will react to enemies + CountdownTimer m_ignoreEnemiesTimer; ///< how long will we ignore enemies + mutable CHandle< CCSPlayer > m_enemy; ///< our current enemy + bool m_isEnemyVisible; ///< result of last visibility test on enemy + unsigned char m_visibleEnemyParts; ///< which parts of the visible enemy do we see + Vector m_lastEnemyPosition; ///< last place we saw the enemy + float m_lastSawEnemyTimestamp; + float m_firstSawEnemyTimestamp; + float m_currentEnemyAcquireTimestamp; + float m_enemyDeathTimestamp; ///< if m_enemy is dead, this is when he died + float m_friendDeathTimestamp; ///< time since we saw a friend die + bool m_isLastEnemyDead; ///< true if we killed or saw our last enemy die + int m_nearbyEnemyCount; ///< max number of enemies we've seen recently + unsigned int m_enemyPlace; ///< the location where we saw most of our enemies + + struct WatchInfo + { + float timestamp; ///< time we last saw this player, zero if never seen + bool isEnemy; + } + m_watchInfo[ MAX_PLAYERS ]; + mutable CHandle< CCSPlayer > m_bomber; ///< points to bomber if we can see him + + int m_nearbyFriendCount; ///< number of nearby teammates + mutable CHandle< CCSPlayer > m_closestVisibleFriend; ///< the closest friend we can see + mutable CHandle< CCSPlayer > m_closestVisibleHumanFriend; ///< the closest human friend we can see + + IntervalTimer m_attentionInterval; ///< time between attention checks + + mutable CHandle< CCSPlayer > m_attacker; ///< last enemy that hurt us (may not be same as m_enemy) + float m_attackedTimestamp; ///< when we were hurt by the m_attacker + + int m_lastVictimID; ///< the entindex of the last victim we killed, or zero + bool m_isAimingAtEnemy; ///< if true, we are trying to aim at our enemy + bool m_isRapidFiring; ///< if true, RunUpkeep() will toggle our primary attack as fast as it can + IntervalTimer m_equipTimer; ///< how long have we had our current weapon equipped + CountdownTimer m_zoomTimer; ///< for delaying firing immediately after zoom + bool DoEquip( CWeaponCSBase *gun ); ///< equip the given item + + void ReloadCheck( void ); ///< reload our weapon if we must + void SilencerCheck( void ); ///< use silencer + + float m_fireWeaponTimestamp; + + bool m_isEnemySniperVisible; ///< do we see an enemy sniper right now + CountdownTimer m_sawEnemySniperTimer; ///< tracking time since saw enemy sniper + + //- reaction time system ----------------------------------------------------------------------------------------- + enum { MAX_ENEMY_QUEUE = 20 }; + struct ReactionState + { + // NOTE: player position & orientation is not currently stored separately + CHandle<CCSPlayer> player; + bool isReloading; + bool isProtectedByShield; + } + m_enemyQueue[ MAX_ENEMY_QUEUE ]; ///< round-robin queue for simulating reaction times + byte m_enemyQueueIndex; + byte m_enemyQueueCount; + byte m_enemyQueueAttendIndex; ///< index of the timeframe we are "conscious" of + + CCSPlayer *FindMostDangerousThreat( void ); ///< return most dangerous threat in my field of view (feeds into reaction time queue) + + + //- stuck detection --------------------------------------------------------------------------------------------- + bool m_isStuck; + float m_stuckTimestamp; ///< time when we got stuck + Vector m_stuckSpot; ///< the location where we became stuck + NavRelativeDirType m_wiggleDirection; + CountdownTimer m_wiggleTimer; + CountdownTimer m_stuckJumpTimer; ///< time for next jump when stuck + + enum { MAX_VEL_SAMPLES = 10 }; + float m_avgVel[ MAX_VEL_SAMPLES ]; + int m_avgVelIndex; + int m_avgVelCount; + Vector m_lastOrigin; + + //- radio -------------------------------------------------------------------------------------------------------- + RadioType m_lastRadioCommand; ///< last radio command we recieved + float m_lastRadioRecievedTimestamp; ///< time we recieved a radio message + float m_lastRadioSentTimestamp; ///< time when we send a radio message + CHandle< CCSPlayer > m_radioSubject; ///< who issued the radio message + Vector m_radioPosition; ///< position referred to in radio message + void RespondToRadioCommands( void ); + bool IsRadioCommand( RadioType event ) const; ///< returns true if the radio message is an order to do something + + /// new-style "voice" chatter gets voice feedback + float m_voiceEndTimestamp; + + BotChatterInterface m_chatter; ///< chatter mechanism +}; + + +// +// Inlines +// + +inline float CCSBot::GetFeetZ( void ) const +{ + return GetAbsOrigin().z; +} + +inline const Vector *CCSBot::GetNoisePosition( void ) const +{ + if (m_noiseTimestamp > 0.0f) + return &m_noisePosition; + + return NULL; +} + +inline bool CCSBot::IsAwareOfEnemyDeath( void ) const +{ + if (GetEnemyDeathTimestamp() == 0.0f) + return false; + + if (m_enemy == NULL) + return true; + + if (!m_enemy->IsAlive() && gpGlobals->curtime - GetEnemyDeathTimestamp() > (1.0f - 0.8f * GetProfile()->GetSkill())) + return true; + + return false; +} + +inline void CCSBot::Panic( void ) +{ + // we are stunned for a moment + Surprise( RandomFloat( 0.2f, 0.3f ) ); + + const float panicTime = 3.0f; + m_panicTimer.Start( panicTime ); + + const float panicRetreatRange = 300.0f; + TryToRetreat( panicRetreatRange, 0.0f ); + + PrintIfWatched( "*** PANIC ***\n" ); +} + +inline bool CCSBot::IsPanicking( void ) const +{ + return !m_panicTimer.IsElapsed(); +} + +inline void CCSBot::StopPanicking( void ) +{ + m_panicTimer.Invalidate(); +} + +inline bool CCSBot::IsNotMoving( float minDuration ) const +{ + return (m_stillTimer.HasStarted() && m_stillTimer.GetElapsedTime() >= minDuration); +} + +inline CWeaponCSBase *CCSBot::GetActiveCSWeapon( void ) const +{ + return reinterpret_cast<CWeaponCSBase *>( GetActiveWeapon() ); +} + + +inline float CCSBot::GetCombatRange( void ) const +{ + return m_combatRange; +} + +inline void CCSBot::SetRogue( bool rogue ) +{ + m_isRogue = rogue; +} + +inline void CCSBot::Hurry( float duration ) +{ + m_hurryTimer.Start( duration ); +} + +inline float CCSBot::GetSafeTime( void ) const +{ + return m_safeTime; +} + +inline bool CCSBot::IsUnhealthy( void ) const +{ + return (GetHealth() <= 40); +} + +inline bool CCSBot::IsAlert( void ) const +{ + return !m_alertTimer.IsElapsed(); +} + +inline void CCSBot::BecomeAlert( void ) +{ + const float alertCooldownTime = 10.0f; + m_alertTimer.Start( alertCooldownTime ); +} + +inline bool CCSBot::IsSneaking( void ) const +{ + return !m_sneakTimer.IsElapsed(); +} + +inline void CCSBot::Sneak( float duration ) +{ + m_sneakTimer.Start( duration ); +} + +inline bool CCSBot::IsFollowing( void ) const +{ + return m_isFollowing; +} + +inline CCSPlayer *CCSBot::GetFollowLeader( void ) const +{ + return m_leader; +} + +inline float CCSBot::GetFollowDuration( void ) const +{ + return gpGlobals->curtime - m_followTimestamp; +} + +inline bool CCSBot::CanAutoFollow( void ) const +{ + return (gpGlobals->curtime > m_allowAutoFollowTime); +} + +inline void CCSBot::AimAtEnemy( void ) +{ + m_isAimingAtEnemy = true; +} + +inline void CCSBot::StopAiming( void ) +{ + m_isAimingAtEnemy = false; +} + +inline bool CCSBot::IsAimingAtEnemy( void ) const +{ + return m_isAimingAtEnemy; +} + +inline float CCSBot::GetStateTimestamp( void ) const +{ + return m_stateTimestamp; +} + +inline CSGameState *CCSBot::GetGameState( void ) +{ + return &m_gameState; +} + +inline const CSGameState *CCSBot::GetGameState( void ) const +{ + return &m_gameState; +} + +inline bool CCSBot::IsAtBombsite( void ) +{ + return m_bInBombZone; +} + +inline void CCSBot::SetTask( TaskType task, CBaseEntity *entity ) +{ + m_task = task; + m_taskEntity = entity; +} + +inline CCSBot::TaskType CCSBot::GetTask( void ) const +{ + return m_task; +} + +inline CBaseEntity *CCSBot::GetTaskEntity( void ) +{ + return static_cast<CBaseEntity *>( m_taskEntity ); +} + +inline CCSBot::MoraleType CCSBot::GetMorale( void ) const +{ + return m_morale; +} + +inline void CCSBot::Surprise( float duration ) +{ + m_surpriseTimer.Start( duration ); +} + +inline bool CCSBot::IsSurprised( void ) const +{ + return !m_surpriseTimer.IsElapsed(); +} + +inline CNavArea *CCSBot::GetNoiseArea( void ) const +{ + return m_noiseArea; +} + +inline void CCSBot::ForgetNoise( void ) +{ + m_noiseTimestamp = 0.0f; +} + +inline float CCSBot::GetNoiseRange( void ) const +{ + if (IsNoiseHeard()) + return m_noiseTravelDistance; + + return 999999999.9f; +} + +inline PriorityType CCSBot::GetNoisePriority( void ) const +{ + return m_noisePriority; +} + +inline BotChatterInterface *CCSBot::GetChatter( void ) +{ + return &m_chatter; +} + +inline CCSPlayer *CCSBot::GetBotEnemy( void ) const +{ + return m_enemy; +} + +inline int CCSBot::GetNearbyEnemyCount( void ) const +{ + return MIN( GetEnemiesRemaining(), m_nearbyEnemyCount ); +} + +inline unsigned int CCSBot::GetEnemyPlace( void ) const +{ + return m_enemyPlace; +} + +inline bool CCSBot::CanSeeBomber( void ) const +{ + return (m_bomber == NULL) ? false : true; +} + +inline CCSPlayer *CCSBot::GetBomber( void ) const +{ + return m_bomber; +} + +inline int CCSBot::GetNearbyFriendCount( void ) const +{ + return MIN( GetFriendsRemaining(), m_nearbyFriendCount ); +} + +inline CCSPlayer *CCSBot::GetClosestVisibleFriend( void ) const +{ + return m_closestVisibleFriend; +} + +inline CCSPlayer *CCSBot::GetClosestVisibleHumanFriend( void ) const +{ + return m_closestVisibleHumanFriend; +} + +inline float CCSBot::GetTimeSinceAttacked( void ) const +{ + return gpGlobals->curtime - m_attackedTimestamp; +} + +inline float CCSBot::GetFirstSawEnemyTimestamp( void ) const +{ + return m_firstSawEnemyTimestamp; +} + +inline float CCSBot::GetLastSawEnemyTimestamp( void ) const +{ + return m_lastSawEnemyTimestamp; +} + +inline float CCSBot::GetTimeSinceLastSawEnemy( void ) const +{ + return gpGlobals->curtime - m_lastSawEnemyTimestamp; +} + +inline float CCSBot::GetTimeSinceAcquiredCurrentEnemy( void ) const +{ + return gpGlobals->curtime - m_currentEnemyAcquireTimestamp; +} + +inline const Vector &CCSBot::GetLastKnownEnemyPosition( void ) const +{ + return m_lastEnemyPosition; +} + +inline bool CCSBot::IsEnemyVisible( void ) const +{ + return m_isEnemyVisible; +} + +inline float CCSBot::GetEnemyDeathTimestamp( void ) const +{ + return m_enemyDeathTimestamp; +} + +inline int CCSBot::GetLastVictimID( void ) const +{ + return m_lastVictimID; +} + +inline bool CCSBot::CanSeeSniper( void ) const +{ + return m_isEnemySniperVisible; +} + +inline bool CCSBot::HasSeenSniperRecently( void ) const +{ + return !m_sawEnemySniperTimer.IsElapsed(); +} + +inline float CCSBot::GetTravelDistanceToPlayer( CCSPlayer *player ) const +{ + if (player == NULL) + return -1.0f; + + if (!player->IsAlive()) + return -1.0f; + + return m_playerTravelDistance[ player->entindex() % MAX_PLAYERS ]; +} + +inline bool CCSBot::HasPath( void ) const +{ + return (m_pathLength) ? true : false; +} + +inline void CCSBot::DestroyPath( void ) +{ + m_isStopping = false; + m_pathLength = 0; + m_pathLadder = NULL; +} + +inline CNavArea *CCSBot::GetLastKnownArea( void ) const +{ + return m_lastKnownArea; +} + +inline const Vector &CCSBot::GetPathEndpoint( void ) const +{ + return m_path[ m_pathLength-1 ].pos; +} + +inline const Vector &CCSBot::GetPathPosition( int index ) const +{ + return m_path[ index ].pos; +} + +inline bool CCSBot::IsUsingLadder( void ) const +{ + return (m_pathLadder) ? true : false; +} + +inline void CCSBot::SetGoalEntity( CBaseEntity *entity ) +{ + m_goalEntity = entity; +} + +inline CBaseEntity *CCSBot::GetGoalEntity( void ) +{ + return m_goalEntity; +} + +inline void CCSBot::ForceRun( float duration ) +{ + Run(); + m_mustRunTimer.Start( duration ); +} + +inline void CCSBot::Wait( float duration ) +{ + m_waitTimer.Start( duration ); +} + +inline bool CCSBot::IsWaiting( void ) const +{ + return !m_waitTimer.IsElapsed(); +} + +inline void CCSBot::StopWaiting( void ) +{ + m_waitTimer.Invalidate(); +} + +inline bool CCSBot::HasVisitedEnemySpawn( void ) const +{ + return m_hasVisitedEnemySpawn; +} + +inline const Vector &CCSBot::EyePositionConst( void ) const +{ + return m_eyePosition; +} + +inline void CCSBot::SetLookAngles( float yaw, float pitch ) +{ + m_lookYaw = yaw; + m_lookPitch = pitch; +} + +inline void CCSBot::SetForwardAngle( float angle ) +{ + m_forwardAngle = angle; +} + +inline void CCSBot::SetLookAheadAngle( float angle ) +{ + m_lookAheadAngle = angle; +} + +inline void CCSBot::ClearLookAt( void ) +{ + //PrintIfWatched( "ClearLookAt()\n" ); + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + m_lookAtDesc = NULL; +} + +inline bool CCSBot::IsLookingAtSpot( PriorityType pri ) const +{ + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && m_lookAtSpotPriority >= pri) + return true; + + return false; +} + +inline bool CCSBot::IsViewMoving( float angleVelThreshold ) const +{ + if (m_lookYawVel < angleVelThreshold && m_lookYawVel > -angleVelThreshold && + m_lookPitchVel < angleVelThreshold && m_lookPitchVel > -angleVelThreshold) + { + return false; + } + + return true; +} + +inline bool CCSBot::HasViewBeenSteady( float duration ) const +{ + return (m_viewSteadyTimer.GetElapsedTime() > duration); +} + +inline bool CCSBot::HasLookAtTarget( void ) const +{ + return (m_lookAtSpotState != NOT_LOOKING_AT_SPOT); +} + +inline bool CCSBot::IsEnemyPartVisible( VisiblePartType part ) const +{ + VPROF_BUDGET( "CCSBot::IsEnemyPartVisible", VPROF_BUDGETGROUP_NPCS ); + + if (!IsEnemyVisible()) + return false; + + return (m_visibleEnemyParts & part) ? true : false; +} + +inline bool CCSBot::IsSignificantlyCloser( const CCSPlayer *testPlayer, const CCSPlayer *referencePlayer ) const +{ + if ( !referencePlayer ) + return true; + + if ( !testPlayer ) + return false; + + float testDist = ( GetAbsOrigin() - testPlayer->GetAbsOrigin() ).Length(); + float referenceDist = ( GetAbsOrigin() - referencePlayer->GetAbsOrigin() ).Length(); + + const float significantRangeFraction = 0.7f; + if ( testDist < referenceDist * significantRangeFraction ) + return true; + + return false; +} + +inline void CCSBot::ClearApproachPoints( void ) +{ + m_approachPointCount = 0; +} + +inline const CNavArea *CCSBot::GetInitialEncounterArea( void ) const +{ + return m_initialEncounterArea; +} + +inline void CCSBot::SetInitialEncounterArea( const CNavArea *area ) +{ + m_initialEncounterArea = area; +} + +inline bool CCSBot::IsThrowingGrenade( void ) const +{ + return m_grenadeTossState != NOT_THROWING; +} + +inline bool CCSBot::IsAvoidingGrenade( void ) const +{ + return !m_isAvoidingGrenade.IsElapsed(); +} + +inline void CCSBot::PrimaryAttack( void ) +{ + if ( IsUsingPistol() && !CanActiveWeaponFire() ) + return; + + BaseClass::PrimaryAttack(); +} + +inline CCSBot::ZoomType CCSBot::GetZoomLevel( void ) +{ + if (GetFOV() > 60.0f) + return NO_ZOOM; + if (GetFOV() > 25.0f) + return LOW_ZOOM; + return HIGH_ZOOM; +} + +inline bool CCSBot::IsWaitingForZoom( void ) const +{ + return !m_zoomTimer.IsElapsed(); +} + +inline int CCSBot::GetHostageEscortCount( void ) const +{ + return m_hostageEscortCount; +} + +inline void CCSBot::IncreaseHostageEscortCount( void ) +{ + ++m_hostageEscortCount; +} + +inline void CCSBot::ResetWaitForHostagePatience( void ) +{ + m_isWaitingForHostage = false; + m_inhibitWaitingForHostageTimer.Invalidate(); +} + + +inline bool CCSBot::IsUsingVoice() const +{ + return m_voiceEndTimestamp > gpGlobals->curtime; +} + +inline bool CCSBot::IsOpeningDoor( void ) const +{ + return m_isOpeningDoor; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the given weapon is a sniper rifle + */ +inline bool IsSniperRifle( CWeaponCSBase *weapon ) +{ + if (weapon == NULL) + return false; + + return weapon->IsKindOf(WEAPONTYPE_SNIPER_RIFLE); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor used with NavAreaBuildPath() + */ +class PathCost +{ +public: + PathCost( CCSBot *bot, RouteType route = SAFEST_ROUTE ) + { + m_bot = bot; + m_route = route; + } + + // HPE_TODO[pmf]: check that these new parameters are okay to be ignored + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) + { + float baseDangerFactor = 100.0f; // 100 + + // respond to the danger modulated by our aggression (even super-aggressives pay SOME attention to danger) + float dangerFactor = (1.0f - (0.95f * m_bot->GetProfile()->GetAggression())) * baseDangerFactor; + + if (fromArea == NULL) + { + if (m_route == FASTEST_ROUTE) + return 0.0f; + + // first area in path, cost is just danger + return dangerFactor * area->GetDanger( m_bot->GetTeamNumber() ); + } + else if ((fromArea->GetAttributes() & NAV_MESH_JUMP) && (area->GetAttributes() & NAV_MESH_JUMP)) + { + // cannot actually walk in jump areas - disallow moving from jump area to jump area + return -1.0f; + } + if ( area->GetAttributes() & NAV_MESH_NO_HOSTAGES && m_bot->GetHostageEscortCount() ) + { + // if we're leading hostages, don't try to go where they can't + return -1.0f; + } + else + { + // compute distance from previous area to this area + float dist; + if (ladder) + { + // ladders are slow to use + const float ladderPenalty = 1.0f; // 3.0f; + dist = ladderPenalty * ladder->m_length; + + // if we are currently escorting hostages, avoid ladders (hostages are confused by them) + //if (m_bot->GetHostageEscortCount()) + // dist *= 100.0f; + } + else + { + dist = (area->GetCenter() - fromArea->GetCenter()).Length(); + } + + // compute distance travelled along path so far + float cost = dist + fromArea->GetCostSoFar(); + + // zombies ignore all path penalties + if (cv_bot_zombie.GetBool()) + return cost; + + // add cost of "jump down" pain unless we're jumping into water + if (!area->IsUnderwater() && area->IsConnected( fromArea, NUM_DIRECTIONS ) == false) + { + // this is a "jump down" (one way drop) transition - estimate damage we will take to traverse it + float fallDistance = -fromArea->ComputeGroundHeightChange( area ); + + // if it's a drop-down ladder, estimate height from the bottom of the ladder to the lower area + if ( ladder && ladder->m_bottom.z < fromArea->GetCenter().z && ladder->m_bottom.z > area->GetCenter().z ) + { + fallDistance = ladder->m_bottom.z - area->GetCenter().z; + } + + float fallDamage = m_bot->GetApproximateFallDamage( fallDistance ); + + if (fallDamage > 0.0f) + { + // if the fall would kill us, don't use it + const float deathFallMargin = 10.0f; + if (fallDamage + deathFallMargin >= m_bot->GetHealth()) + return -1.0f; + + // if we need to get there in a hurry, ignore minor pain + const float painTolerance = 15.0f * m_bot->GetProfile()->GetAggression() + 10.0f; + if (m_route != FASTEST_ROUTE || fallDamage > painTolerance) + { + // cost is proportional to how much it hurts when we fall + // 10 points - not a big deal, 50 points - ouch! + cost += 100.0f * fallDamage * fallDamage; + } + } + } + + // if this is a "crouch" or "walk" area, add penalty + if (area->GetAttributes() & (NAV_MESH_CROUCH | NAV_MESH_WALK)) + { + // these areas are very slow to move through + float penalty = (m_route == FASTEST_ROUTE) ? 20.0f : 5.0f; + + // avoid crouch areas if we are rescuing hostages + if ((area->GetAttributes() & NAV_MESH_CROUCH) && m_bot->GetHostageEscortCount()) + { + penalty *= 3.0f; + } + + cost += penalty * dist; + } + + // if this is a "jump" area, add penalty + if (area->GetAttributes() & NAV_MESH_JUMP) + { + // jumping can slow you down + //const float jumpPenalty = (m_route == FASTEST_ROUTE) ? 100.0f : 0.5f; + const float jumpPenalty = 1.0f; + cost += jumpPenalty * dist; + } + + // if this is an area to avoid, add penalty + if (area->GetAttributes() & NAV_MESH_AVOID) + { + const float avoidPenalty = 20.0f; + cost += avoidPenalty * dist; + } + + if (m_route == SAFEST_ROUTE) + { + // add in the danger of this path - danger is per unit length travelled + cost += dist * dangerFactor * area->GetDanger( m_bot->GetTeamNumber() ); + } + + if (!m_bot->IsAttacking()) + { + // add in cost of teammates in the way + + // approximate density of teammates based on area + float size = (area->GetSizeX() + area->GetSizeY())/2.0f; + + // degenerate check + if (size >= 1.0f) + { + // cost is proportional to the density of teammates in this area + const float costPerFriendPerUnit = 50000.0f; + cost += costPerFriendPerUnit * (float)area->GetPlayerCount( m_bot->GetTeamNumber() ) / size; + } + } + + return cost; + } + } + +private: + CCSBot *m_bot; + RouteType m_route; +}; + + +//-------------------------------------------------------------------------------------------------------------- +// +// Prototypes +// +extern int GetBotFollowCount( CCSPlayer *leader ); +extern const Vector *FindNearbyRetreatSpot( CCSBot *me, float maxRange = 250.0f ); +extern const HidingSpot *FindInitialEncounterSpot( CBaseEntity *me, const Vector &searchOrigin, float enemyArriveTime, float maxRange, bool isSniper ); + + +#endif // _CS_BOT_H_ + diff --git a/game/server/cstrike/bot/cs_bot_chatter.cpp b/game/server/cstrike/bot/cs_bot_chatter.cpp new file mode 100644 index 0000000..4d8b4dc --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_chatter.cpp @@ -0,0 +1,2582 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_player.h" +#include "shared_util.h" +#include "engine/IEngineSound.h" +#include "KeyValues.h" + +#include "bot.h" +#include "bot_util.h" +#include "cs_bot.h" +#include "cs_bot_chatter.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +/** + * @todo Fix this + */ +const Vector *GetRandomSpotAtPlace( Place place ) +{ + int count = 0; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if (area->GetPlace() == place) + ++count; + } + + if (count == 0) + return NULL; + + int which = RandomInt( 0, count-1 ); + + FOR_EACH_VEC( TheNavAreas, rit ) + { + CNavArea *area = TheNavAreas[ rit ]; + + if (area->GetPlace() == place && which == 0) + return &area->GetCenter(); + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Transmit meme to other bots + */ +void BotMeme::Transmit( CCSBot *sender ) const +{ + for( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + +// if (FNullEnt( player->pev )) +// continue; + +// if (FStrEq( STRING( player->pev->netname ), "" )) +// continue; + + // skip self + if (sender == player) + continue; + + // ignore dead humans + if (!player->IsBot() && !player->IsAlive()) + continue; + + // ignore enemies, since we can't hear them talk + if (!player->InSameTeam( sender )) + continue; + + // if not a bot, fail the test + if (!player->IsBot()) + continue; + + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + + if ( !bot ) + continue; + + // allow bot to interpret our meme + Interpret( sender, bot ); + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate called for help - respond + */ +void BotHelpMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + const float maxHelpRange = 3000.0f; // 2000 + receiver->RespondToHelpRequest( sender, m_place, maxHelpRange ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate reported information about a bombsite + */ +void BotBombsiteStatusMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + // remember this bombsite's status + if (m_status == CLEAR) + receiver->GetGameState()->ClearBombsite( m_zoneIndex ); + else + receiver->GetGameState()->MarkBombsiteAsPlanted( m_zoneIndex ); + + // if we were heading to the just-cleared bombsite, pick another one to search + // if our target bombsite wasn't cleared, will will continue going to it, + // because GetNextBombsiteToSearch() will return the same zone (since its not cleared) + // if the bomb was planted, we will head to that bombsite + if (receiver->GetTask() == CCSBot::FIND_TICKING_BOMB) + { + receiver->Idle(); + receiver->GetChatter()->Affirmative(); + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate reported information about the bomb + */ +void BotBombStatusMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + // update our gamestate based on teammate's report + switch( m_state ) + { + case CSGameState::MOVING: + receiver->GetGameState()->UpdateBomber( m_pos ); + + // if we are hunting and see no enemies, respond + if (!receiver->IsRogue() && receiver->IsHunting() && receiver->GetNearbyEnemyCount() == 0) + receiver->RespondToHelpRequest( sender, TheNavMesh->GetPlace( m_pos ) ); + + break; + + case CSGameState::LOOSE: + receiver->GetGameState()->UpdateLooseBomb( m_pos ); + + if (receiver->GetTask() == CCSBot::GUARD_BOMB_ZONE) + { + receiver->Idle(); + receiver->GetChatter()->Affirmative(); + } + break; + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked that we follow him + */ +void BotFollowMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + if (receiver->IsRogue()) + return; + + // if we're busy, ignore + if (receiver->IsBusy()) + return; + + // if we are too far away, ignore + // compute actual travel distance + Vector senderOrigin = GetCentroid( sender ); + PathCost cost( receiver ); + float travelDistance = NavAreaTravelDistance( receiver->GetLastKnownArea(), + TheNavMesh->GetNearestNavArea( senderOrigin ), + cost ); + if (travelDistance < 0.0f) + return; + + const float tooFar = 1000.0f; + if (travelDistance > tooFar) + return; + + // begin following + receiver->Follow( sender ); + + // acknowledge + receiver->GetChatter()->Say( "CoveringFriend" ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked us to defend a place + */ +void BotDefendHereMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + if (receiver->IsRogue()) + return; + + // if we're busy, ignore + if (receiver->IsBusy()) + return; + + Place place = TheNavMesh->GetPlace( m_pos ); + if (place != UNDEFINED_PLACE) + { + // pick a random hiding spot in this place + const Vector *spot = FindRandomHidingSpot( receiver, place, receiver->IsSniper() ); + if (spot) + { + receiver->SetTask( CCSBot::HOLD_POSITION ); + receiver->Hide( *spot ); + return; + } + } + + // hide nearby + receiver->SetTask( CCSBot::HOLD_POSITION ); + receiver->Hide( TheNavMesh->GetNearestNavArea( m_pos ) ); + + // acknowledge + receiver->GetChatter()->Say( "Affirmative" ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked where the bomb is planted + */ +void BotWhereBombMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + int zone = receiver->GetGameState()->GetPlantedBombsite(); + + if (zone != CSGameState::UNKNOWN) + receiver->GetChatter()->FoundPlantedBomb( zone ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked us to report in + */ +void BotRequestReportMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetChatter()->ReportingIn(); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate told us all the hostages are gone + */ +void BotAllHostagesGoneMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetGameState()->AllHostagesGone(); + + // acknowledge + receiver->GetChatter()->Say( "Affirmative" ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate told us a CT is talking to a hostage + */ +void BotHostageBeingTakenMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetGameState()->HostageWasTaken(); + + // if we're busy, ignore + if (receiver->IsBusy()) + return; + + receiver->Idle(); + + // acknowledge + receiver->GetChatter()->Say( "Affirmative" ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate heard a noise, so we shouldn't report noises for a while + */ +void BotHeardNoiseMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetChatter()->FriendHeardNoise(); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate warned about snipers, so we shouldn't warn again for awhile + */ +void BotWarnSniperMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetChatter()->FriendSpottedSniper(); +} + + +//--------------------------------------------------------------------------------------------------------------- +BotSpeakable::BotSpeakable() +{ + m_phrase = NULL; +} + +//--------------------------------------------------------------------------------------------------------------- +BotSpeakable::~BotSpeakable() +{ + if ( m_phrase ) + { + delete[] m_phrase; + m_phrase = NULL; + } +} + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +BotPhrase::BotPhrase( bool isPlace ) +{ + m_name = NULL; + m_place = UNDEFINED_PLACE; + m_isPlace = isPlace; + m_radioEvent = RADIO_INVALID; + m_isImportant = false; + ClearCriteria(); + m_numVoiceBanks = 0; + InitVoiceBank( 0 ); +} + +BotPhrase::~BotPhrase() +{ + for( int bank=0; bank<m_voiceBank.Count(); ++bank ) + { + for( int speakable=0; speakable<m_voiceBank[bank]->Count(); ++speakable ) + { + delete (*m_voiceBank[bank])[speakable]; + } + delete m_voiceBank[bank]; + } + + if ( m_name ) + delete [] m_name; +} + +void BotPhrase::InitVoiceBank( int bankIndex ) +{ + while ( m_numVoiceBanks <= bankIndex ) + { + m_count.AddToTail(0); + m_index.AddToTail(0); + m_voiceBank.AddToTail( new BotSpeakableVector ); + ++m_numVoiceBanks; + } +} + +/** + * Return a random speakable - avoid repeating + */ +char *BotPhrase::GetSpeakable( int bankIndex, float *duration ) const +{ + if (bankIndex < 0 || bankIndex >= m_numVoiceBanks || m_count[bankIndex] == 0) + { + if (duration) + *duration = 0.0f; + + return NULL; + } + + // find phrase that meets the current criteria + int start = m_index[bankIndex]; + while(true) + { + BotSpeakableVector *speakables = m_voiceBank[bankIndex]; + int& index = m_index[bankIndex]; + const BotSpeakable *speak = (*speakables)[index++]; + + if (m_index[bankIndex] >= m_count[bankIndex]) + m_index[bankIndex] = 0; + + // check place criteria + // if this speakable has a place criteria, it must match to be used + // speakables with Place of ANY will match any place + // speakables with a specific Place will only be used if Place matches + // speakables with Place of UNDEFINED only match Place of UNDEFINED + if (speak->m_place == ANY_PLACE || speak->m_place == m_placeCriteria) + { + // check count criteria + // if this speakable has a count criteria, it must match to be used + // if this speakable does not have a count criteria, we dont care what the count is set to + if (speak->m_count == UNDEFINED_COUNT || speak->m_count == MIN( m_countCriteria, COUNT_MANY )) + { + if (duration) + *duration = speak->m_duration; + + return speak->m_phrase; + } + } + + // check if we exhausted all speakables + if (m_index[bankIndex] == start) + { + if (duration) + *duration = 0.0f; + + return NULL; + } + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Randomly shuffle the speakable order + */ +void BotPhrase::Randomize( void ) +{ + for ( int bank = 0; bank < m_voiceBank.Count(); ++bank ) + { + BotSpeakableVector *speakables = m_voiceBank[bank]; + if ( speakables->Count() == 1 ) + continue; + + // A simple shuffle: for each array index, swap it with a random index + for ( int index = 0; index < speakables->Count(); ++index ) + { + int newIndex = RandomInt( 0, speakables->Count()-1 ); + + BotSpeakable *speakable = (*speakables)[index]; + (*speakables)[index] = (*speakables)[newIndex]; + (*speakables)[newIndex] = speakable; + } + } +} + + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +BotPhraseManager *TheBotPhrases = NULL; + +BotPhraseManager::BotPhraseManager( void ) +{ + m_placeCount = 0; +} + + +/** + * Invoked when map changes + */ +void BotPhraseManager::OnMapChange( void ) +{ + m_placeCount = 0; +} + +/** + * Removes everything from memory + */ +void BotPhraseManager::Reset( void ) +{ + int i; + + // free phrase resources + for( i=0; i<m_list.Count(); ++i ) + { + delete m_list[i]; + } + + for( i=0; i<m_placeList.Count(); ++i ) + { + delete m_placeList[i]; + } + + m_list.RemoveAll(); + m_placeList.RemoveAll(); + + m_painPhrase = NULL; + m_agreeWithPlanPhrase = NULL; +} + + +/** + * Invoked when the round resets + */ +void BotPhraseManager::OnRoundRestart( void ) +{ + // effectively reset all interval timers + m_placeCount = 0; + + // shuffle all the speakables + int i; + for( i=0; i<m_placeList.Count(); ++i ) + m_placeList[i]->Randomize(); + + for( i=0; i<m_list.Count(); ++i ) + m_list[i]->Randomize(); +} + +BotChatterOutputType BotPhraseManager::GetOutputType( int voiceBank ) const +{ + if ( voiceBank >= 0 && voiceBank < m_output.Count() ) + { + return m_output[voiceBank]; + } + return BOT_CHATTER_RADIO; +} + +/** + * Initialize phrase system from database file + */ +bool BotPhraseManager::Initialize( const char *filename, int bankIndex ) +{ + bool isDefault = (bankIndex == 0); + + FileHandle_t file = filesystem->Open( filename, "r" ); + if (!file) + { + CONSOLE_ECHO( "WARNING: Cannot access bot phrase database '%s'\n", filename ); + return false; + } + + // BOTPORT: Redo file reading to avoid loading whole file into memory at once + int phraseDataLength = filesystem->Size( filename ); + char *phraseDataFile = new char[ phraseDataLength ]; + + int dataReadLength = filesystem->Read( phraseDataFile, phraseDataLength, 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. + phraseDataFile[ dataReadLength - 1 ] = 0; + } + + const char *phraseData = phraseDataFile; + + + const int RadioPathLen = 128; // wav filenames need to be shorter than this to go over the net anyway. + char baseDir[RadioPathLen] = ""; + char compositeFilename[RadioPathLen]; + + // + // Parse the BotChatter.db into BotPhrase collections + // + while( true ) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + break; + + char *token = SharedGetToken(); + + if ( !stricmp( token, "Output" ) ) + { + // get name of this output device + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename ); + delete [] phraseDataFile; + return false; + } + + while ( m_output.Count() <= bankIndex ) + { + m_output.AddToTail(BOT_CHATTER_RADIO); + } + + char *token = SharedGetToken(); + if ( !stricmp( token, "Voice" ) ) + { + m_output[bankIndex] = BOT_CHATTER_VOICE; + } + } + else if ( !stricmp( token, "BaseDir" ) ) + { + // get name of this output device + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename ); + delete [] phraseDataFile; + return false; + } + char *token = SharedGetToken(); + Q_strncpy( baseDir, token, RadioPathLen ); + Q_strncat( baseDir, "\\", RadioPathLen, -1 ); + baseDir[RadioPathLen-1] = 0; + } + else if (!stricmp( token, "Place" ) || !stricmp( token, "Chatter" )) + { + bool isPlace = (stricmp( token, "Place" )) ? false : true; + + // encountered a new phrase collection + BotPhrase *phrase = NULL; + if ( isDefault ) + { + phrase = new BotPhrase( isPlace ); + } + + // get name of this phrase + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename ); + delete [] phraseDataFile; + return false; + } + if ( isDefault ) + { + phrase->m_name = CloneString( SharedGetToken() ); + + phrase->m_place = (isPlace) ? TheNavMesh->NameToPlace( phrase->m_name ) : UNDEFINED_PLACE; + } + else // look up the existing phrase + { + if ( isPlace ) + { + phrase = const_cast<BotPhrase *>(GetPlace( SharedGetToken() )); + } + else + { + phrase = const_cast<BotPhrase *>(GetPhrase( SharedGetToken() )); + } + + if ( !phrase ) + { + CONSOLE_ECHO( "Error parsing '%s' - phrase '%s' is invalid\n", filename, SharedGetToken() ); + delete [] phraseDataFile; + return false; + } + } + phrase->InitVoiceBank( bankIndex ); + + PlaceCriteria placeCriteria = ANY_PLACE; + CountCriteria countCriteria = UNDEFINED_COUNT; + RadioType radioEvent = RADIO_INVALID; + bool isImportant = false; + + // read attributes of this phrase + while( true ) + { + // get next token + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + // check for Place criteria + if (!stricmp( token, "Place" )) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected Place name\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + // update place criteria for subsequent speak lines + // NOTE: this assumes places must be first in the chatter database + + // check for special identifiers + if (!stricmp( "ANY", token )) + placeCriteria = ANY_PLACE; + else if (!stricmp( "UNDEFINED", token )) + placeCriteria = UNDEFINED_PLACE; + else + placeCriteria = TheNavMesh->NameToPlace( token ); + + continue; + } + + // check for Count criteria + if (!stricmp( token, "Count" )) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected Count value\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + // update count criteria for subsequent speak lines + if (!stricmp( token, "Many" )) + countCriteria = COUNT_MANY; + else + countCriteria = atoi( token ); + + continue; + } + + // check for radio equivalent + if (!stricmp( token, "Radio" )) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected radio event\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + RadioType event = NameToRadioEvent( token ); + if (event <= RADIO_START_1 || event >= RADIO_END) + { + CONSOLE_ECHO( "Error parsing %s - invalid radio event '%s'\n", filename, token ); + delete [] phraseDataFile; + return false; + } + + radioEvent = event; + + continue; + } + + // check for "important" flag + if (!stricmp( token, "Important" )) + { + isImportant = true; + continue; + } + + // check for End delimiter + if (!stricmp( token, "End" )) + break; + + // found a phrase - add it to the collection + BotSpeakable *speak = new BotSpeakable; + if ( baseDir[0] ) + { + Q_snprintf( compositeFilename, RadioPathLen, "%s%s", baseDir, token ); + speak->m_phrase = CloneString( compositeFilename ); + } + else + { + speak->m_phrase = CloneString( token ); + } + speak->m_place = placeCriteria; + speak->m_count = countCriteria; +#ifdef POSIX + Q_FixSlashes( speak->m_phrase ); + Q_strlower( speak->m_phrase ); +#endif + + speak->m_duration = enginesound->GetSoundDuration( speak->m_phrase ); + + if (speak->m_duration <= 0.0f) + { + if ( !engine->IsDedicatedServer() ) + { + DevMsg( "Warning: Couldn't get duration of phrase '%s'\n", speak->m_phrase ); + } + speak->m_duration = 1.0f; + } + + BotSpeakableVector * speakables = phrase->m_voiceBank[ bankIndex ]; + speakables->AddToTail( speak ); + + ++phrase->m_count[ bankIndex ]; + } + + if ( isDefault ) + { + phrase->m_radioEvent = radioEvent; + phrase->m_isImportant = isImportant; + } + + // add phrase collection to the appropriate master list + if (isPlace) + m_placeList.AddToTail( phrase ); + else + m_list.AddToTail( phrase ); + } + } + + delete [] phraseDataFile; + + m_painPhrase = GetPhrase( "Pain" ); + m_agreeWithPlanPhrase = GetPhrase( "AgreeWithPlan" ); + + return true; +} + +BotPhraseManager::~BotPhraseManager() +{ + Reset(); +} + +/** + * Given a name, return the associated phrase collection + */ +const BotPhrase *BotPhraseManager::GetPhrase( const char *name ) const +{ + for( int i=0; i<m_list.Count(); ++i ) + { + if (!stricmp( m_list[i]->m_name, name )) + return m_list[i]; + } + + //CONSOLE_ECHO( "GetPhrase: ERROR - Invalid phrase '%s'\n", name ); + return NULL; +} + +/** + * Given an id, return the associated phrase collection + * @todo Store phrases in a vector to make this fast + */ +/* +const BotPhrase *BotPhraseManager::GetPhrase( unsigned int place ) const +{ + for( BotPhraseList::const_iterator iter = m_list.begin(); iter != m_list.end(); ++iter ) + { + const BotPhrase *phrase = *iter; + if (phrase->m_place == id) + return phrase; + } + + CONSOLE_ECHO( "GetPhrase: ERROR - Invalid phrase id #%d\n", id ); + return NULL; +} +*/ + +/** + * Given a name, return the associated Place phrase collection + */ +const BotPhrase *BotPhraseManager::GetPlace( const char *name ) const +{ + if (name == NULL) + return NULL; + + for( int i=0; i<m_placeList.Count(); ++i ) + { + if (!stricmp( m_placeList[i]->m_name, name )) + return m_placeList[i]; + } + + return NULL; +} + +/** + * Given a name, return the associated Place phrase collection + */ +const BotPhrase *BotPhraseManager::GetPlace( PlaceCriteria place ) const +{ + if (place == UNDEFINED_PLACE) + return NULL; + + for( int i=0; i<m_placeList.Count(); ++i ) + { + if (m_placeList[i]->m_place == place) + return m_placeList[i]; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +BotStatement::BotStatement( BotChatterInterface *chatter, BotStatementType type, float expireDuration ) +{ + m_chatter = chatter; + + m_next = NULL; + m_prev = NULL; + m_timestamp = gpGlobals->curtime; + m_speakTimestamp = 0.0f; + + m_type = type; + m_subject = UNDEFINED_SUBJECT; + m_place = UNDEFINED_PLACE; + m_meme = NULL; + + m_startTime = gpGlobals->curtime; + m_expireTime = gpGlobals->curtime + expireDuration; + m_isSpeaking = false; + + m_nextTime = 0.0f; + m_index = -1; + m_count = 0; + + m_conditionCount = 0; +} + +BotStatement::~BotStatement() +{ + if (m_meme) + delete m_meme; +} + + +//--------------------------------------------------------------------------------------------------------------- +CCSBot *BotStatement::GetOwner( void ) const +{ + return m_chatter->GetOwner(); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Attach a meme to this statement, to be transmitted to other friendly bots when spoken + */ +void BotStatement::AttachMeme( BotMeme *meme ) +{ + m_meme = meme; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Add a conditions that must be true for the statement to be spoken + */ +void BotStatement::AddCondition( ConditionType condition ) +{ + if (m_conditionCount < MAX_BOT_CONDITIONS) + m_condition[ m_conditionCount++ ] = condition; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Return true if this statement is "important" and not personality chatter + */ +bool BotStatement::IsImportant( void ) const +{ + // if a statement contains any important phrases, it is important + for( int i=0; i<m_count; ++i ) + { + if (m_statement[i].isPhrase && m_statement[i].phrase->IsImportant()) + return true; + + // hack for now - phrases with enemy counts are important + if (!m_statement[i].isPhrase && m_statement[i].context == BotStatement::CURRENT_ENEMY_COUNT) + return true; + } + + return false; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Verify all attached conditions + */ +bool BotStatement::IsValid( void ) const +{ + for( int i=0; i<m_conditionCount; ++i ) + { + switch( m_condition[i] ) + { + case IS_IN_COMBAT: + { + if (!GetOwner()->IsAttacking()) + return false; + break; + } + +/* + case RADIO_SILENCE: + { + if (GetOwner()->GetChatter()->GetRadioSilenceDuration() < 10.0f) + return false; + break; + } +*/ + + case ENEMIES_REMAINING: + { + if (GetOwner()->GetEnemiesRemaining() == 0) + return false; + break; + } + } + } + + return true; +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Return true if this statement is essentially the same as the given one + */ +bool BotStatement::IsRedundant( const BotStatement *say ) const +{ + // special cases + if (GetType() == REPORT_MY_PLAN || + GetType() == REPORT_REQUEST_HELP || + GetType() == REPORT_CRITICAL_EVENT || + GetType() == REPORT_ACKNOWLEDGE) + return false; + + // check if topics are different + if (say->GetType() != GetType()) + return false; + + if (!say->HasPlace() && !HasPlace() && !say->HasSubject() && !HasSubject()) + { + // neither has place or subject, so they are the same + return true; + } + + // check if subject matter is the same + if (say->HasPlace() && HasPlace() && say->GetPlace() == GetPlace()) + { + // talking about the same place + return true; + } + + if (say->HasSubject() && HasSubject() && say->GetSubject() == GetSubject()) + { + // talking about the same player + return true; + } + + return false; +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Return true if this statement is no longer appropriate to say + */ +bool BotStatement::IsObsolete( void ) const +{ + // if the round is over, the only things we should say are emotes + if (GetOwner()->GetGameState()->IsRoundOver()) + { + if (m_type != REPORT_EMOTE) + return true; + } + + // If we're wanting to say "I lost him" but we've spotted another enemy, + // we no longer need to report losing someone. + if ( GetOwner()->GetChatter()->SeesAtLeastOneEnemy() && m_type == REPORT_ENEMY_LOST ) + { + return true; + } + + // check if statement lifetime has expired + return (gpGlobals->curtime > m_expireTime); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Possibly change what were going to say base on what teammate is saying + */ +void BotStatement::Convert( const BotStatement *say ) +{ + if (GetType() == REPORT_MY_PLAN && say->GetType() == REPORT_MY_PLAN) + { + const BotPhrase *meToo = TheBotPhrases->GetAgreeWithPlanPhrase(); + + // don't reconvert + if (m_statement[0].phrase == meToo) + return; + + // if our plans are the same, change our statement to "me too" + if (m_statement[0].phrase == say->m_statement[0].phrase) + { + if (m_place == say->m_place) + { + // same plan at the same place - convert to "me too" + m_statement[0].phrase = meToo; + m_startTime = gpGlobals->curtime + RandomFloat( 0.5f, 1.0f ); + } + else + { + // same plan at different place - wait a bit to allow others to respond "me too" + m_startTime = gpGlobals->curtime + RandomFloat( 3.0f, 4.0f ); + } + } + } +} + +//--------------------------------------------------------------------------------------------------------------- +void BotStatement::AppendPhrase( const BotPhrase *phrase ) +{ + if (phrase == NULL) + return; + + if (m_count < MAX_BOT_PHRASES) + { + m_statement[ m_count ].isPhrase = true; + m_statement[ m_count++ ].phrase = phrase; + } +} + +/** + * Special phrases that depend on the context + */ +void BotStatement::AppendPhrase( ContextType contextPhrase ) +{ + if (m_count < MAX_BOT_PHRASES) + { + m_statement[ m_count ].isPhrase = false; + m_statement[ m_count++ ].context = contextPhrase; + } +} + +/** + * Say our statement + * m_index refers to the phrase currently being spoken, or -1 if we havent started yet + */ +bool BotStatement::Update( void ) +{ + CCSBot *me = GetOwner(); + + // if all of our teammates are dead, the only non-redundant statements are emotes + if (me->GetFriendsRemaining() == 0 && GetType() != REPORT_EMOTE) + return false; + + if (!m_isSpeaking) + { + m_isSpeaking = true; + m_speakTimestamp = gpGlobals->curtime; + } + + // special case - context dependent delay + if (m_index >= 0 && m_statement[ m_index ].context == ACCUMULATE_ENEMIES_DELAY) + { + // report if we see a lot of enemies, or if enough time has passed + const float reportTime = 2.0f; // 1 + if (me->GetNearbyEnemyCount() > 3 || gpGlobals->curtime - m_speakTimestamp > reportTime) + { + // enough enemies have accumulated to expire this delay + m_nextTime = 0.0f; + } + } + + + if (gpGlobals->curtime > m_nextTime) + { + // check for end of statement + if (++m_index == m_count) + { + // transmit any memes carried in this statement to our teammates + if (m_meme) + m_meme->Transmit( me ); + + return false; + } + + // start next part of statement + float duration = 0.0f; + const BotPhrase *phrase = NULL; + + if (m_statement[ m_index ].isPhrase) + { + // normal phrase + phrase = m_statement[ m_index ].phrase; + } + else + { + // context-dependant phrase + switch( m_statement[ m_index ].context ) + { + case CURRENT_ENEMY_COUNT: + { + int enemyCount = me->GetNearbyEnemyCount(); + + // if we are outnumbered, ask for help + if (enemyCount-1 > me->GetNearbyFriendCount()) + { + phrase = TheBotPhrases->GetPhrase( "Help" ); + AttachMeme( new BotHelpMeme() ); + } + else if (enemyCount > 1) + { + phrase = TheBotPhrases->GetPhrase( "EnemySpotted" ); + phrase->SetCountCriteria( enemyCount ); + } + break; + } + + case REMAINING_ENEMY_COUNT: + { + static const char *speak[] = + { + "NoEnemiesLeft", "OneEnemyLeft", "TwoEnemiesLeft", "ThreeEnemiesLeft" + }; + + int enemyCount = me->GetEnemiesRemaining(); + + // dont report if there are lots of enemies left + if (enemyCount < 0 || enemyCount > 3) + { + phrase = NULL; + } + else + { + phrase = TheBotPhrases->GetPhrase( speak[ enemyCount ] ); + } + break; + } + + case SHORT_DELAY: + { + m_nextTime = gpGlobals->curtime + RandomFloat( 0.1f, 0.5f ); + return true; + } + + case LONG_DELAY: + { + m_nextTime = gpGlobals->curtime + RandomFloat( 1.0f, 2.0f ); + return true; + } + + case ACCUMULATE_ENEMIES_DELAY: + { + // wait until test becomes true + m_nextTime = 99999999.9f; + return true; + } + } + } + + if (phrase) + { + // if chatter system is in "standard radio" mode, send the equivalent radio command + if (me->GetChatter()->GetVerbosity() == BotChatterInterface::RADIO) + { + RadioType radioEvent = phrase->GetRadioEquivalent(); + if (radioEvent == RADIO_INVALID) + { + // skip directly to the next phrase + m_nextTime = 0.0f; + } + else + { + // use the standard radio + me->GetChatter()->ResetRadioSilenceDuration(); + me->SendRadioMessage( radioEvent ); + duration = 2.0f; + } + } + else + { + // set place criteria + phrase->SetPlaceCriteria( m_place ); + + const char *filename = phrase->GetSpeakable( me->GetProfile()->GetVoiceBank(), &duration ); + // CONSOLE_ECHO( "%s: Radio( '%s' )\n", STRING( me->pev->netname ), filename ); + + bool sayIt = true; + + if (phrase->IsPlace()) + { + // don't repeat the place if someone just mentioned it not too long ago + float timeSince = TheBotPhrases->GetPlaceStatementInterval( phrase->GetPlace() ); + const float minRepeatTime = 20.0f; // 30 + if (timeSince < minRepeatTime) + { + sayIt = false; + } + else + { + TheBotPhrases->ResetPlaceStatementInterval( phrase->GetPlace() ); + } + } + + if (sayIt) + { + if ( !filename ) + { + RadioType radioEvent = phrase->GetRadioEquivalent(); + if (radioEvent == RADIO_INVALID) + { + // skip directly to the next phrase + m_nextTime = 0.0f; + } + else + { + // use the standard radio + me->SendRadioMessage( radioEvent ); + me->GetChatter()->ResetRadioSilenceDuration(); + duration = 2.0f; + } + } + /* BOTPORT: Wire up bot voice over IP + else if ( g_engfuncs.pfnPlayClientVoice && TheBotPhrases->GetOutputType( me->GetProfile()->GetVoiceBank() ) == BOT_CHATTER_VOICE ) + { + me->GetChatter()->ResetRadioSilenceDuration(); + g_engfuncs.pfnPlayClientVoice( me->entindex() - 1, filename ); + } + */ + else + { + me->SpeakAudio( filename, duration + 1.0f, me->GetProfile()->GetVoicePitch() ); + } + } + } + + const float gap = 0.1f; + m_nextTime = gpGlobals->curtime + duration + gap; + } + else + { + // skip directly to the next phrase + m_nextTime = 0.0f; + } + } + + return true; +} + +/** + * If this statement refers to a specific place, return that place + * Places can be implicit in the statement, or explicitly defined + */ +unsigned int BotStatement::GetPlace( void ) const +{ + // return any explicitly set place if we have one + if (m_place != UNDEFINED_PLACE) + return m_place; + + // look for an implicit place in our statement + for( int i=0; i<m_count; ++i ) + if (m_statement[i].isPhrase && m_statement[i].phrase->IsPlace()) + return m_statement[i].phrase->GetPlace(); + + return 0; +} + +/** + * Return true if this statement has an associated count + */ +bool BotStatement::HasCount( void ) const +{ + for( int i=0; i<m_count; ++i ) + if (!m_statement[i].isPhrase && m_statement[i].context == CURRENT_ENEMY_COUNT) + return true; + + return false; +} + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +CountdownTimer BotChatterInterface::m_encourageTimer; +IntervalTimer BotChatterInterface::m_radioSilenceInterval[ 2 ]; + + +enum PitchHack +{ + P_HI, + P_NORMAL, + P_LOW +}; + +static int nextPitch = P_HI; + +BotChatterInterface::BotChatterInterface( CCSBot *me ) +{ + m_me = me; + m_statementList = NULL; + + switch( nextPitch ) + { + case P_HI: + m_pitch = RandomInt( 105, 110 ); + break; + + case P_NORMAL: + m_pitch = RandomInt( 95, 105 ); + break; + + case P_LOW: + m_pitch = RandomInt( 85, 95 ); + break; + } + + nextPitch = (nextPitch + 1) % 3; + + Reset(); +} + +//--------------------------------------------------------------------------------------------------------------- +BotChatterInterface::~BotChatterInterface() +{ + // free pending statements + BotStatement *next; + for( BotStatement *msg = m_statementList; msg; msg = next ) + { + next = msg->m_next; + delete msg; + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Reset to initial state + */ +void BotChatterInterface::Reset( void ) +{ + BotStatement *msg, *nextMsg; + + // removing pending statements - except for those about the round results + for( msg = m_statementList; msg; msg = nextMsg ) + { + nextMsg = msg->m_next; + + if (msg->GetType() != REPORT_ROUND_END) + RemoveStatement( msg ); + } + + m_seeAtLeastOneEnemy = false; + m_timeWhenSawFirstEnemy = 0.0f; + m_reportedEnemies = false; + m_requestedBombLocation = false; + + ResetRadioSilenceDuration(); + + m_needBackupInterval.Invalidate(); + m_spottedBomberInterval.Invalidate(); + m_spottedLooseBombTimer.Invalidate(); + m_heardNoiseTimer.Invalidate(); + m_scaredInterval.Invalidate(); + m_planInterval.Invalidate(); + m_encourageTimer.Invalidate(); + m_escortingHostageTimer.Invalidate(); + m_warnSniperTimer.Invalidate(); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Register a statement for speaking + */ +void BotChatterInterface::AddStatement( BotStatement *statement, bool mustAdd ) +{ + // don't add statements if bot chatter is shut off + if (GetVerbosity() == OFF) + { + delete statement; + return; + } + + // if we only want mission-critical radio chatter, ignore non-important phrases + if (GetVerbosity() == MINIMAL && !statement->IsImportant()) + { + delete statement; + return; + } + + // don't add statements if we're dead + if (!m_me->IsAlive() && !mustAdd) + { + delete statement; + return; + } + + // don't add empty statements + if (statement->m_count == 0) + { + delete statement; + return; + } + + // don't add statements that are redundant with something we're already waiting to say + BotStatement *s; + for( s=m_statementList; s; s = s->m_next ) + { + if (statement->IsRedundant( s )) + { + m_me->PrintIfWatched( "I tried to say something I'm already saying.\n" ); + delete statement; + return; + } + } + + // keep statements in order of start time + + // check list is empty + if (m_statementList == NULL) + { + statement->m_next = NULL; + statement->m_prev = NULL; + m_statementList = statement; + return; + } + + // list has at least one statement on it + + // insert into list in order + BotStatement *earlier = NULL; + for( s=m_statementList; s; s = s->m_next ) + { + if (s->GetStartTime() > statement->GetStartTime()) + break; + + earlier = s; + } + + // insert just after "earlier" + if (earlier) + { + if (earlier->m_next) + earlier->m_next->m_prev = statement; + + statement->m_next = earlier->m_next; + + earlier->m_next = statement; + statement->m_prev = earlier; + } + else + { + // insert at head + statement->m_prev = NULL; + statement->m_next = m_statementList; + m_statementList->m_prev = statement; + m_statementList = statement; + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Remove a statement + */ +void BotChatterInterface::RemoveStatement( BotStatement *statement ) +{ + if (statement->m_next) + statement->m_next->m_prev = statement->m_prev; + + if (statement->m_prev) + statement->m_prev->m_next = statement->m_next; + else + m_statementList = statement->m_next; + + delete statement; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Track nearby enemy count and report enemy activity + */ +void BotChatterInterface::ReportEnemies( void ) +{ + if (!m_me->IsAlive()) + return; + + if (m_me->GetNearbyEnemyCount() == 0) + { + m_seeAtLeastOneEnemy = false; + m_reportedEnemies = false; + } + else if (!m_seeAtLeastOneEnemy) + { + m_seeAtLeastOneEnemy = true; + m_timeWhenSawFirstEnemy = gpGlobals->curtime; + } + + // determine whether we should report enemy activity + if (!m_reportedEnemies && m_seeAtLeastOneEnemy) + { + // request backup if we're outnumbered + if (m_me->IsOutnumbered() && NeedBackup()) + { + m_reportedEnemies = true; + return; + } + + m_me->GetChatter()->EnemySpotted(); + m_reportedEnemies = true; + } +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Invoked when we die + */ +void BotChatterInterface::OnDeath( void ) +{ + if (IsTalking()) + { + if (m_me->GetChatter()->GetVerbosity() == BotChatterInterface::MINIMAL || + m_me->GetChatter()->GetVerbosity() == BotChatterInterface::NORMAL) + { + // we've died mid-sentance - emit a gargle of pain + const BotPhrase *pain = TheBotPhrases->GetPainPhrase(); + if (pain) + { + /* + if ( g_engfuncs.pfnPlayClientVoice && TheBotPhrases->GetOutputType( m_me->GetProfile()->GetVoiceBank() ) == BOT_CHATTER_VOICE ) + { + g_engfuncs.pfnPlayClientVoice( m_me->entindex() - 1, pain->GetSpeakable(m_me->GetProfile()->GetVoiceBank()) ); + m_me->GetChatter()->ResetRadioSilenceDuration(); + } + else + */ + { + m_me->SpeakAudio( pain->GetSpeakable( m_me->GetProfile()->GetVoiceBank() ), 0.0f, m_me->GetProfile()->GetVoicePitch() ); + } + } + } + } + + // remove all of our statements + Reset(); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Process ongoing chatter for this bot + */ +void BotChatterInterface::Update( void ) +{ + // report enemy activity + ReportEnemies(); + + // ask team to report in if we havent heard anything in awhile + if (ShouldSpeak()) + { + const float longTime = 30.0f; + if (m_me->GetEnemiesRemaining() > 0 && GetRadioSilenceDuration() > longTime) + { + ReportIn(); + } + } + + // speak if it is our turn + BotStatement *say = GetActiveStatement(); + + if (say) + { + // if our statement is active, speak it + if (say->GetOwner() == m_me) + { + if (say->Update() == false) + { + // this statement is complete - destroy it + RemoveStatement( say ); + } + } + } + + + // + // Process active statements. + // Removed expired statements, re-order statements according to their relavence and importance + // Remove redundant statements (ie: our teammates already said them) + // + const BotStatement *friendSay = GetActiveStatement(); + if (friendSay && friendSay->GetOwner() == m_me) + friendSay = NULL; + + BotStatement *nextSay; + for( say = m_statementList; say; say = nextSay ) + { + nextSay = say->m_next; + + // check statement conditions + if (!say->IsValid()) + { + RemoveStatement( say ); + continue; + } + + // don't interrupt ourselves + if (say->IsSpeaking()) + continue; + + // check for obsolete statements + if (say->IsObsolete()) + { + m_me->PrintIfWatched( "Statement obsolete - removing.\n" ); + RemoveStatement( say ); + continue; + } + + // if a teammate is saying what we were going to say, dont repeat it + if (friendSay) + { + // convert what we're about to say based on what our teammate is currently saying + say->Convert( friendSay ); + + // don't say things our teammates have just said + if (say->IsRedundant( friendSay )) + { + // thie statement is redundant - destroy it + //m_me->PrintIfWatched( "Teammate said what I was going to say - shutting up.\n" ); + m_me->PrintIfWatched( "Teammate said what I was going to say - shutting up.\n" ); + RemoveStatement( say ); + } + } + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Returns the statement that is being spoken, or is next to be spoken if no-one is speaking now + */ +BotStatement *BotChatterInterface::GetActiveStatement( void ) +{ + // keep track of statement waiting longest to be spoken - it is next + BotStatement *earliest = NULL; + float earlyTime = 999999999.9f; + + for( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + // ignore dead humans + if (!player->IsBot() && !player->IsAlive()) + continue; + + // ignore enemies, since we can't hear them talk + if (!m_me->InSameTeam( player )) + continue; + + CCSBot *bot = dynamic_cast<CCSBot *>(player); + + // if not a bot, fail the test + /// @todo Check if human is currently talking + if (!bot) + continue; + + for( BotStatement *say = bot->GetChatter()->m_statementList; say; say = say->m_next ) + { + // if this statement is currently being spoken, return it + if (say->IsSpeaking()) + return say; + + // keep track of statement that has been waiting longest to be spoken of anyone on our team + if (say->GetStartTime() < earlyTime) + { + earlyTime = say->GetTimestamp(); + earliest = say; + } + } + } + + // make sure it is time to start this statement + if (earliest && earliest->GetStartTime() > gpGlobals->curtime) + return NULL; + + return earliest; +} + +/** + * Return true if we speaking makes sense now + */ +bool BotChatterInterface::ShouldSpeak( void ) const +{ + // don't talk to non-existent friends + if (m_me->GetFriendsRemaining() == 0) + return false; + + // if everyone is together, no need to tell them what's going on + if (m_me->GetNearbyFriendCount() == m_me->GetFriendsRemaining()) + return false; + + return true; +} + +//--------------------------------------------------------------------------------------------------------------- +float BotChatterInterface::GetRadioSilenceDuration( void ) +{ + return m_radioSilenceInterval[ m_me->GetTeamNumber() % 2 ].GetElapsedTime(); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::ResetRadioSilenceDuration( void ) +{ + m_radioSilenceInterval[ m_me->GetTeamNumber() % 2 ].Reset(); +} + + + +//--------------------------------------------------------------------------------------------------------------- +inline const BotPhrase *GetPlacePhrase( CCSBot *me ) +{ + Place place = me->GetPlace(); + if (place != UNDEFINED_PLACE) + return TheBotPhrases->GetPlace( place ); + + return NULL; +} + + +inline void SayWhere( BotStatement *say, Place place ) +{ + say->AppendPhrase( TheBotPhrases->GetPlace( place ) ); +} + +/** + * Report enemy sightings + */ +void BotChatterInterface::EnemySpotted( void ) +{ + // NOTE: This could be a few seconds out of date (enemy is in an adjacent place) + Place place = m_me->GetEnemyPlace(); + + BotStatement *say = new BotStatement( this, REPORT_VISIBLE_ENEMIES, 10.0f ); + + // where are the enemies + say->AppendPhrase( TheBotPhrases->GetPlace( place ) ); + + // how many are there + say->AppendPhrase( BotStatement::ACCUMULATE_ENEMIES_DELAY ); + say->AppendPhrase( BotStatement::CURRENT_ENEMY_COUNT ); + say->AddCondition( BotStatement::IS_IN_COMBAT ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * If a friend warned of snipers, don't warn again for awhile + */ +void BotChatterInterface::FriendSpottedSniper( void ) +{ + m_warnSniperTimer.Start( 60.0f ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Warn of an enemy sniper + */ +void BotChatterInterface::SpottedSniper( void ) +{ + if (!m_warnSniperTimer.IsElapsed()) + { + return; + } + + if (m_me->GetFriendsRemaining() == 0) + { + // no-one to warn + return; + } + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "SniperWarning" ) ); + say->AttachMeme( new BotWarnSniperMeme() ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Clear( Place place ) +{ + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + SayWhere( say, place ); + say->AppendPhrase( TheBotPhrases->GetPhrase( "Clear" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Request enemy activity report + */ +void BotChatterInterface::ReportIn( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_REQUEST_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "RequestReport" ) ); + say->AddCondition( BotStatement::RADIO_SILENCE ); + say->AttachMeme( new BotRequestReportMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Report our situtation + */ +void BotChatterInterface::ReportingIn( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where are we + Place place = m_me->GetPlace(); + SayWhere( say, place ); + + // what are we doing + switch( m_me->GetTask() ) + { + case CCSBot::PLANT_BOMB: + { + m_me->GetChatter()->GoingToPlantTheBomb( UNDEFINED_PLACE ); + break; + } + + case CCSBot::DEFUSE_BOMB: + { + m_me->GetChatter()->Say( "DefusingBomb" ); + break; + } + + case CCSBot::GUARD_LOOSE_BOMB: + { + if (TheCSBots()->GetLooseBomb()) + { + say->AppendPhrase( TheBotPhrases->GetPhrase( "GuardingLooseBomb" ) ); + say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, TheCSBots()->GetLooseBomb()->GetAbsOrigin() ) ); + } + break; + } + + case CCSBot::GUARD_HOSTAGES: + { + m_me->GetChatter()->GuardingHostages( UNDEFINED_PLACE, !m_me->IsAtHidingSpot() ); + break; + } + + case CCSBot::GUARD_HOSTAGE_RESCUE_ZONE: + { + m_me->GetChatter()->GuardingHostageEscapeZone( !m_me->IsAtHidingSpot() ); + break; + } + + case CCSBot::COLLECT_HOSTAGES: + { + break; + } + + case CCSBot::RESCUE_HOSTAGES: + { + m_me->GetChatter()->EscortingHostages(); + break; + } + + case CCSBot::GUARD_VIP_ESCAPE_ZONE: + { + break; + } + + } + + + // what do we see + if (m_me->IsAttacking()) + { + if (m_me->IsOutnumbered()) + { + // in trouble in a firefight + say->AppendPhrase( TheBotPhrases->GetPhrase( "Help" ) ); + say->AttachMeme( new BotHelpMeme( place ) ); + } + else + { + // battling enemies + say->AppendPhrase( TheBotPhrases->GetPhrase( "InCombat" ) ); + } + } + else + { + // not in combat, start our report a little later + say->SetStartTime( gpGlobals->curtime + 2.0f ); + + const float recentTime = 10.0f; + if (m_me->GetEnemyDeathTimestamp() < recentTime && m_me->GetEnemyDeathTimestamp() >= m_me->GetTimeSinceLastSawEnemy() + 0.5f) + { + // recently saw an enemy die + say->AppendPhrase( TheBotPhrases->GetPhrase( "EnemyDown" ) ); + } + else if (m_me->GetTimeSinceLastSawEnemy() < recentTime) + { + // recently saw an enemy + say->AppendPhrase( TheBotPhrases->GetPhrase( "EnemySpotted" ) ); + } + else + { + // haven't seen enemies + say->AppendPhrase( TheBotPhrases->GetPhrase( "Clear" ) ); + } + } + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +bool BotChatterInterface::NeedBackup( void ) +{ + const float minRequestInterval = 10.0f; + if (m_needBackupInterval.IsLessThen( minRequestInterval )) + return false; + + m_needBackupInterval.Reset(); + + if (m_me->GetFriendsRemaining() == 0) + { + // we're all alone... + Scared(); + return true; + } + else + { + // ask friends for help + BotStatement *say = new BotStatement( this, REPORT_REQUEST_HELP, 10.0f ); + + // where are we + Place place = m_me->GetPlace(); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "Help" ) ); + say->AttachMeme( new BotHelpMeme( place ) ); + + AddStatement( say ); + } + + return true; +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::PinnedDown( void ) +{ + // this is a form of "need backup" + const float minRequestInterval = 10.0f; + if (m_needBackupInterval.IsLessThen( minRequestInterval )) + return; + + m_needBackupInterval.Reset(); + + BotStatement *say = new BotStatement( this, REPORT_REQUEST_HELP, 10.0f ); + + // where are we + Place place = m_me->GetPlace(); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "PinnedDown" ) ); + say->AttachMeme( new BotHelpMeme( place ) ); + say->AddCondition( BotStatement::IS_IN_COMBAT ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * If a friend said that they heard something, we don't want to say something similar + * for a while. + */ +void BotChatterInterface::FriendHeardNoise( void ) +{ + m_heardNoiseTimer.Start( 20.0f ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HeardNoise( const Vector &pos ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + if (m_heardNoiseTimer.IsElapsed()) + { + // throttle frequency + m_heardNoiseTimer.Start( 20.0f ); + + // make rare, since many teammates may try to say this + if (RandomFloat( 0, 100 ) < 33) + { + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 5.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HeardNoise" ) ); + say->SetPlace( TheNavMesh->GetPlace( pos ) ); + say->AttachMeme( new BotHeardNoiseMeme() ); + + AddStatement( say ); + } + } +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::KilledMyEnemy( int victimID ) +{ + // only report if we killed the last enemy in the area + if (m_me->GetNearbyEnemyCount() <= 1) + return; + + BotStatement *say = new BotStatement( this, REPORT_ENEMY_ACTION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "KilledMyEnemy" ) ); + say->SetSubject( victimID ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::EnemiesRemaining( void ) +{ + // only report if we killed the last enemy in the area + if (m_me->GetNearbyEnemyCount() > 1) + return; + + BotStatement *say = new BotStatement( this, REPORT_ENEMIES_REMAINING, 5.0f ); + say->AppendPhrase( BotStatement::REMAINING_ENEMY_COUNT ); + say->SetStartTime( gpGlobals->curtime + RandomFloat( 2.0f, 4.0f ) ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Affirmative( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_ACKNOWLEDGE, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "Affirmative" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Negative( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_ACKNOWLEDGE, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "Negative" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GoingToPlantTheBomb( Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + BotStatement *say = new BotStatement( this, REPORT_CRITICAL_EVENT, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "GoingToPlantBomb" ) ); + say->SetPlace( place ); + say->AttachMeme( new BotFollowMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::PlantingTheBomb( Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_CRITICAL_EVENT, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "PlantingBomb" ) ); + say->SetPlace( place ); + + Vector myOrigin = GetCentroid( m_me ); + say->AttachMeme( new BotDefendHereMeme( myOrigin ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::TheyPickedUpTheBomb( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + // if we already know the bomb is not loose, this is old news + if (!m_me->GetGameState()->IsBombLoose()) + return; + + // update our gamestate - use our own position for now + const Vector &myOrigin = GetCentroid( m_me ); + m_me->GetGameState()->UpdateBomber( myOrigin ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "TheyPickedUpTheBomb" ) ); + + say->AttachMeme( new BotBombStatusMeme( CSGameState::MOVING, myOrigin ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::SpottedBomber( CBasePlayer *bomber ) +{ + const Vector &bomberOrigin = GetCentroid( bomber ); + + if (m_me->GetGameState()->IsBombMoving()) + { + // if we knew where the bomber was, this is old news + const Vector *bomberPos = m_me->GetGameState()->GetBombPosition(); + const float closeRangeSq = 1000.0f * 1000.0f; + if (bomberPos && (bomberOrigin - *bomberPos).LengthSqr() < closeRangeSq) + return; + } + + // update our gamestate + m_me->GetGameState()->UpdateBomber( bomberOrigin ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where is the bomber + Place place = TheNavMesh->GetPlace( bomberOrigin ); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "SpottedBomber" ) ); + + say->SetSubject( bomber->entindex() ); + + //say->AttachMeme( new BotHelpMeme( place ) ); + say->AttachMeme( new BotBombStatusMeme( CSGameState::MOVING, bomberOrigin ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::SpottedLooseBomb( CBaseEntity *bomb ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + // if we already know the bomb is loose, this is old news + if (m_me->GetGameState()->IsBombLoose()) + return; + + // update our gamestate + m_me->GetGameState()->UpdateLooseBomb( bomb->GetAbsOrigin() ); + + if (m_spottedLooseBombTimer.IsElapsed()) + { + // throttle frequency + m_spottedLooseBombTimer.Start( 10.0f ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where is the bomb + Place place = TheNavMesh->GetPlace( bomb->GetAbsOrigin() ); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "SpottedLooseBomb" ) ); + + if (TheCSBots()->GetLooseBomb()) + say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, bomb->GetAbsOrigin() ) ); + + AddStatement( say ); + } +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingLooseBomb( CBaseEntity *bomb ) +{ + // if we already know the bomb is loose, this is old news +// if (m_me->GetGameState()->IsBombLoose()) +// return; + + if (TheCSBots()->IsRoundOver() || !bomb) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + // update our gamestate + m_me->GetGameState()->UpdateLooseBomb( bomb->GetAbsOrigin() ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where is the bomb + Place place = TheNavMesh->GetPlace( bomb->GetAbsOrigin() ); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "GuardingLooseBomb" ) ); + + if (TheCSBots()->GetLooseBomb()) + say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, bomb->GetAbsOrigin() ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::RequestBombLocation( void ) +{ + // only ask once per round + if (m_requestedBombLocation) + return; + + m_requestedBombLocation = true; + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_REQUEST_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "WhereIsTheBomb" ) ); + + say->AttachMeme( new BotWhereBombMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::BombsiteClear( int zoneIndex ) +{ + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( zoneIndex ); + if (zone == NULL) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + SayWhere( say, TheNavMesh->GetPlace( zone->m_center ) ); + say->AppendPhrase( TheBotPhrases->GetPhrase( "BombsiteClear" ) ); + + say->AttachMeme( new BotBombsiteStatusMeme( zoneIndex, BotBombsiteStatusMeme::CLEAR ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::FoundPlantedBomb( int zoneIndex ) +{ + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( zoneIndex ); + if (zone == NULL) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "PlantedBombPlace" ) ); + say->SetPlace( TheNavMesh->GetPlace( zone->m_center ) ); + + say->AttachMeme( new BotBombsiteStatusMeme( zoneIndex, BotBombsiteStatusMeme::PLANTED ) ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Scared( void ) +{ + const float minInterval = 10.0f; + if (m_scaredInterval.IsLessThen( minInterval )) + return; + + m_scaredInterval.Reset(); + + BotStatement *say = new BotStatement( this, REPORT_EMOTE, 1.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "ScaredEmote" ) ); + say->AddCondition( BotStatement::IS_IN_COMBAT ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::CelebrateWin( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_EMOTE, 15.0f ); + + // wait a bit before speaking + say->SetStartTime( gpGlobals->curtime + RandomFloat( 2.0f, 5.0f ) ); + + const float quickRound = 45.0f; + + if (m_me->GetFriendsRemaining() == 0) + { + // we were the last man standing + if (TheCSBots()->GetElapsedRoundTime() < quickRound) + say->AppendPhrase( TheBotPhrases->GetPhrase( "WonRoundQuickly" ) ); + else if (RandomFloat( 0.0f, 100.0f ) < 33.3f) + say->AppendPhrase( TheBotPhrases->GetPhrase( "LastManStanding" ) ); + } + else + { + if (TheCSBots()->GetElapsedRoundTime() < quickRound) + { + if (RandomFloat( 0.0f, 100.0f ) < 33.3f) + say->AppendPhrase( TheBotPhrases->GetPhrase( "WonRoundQuickly" ) ); + } + else if (RandomFloat( 0.0f, 100.0f ) < 10.0f) + { + say->AppendPhrase( TheBotPhrases->GetPhrase( "WonRound" ) ); + } + } + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::AnnouncePlan( const char *phraseName, Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_MY_PLAN, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( phraseName ) ); + say->SetPlace( place ); + + // wait at least a short time after round start + say->SetStartTime( TheCSBots()->GetRoundStartTime() + RandomFloat( 2.0, 3.0f ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingBombsite( Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + AnnouncePlan( "GoingToDefendBombsite", place ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingHostages( Place place, bool isPlan ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + if (isPlan) + AnnouncePlan( "GoingToGuardHostages", place ); + else + Say( "GuardingHostages" ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingHostageEscapeZone( bool isPlan ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + if (isPlan) + AnnouncePlan( "GoingToGuardHostageEscapeZone", UNDEFINED_PLACE ); + else + Say( "GuardingHostageEscapeZone" ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HostagesBeingTaken( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HostagesBeingTaken" ) ); + say->AttachMeme( new BotHostageBeingTakenMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HostagesTaken( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HostagesTaken" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::TalkingToHostages( void ) +{ +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::EscortingHostages( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + if (m_escortingHostageTimer.IsElapsed()) + { + // throttle frequency + m_escortingHostageTimer.Start( 10.0f ); + + BotStatement *say = new BotStatement( this, REPORT_MY_PLAN, 5.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "EscortingHostages" ) ); + + AddStatement( say ); + } +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HostageDown( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HostageDown" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Encourage( const char *phraseName, float repeatInterval, float lifetime ) +{ + if (m_encourageTimer.IsElapsed()) + { + Say( phraseName, lifetime ); + m_encourageTimer.Start( repeatInterval ); + } +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::KilledFriend( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_KILLED_FRIEND, 2.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "KilledFriend" ) ); + + // give them time to react + say->SetStartTime( gpGlobals->curtime + RandomFloat( 0.5f, 1.0f ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::FriendlyFire( void ) +{ + if ( !friendlyfire.GetBool() ) + return; + + BotStatement *say = new BotStatement( this, REPORT_FRIENDLY_FIRE, 1.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "FriendlyFire" ) ); + + // give them time to react + say->SetStartTime( gpGlobals->curtime + RandomFloat( 0.3f, 0.5f ) ); + + AddStatement( say ); +} + + diff --git a/game/server/cstrike/bot/cs_bot_chatter.h b/game/server/cstrike/bot/cs_bot_chatter.h new file mode 100644 index 0000000..47ba802 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_chatter.h @@ -0,0 +1,656 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bot radio chatter system +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef CS_BOT_CHATTER_H +#define CS_BOT_CHATTER_H + +#pragma warning( disable : 4786 ) // long STL names get truncated in browse info. + +#include "nav_mesh.h" +#include "cs_gamestate.h" + +class CCSBot; +class BotChatterInterface; + +#define MAX_PLACES_PER_MAP 64 + +typedef unsigned int PlaceCriteria; + +typedef unsigned int CountCriteria; +#define UNDEFINED_COUNT 0xFFFF +#define COUNT_CURRENT_ENEMIES 0xFF // use the number of enemies we see right when we speak +#define COUNT_MANY 4 // equal to or greater than this is "many" + +#define UNDEFINED_SUBJECT (-1) + +/// @todo Make Place a class with member fuctions for this +const Vector *GetRandomSpotAtPlace( Place place ); + +//---------------------------------------------------------------------------------------------------- +/** + * A meme is a unit information that bots use to + * transmit information to each other via the radio + */ +class BotMeme +{ +public: + void Transmit( CCSBot *sender ) const; ///< transmit meme to other bots + // It is a best practice to always have a virtual destructor in an interface + // class. Otherwise if the derived classes have destructors they will not be + // called. + virtual ~BotMeme() {} + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const = 0; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotHelpMeme : public BotMeme +{ +public: + BotHelpMeme( Place place = UNDEFINED_PLACE ) + { + m_place = place; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + Place m_place; ///< where the help is needed +}; + +//---------------------------------------------------------------------------------------------------- +class BotBombsiteStatusMeme : public BotMeme +{ +public: + enum StatusType { CLEAR, PLANTED }; + + BotBombsiteStatusMeme( int zoneIndex, StatusType status ) + { + m_zoneIndex = zoneIndex; + m_status = status; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + int m_zoneIndex; ///< the bombsite + StatusType m_status; ///< whether it is cleared or the bomb is there (planted) +}; + +//---------------------------------------------------------------------------------------------------- +class BotBombStatusMeme : public BotMeme +{ +public: + BotBombStatusMeme( CSGameState::BombState state, const Vector &pos ) + { + m_state = state; + m_pos = pos; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + CSGameState::BombState m_state; + Vector m_pos; +}; + +//---------------------------------------------------------------------------------------------------- +class BotFollowMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotDefendHereMeme : public BotMeme +{ +public: + BotDefendHereMeme( const Vector &pos ) + { + m_pos = pos; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + Vector m_pos; +}; + +//---------------------------------------------------------------------------------------------------- +class BotWhereBombMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotRequestReportMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotAllHostagesGoneMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotHostageBeingTakenMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotHeardNoiseMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotWarnSniperMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +enum BotStatementType +{ + REPORT_VISIBLE_ENEMIES, + REPORT_ENEMY_ACTION, + REPORT_MY_CURRENT_TASK, + REPORT_MY_INTENTION, + REPORT_CRITICAL_EVENT, + REPORT_REQUEST_HELP, + REPORT_REQUEST_INFORMATION, + REPORT_ROUND_END, + REPORT_MY_PLAN, + REPORT_INFORMATION, + REPORT_EMOTE, + REPORT_ACKNOWLEDGE, ///< affirmative or negative + REPORT_ENEMIES_REMAINING, + REPORT_FRIENDLY_FIRE, + REPORT_KILLED_FRIEND, + REPORT_ENEMY_LOST, + + NUM_BOT_STATEMENT_TYPES +}; + +//---------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------- +/** + * BotSpeakables are the smallest unit of bot chatter. + * They represent a specific wav file of a phrase, and the criteria for which it is useful + */ +class BotSpeakable +{ +public: + BotSpeakable(); + ~BotSpeakable(); + char *m_phrase; + float m_duration; + PlaceCriteria m_place; + CountCriteria m_count; +}; +typedef CUtlVector< BotSpeakable * > BotSpeakableVector; +typedef CUtlVector< BotSpeakableVector * > BotVoiceBankVector; + + +//---------------------------------------------------------------------------------------------------- +/** + * The BotPhrase class is a collection of Speakables associated with a name, ID, and criteria + */ +class BotPhrase +{ +public: + char *GetSpeakable( int bankIndex, float *duration = NULL ) const; ///< return a random speakable and its duration in seconds that meets the current criteria + + // NOTE: Criteria must be set just before the GetSpeakable() call, since they are shared among all bots + void ClearCriteria( void ) const; + void SetPlaceCriteria( PlaceCriteria place ) const; ///< all returned phrases must have this place criteria + void SetCountCriteria( CountCriteria count ) const; ///< all returned phrases must have this count criteria + + const char *GetName( void ) const { return m_name; } + const unsigned int GetPlace( void ) const { return m_place; } + RadioType GetRadioEquivalent( void ) const { return m_radioEvent; } ///< return equivalent "standard radio" event + bool IsImportant( void ) const { return m_isImportant; } ///< return true if this phrase is part of an important statement + + bool IsPlace( void ) const { return m_isPlace; } + + void Randomize( void ); ///< randomly shuffle the speakable order + +private: + friend class BotPhraseManager; + BotPhrase( bool isPlace ); + ~BotPhrase(); + + char *m_name; + Place m_place; + bool m_isPlace; ///< true if this is a Place phrase + RadioType m_radioEvent; ///< equivalent radio event + bool m_isImportant; ///< mission-critical statement + + mutable BotVoiceBankVector m_voiceBank; ///< array of voice banks (arrays of speakables) + CUtlVector< int > m_count; ///< number of speakables + mutable CUtlVector< int > m_index; ///< index of next speakable to return + int m_numVoiceBanks; ///< number of voice banks that have been initialized + void InitVoiceBank( int bankIndex ); ///< sets up the vector of voice banks for the first bankIndex voice banks + + mutable PlaceCriteria m_placeCriteria; + mutable CountCriteria m_countCriteria; +}; +typedef CUtlVector<BotPhrase *> BotPhraseList; + +inline void BotPhrase::ClearCriteria( void ) const +{ + m_placeCriteria = ANY_PLACE; + m_countCriteria = UNDEFINED_COUNT; +} + +inline void BotPhrase::SetPlaceCriteria( PlaceCriteria place ) const +{ + m_placeCriteria = place; +} + +inline void BotPhrase::SetCountCriteria( CountCriteria count ) const +{ + m_countCriteria = count; +} + +enum BotChatterOutputType +{ + BOT_CHATTER_RADIO, + BOT_CHATTER_VOICE +}; +typedef CUtlVector<BotChatterOutputType> BotOutputList; + +//---------------------------------------------------------------------------------------------------- +/** + * The BotPhraseManager is a singleton that provides an interface to all BotPhrase collections + */ +class BotPhraseManager +{ +public: + BotPhraseManager( void ); + ~BotPhraseManager(); + + bool Initialize( const char *filename, int bankIndex ); ///< initialize phrase system from database file for a specific voice bank (0 is the default voice bank) + + void OnRoundRestart( void ); ///< invoked when round resets + void OnMapChange( void ); ///< invoked when map changes + void Reset( void ); + + const BotPhrase *GetPhrase( const char *name ) const; ///< given a name, return the associated phrase collection + const BotPhrase *GetPainPhrase( void ) const { return m_painPhrase; } ///< optimization, replaces a static pointer to the phrase + const BotPhrase *GetAgreeWithPlanPhrase( void ) const { return m_agreeWithPlanPhrase; } ///< optimization, replaces a static pointer to the phrase + + const BotPhrase *GetPlace( const char *name ) const; ///< given a name, return the associated Place phrase collection + const BotPhrase *GetPlace( unsigned int id ) const; ///< given an id, return the associated Place phrase collection + + const BotPhraseList *GetPlaceList( void ) const { return &m_placeList; } + + float GetPlaceStatementInterval( Place where ) const; ///< return time last statement of given type was emitted by a teammate for the given place + void ResetPlaceStatementInterval( Place where ); ///< set time of last statement of given type was emitted by a teammate for the given place + + BotChatterOutputType GetOutputType( int voiceBank ) const; + +private: + BotPhraseList m_list; ///< master list of all phrase collections + BotPhraseList m_placeList; ///< master list of all Place phrases + + BotOutputList m_output; + + const BotPhrase *m_painPhrase; + const BotPhrase *m_agreeWithPlanPhrase; + + struct PlaceTimeInfo + { + Place placeID; + IntervalTimer timer; + }; + mutable PlaceTimeInfo m_placeStatementHistory[ MAX_PLACES_PER_MAP ]; + mutable int m_placeCount; + int FindPlaceIndex( Place where ) const; +}; + +inline int BotPhraseManager::FindPlaceIndex( Place where ) const +{ + for( int i=0; i<m_placeCount; ++i ) + if (m_placeStatementHistory[i].placeID == where) + return i; + + // no such place - allocate it + if (m_placeCount < MAX_PLACES_PER_MAP) + { + m_placeStatementHistory[ ++m_placeCount ].placeID = where; + m_placeStatementHistory[ ++m_placeCount ].timer.Invalidate(); + return m_placeCount-1; + } + + // place directory is full + return -1; +} + +/** + * Return time last statement of given type was emitted by a teammate for the given place + */ +inline float BotPhraseManager::GetPlaceStatementInterval( Place place ) const +{ + int index = FindPlaceIndex( place ); + + if (index < 0) + return 999999.9f; + + if (index >= m_placeCount) + return 999999.9f; + + return m_placeStatementHistory[ index ].timer.GetElapsedTime(); +} + +/** + * Set time of last statement of given type was emitted by a teammate for the given place + */ +inline void BotPhraseManager::ResetPlaceStatementInterval( Place place ) +{ + int index = FindPlaceIndex( place ); + + if (index < 0) + return; + + if (index >= m_placeCount) + return; + + // update entry + m_placeStatementHistory[ index ].timer.Reset(); +} + +extern BotPhraseManager *TheBotPhrases; + + + +//---------------------------------------------------------------------------------------------------- +/** + * Statements are meaningful collections of phrases + */ +class BotStatement +{ +public: + BotStatement( BotChatterInterface *chatter, BotStatementType type, float expireDuration ); + ~BotStatement(); + + BotChatterInterface *GetChatter( void ) const { return m_chatter; } + CCSBot *GetOwner( void ) const; + + BotStatementType GetType( void ) const { return m_type; } ///< return the type of statement this is + bool IsImportant( void ) const; ///< return true if this statement is "important" and not personality chatter + + bool HasSubject( void ) const { return (m_subject == UNDEFINED_SUBJECT) ? false : true; } + void SetSubject( int playerID ) { m_subject = playerID; } ///< who this statement is about + int GetSubject( void ) const { return m_subject; } ///< who this statement is about + + bool HasPlace( void ) const { return (GetPlace()) ? true : false; } + Place GetPlace( void ) const; ///< if this statement refers to a specific place, return that place + void SetPlace( Place where ) { m_place = where; } ///< explicitly set place + + bool HasCount( void ) const; ///< return true if this statement has an associated count + + bool IsRedundant( const BotStatement *say ) const; ///< return true if this statement is the same as the given one + bool IsObsolete( void ) const; ///< return true if this statement is no longer appropriate to say + void Convert( const BotStatement *say ); ///< possibly change what were going to say base on what teammate is saying + + void AppendPhrase( const BotPhrase *phrase ); + + void SetStartTime( float timestamp ) { m_startTime = timestamp; } ///< define the earliest time this statement can be spoken + float GetStartTime( void ) const { return m_startTime; } + + enum ConditionType + { + IS_IN_COMBAT, + RADIO_SILENCE, + ENEMIES_REMAINING, + + NUM_CONDITIONS + }; + + void AddCondition( ConditionType condition ); ///< conditions must be true for the statement to be spoken + bool IsValid( void ) const; ///< verify all attached conditions + + enum ContextType + { + CURRENT_ENEMY_COUNT, + REMAINING_ENEMY_COUNT, + SHORT_DELAY, + LONG_DELAY, + ACCUMULATE_ENEMIES_DELAY + }; + void AppendPhrase( ContextType contextPhrase ); ///< special phrases that depend on the context + + bool Update( void ); ///< emit statement over time, return false if statement is done + bool IsSpeaking( void ) const { return m_isSpeaking; } ///< return true if this statement is currently being spoken + float GetTimestamp( void ) const { return m_timestamp; } ///< get time statement was created (but not necessarily started talking) + + void AttachMeme( BotMeme *meme ); ///< attach a meme to this statement, to be transmitted to other friendly bots when spoken + +private: + friend class BotChatterInterface; + + BotChatterInterface *m_chatter; ///< the chatter system this statement is part of + + BotStatement *m_next, *m_prev; ///< linked list hooks + + BotStatementType m_type; ///< what kind of statement this is + int m_subject; ///< who this subject is about + Place m_place; ///< explicit place - note some phrases have implicit places as well + BotMeme *m_meme; ///< a statement can only have a single meme for now + + float m_timestamp; ///< time when message was created + float m_startTime; ///< the earliest time this statement can be spoken + float m_expireTime; ///< time when this statement is no longer valid + float m_speakTimestamp; ///< time when message began being spoken + bool m_isSpeaking; ///< true if this statement is current being spoken + + float m_nextTime; ///< time for next phrase to begin + + enum { MAX_BOT_PHRASES = 4 }; + struct + { + bool isPhrase; + union + { + const BotPhrase *phrase; + ContextType context; + }; + } + m_statement[ MAX_BOT_PHRASES ]; + + enum { MAX_BOT_CONDITIONS = 4 }; + ConditionType m_condition[ MAX_BOT_CONDITIONS ]; ///< conditions that must be true for the statement to be said + int m_conditionCount; + + int m_index; ///< m_index refers to the phrase currently being spoken, or -1 if we havent started yet + int m_count; +}; + +//---------------------------------------------------------------------------------------------------- +/** + * This class defines the interface to the bot radio chatter system + */ +class BotChatterInterface +{ +public: + BotChatterInterface( CCSBot *me ); + ~BotChatterInterface( ); + + void Reset( void ); ///< reset to initial state + void Update( void ); ///< process ongoing chatter + + /// invoked when event occurs in the game (some events have NULL entities) + void OnDeath( void ); ///< invoked when we die + + enum VerbosityType + { + NORMAL, ///< full chatter + MINIMAL, ///< only scenario-critical events + RADIO, ///< use the standard radio instead + OFF ///< no chatter at all + }; + VerbosityType GetVerbosity( void ) const; ///< return our current level of verbosity + + CCSBot *GetOwner( void ) const { return m_me; } + + bool IsTalking( void ) const; ///< return true if we are currently talking + float GetRadioSilenceDuration( void ); ///< return time since any teammate said anything + void ResetRadioSilenceDuration( void ); + + enum { MUST_ADD = 1 }; + void AddStatement( BotStatement *statement, bool mustAdd = false ); ///< register a statement for speaking + void RemoveStatement( BotStatement *statement ); ///< remove a statement + + BotStatement *GetActiveStatement( void ); ///< returns the statement that is being spoken, or is next to be spoken if no-one is speaking now + BotStatement *GetStatement( void ) const; ///< returns our current statement, or NULL if we aren't speaking + + int GetPitch( void ) const { return m_pitch; } + + + //-- things the bots can say --------------------------------------------------------------------- + void Say( const char *phraseName, float lifetime = 3.0f, float delay = 0.0f ); + + void AnnouncePlan( const char *phraseName, Place where ); + void Affirmative( void ); + void Negative( void ); + + void EnemySpotted( void ); ///< report enemy sightings + void KilledMyEnemy( int victimID ); + void EnemiesRemaining( void ); + + void SpottedSniper( void ); + void FriendSpottedSniper( void ); + + void Clear( Place where ); + + void ReportIn( void ); ///< ask for current situation + void ReportingIn( void ); ///< report current situation + + bool NeedBackup( void ); + void PinnedDown( void ); + void Scared( void ); + void HeardNoise( const Vector &pos ); + void FriendHeardNoise( void ); + + void TheyPickedUpTheBomb( void ); + void GoingToPlantTheBomb( Place where ); + void BombsiteClear( int zoneIndex ); + void FoundPlantedBomb( int zoneIndex ); + void PlantingTheBomb( Place where ); + void SpottedBomber( CBasePlayer *bomber ); + void SpottedLooseBomb( CBaseEntity *bomb ); + void GuardingLooseBomb( CBaseEntity *bomb ); + void RequestBombLocation( void ); + + #define IS_PLAN true + void GuardingHostages( Place where, bool isPlan = false ); + void GuardingHostageEscapeZone( bool isPlan = false ); + void HostagesBeingTaken( void ); + void HostagesTaken( void ); + void TalkingToHostages( void ); + void EscortingHostages( void ); + void HostageDown( void ); + void GuardingBombsite( Place where ); + + void CelebrateWin( void ); + + void Encourage( const char *phraseName, float repeatInterval = 10.0f, float lifetime = 3.0f ); ///< "encourage" the player to do the scenario + + void KilledFriend( void ); + void FriendlyFire( void ); + + bool SeesAtLeastOneEnemy( void ) const { return m_seeAtLeastOneEnemy; } + +private: + BotStatement *m_statementList; ///< list of all active/pending messages for this bot + + void ReportEnemies( void ); ///< track nearby enemy count and generate enemy activity statements + bool ShouldSpeak( void ) const; ///< return true if we speaking makes sense now + + CCSBot *m_me; ///< the bot this chatter is for + + bool m_seeAtLeastOneEnemy; + float m_timeWhenSawFirstEnemy; + bool m_reportedEnemies; + bool m_requestedBombLocation; ///< true if we already asked where the bomb has been planted + + int m_pitch; + + static IntervalTimer m_radioSilenceInterval[ 2 ]; ///< one timer for each team + + IntervalTimer m_needBackupInterval; + IntervalTimer m_spottedBomberInterval; + IntervalTimer m_scaredInterval; + IntervalTimer m_planInterval; + CountdownTimer m_spottedLooseBombTimer; + CountdownTimer m_heardNoiseTimer; + CountdownTimer m_escortingHostageTimer; + CountdownTimer m_warnSniperTimer; + static CountdownTimer m_encourageTimer; ///< timer to know when we can "encourage" the human player again - shared by all bots +}; + +inline BotChatterInterface::VerbosityType BotChatterInterface::GetVerbosity( void ) const +{ + const char *string = cv_bot_chatter.GetString(); + + if (string == NULL) + return NORMAL; + + if (string[0] == 'm' || string[0] == 'M') + return MINIMAL; + + if (string[0] == 'r' || string[0] == 'R') + return RADIO; + + if (string[0] == 'o' || string[0] == 'O') + return OFF; + + return NORMAL; +} + + +inline bool BotChatterInterface::IsTalking( void ) const +{ + if (m_statementList) + return m_statementList->IsSpeaking(); + + return false; +} + +inline BotStatement *BotChatterInterface::GetStatement( void ) const +{ + return m_statementList; +} + + +inline void BotChatterInterface::Say( const char *phraseName, float lifetime, float delay ) +{ + BotStatement *say = new BotStatement( this, REPORT_MY_INTENTION, lifetime ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( phraseName ) ); + + if (delay > 0.0f) + say->SetStartTime( gpGlobals->curtime + delay ); + + AddStatement( say ); +} + + + + +#endif // CS_BOT_CHATTER_H diff --git a/game/server/cstrike/bot/cs_bot_event.cpp b/game/server/cstrike/bot/cs_bot_event.cpp new file mode 100644 index 0000000..24bdae4 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event.cpp @@ -0,0 +1,427 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Checks if the bot can hear the event + */ +void CCSBot::OnAudibleEvent( IGameEvent *event, CBasePlayer *player, float range, PriorityType priority, bool isHostile, bool isFootstep, const Vector *actualOrigin ) +{ + /// @todo Listen to non-player sounds + if (player == NULL) + return; + + // don't pay attention to noise that friends make + if (!IsEnemy( player )) + return; + + Vector playerOrigin = GetCentroid( player ); + Vector myOrigin = GetCentroid( this ); + + // If the event occurs far from the triggering player, it may override the origin + if ( actualOrigin ) + { + playerOrigin = *actualOrigin; + } + + // check if noise is close enough for us to hear + const Vector *newNoisePosition = &playerOrigin; + float newNoiseDist = (myOrigin - *newNoisePosition).Length(); + if (newNoiseDist < range) + { + // we heard the sound + if ((IsLocalPlayerWatchingMe() && cv_bot_debug.GetInt() == 3) || cv_bot_debug.GetInt() == 4) + { + PrintIfWatched( "Heard noise (%s from %s, pri %s, time %3.1f)\n", + (FStrEq( "weapon_fire", event->GetName() )) ? "Weapon fire " : "", + (player) ? player->GetPlayerName() : "NULL", + (priority == PRIORITY_HIGH) ? "HIGH" : ((priority == PRIORITY_MEDIUM) ? "MEDIUM" : "LOW"), + gpGlobals->curtime ); + } + + // should we pay attention to it + // if noise timestamp is zero, there is no prior noise + if (m_noiseTimestamp > 0.0f) + { + // only overwrite recent sound if we are louder (closer), or more important - if old noise was long ago, its faded + const float shortTermMemoryTime = 3.0f; + if (gpGlobals->curtime - m_noiseTimestamp < shortTermMemoryTime) + { + // prior noise is more important - ignore new one + if (priority < m_noisePriority) + return; + + float oldNoiseDist = (myOrigin - m_noisePosition).Length(); + if (newNoiseDist >= oldNoiseDist) + return; + } + } + + // find the area in which the noise occured + /// @todo Better handle when noise occurs off the nav mesh + /// @todo Make sure noise area is not through a wall or ceiling from source of noise + /// @todo Change GetNavTravelTime to better deal with NULL destination areas + CNavArea *noiseArea = TheNavMesh->GetNearestNavArea( *newNoisePosition ); + if (noiseArea == NULL) + { + PrintIfWatched( " *** Noise occurred off the nav mesh - ignoring!\n" ); + return; + } + + m_noiseArea = noiseArea; + + // remember noise priority + m_noisePriority = priority; + + // randomize noise position in the area a bit - hearing isn't very accurate + // the closer the noise is, the more accurate our placement + /// @todo Make sure not to pick a position on the opposite side of ourselves. + const float maxErrorRadius = 400.0f; + const float maxHearingRange = 2000.0f; + float errorRadius = maxErrorRadius * newNoiseDist/maxHearingRange; + + m_noisePosition.x = newNoisePosition->x + RandomFloat( -errorRadius, errorRadius ); + m_noisePosition.y = newNoisePosition->y + RandomFloat( -errorRadius, errorRadius ); + + // note the *travel distance* to the noise + m_noiseTravelDistance = GetTravelDistanceToPlayer( (CCSPlayer *)player ); + + // make sure noise position remains in the same area + m_noiseArea->GetClosestPointOnArea( m_noisePosition, &m_noisePosition ); + + // note when we heard the noise + m_noiseTimestamp = gpGlobals->curtime; + + // if we hear a nearby enemy, become alert + const float nearbyNoiseRange = 1000.0f; + if (m_noiseTravelDistance < nearbyNoiseRange && m_noiseTravelDistance > 0.0f) + { + BecomeAlert(); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnHEGrenadeDetonate( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 99999.0f, PRIORITY_HIGH, true ); // hegrenade_detonate +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnFlashbangDetonate( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1000.0f, PRIORITY_LOW, true ); // flashbang_detonate +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnSmokeGrenadeDetonate( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1000.0f, PRIORITY_LOW, true ); // smokegrenade_detonate +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnGrenadeBounce( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 500.0f, PRIORITY_LOW, true ); // grenade_bounce +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBulletImpact( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // Construct an origin for the sound, since it can be far from the originating player + Vector actualOrigin; + actualOrigin.x = event->GetFloat( "x", 0.0f ); + actualOrigin.y = event->GetFloat( "y", 0.0f ); + actualOrigin.z = event->GetFloat( "z", 0.0f ); + + /// @todo Ignoring bullet impact events for now - we dont want bots to look directly at them! + //OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, true, false, &actualOrigin ); // bullet_impact +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBreakProp( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, true ); // break_prop +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBreakBreakable( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, true ); // break_glass +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnDoorMoving( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, false ); // door_moving +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnHostageFollows( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // player_follows needs a player + if (player == NULL) + return; + + // don't pay attention to noise that friends make + if (!IsEnemy( player )) + return; + + Vector playerOrigin = GetCentroid( player ); + Vector myOrigin = GetCentroid( this ); + const float range = 1200.0f; + + // this is here so T's not only act on the noise, but look at it, too + if (GetTeamNumber() == TEAM_TERRORIST) + { + // make sure we can hear the noise + if ((playerOrigin - myOrigin).IsLengthGreaterThan( range )) + return; + + // tell our teammates that the hostages are being taken + GetChatter()->HostagesBeingTaken(); + + // only move if we hear them being rescued and can't see any hostages + if (GetGameState()->GetNearestVisibleFreeHostage() == NULL) + { + // since we are guarding the hostages, presumably we know where they are + // if we're close enough to "hear" this event, either go to where the event occured, + // or head for an escape zone to head them off + if (GetTask() != CCSBot::GUARD_HOSTAGE_RESCUE_ZONE) + { + //const float headOffChance = 33.3f; + if (true) // || RandomFloat( 0, 100 ) < headOffChance) + { + // head them off at a rescue zone + if (GuardRandomZone()) + { + SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + PrintIfWatched( "Trying to beat them to an escape zone!\n" ); + } + } + else + { + SetTask( SEEK_AND_DESTROY ); + StandUp(); + Run(); + MoveTo( playerOrigin, FASTEST_ROUTE ); + } + } + } + } + else + { + // CT's don't care about this noise + return; + } + + OnAudibleEvent( event, player, range, PRIORITY_MEDIUM, false ); // hostage_follows +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnRoundEnd( IGameEvent *event ) +{ + // Morale adjustments happen even for dead players + int winner = event->GetInt( "winner" ); + switch ( winner ) + { + case WINNER_TER: + if (GetTeamNumber() == TEAM_CT) + { + DecreaseMorale(); + } + else + { + IncreaseMorale(); + } + break; + + case WINNER_CT: + if (GetTeamNumber() == TEAM_CT) + { + IncreaseMorale(); + } + else + { + DecreaseMorale(); + } + break; + + default: + break; + } + + m_gameState.OnRoundEnd( event ); + + if ( !IsAlive() ) + return; + + if ( event->GetInt( "winner" ) == WINNER_TER ) + { + if (GetTeamNumber() == TEAM_TERRORIST) + GetChatter()->CelebrateWin(); + } + else if ( event->GetInt( "winner" ) == WINNER_CT ) + { + if (GetTeamNumber() == TEAM_CT) + GetChatter()->CelebrateWin(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnRoundStart( IGameEvent *event ) +{ + m_gameState.OnRoundStart( event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnHostageRescuedAll( IGameEvent *event ) +{ + m_gameState.OnHostageRescuedAll( event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnNavBlocked( IGameEvent *event ) +{ + if ( event->GetBool( "blocked" ) ) + { + unsigned int areaID = event->GetInt( "area" ); + if ( areaID ) + { + // An area was blocked off. Reset our path if it has this area on it. + for( int i=0; i<m_pathLength; ++i ) + { + const ConnectInfo *info = &m_path[ i ]; + if ( info->area && info->area->GetID() == areaID ) + { + DestroyPath(); + return; + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when bot enters a nav area + */ +void CCSBot::OnEnteredNavArea( CNavArea *newArea ) +{ + // assume that we "clear" an area of enemies when we enter it + newArea->SetClearedTimestamp( GetTeamNumber()-1 ); + + // if we just entered a 'stop' area, set the flag + if ( newArea->GetAttributes() & NAV_MESH_STOP ) + { + m_isStopping = true; + } + + /// @todo Flag these areas as spawn areas during load + if (IsAtEnemySpawn()) + { + m_hasVisitedEnemySpawn = true; + } +} diff --git a/game/server/cstrike/bot/cs_bot_event_bomb.cpp b/game/server/cstrike/bot/cs_bot_event_bomb.cpp new file mode 100644 index 0000000..b1dd75d --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event_bomb.cpp @@ -0,0 +1,158 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombPickedUp( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + if (GetTeamNumber() == TEAM_CT && player) + { + // check if we're close enough to hear it + const float bombPickupHearRangeSq = 1000.0f * 1000.0f; + Vector myOrigin = GetCentroid( this ); + + if ((myOrigin - player->GetAbsOrigin()).LengthSqr() < bombPickupHearRangeSq) + { + GetChatter()->TheyPickedUpTheBomb(); + GetGameState()->UpdateBomber( player->GetAbsOrigin() ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombPlanted( IGameEvent *event ) +{ + m_gameState.OnBombPlanted( event ); + + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // if we're a TEAM_CT, forget what we're doing and go after the bomb + if (GetTeamNumber() == TEAM_CT) + { + Idle(); + } + + // if we are following someone, stop following + if (IsFollowing()) + { + StopFollowing(); + Idle(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombBeep( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + CBaseEntity *entity = UTIL_EntityByIndex( event->GetInt( "entindex" ) ); + Vector myOrigin = GetCentroid( this ); + + // if we don't know where the bomb is, but heard it beep, we've discovered it + if (GetGameState()->IsPlantedBombLocationKnown() == false && entity) + { + // check if we're close enough to hear it + const float bombBeepHearRangeSq = 1500.0f * 1500.0f; + if ((myOrigin - entity->GetAbsOrigin()).LengthSqr() < bombBeepHearRangeSq) + { + // radio the news to our team + if (GetTeamNumber() == TEAM_CT && GetGameState()->GetPlantedBombsite() == CSGameState::UNKNOWN) + { + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( entity->GetAbsOrigin() ); + if (zone) + GetChatter()->FoundPlantedBomb( zone->m_index ); + } + + // remember where the bomb is + GetGameState()->UpdatePlantedBomb( entity->GetAbsOrigin() ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombDefuseBegin( IGameEvent *event ) +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombDefused( IGameEvent *event ) +{ + m_gameState.OnBombDefused( event ); + + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + if (GetTeamNumber() == TEAM_CT) + { + if (TheCSBots()->GetBombTimeLeft() < 2.0f) + GetChatter()->Say( "BarelyDefused" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombDefuseAbort( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + PrintIfWatched( "BOMB DEFUSE ABORTED\n" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombExploded( IGameEvent *event ) +{ + m_gameState.OnBombExploded( event ); +} + + + diff --git a/game/server/cstrike/bot/cs_bot_event_player.cpp b/game/server/cstrike/bot/cs_bot_event_player.cpp new file mode 100644 index 0000000..d5734ab --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event_player.cpp @@ -0,0 +1,227 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerDeath( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + Vector playerOrigin = (player) ? GetCentroid( player ) : Vector( 0, 0, 0 ); + + CBasePlayer *other = UTIL_PlayerByUserId( event->GetInt( "attacker" ) ); + CBasePlayer *victim = player; + + CBasePlayer *killer = (other && other->IsPlayer()) ? static_cast<CBasePlayer *>( other ) : NULL; + + // if the human player died in the single player game, tell the team + if (CSGameRules()->IsCareer() && victim && !victim->IsBot() && victim->GetTeamNumber() == GetTeamNumber()) + { + GetChatter()->Say( "CommanderDown", 20.0f ); + } + + // keep track of the last player we killed + if (killer == this) + { + m_lastVictimID = victim ? victim->entindex() : 0; + } + + // react to teammate death + if (victim && victim->GetTeamNumber() == GetTeamNumber()) + { + // note time of death + m_friendDeathTimestamp = gpGlobals->curtime; + + // chastise friendly fire from humans + if (killer && !killer->IsBot() && killer->GetTeamNumber() == GetTeamNumber() && killer != this) + { + GetChatter()->KilledFriend(); + } + + if (IsAttacking()) + { + if (GetTimeSinceLastSawEnemy() > 0.4f) + { + PrintIfWatched( "Rethinking my attack due to teammate death\n" ); + + // allow us to sneak past windows, doors, etc + IgnoreEnemies( 1.0f ); + + // move to last known position of enemy - this could cause us to flank if + // the danger has changed due to our teammate's recent death + SetTask( MOVE_TO_LAST_KNOWN_ENEMY_POSITION, GetBotEnemy() ); + MoveTo( GetLastKnownEnemyPosition() ); + return; + } + } + else // not attacking + { + // + // If we just saw a nearby friend die, and we haven't yet acquired an enemy + // automatically acquire our dead friend's killer + // + if (GetDisposition() == ENGAGE_AND_INVESTIGATE || GetDisposition() == OPPORTUNITY_FIRE) + { + CBasePlayer *other = UTIL_PlayerByUserId( event->GetInt( "attacker" ) ); + + // check that attacker is an enemy (for friendly fire, etc) + if (other && other->IsPlayer()) + { + CCSPlayer *killer = static_cast<CCSPlayer *>( other ); + if (killer->GetTeamNumber() != GetTeamNumber()) + { + // check if we saw our friend die - dont check FOV - assume we're aware of our surroundings in combat + // snipers stay put + if (!IsSniper() && IsVisible( playerOrigin )) + { + // people are dying - we should hurry + Hurry( RandomFloat( 10.0f, 15.0f ) ); + + // if we're hiding with only our knife, be a little more cautious + const float knifeAmbushChance = 50.0f; + if (!IsHiding() || !IsUsingKnife() || RandomFloat( 0, 100 ) < knifeAmbushChance) + { + PrintIfWatched( "Attacking our friend's killer!\n" ); + Attack( killer ); + return; + } + } + + // if friend was far away and we haven't seen an enemy in awhile, go to where our friend was killed + const float longHidingTime = 20.0f; + if (IsHunting() || IsInvestigatingNoise() || (IsHiding() && GetTask() != FOLLOW && GetHidingTime() > longHidingTime)) + { + const float someTime = 10.0f; + const float farAway = 750.0f; + if (GetTimeSinceLastSawEnemy() > someTime && (playerOrigin - GetAbsOrigin()).IsLengthGreaterThan( farAway )) + { + PrintIfWatched( "Checking out where our friend was killed\n" ); + MoveTo( playerOrigin, FASTEST_ROUTE ); + return; + } + } + } + } + } + } + } + else // an enemy was killed + { + // forget our current noise - it may have come from the now dead enemy + ForgetNoise(); + + if (killer && killer->GetTeamNumber() == GetTeamNumber()) + { + // only chatter about enemy kills if we see them occur, and they were the last one we see + if (GetNearbyEnemyCount() <= 1) + { + // report if number of enemies left is few and we killed the last one we saw locally + GetChatter()->EnemiesRemaining(); + + Vector victimOrigin = (victim) ? GetCentroid( victim ) : Vector( 0, 0, 0 ); + if (IsVisible( victimOrigin, CHECK_FOV )) + { + // congratulate teammates on their kills + if (killer && killer != this) + { + float delay = RandomFloat( 2.0f, 3.0f ); + if (killer->IsBot()) + { + if (RandomFloat( 0.0f, 100.0f ) < 40.0f) + GetChatter()->Say( "NiceShot", 3.0f, delay ); + } + else + { + // humans get the honorific + if (CSGameRules()->IsCareer()) + GetChatter()->Say( "NiceShotCommander", 3.0f, delay ); + else + GetChatter()->Say( "NiceShotSir", 3.0f, delay ); + } + } + } + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerRadio( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CCSPlayer *player = ToCSPlayer( UTIL_PlayerByUserId( event->GetInt( "userid" ) ) ); + if ( player == this ) + return; + + // + // Process radio events from our team + // + if (player && player->GetTeamNumber() == GetTeamNumber() ) + { + /// @todo Distinguish between radio commands and responses + RadioType radioEvent = (RadioType)event->GetInt( "slot" ); + + if (radioEvent != RADIO_INVALID && radioEvent != RADIO_AFFIRMATIVE && radioEvent != RADIO_NEGATIVE && radioEvent != RADIO_REPORTING_IN) + { + m_lastRadioCommand = radioEvent; + m_lastRadioRecievedTimestamp = gpGlobals->curtime; + m_radioSubject = player; + m_radioPosition = GetCentroid( player ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerFallDamage( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // player_falldamage +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerFootstep( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false, IS_FOOTSTEP ); // player_footstep +} + + diff --git a/game/server/cstrike/bot/cs_bot_event_weapon.cpp b/game/server/cstrike/bot/cs_bot_event_weapon.cpp new file mode 100644 index 0000000..90f8b9f --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event_weapon.cpp @@ -0,0 +1,167 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponFire( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // for knife fighting - if our victim is attacking or reloading, rush him + /// @todo Propagate events into active state + if (GetEnemy() == player && IsUsingKnife()) + { + ForceRun( 5.0f ); + } + + const float ShortRange = 1000.0f; + const float NormalRange = 2000.0f; + + float range; + + /// @todo Check weapon type (knives are pretty quiet) + /// @todo Use actual volume, account for silencers, etc. + CWeaponCSBase *weapon = (CWeaponCSBase *)((player)?player->GetActiveWeapon():NULL); + + if (weapon == NULL) + return; + + switch( weapon->GetWeaponID() ) + { + // silent "firing" + case WEAPON_HEGRENADE: + case WEAPON_SMOKEGRENADE: + case WEAPON_FLASHBANG: + case WEAPON_SHIELDGUN: + case WEAPON_C4: + return; + + // quiet + case WEAPON_KNIFE: + case WEAPON_TMP: + range = ShortRange; + break; + + // M4A1 - check for silencer + case WEAPON_M4A1: + { + if (weapon->IsSilenced()) + { + range = ShortRange; + } + else + { + range = NormalRange; + } + break; + } + + // USP - check for silencer + case WEAPON_USP: + { + if (weapon->IsSilenced()) + { + range = ShortRange; + } + else + { + range = NormalRange; + } + break; + } + + // loud + case WEAPON_AWP: + range = 99999.0f; + break; + + // normal + default: + range = NormalRange; + break; + } + + OnAudibleEvent( event, player, range, PRIORITY_HIGH, true ); // weapon_fire +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponFireOnEmpty( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // for knife fighting - if our victim is attacking or reloading, rush him + /// @todo Propagate events into active state + if (GetEnemy() == player && IsUsingKnife()) + { + ForceRun( 5.0f ); + } + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // weapon_fire_on_empty +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponReload( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // for knife fighting - if our victim is attacking or reloading, rush him + /// @todo Propagate events into active state + if (GetEnemy() == player && IsUsingKnife()) + { + ForceRun( 5.0f ); + } + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // weapon_reload +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponZoom( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // weapon_zoom +} + + + diff --git a/game/server/cstrike/bot/cs_bot_init.cpp b/game/server/cstrike/bot/cs_bot_init.cpp new file mode 100644 index 0000000..d6a5c77 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_init.cpp @@ -0,0 +1,359 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "cs_shareddefs.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#pragma warning( disable : 4355 ) // warning 'this' used in base member initializer list - we're using it safely + + +//-------------------------------------------------------------------------------------------------------------- +static void PrefixChanged( IConVar *c, const char *oldPrefix, float flOldValue ) +{ + if ( TheCSBots() && TheCSBots()->IsServerActive() ) + { + for( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if ( !player ) + continue; + + if ( !player->IsBot() || !IsEntityValid( player ) ) + continue; + + CCSBot *bot = dynamic_cast< CCSBot * >( player ); + + if ( !bot ) + continue; + + // set the bot's name + char botName[MAX_PLAYER_NAME_LENGTH]; + UTIL_ConstructBotNetName( botName, MAX_PLAYER_NAME_LENGTH, bot->GetProfile() ); + + engine->SetFakeClientConVarValue( bot->edict(), "name", botName ); + } + } +} + + +ConVar cv_bot_traceview( "bot_traceview", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_stop( "bot_stop", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, immediately stops all bot processing." ); +ConVar cv_bot_show_nav( "bot_show_nav", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_walk( "bot_walk", "0", FCVAR_REPLICATED, "If nonzero, bots can only walk, not run." ); +ConVar cv_bot_difficulty( "bot_difficulty", "1", FCVAR_REPLICATED, "Defines the skill of bots joining the game. Values are: 0=easy, 1=normal, 2=hard, 3=expert." ); +ConVar cv_bot_debug( "bot_debug", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_debug_target( "bot_debug_target", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_quota( "bot_quota", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Determines the total number of bots in the game." ); +ConVar cv_bot_quota_mode( "bot_quota_mode", "normal", FCVAR_REPLICATED, "Determines the type of quota.\nAllowed values: 'normal', 'fill', and 'match'.\nIf 'fill', the server will adjust bots to keep N players in the game, where N is bot_quota.\nIf 'match', the server will maintain a 1:N ratio of humans to bots, where N is bot_quota." ); +ConVar cv_bot_prefix( "bot_prefix", "", FCVAR_REPLICATED, "This string is prefixed to the name of all bots that join the game.\n<difficulty> will be replaced with the bot's difficulty.\n<weaponclass> will be replaced with the bot's desired weapon class.\n<skill> will be replaced with a 0-100 representation of the bot's skill.", PrefixChanged ); +ConVar cv_bot_allow_rogues( "bot_allow_rogues", "1", FCVAR_REPLICATED, "If nonzero, bots may occasionally go 'rogue'. Rogue bots do not obey radio commands, nor pursue scenario goals." ); +ConVar cv_bot_allow_pistols( "bot_allow_pistols", "1", FCVAR_REPLICATED, "If nonzero, bots may use pistols." ); +ConVar cv_bot_allow_shotguns( "bot_allow_shotguns", "1", FCVAR_REPLICATED, "If nonzero, bots may use shotguns." ); +ConVar cv_bot_allow_sub_machine_guns( "bot_allow_sub_machine_guns", "1", FCVAR_REPLICATED, "If nonzero, bots may use sub-machine guns." ); +ConVar cv_bot_allow_rifles( "bot_allow_rifles", "1", FCVAR_REPLICATED, "If nonzero, bots may use rifles." ); +ConVar cv_bot_allow_machine_guns( "bot_allow_machine_guns", "1", FCVAR_REPLICATED, "If nonzero, bots may use the machine gun." ); +ConVar cv_bot_allow_grenades( "bot_allow_grenades", "1", FCVAR_REPLICATED, "If nonzero, bots may use grenades." ); +ConVar cv_bot_allow_snipers( "bot_allow_snipers", "1", FCVAR_REPLICATED, "If nonzero, bots may use sniper rifles." ); +#ifdef CS_SHIELD_ENABLED +ConVar cv_bot_allow_shield( "bot_allow_shield", "1", FCVAR_REPLICATED ); +#endif // CS_SHIELD_ENABLED +ConVar cv_bot_join_team( "bot_join_team", "any", FCVAR_REPLICATED, "Determines the team bots will join into. Allowed values: 'any', 'T', or 'CT'." ); +ConVar cv_bot_join_after_player( "bot_join_after_player", "1", FCVAR_REPLICATED, "If nonzero, bots wait until a player joins before entering the game." ); +ConVar cv_bot_auto_vacate( "bot_auto_vacate", "1", FCVAR_REPLICATED, "If nonzero, bots will automatically leave to make room for human players." ); +ConVar cv_bot_zombie( "bot_zombie", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, bots will stay in idle mode and not attack." ); +ConVar cv_bot_defer_to_human( "bot_defer_to_human", "0", FCVAR_REPLICATED, "If nonzero and there is a human on the team, the bots will not do the scenario tasks." ); +ConVar cv_bot_chatter( "bot_chatter", "normal", FCVAR_REPLICATED, "Control how bots talk. Allowed values: 'off', 'radio', 'minimal', or 'normal'." ); +ConVar cv_bot_profile_db( "bot_profile_db", "BotProfile.db", FCVAR_REPLICATED, "The filename from which bot profiles will be read." ); +ConVar cv_bot_dont_shoot( "bot_dont_shoot", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, bots will not fire weapons (for debugging)." ); +ConVar cv_bot_eco_limit( "bot_eco_limit", "2000", FCVAR_REPLICATED, "If nonzero, bots will not buy if their money falls below this amount." ); +ConVar cv_bot_auto_follow( "bot_auto_follow", "0", FCVAR_REPLICATED, "If nonzero, bots with high co-op may automatically follow a nearby human player." ); +ConVar cv_bot_flipout( "bot_flipout", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, bots use no CPU for AI. Instead, they run around randomly." ); + + +extern void FinishClientPutInServer( CCSPlayer *pPlayer ); + + +//-------------------------------------------------------------------------------------------------------------- +// Engine callback for custom server commands +void Bot_ServerCommand( void ) +{ +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor + */ +CCSBot::CCSBot( void ) : m_chatter( this ), m_gameState( this ) +{ + m_hasJoined = false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Destructor + */ +CCSBot::~CCSBot() +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Prepare bot for action + */ +bool CCSBot::Initialize( const BotProfile *profile, int team ) +{ + // extend + BaseClass::Initialize( profile, team ); + + // CS bot initialization + m_diedLastRound = false; + m_morale = POSITIVE; // starting a new round makes everyone a little happy + + m_combatRange = RandomFloat( 325.0f, 425.0f ); + + // set initial safe time guess for this map + m_safeTime = 15.0f + 5.0f * GetProfile()->GetAggression(); + + m_name[0] = '\000'; + + ResetValues(); + + m_desiredTeam = team; + + if (GetTeamNumber() == 0) + { + HandleCommand_JoinTeam( m_desiredTeam ); + int desiredClass = GetProfile()->GetSkin(); + if ( m_desiredTeam == TEAM_CT && desiredClass ) + { + desiredClass = FIRST_CT_CLASS + desiredClass - 1; + } + else if ( m_desiredTeam == TEAM_TERRORIST && desiredClass ) + { + desiredClass = FIRST_T_CLASS + desiredClass - 1; + } + HandleCommand_JoinClass( desiredClass ); + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset internal data to initial state + */ +void CCSBot::ResetValues( void ) +{ + m_chatter.Reset(); + m_gameState.Reset(); + + m_avoid = NULL; + m_avoidTimestamp = 0.0f; + + m_hurryTimer.Invalidate(); + m_alertTimer.Invalidate(); + m_sneakTimer.Invalidate(); + m_noiseBendTimer.Invalidate(); + m_bendNoisePositionValid = false; + + m_isStuck = false; + m_stuckTimestamp = 0.0f; + m_wiggleTimer.Invalidate(); + m_stuckJumpTimer.Invalidate(); + + m_pathLength = 0; + m_pathIndex = 0; + m_areaEnteredTimestamp = 0.0f; + m_currentArea = NULL; + m_lastKnownArea = NULL; + m_isStopping = false; + + m_avoidFriendTimer.Invalidate(); + m_isFriendInTheWay = false; + m_isWaitingBehindFriend = false; + m_isAvoidingGrenade.Invalidate(); + + StopPanicking(); + + m_disposition = ENGAGE_AND_INVESTIGATE; + + m_enemy = NULL; + + m_grenadeTossState = NOT_THROWING; + m_initialEncounterArea = NULL; + + m_wasSafe = true; + + m_nearbyEnemyCount = 0; + m_enemyPlace = 0; + m_nearbyFriendCount = 0; + m_closestVisibleFriend = NULL; + m_closestVisibleHumanFriend = NULL; + + for( int w=0; w<MAX_PLAYERS; ++w ) + { + m_watchInfo[w].timestamp = 0.0f; + m_watchInfo[w].isEnemy = false; + + m_playerTravelDistance[ w ] = -1.0f; + } + + // randomly offset each bot's timer to spread computation out + m_updateTravelDistanceTimer.Start( RandomFloat( 0.0f, 0.9f ) ); + m_travelDistancePhase = 0; + + m_isEnemyVisible = false; + m_visibleEnemyParts = NONE; + m_lastSawEnemyTimestamp = -999.9f; + m_firstSawEnemyTimestamp = 0.0f; + m_currentEnemyAcquireTimestamp = 0.0f; + m_isLastEnemyDead = true; + m_attacker = NULL; + m_attackedTimestamp = 0.0f; + m_enemyDeathTimestamp = 0.0f; + m_friendDeathTimestamp = 0.0f; + m_lastVictimID = 0; + m_isAimingAtEnemy = false; + m_fireWeaponTimestamp = 0.0f; + m_equipTimer.Invalidate(); + m_zoomTimer.Invalidate(); + + m_isFollowing = false; + m_leader = NULL; + m_followTimestamp = 0.0f; + m_allowAutoFollowTime = 0.0f; + + m_enemyQueueIndex = 0; + m_enemyQueueCount = 0; + m_enemyQueueAttendIndex = 0; + m_bomber = NULL; + + m_isEnemySniperVisible = false; + m_sawEnemySniperTimer.Invalidate(); + + m_lookAroundStateTimestamp = 0.0f; + m_inhibitLookAroundTimestamp = 0.0f; + + m_lookPitch = 0.0f; + m_lookPitchVel = 0.0f; + m_lookYaw = 0.0f; + m_lookYawVel = 0.0f; + + m_aimOffsetTimestamp = 0.0f; + m_aimSpreadTimestamp = 0.0f; + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + + for( int p=0; p<MAX_PLAYERS; ++p ) + { + m_partInfo[p].m_validFrame = 0; + } + + m_spotEncounter = NULL; + m_spotCheckTimestamp = 0.0f; + m_peripheralTimestamp = 0.0f; + + m_avgVelIndex = 0; + m_avgVelCount = 0; + + m_lastOrigin = GetCentroid( this ); + + m_lastRadioCommand = RADIO_INVALID; + m_lastRadioRecievedTimestamp = 0.0f; + m_lastRadioSentTimestamp = 0.0f; + m_radioSubject = NULL; + m_voiceEndTimestamp = 0.0f; + + m_hostageEscortCount = 0; + m_hostageEscortCountTimestamp = 0.0f; + + m_noisePosition = Vector( 0, 0, 0 ); + m_noiseTimestamp = 0.0f; + + m_stateTimestamp = 0.0f; + m_task = SEEK_AND_DESTROY; + m_taskEntity = NULL; + + m_approachPointCount = 0; + m_approachPointViewPosition.x = 99999999999.9f; + m_approachPointViewPosition.y = 0.0f; + m_approachPointViewPosition.z = 0.0f; + + m_checkedHidingSpotCount = 0; + + StandUp(); + Run(); + m_mustRunTimer.Invalidate(); + m_waitTimer.Invalidate(); + m_pathLadder = NULL; + + m_repathTimer.Invalidate(); + + m_huntState.ClearHuntArea(); + m_hasVisitedEnemySpawn = false; + m_stillTimer.Invalidate(); + + // adjust morale - if we died, our morale decreased, + // but if we live, no adjustement (round win/loss also adjusts morale) + if (m_diedLastRound) + DecreaseMorale(); + + m_diedLastRound = false; + + + // IsRogue() randomly changes this + m_isRogue = false; + + m_surpriseTimer.Invalidate(); + + // even though these are EHANDLEs, they need to be NULL-ed + m_goalEntity = NULL; + m_avoid = NULL; + m_enemy = NULL; + + for ( int i=0; i<MAX_ENEMY_QUEUE; ++i ) + { + m_enemyQueue[i].player = NULL; + m_enemyQueue[i].isReloading = false; + m_enemyQueue[i].isProtectedByShield = false; + } + + // start in idle state + m_isOpeningDoor = false; + StopAttacking(); + Idle(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called when bot is placed in map, and when bots are reset after a round ends. + * NOTE: For some reason, this can be called twice when a bot is added. + */ +void CCSBot::Spawn( void ) +{ + // do the normal player spawn process + BaseClass::Spawn(); + + ResetValues(); + + V_strcpy_safe( m_name, GetPlayerName() ); + + Buy(); +} + diff --git a/game/server/cstrike/bot/cs_bot_listen.cpp b/game/server/cstrike/bot/cs_bot_listen.cpp new file mode 100644 index 0000000..54e47e2 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_listen.cpp @@ -0,0 +1,230 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +bool CCSBot::IsNoiseHeard( void ) const +{ + if (m_noiseTimestamp <= 0.0f) + return false; + + // primitive reaction time simulation - cannot "hear" noise until reaction time has elapsed + if (gpGlobals->curtime - m_noiseTimestamp >= GetProfile()->GetReactionTime()) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Listen for enemy noises, and determine if we should react to them. + * Returns true if heard a noise and should move to investigate. + */ +bool CCSBot::HeardInterestingNoise( void ) +{ + if (IsBlind()) + return false; + + // don't investigate noises during safe time + if (!IsWellPastSafe()) + return false; + + // if our disposition is not to investigate, dont investigate + if (GetDisposition() != ENGAGE_AND_INVESTIGATE) + return false; + + // listen for enemy noises + if (IsNoiseHeard()) + { + // if we are hiding, only react to noises very nearby, depending on how aggressive we are + if (IsAtHidingSpot() && GetNoiseRange() > 100.0f + 400.0f * GetProfile()->GetAggression()) + return false; + + // chance of investigating is inversely proportional to distance + const float maxNoiseDist = 3000.0f; + float chance = 100.0f * (1.0f - (GetNoiseRange()/maxNoiseDist)); + + // modify chance by number of friends remaining + // if we have lots of friends, presumably one of them is closer and will check it out + if (GetFriendsRemaining() >= 3) + { + float friendFactor = 5.0f * GetFriendsRemaining(); + if (friendFactor > 50.0f) + friendFactor = 50.0f; + + chance -= friendFactor; + } + + if (RandomFloat( 0.0f, 100.0f ) <= chance) + { + return true; + } + } + + return false; +} + + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we hear nearby threatening enemy gunfire within given range + * -1 == infinite range + */ +bool CCSBot::CanHearNearbyEnemyGunfire( float range ) const +{ + Vector myOrigin = GetCentroid( this ); + + // only attend to noise if it just happened + if (gpGlobals->curtime - m_noiseTimestamp > 0.5f) + return false; + + // gunfire is high priority + if (m_noisePriority < PRIORITY_HIGH) + return false; + + // check noise range + if (range > 0.0f && (myOrigin - m_noisePosition).IsLengthGreaterThan( range )) + return false; + + // if we dont have line of sight, it's not threatening (cant get shot) + if (!CanSeeNoisePosition()) + return false; + + if (IsAttacking() && m_enemy != NULL && GetTimeSinceLastSawEnemy() < 1.0f) + { + // gunfire is only threatening if it is closer than our current enemy + float gunfireDistSq = (m_noisePosition - myOrigin).LengthSqr(); + float enemyDistSq = (GetCentroid( m_enemy ) - myOrigin).LengthSqr(); + const float muchCloserSq = 100.0f * 100.0f; + if (gunfireDistSq > enemyDistSq - muchCloserSq) + return false; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we directly see where we think the noise came from + * NOTE: Dont check FOV, since this is used to determine if we should turn our head to look at the noise + * NOTE: Dont use IsVisible(), because smoke shouldnt cause us to not look toward noises + */ +bool CCSBot::CanSeeNoisePosition( void ) const +{ + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( this, COLLISION_GROUP_NONE ); + UTIL_TraceLine( EyePositionConst(), m_noisePosition + Vector( 0, 0, HalfHumanHeight ), MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + if (result.fraction == 1.0f) + { + // we can see the source of the noise + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we decided to look towards the most recent noise source + * Assumes m_noisePosition is valid. + */ +bool CCSBot::UpdateLookAtNoise( void ) +{ + // make sure a noise exists + if (!IsNoiseHeard()) + { + return false; + } + + Vector spot; + + // if we have clear line of sight to noise position, look directly at it + if (CanSeeNoisePosition()) + { + /// @todo adjust noise Z to keep consistent with current height while fighting + spot = m_noisePosition + Vector( 0, 0, HalfHumanHeight ); + + // since we can see the noise spot, forget about it + ForgetNoise(); + } + else + { + // line of sight is blocked, bend it + + // the bending algorithm is very expensive, throttle how often it is done + if (m_noiseBendTimer.IsElapsed()) + { + const float noiseBendLOSInterval = RandomFloat( 0.2f, 0.3f ); + m_noiseBendTimer.Start( noiseBendLOSInterval ); + + // line of sight is blocked, bend it + if (BendLineOfSight( EyePosition(), m_noisePosition, &spot ) == false) + { + m_bendNoisePositionValid = false; + return false; + } + + m_bentNoisePosition = spot; + m_bendNoisePositionValid = true; + } + else if (m_bendNoisePositionValid) + { + // use result of prior bend computation + spot = m_bentNoisePosition; + } + else + { + // prior bend failed + return false; + } + } + + // it's always important to look at enemy noises, because they come from ... enemies! + PriorityType pri = PRIORITY_HIGH; + + // look longer if we're hiding + if (IsAtHidingSpot()) + { + // if there is only one enemy left, look for a long time + if (GetEnemiesRemaining() == 1) + { + SetLookAt( "Noise", spot, pri, RandomFloat( 5.0f, 15.0f ), true ); + } + else + { + SetLookAt( "Noise", spot, pri, RandomFloat( 3.0f, 5.0f ), true ); + } + } + else + { + const float closeRange = 500.0f; + if (GetNoiseRange() < closeRange) + { + // look at nearby enemy noises for a longer time + SetLookAt( "Noise", spot, pri, RandomFloat( 3.0f, 5.0f ), true ); + } + else + { + SetLookAt( "Noise", spot, pri, RandomFloat( 1.0f, 2.0f ), true ); + } + } + + return true; +} + diff --git a/game/server/cstrike/bot/cs_bot_manager.cpp b/game/server/cstrike/bot/cs_bot_manager.cpp new file mode 100644 index 0000000..aec0ef3 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_manager.cpp @@ -0,0 +1,2390 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#pragma warning( disable : 4530 ) // STL uses exceptions, but we are not compiling with them - ignore warning + +#include "cbase.h" + +#include "cs_bot.h" +#include "nav_area.h" +#include "cs_gamerules.h" +#include "shared_util.h" +#include "KeyValues.h" +#include "tier0/icommandline.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 + +CBotManager *TheBots = NULL; + +bool CCSBotManager::m_isMapDataLoaded = false; + +int g_nClientPutInServerOverrides = 0; + + +void DrawOccupyTime( void ); +ConVar bot_show_occupy_time( "bot_show_occupy_time", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show when each nav area can first be reached by each team." ); + +void DrawBattlefront( void ); +ConVar bot_show_battlefront( "bot_show_battlefront", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show areas where rushing players will initially meet." ); + +int UTIL_CSSBotsInGame( void ); + +ConVar bot_join_delay( "bot_join_delay", "0", FCVAR_GAMEDLL, "Prevents bots from joining the server for this many seconds after a map change." ); + +/** + * Determine whether bots can be used or not + */ +inline bool AreBotsAllowed() +{ + // If they pass in -nobots, don't allow bots. This is for people who host servers, to + // allow them to disallow bots to enforce CPU limits. + const char *nobots = CommandLine()->CheckParm( "-nobots" ); + if ( nobots ) + { + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +void InstallBotControl( void ) +{ + if ( TheBots != NULL ) + delete TheBots; + + TheBots = new CCSBotManager; +} + + +//-------------------------------------------------------------------------------------------------------------- +void RemoveBotControl( void ) +{ + if ( TheBots != NULL ) + delete TheBots; + + TheBots = NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +CBasePlayer* ClientPutInServerOverride_Bot( edict_t *pEdict, const char *playername ) +{ + CBasePlayer *pPlayer = TheBots->AllocateAndBindBotEntity( pEdict ); + if ( pPlayer ) + { + pPlayer->SetPlayerName( playername ); + } + ++g_nClientPutInServerOverrides; + + return pPlayer; +} + +//-------------------------------------------------------------------------------------------------------------- +// Constructor +CCSBotManager::CCSBotManager() +{ + m_zoneCount = 0; + SetLooseBomb( NULL ); + m_serverActive = false; + + m_isBombPlanted = false; + m_bombDefuser = NULL; + m_roundStartTimestamp = 0.0f; + + m_eventListenersEnabled = true; + m_commonEventListeners.AddToTail( &m_PlayerFootstepEvent ); + m_commonEventListeners.AddToTail( &m_PlayerRadioEvent ); + m_commonEventListeners.AddToTail( &m_PlayerFallDamageEvent ); + m_commonEventListeners.AddToTail( &m_BombBeepEvent ); + m_commonEventListeners.AddToTail( &m_DoorMovingEvent ); + m_commonEventListeners.AddToTail( &m_BreakPropEvent ); + m_commonEventListeners.AddToTail( &m_BreakBreakableEvent ); + m_commonEventListeners.AddToTail( &m_WeaponFireEvent ); + m_commonEventListeners.AddToTail( &m_WeaponFireOnEmptyEvent ); + m_commonEventListeners.AddToTail( &m_WeaponReloadEvent ); + m_commonEventListeners.AddToTail( &m_WeaponZoomEvent ); + m_commonEventListeners.AddToTail( &m_BulletImpactEvent ); + m_commonEventListeners.AddToTail( &m_GrenadeBounceEvent ); + m_commonEventListeners.AddToTail( &m_NavBlockedEvent ); + + TheBotPhrases = new BotPhraseManager; + TheBotProfiles = new BotProfileManager; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when a new round begins + */ +void CCSBotManager::RestartRound( void ) +{ + // extend + CBotManager::RestartRound(); + + SetLooseBomb( NULL ); + m_isBombPlanted = false; + m_earliestBombPlantTimestamp = gpGlobals->curtime + RandomFloat( 10.0f, 30.0f ); // 60 + m_bombDefuser = NULL; + + ResetRadioMessageTimestamps(); + + m_lastSeenEnemyTimestamp = -9999.9f; + + m_roundStartTimestamp = gpGlobals->curtime + mp_freezetime.GetFloat(); + + // randomly decide if defensive team wants to "rush" as a whole + const float defenseRushChance = 33.3f; // 25.0f; + m_isDefenseRushing = (RandomFloat( 0.0f, 100.0f ) <= defenseRushChance) ? true : false; + + TheBotPhrases->OnRoundRestart(); + + m_isRoundOver = false; +} + +//-------------------------------------------------------------------------------------------------------------- + +void UTIL_DrawBox( Extent *extent, int lifetime, int red, int green, int blue ) +{ + int darkRed = red/2; + int darkGreen = green/2; + int darkBlue = blue/2; + + Vector v[8]; + v[0].x = extent->lo.x; v[0].y = extent->lo.y; v[0].z = extent->lo.z; + v[1].x = extent->hi.x; v[1].y = extent->lo.y; v[1].z = extent->lo.z; + v[2].x = extent->hi.x; v[2].y = extent->hi.y; v[2].z = extent->lo.z; + v[3].x = extent->lo.x; v[3].y = extent->hi.y; v[3].z = extent->lo.z; + v[4].x = extent->lo.x; v[4].y = extent->lo.y; v[4].z = extent->hi.z; + v[5].x = extent->hi.x; v[5].y = extent->lo.y; v[5].z = extent->hi.z; + v[6].x = extent->hi.x; v[6].y = extent->hi.y; v[6].z = extent->hi.z; + v[7].x = extent->lo.x; v[7].y = extent->hi.y; v[7].z = extent->hi.z; + + static int edge[] = + { + 1, 2, 3, 4, -1, + 5, 6, 7, 8, -5, + 1, -5, + 2, -6, + 3, -7, + 4, -8, + 0 + }; + + Vector from, to; + bool restart = true; + for( int i=0; edge[i] != 0; ++i ) + { + if (restart) + { + to = v[ edge[i]-1 ]; + restart = false; + continue; + } + + from = to; + + int index = edge[i]; + if (index < 0) + { + restart = true; + index = -index; + } + + to = v[ index-1 ]; + + NDebugOverlay::Line( from, to, darkRed, darkGreen, darkBlue, true, 0.1f ); + NDebugOverlay::Line( from, to, red, green, blue, false, 0.15f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::EnableEventListeners( bool enable ) +{ + if ( m_eventListenersEnabled == enable ) + { + return; + } + + m_eventListenersEnabled = enable; + + // enable/disable the most frequent event listeners, to improve performance when no bots are present. + for ( int i=0; i<m_commonEventListeners.Count(); ++i ) + { + if ( enable ) + { + gameeventmanager->AddListener( m_commonEventListeners[i], m_commonEventListeners[i]->GetEventName(), true ); + } + else + { + gameeventmanager->RemoveListener( m_commonEventListeners[i] ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called each frame + */ +void CCSBotManager::StartFrame( void ) +{ + if ( !AreBotsAllowed() ) + { + EnableEventListeners( false ); + return; + } + + // EXTEND + CBotManager::StartFrame(); + + MaintainBotQuota(); + EnableEventListeners( UTIL_CSSBotsInGame() > 0 ); + + // debug zone extent visualization + if (cv_bot_debug.GetInt() == 5) + { + for( int z=0; z<m_zoneCount; ++z ) + { + Zone *zone = &m_zone[z]; + + if ( zone->m_isBlocked ) + { + UTIL_DrawBox( &zone->m_extent, 1, 255, 0, 200 ); + } + else + { + UTIL_DrawBox( &zone->m_extent, 1, 255, 100, 0 ); + } + } + } + + if (bot_show_occupy_time.GetBool()) + { + DrawOccupyTime(); + } + + if (bot_show_battlefront.GetBool()) + { + DrawBattlefront(); + } + + if ( m_checkTransientAreasTimer.IsElapsed() && !nav_edit.GetBool() ) + { + CUtlVector< CNavArea * >& transientAreas = TheNavMesh->GetTransientAreas(); + for ( int i=0; i<transientAreas.Count(); ++i ) + { + CNavArea *area = transientAreas[i]; + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + area->UpdateBlocked(); + } + } + + m_checkTransientAreasTimer.Start( 2.0f ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the bot can use this weapon + */ +bool CCSBotManager::IsWeaponUseable( const CWeaponCSBase *weapon ) const +{ + if (weapon == NULL) + return false; + + if (weapon->IsA( WEAPON_C4 )) + return true; + + if ((!AllowShotguns() && weapon->IsKindOf( WEAPONTYPE_SHOTGUN )) || + (!AllowMachineGuns() && weapon->IsKindOf( WEAPONTYPE_MACHINEGUN )) || + (!AllowRifles() && weapon->IsKindOf( WEAPONTYPE_RIFLE )) || + (!AllowShotguns() && weapon->IsKindOf( WEAPONTYPE_SHOTGUN )) || + (!AllowSnipers() && weapon->IsKindOf( WEAPONTYPE_SNIPER_RIFLE )) || + (!AllowSubMachineGuns() && weapon->IsKindOf( WEAPONTYPE_SUBMACHINEGUN )) || + (!AllowPistols() && weapon->IsKindOf( WEAPONTYPE_PISTOL )) || + (!AllowGrenades() && weapon->IsKindOf( WEAPONTYPE_GRENADE ))) + { + return false; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this player is on "defense" + */ +bool CCSBotManager::IsOnDefense( const CCSPlayer *player ) const +{ + switch (GetScenario()) + { + case SCENARIO_DEFUSE_BOMB: + return (player->GetTeamNumber() == TEAM_CT); + + case SCENARIO_RESCUE_HOSTAGES: + return (player->GetTeamNumber() == TEAM_TERRORIST); + + case SCENARIO_ESCORT_VIP: + return (player->GetTeamNumber() == TEAM_TERRORIST); + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this player is on "offense" + */ +bool CCSBotManager::IsOnOffense( const CCSPlayer *player ) const +{ + return !IsOnDefense( player ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when a map has just been loaded + */ +void CCSBotManager::ServerActivate( void ) +{ + m_isMapDataLoaded = false; + + // load the database of bot radio chatter + TheBotPhrases->Reset(); + TheBotPhrases->Initialize( "BotChatter.db", 0 ); + + TheBotProfiles->Reset(); + TheBotProfiles->FindVoiceBankIndex( "BotChatter.db" ); // make sure default voice bank is first + const char *filename; + if ( false ) // g_engfuncs.pfnIsCareerMatch() ) + { + filename = "MissionPacks/BotPackList.db"; + } + else + { + filename = "BotPackList.db"; + } + + // read in the list of bot profile DBs + FileHandle_t file = filesystem->Open( filename, "r" ); + + if ( !file ) + { + TheBotProfiles->Init( "BotProfile.db" ); + } + else + { + int dataLength = filesystem->Size( filename ); + char *dataPointer = new char[ dataLength ]; + + filesystem->Read( dataPointer, dataLength, file ); + filesystem->Close( file ); + + const char *dataFile = SharedParse( dataPointer ); + const char *token; + + while ( dataFile ) + { + token = SharedGetToken(); + char *clone = CloneString( token ); + TheBotProfiles->Init( clone ); + delete[] clone; + dataFile = SharedParse( dataFile ); + } + + delete [] dataPointer; + } + + // Now that we've parsed all the profiles, we have a list of the voice banks they're using. + // Go back and parse the custom voice speakables. + const BotProfileManager::VoiceBankList *voiceBanks = TheBotProfiles->GetVoiceBanks(); + for ( int i=1; i<voiceBanks->Count(); ++i ) + { + TheBotPhrases->Initialize( (*voiceBanks)[i], i ); + } + + // tell the Navigation Mesh system what CS spawn points are named + TheNavMesh->SetPlayerSpawnName( "info_player_terrorist" ); + + ExtractScenarioData(); + + RestartRound(); + + TheBotPhrases->OnMapChange(); + + m_serverActive = true; +} + + +void CCSBotManager::ServerDeactivate( void ) +{ + m_serverActive = false; +} + +void CCSBotManager::ClientDisconnect( CBaseEntity *entity ) +{ +/* + if ( FBitSet( entity->GetFlags(), FL_FAKECLIENT ) ) + { + FREE_PRIVATE( entity ); + } +*/ + + /* + // make sure voice feedback is turned off + CBasePlayer *pPlayer = (CBasePlayer *)CBaseEntity::Instance( pEntity ); + if ( pPlayer && pPlayer->IsBot() ) + { + CCSBot *pBot = static_cast<CCSBot *>(pPlayer); + if (pBot) + { + pBot->EndVoiceFeedback( true ); + } + } + */ +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Parses out bot name/template/etc params from the current ConCommand +*/ +void BotArgumentsFromArgv( const CCommand &args, const char **name, CSWeaponType *weaponType, BotDifficultyType *difficulty, int *team = NULL, bool *all = NULL ) +{ + static char s_name[MAX_PLAYER_NAME_LENGTH]; + + s_name[0] = 0; + *name = s_name; + *difficulty = NUM_DIFFICULTY_LEVELS; + if ( team ) + { + *team = TEAM_UNASSIGNED; + } + if ( all ) + { + *all = false; + } + + *weaponType = WEAPONTYPE_UNKNOWN; + + for ( int arg=1; arg<args.ArgC(); ++arg ) + { + bool found = false; + + const char *token = args[arg]; + if ( all && FStrEq( token, "all" ) ) + { + *all = true; + found = true; + } + else if ( team && FStrEq( token, "t" ) ) + { + *team = TEAM_TERRORIST; + found = true; + } + else if ( team && FStrEq( token, "ct" ) ) + { + *team = TEAM_CT; + found = true; + } + + for( int i=0; i<NUM_DIFFICULTY_LEVELS && !found; ++i ) + { + if (!stricmp( BotDifficultyName[i], token )) + { + *difficulty = (BotDifficultyType)i; + found = true; + } + } + + if ( !found ) + { + *weaponType = WeaponClassFromString( token ); + if ( *weaponType != WEAPONTYPE_UNKNOWN ) + { + found = true; + } + } + + if ( !found ) + { + Q_strncpy( s_name, token, sizeof( s_name ) ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_add, "bot_add <t|ct> <type> <difficulty> <name> - Adds a bot matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + int team; + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty, &team ); + TheCSBots()->BotAddCommand( team, FROM_CONSOLE, name, weaponType, difficulty ); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_add_t, "bot_add_t <type> <difficulty> <name> - Adds a terrorist bot matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty ); + TheCSBots()->BotAddCommand( TEAM_TERRORIST, FROM_CONSOLE, name, weaponType, difficulty ); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_add_ct, "bot_add_ct <type> <difficulty> <name> - Adds a Counter-Terrorist bot matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty ); + TheCSBots()->BotAddCommand( TEAM_CT, FROM_CONSOLE, name, weaponType, difficulty ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Collects all bots matching the given criteria (player name, profile template name, difficulty, and team) + */ +class CollectBots +{ +public: + CollectBots( const char *name, CSWeaponType weaponType, BotDifficultyType difficulty, int team ) + { + m_name = name; + m_difficulty = difficulty; + m_team = team; + m_weaponType = weaponType; + } + + bool operator() ( CBasePlayer *player ) + { + if ( !player->IsBot() ) + { + return true; + } + + CCSBot *bot = dynamic_cast< CCSBot * >(player); + if ( !bot || !bot->GetProfile() ) + { + return true; + } + + if ( m_name && *m_name ) + { + // accept based on name + if ( FStrEq( m_name, bot->GetProfile()->GetName() ) ) + { + m_bots.RemoveAll(); + m_bots.AddToTail( bot ); + return false; + } + + // Reject based on profile template name + if ( !bot->GetProfile()->InheritsFrom( m_name ) ) + { + return true; + } + } + + // reject based on difficulty + if ( m_difficulty != NUM_DIFFICULTY_LEVELS ) + { + if ( !bot->GetProfile()->IsDifficulty( m_difficulty ) ) + { + return true; + } + } + + // reject based on team + if ( m_team == TEAM_CT || m_team == TEAM_TERRORIST ) + { + if ( bot->GetTeamNumber() != m_team ) + { + return true; + } + } + + // reject based on weapon preference + if ( m_weaponType != WEAPONTYPE_UNKNOWN ) + { + if ( !bot->GetProfile()->GetWeaponPreferenceCount() ) + { + return true; + } + + if ( m_weaponType != WeaponClassFromWeaponID( (CSWeaponID)bot->GetProfile()->GetWeaponPreference( 0 ) ) ) + { + return true; + } + } + + // A match! + m_bots.AddToTail( bot ); + + return true; + } + + CUtlVector< CCSBot * > m_bots; + +private: + const char *m_name; + CSWeaponType m_weaponType; + BotDifficultyType m_difficulty; + int m_team; +}; + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_kill, "bot_kill <all> <t|ct> <type> <difficulty> <name> - Kills a specific bot, or all bots, matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + int team; + bool all; + + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty, &team, &all ); + if ( (!name || !*name) && team == TEAM_UNASSIGNED && difficulty == NUM_DIFFICULTY_LEVELS ) + { + all = true; + } + + CollectBots collector( name, weaponType, difficulty, team ); + ForEachPlayer( collector ); + + for ( int i=0; i<collector.m_bots.Count(); ++i ) + { + CCSBot *bot = collector.m_bots[i]; + if ( !bot->IsAlive() ) + continue; + + bot->CommitSuicide(); + + if ( !all ) + { + return; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_kick, "bot_kick <all> <t|ct> <type> <difficulty> <name> - Kicks a specific bot, or all bots, matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + int team; + bool all; + + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty, &team, &all ); + if ( (!name || !*name) && team == TEAM_UNASSIGNED && difficulty == NUM_DIFFICULTY_LEVELS ) + { + all = true; + } + + CollectBots collector( name, weaponType, difficulty, team ); + ForEachPlayer( collector ); + + for ( int i=0; i<collector.m_bots.Count(); ++i ) + { + CCSBot *bot = collector.m_bots[i]; + engine->ServerCommand( UTIL_VarArgs( "kick \"%s\"\n", bot->GetPlayerName() ) ); + if ( !all ) + { + // adjust bot quota so kicked bot is not immediately added back in + int newQuota = cv_bot_quota.GetInt() - 1; + cv_bot_quota.SetValue( clamp( newQuota, 0, cv_bot_quota.GetInt() ) ); + return; + } + } + + // adjust bot quota so kicked bot is not immediately added back in + if ( all && (!name || !*name) && team == TEAM_UNASSIGNED && difficulty == NUM_DIFFICULTY_LEVELS ) + { + cv_bot_quota.SetValue( 0 ); + } + else + { + int newQuota = cv_bot_quota.GetInt() - collector.m_bots.Count(); + cv_bot_quota.SetValue( clamp( newQuota, 0, cv_bot_quota.GetInt() ) ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_knives_only, "Restricts the bots to only using knives", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 0 ); + cv_bot_allow_shotguns.SetValue( 0 ); + cv_bot_allow_sub_machine_guns.SetValue( 0 ); + cv_bot_allow_rifles.SetValue( 0 ); + cv_bot_allow_machine_guns.SetValue( 0 ); + cv_bot_allow_grenades.SetValue( 0 ); + cv_bot_allow_snipers.SetValue( 0 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 0 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_pistols_only, "Restricts the bots to only using pistols", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 1 ); + cv_bot_allow_shotguns.SetValue( 0 ); + cv_bot_allow_sub_machine_guns.SetValue( 0 ); + cv_bot_allow_rifles.SetValue( 0 ); + cv_bot_allow_machine_guns.SetValue( 0 ); + cv_bot_allow_grenades.SetValue( 0 ); + cv_bot_allow_snipers.SetValue( 0 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 0 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_snipers_only, "Restricts the bots to only using sniper rifles", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 0 ); + cv_bot_allow_shotguns.SetValue( 0 ); + cv_bot_allow_sub_machine_guns.SetValue( 0 ); + cv_bot_allow_rifles.SetValue( 0 ); + cv_bot_allow_machine_guns.SetValue( 0 ); + cv_bot_allow_grenades.SetValue( 0 ); + cv_bot_allow_snipers.SetValue( 1 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 0 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_all_weapons, "Allows the bots to use all weapons", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 1 ); + cv_bot_allow_shotguns.SetValue( 1 ); + cv_bot_allow_sub_machine_guns.SetValue( 1 ); + cv_bot_allow_rifles.SetValue( 1 ); + cv_bot_allow_machine_guns.SetValue( 1 ); + cv_bot_allow_grenades.SetValue( 1 ); + cv_bot_allow_snipers.SetValue( 1 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 1 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_goto_mark, "Sends a bot to the selected nav area (useful for testing navigation meshes)", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + // tell the first bot we find to go to our marked area + CNavArea *area = TheNavMesh->GetMarkedArea(); + if (area) + { + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (player->IsBot()) + { + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + + if ( bot ) + { + bot->MoveTo( area->GetCenter(), FASTEST_ROUTE ); + } + + break; + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +#if 0 +CON_COMMAND_F( bot_memory_usage, "Reports on the bots' memory usage", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + Msg( "Memory usage:\n" ); + + Msg( " %d bytes per bot\n", sizeof(CCSBot) ); + + Msg( " %d Navigation Areas @ %d bytes each = %d bytes\n", + TheNavMesh->GetNavAreaCount(), + sizeof( CNavArea ), + TheNavMesh->GetNavAreaCount() * sizeof( CNavArea ) ); + + Msg( " %d Hiding Spots @ %d bytes each = %d bytes\n", + TheHidingSpotList.Count(), + sizeof( HidingSpot ), + TheHidingSpotList.Count() * sizeof( HidingSpot ) ); + +/* + unsigned int encounterMem = 0; + FOR_EACH_LL( TheNavAreaList, it ) + { + CNavArea *area = TheNavAreaList[ it ]; + + FOR_EACH_LL( area->m_spotEncounterList, it ) + { + SpotEncounter *se = area->m_spotEncounterList[ it ]; + + encounterMem += sizeof( SpotEncounter ); + encounterMem += se->spotList.Count() * sizeof( SpotOrder ); + } + } + + Msg( " Encounter Spot data = %d bytes\n", encounterMem ); +*/ +} +#endif + + +bool CCSBotManager::ServerCommand( const char *cmd ) +{ + return false; +} + + +bool CCSBotManager::ClientCommand( CBasePlayer *player, const CCommand &args ) +{ + return false; +} + + +/** + * Process the "bot_add" console command + */ +bool CCSBotManager::BotAddCommand( int team, bool isFromConsole, const char *profileName, CSWeaponType weaponType, BotDifficultyType difficulty ) +{ + if ( !TheNavMesh->IsLoaded() ) + { + // If there isn't a Navigation Mesh in memory, create one + if ( !TheNavMesh->IsGenerating() ) + { + if ( !m_isMapDataLoaded ) + { + TheNavMesh->BeginGeneration(); + m_isMapDataLoaded = true; + } + return false; + } + } + + // dont allow bots to join if the Navigation Mesh is being generated + if (TheNavMesh->IsGenerating()) + return false; + + const BotProfile *profile = NULL; + + if ( !isFromConsole ) + { + profileName = NULL; + difficulty = GetDifficultyLevel(); + } + else + { + if ( difficulty == NUM_DIFFICULTY_LEVELS ) + { + difficulty = GetDifficultyLevel(); + } + + // if team not specified, check bot_join_team cvar for preference + if (team == TEAM_UNASSIGNED) + { + if (!stricmp( cv_bot_join_team.GetString(), "T" )) + team = TEAM_TERRORIST; + else if (!stricmp( cv_bot_join_team.GetString(), "CT" )) + team = TEAM_CT; + else + team = CSGameRules()->SelectDefaultTeam(); + } + } + + if ( profileName && *profileName ) + { + // in career, ignore humans, since we want to add anyway + bool ignoreHumans = CSGameRules()->IsCareer(); + if (UTIL_IsNameTaken( profileName, ignoreHumans )) + { + if ( isFromConsole ) + { + Msg( "Error - %s is already in the game.\n", profileName ); + } + return true; + } + + // try to add a bot by name + profile = TheBotProfiles->GetProfile( profileName, team ); + if ( !profile ) + { + // try to add a bot by template + profile = TheBotProfiles->GetProfileMatchingTemplate( profileName, team, difficulty ); + if ( !profile ) + { + if ( isFromConsole ) + { + Msg( "Error - no profile for '%s' exists.\n", profileName ); + } + return true; + } + } + } + else + { + // if team not specified, check bot_join_team cvar for preference + if (team == TEAM_UNASSIGNED) + { + if (!stricmp( cv_bot_join_team.GetString(), "T" )) + team = TEAM_TERRORIST; + else if (!stricmp( cv_bot_join_team.GetString(), "CT" )) + team = TEAM_CT; + else + team = CSGameRules()->SelectDefaultTeam(); + } + + profile = TheBotProfiles->GetRandomProfile( difficulty, team, weaponType ); + if (profile == NULL) + { + if ( isFromConsole ) + { + Msg( "All bot profiles at this difficulty level are in use.\n" ); + } + return true; + } + } + + if (team == TEAM_UNASSIGNED || team == TEAM_SPECTATOR) + { + if ( isFromConsole ) + { + Msg( "Could not add bot to the game: The game is full\n" ); + } + return false; + } + + if (CSGameRules()->TeamFull( team )) + { + if ( isFromConsole ) + { + Msg( "Could not add bot to the game: Team is full\n" ); + } + return false; + } + + if (CSGameRules()->TeamStacked( team, TEAM_UNASSIGNED )) + { + if ( isFromConsole ) + { + Msg( "Could not add bot to the game: Team is stacked (to disable this check, set mp_autoteambalance to zero, increase mp_limitteams, and restart the round).\n" ); + } + return false; + } + + // create the actual bot + CCSBot *bot = CreateBot<CCSBot>( profile, team ); + + if (bot == NULL) + { + if ( isFromConsole ) + { + Msg( "Error: CreateBot() failed.\n" ); + } + return false; + } + + if (isFromConsole) + { + // increase the bot quota to account for manually added bot + cv_bot_quota.SetValue( cv_bot_quota.GetInt() + 1 ); + } + + return true; +} + +int UTIL_CSSBotsInGame() +{ + int count = 0; + + for (int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSBot *player = dynamic_cast<CCSBot *>(UTIL_PlayerByIndex( i )); + + if ( player == NULL ) + continue; + + count++; + } + + return count; +} + +bool UTIL_CSSKickBotFromTeam( int kickTeam ) +{ + int i; + + // try to kick a dead bot first + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSBot *player = dynamic_cast<CCSBot *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + 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 ) + { + CCSBot *player = dynamic_cast<CCSBot *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + 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; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Keep a minimum quota of bots in the game + */ +void CCSBotManager::MaintainBotQuota( void ) +{ + if ( !AreBotsAllowed() ) + return; + + if (TheNavMesh->IsGenerating()) + return; + + int totalHumansInGame = UTIL_HumansInGame(); + int humanPlayersInGame = UTIL_HumansInGame( IGNORE_SPECTATORS ); + + // don't add bots until local player has been registered, to make sure he's player ID #1 + if (!engine->IsDedicatedServer() && totalHumansInGame == 0) + return; + + // new players can't spawn immediately after the round has been going for some time + if ( !CSGameRules() || !TheCSBots() ) + { + return; + } + + int desiredBotCount = cv_bot_quota.GetInt(); + int botsInGame = UTIL_CSSBotsInGame(); + + /// isRoundInProgress is true if the round has progressed far enough that new players will join as dead. + bool isRoundInProgress = CSGameRules()->m_bFirstConnected && + !TheCSBots()->IsRoundOver() && + ( CSGameRules()->GetRoundElapsedTime() >= 20.0f ); + + if ( FStrEq( cv_bot_quota_mode.GetString(), "fill" ) ) + { + // If bot_quota_mode is 'fill', we want the number of bots and humans together to equal bot_quota + // unless the round is already in progress, in which case we play with what we've been dealt + if ( !isRoundInProgress ) + { + desiredBotCount = MAX( 0, desiredBotCount - humanPlayersInGame ); + } + else + { + desiredBotCount = botsInGame; + } + } + else if ( FStrEq( cv_bot_quota_mode.GetString(), "match" ) ) + { + // If bot_quota_mode is 'match', we want the number of bots to be bot_quota * total humans + // unless the round is already in progress, in which case we play with what we've been dealt + if ( !isRoundInProgress ) + { + desiredBotCount = (int)MAX( 0, cv_bot_quota.GetFloat() * humanPlayersInGame ); + } + else + { + desiredBotCount = botsInGame; + } + } + + // wait for a player to join, if necessary + if (cv_bot_join_after_player.GetBool()) + { + if (humanPlayersInGame == 0) + desiredBotCount = 0; + } + + // wait until the map has been loaded for a bit, to allow players to transition across + // the transition without missing the pistol round + if ( bot_join_delay.GetInt() > CSGameRules()->GetMapElapsedTime() ) + { + desiredBotCount = 0; + } + + // if bots will auto-vacate, we need to keep one slot open to allow players to join + if (cv_bot_auto_vacate.GetBool()) + desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - (humanPlayersInGame + 1) ); + else + desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - humanPlayersInGame ); + + // Try to balance teams, if we are in the first 20 seconds of a round and bots can join either team. + if ( botsInGame > 0 && desiredBotCount == botsInGame && CSGameRules()->m_bFirstConnected ) + { + if ( CSGameRules()->GetRoundElapsedTime() < 20.0f ) // new bots can still spawn during this time + { + if ( mp_autoteambalance.GetBool() ) + { + int numAliveTerrorist; + int numAliveCT; + int numDeadTerrorist; + int numDeadCT; + CSGameRules()->InitializePlayerCounts( numAliveTerrorist, numAliveCT, numDeadTerrorist, numDeadCT ); + + if ( !FStrEq( cv_bot_join_team.GetString(), "T" ) && + !FStrEq( cv_bot_join_team.GetString(), "CT" ) ) + { + if ( numAliveTerrorist > CSGameRules()->m_iNumCT + 1 ) + { + if ( UTIL_KickBotFromTeam( TEAM_TERRORIST ) ) + return; + } + else if ( numAliveCT > CSGameRules()->m_iNumTerrorist + 1 ) + { + if ( UTIL_KickBotFromTeam( TEAM_CT ) ) + return; + } + } + } + } + } + + // add bots if necessary + if (desiredBotCount > botsInGame) + { + // don't try to add a bot if all teams are full + if (!CSGameRules()->TeamFull( TEAM_TERRORIST ) || !CSGameRules()->TeamFull( TEAM_CT )) + TheCSBots()->BotAddCommand( TEAM_UNASSIGNED ); + } + else if (desiredBotCount < botsInGame) + { + // kick a bot to maintain quota + + // first remove any unassigned bots + if (UTIL_CSSKickBotFromTeam( TEAM_UNASSIGNED )) + return; + + int kickTeam; + + // remove from the team that has more players + if (CSGameRules()->m_iNumTerrorist > CSGameRules()->m_iNumCT) + { + kickTeam = TEAM_TERRORIST; + } + else if (CSGameRules()->m_iNumTerrorist < CSGameRules()->m_iNumCT) + { + kickTeam = TEAM_CT; + } + + // remove from the team that's winning + else if (CSGameRules()->m_iNumTerroristWins > CSGameRules()->m_iNumCTWins) + { + kickTeam = TEAM_TERRORIST; + } + else if (CSGameRules()->m_iNumCTWins > CSGameRules()->m_iNumTerroristWins) + { + kickTeam = TEAM_CT; + } + else + { + // teams and scores are equal, pick a team at random + kickTeam = (RandomInt( 0, 1 ) == 0) ? TEAM_CT : TEAM_TERRORIST; + } + + // attempt to kick a bot from the given team + if (UTIL_CSSKickBotFromTeam( kickTeam )) + return; + + // if there were no bots on the team, kick a bot from the other team + if (kickTeam == TEAM_TERRORIST) + UTIL_CSSKickBotFromTeam( TEAM_CT ); + else + UTIL_CSSKickBotFromTeam( TEAM_TERRORIST ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Collect all nav areas that overlap the given zone + */ +class CollectOverlappingAreas +{ +public: + CollectOverlappingAreas( CCSBotManager::Zone *zone ) + { + m_zone = zone; + + zone->m_areaCount = 0; + } + + bool operator() ( CNavArea *area ) + { + Extent areaExtent; + area->GetExtent(&areaExtent); + + if (areaExtent.hi.x >= m_zone->m_extent.lo.x && areaExtent.lo.x <= m_zone->m_extent.hi.x && + areaExtent.hi.y >= m_zone->m_extent.lo.y && areaExtent.lo.y <= m_zone->m_extent.hi.y && + areaExtent.hi.z >= m_zone->m_extent.lo.z && areaExtent.lo.z <= m_zone->m_extent.hi.z) + { + // area overlaps m_zone + m_zone->m_area[ m_zone->m_areaCount++ ] = area; + if (m_zone->m_areaCount == CCSBotManager::MAX_ZONE_NAV_AREAS) + { + return false; + } + } + + return true; + } + +private: + CCSBotManager::Zone *m_zone; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Search the map entities to determine the game scenario and define important zones. + */ +void CCSBotManager::ExtractScenarioData( void ) +{ + if (!TheNavMesh->IsLoaded()) + return; + + m_zoneCount = 0; + m_gameScenario = SCENARIO_DEATHMATCH; + + + // + // Search all entities in the map and set the game type and + // store all zones (bomb target, etc). + // + CBaseEntity *entity; + int i; + for( i=1; i<gpGlobals->maxEntities; ++i ) + { + entity = CBaseEntity::Instance( engine->PEntityOfEntIndex( i ) ); + + if (entity == NULL) + continue; + + bool found = false; + bool isLegacy = false; + + if (FClassnameIs( entity, "func_bomb_target" )) + { + m_gameScenario = SCENARIO_DEFUSE_BOMB; + found = true; + isLegacy = false; + } + else if (FClassnameIs( entity, "info_bomb_target" )) + { + m_gameScenario = SCENARIO_DEFUSE_BOMB; + found = true; + isLegacy = true; + } + else if (FClassnameIs( entity, "func_hostage_rescue" )) + { + m_gameScenario = SCENARIO_RESCUE_HOSTAGES; + found = true; + isLegacy = false; + } + else if (FClassnameIs( entity, "info_hostage_rescue" )) + { + m_gameScenario = SCENARIO_RESCUE_HOSTAGES; + found = true; + isLegacy = true; + } + else if (FClassnameIs( entity, "hostage_entity" )) + { + // some very old maps (ie: cs_assault) use info_player_start + // as rescue zones, so set the scenario if there are hostages + // in the map + m_gameScenario = SCENARIO_RESCUE_HOSTAGES; + } + else if (FClassnameIs( entity, "func_vip_safetyzone" )) + { + m_gameScenario = SCENARIO_ESCORT_VIP; + found = true; + isLegacy = false; + } + + if (found) + { + if (m_zoneCount < MAX_ZONES) + { + Vector absmin, absmax; + entity->CollisionProp()->WorldSpaceAABB( &absmin, &absmax ); + + m_zone[ m_zoneCount ].m_isBlocked = false; + m_zone[ m_zoneCount ].m_center = (isLegacy) ? entity->GetAbsOrigin() : (absmin + absmax)/2.0f; + m_zone[ m_zoneCount ].m_isLegacy = isLegacy; + m_zone[ m_zoneCount ].m_index = m_zoneCount; + m_zone[ m_zoneCount++ ].m_entity = entity; + } + else + Msg( "Warning: Too many zones, some will be ignored.\n" ); + } + } + + // + // If there are no zones and the scenario is hostage rescue, + // use the info_player_start entities as rescue zones. + // + if (m_zoneCount == 0 && m_gameScenario == SCENARIO_RESCUE_HOSTAGES) + { + for( entity = gEntList.FindEntityByClassname( NULL, "info_player_start" ); + entity && !FNullEnt( entity->edict() ); + entity = gEntList.FindEntityByClassname( entity, "info_player_start" ) ) + { + if (m_zoneCount < MAX_ZONES) + { + m_zone[ m_zoneCount ].m_isBlocked = false; + m_zone[ m_zoneCount ].m_center = entity->GetAbsOrigin(); + m_zone[ m_zoneCount ].m_isLegacy = true; + m_zone[ m_zoneCount ].m_index = m_zoneCount; + m_zone[ m_zoneCount++ ].m_entity = entity; + } + else + { + Msg( "Warning: Too many zones, some will be ignored.\n" ); + } + } + } + + // + // Collect nav areas that overlap each zone + // + for( i=0; i<m_zoneCount; ++i ) + { + Zone *zone = &m_zone[i]; + + if (zone->m_isLegacy) + { + const float legacyRange = 256.0f; + zone->m_extent.lo.x = zone->m_center.x - legacyRange; + zone->m_extent.lo.y = zone->m_center.y - legacyRange; + zone->m_extent.lo.z = zone->m_center.z - legacyRange; + zone->m_extent.hi.x = zone->m_center.x + legacyRange; + zone->m_extent.hi.y = zone->m_center.y + legacyRange; + zone->m_extent.hi.z = zone->m_center.z + legacyRange; + } + else + { + Vector absmin, absmax; + zone->m_entity->CollisionProp()->WorldSpaceAABB( &absmin, &absmax ); + + zone->m_extent.lo = absmin; + zone->m_extent.hi = absmax; + } + + // ensure Z overlap + const float zFudge = 50.0f; + zone->m_extent.lo.z -= zFudge; + zone->m_extent.hi.z += zFudge; + + // build a list of nav areas that overlap this zone + CollectOverlappingAreas collector( zone ); + TheNavMesh->ForAllAreas( collector ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the zone that contains the given position + */ +const CCSBotManager::Zone *CCSBotManager::GetZone( const Vector &pos ) const +{ + for( int z=0; z<m_zoneCount; ++z ) + { + if (m_zone[z].m_extent.Contains( pos )) + { + return &m_zone[z]; + } + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest zone to the given position + */ +const CCSBotManager::Zone *CCSBotManager::GetClosestZone( const Vector &pos ) const +{ + const Zone *close = NULL; + float closeRangeSq = 999999999.9f; + + for( int z=0; z<m_zoneCount; ++z ) + { + if ( m_zone[z].m_isBlocked ) + continue; + + float rangeSq = (m_zone[z].m_center - pos).LengthSqr(); + + if (rangeSq < closeRangeSq) + { + closeRangeSq = rangeSq; + close = &m_zone[z]; + } + } + + return close; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return a random position inside the given zone + */ +const Vector *CCSBotManager::GetRandomPositionInZone( const Zone *zone ) const +{ + static Vector pos; + + if (zone == NULL) + return NULL; + + if (zone->m_areaCount == 0) + return NULL; + + // pick a random overlapping area + CNavArea *area = GetRandomAreaInZone(zone); + + // pick a location inside both the nav area and the zone + /// @todo Randomize this + + if (zone->m_isLegacy) + { + /// @todo It is possible that the radius might not overlap this area at all... + area->GetClosestPointOnArea( zone->m_center, &pos ); + } + else + { + Extent areaExtent; + area->GetExtent(&areaExtent); + Extent overlap; + overlap.lo.x = MAX( areaExtent.lo.x, zone->m_extent.lo.x ); + overlap.lo.y = MAX( areaExtent.lo.y, zone->m_extent.lo.y ); + overlap.hi.x = MIN( areaExtent.hi.x, zone->m_extent.hi.x ); + overlap.hi.y = MIN( areaExtent.hi.y, zone->m_extent.hi.y ); + + pos.x = (overlap.lo.x + overlap.hi.x)/2.0f; + pos.y = (overlap.lo.y + overlap.hi.y)/2.0f; + pos.z = area->GetZ( pos ); + } + + return &pos; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return a random area inside the given zone + */ +CNavArea *CCSBotManager::GetRandomAreaInZone( const Zone *zone ) const +{ + int areaCount = zone->m_areaCount; + if( areaCount == 0 ) + { + assert( false && "CCSBotManager::GetRandomAreaInZone: No areas for this zone" ); + return NULL; + } + + // Random, but weighted. Jump areas score zero, since you aren't ever meant to stop on one of those. + // Avoid areas score 1 to a normal area's 20 because pathfinding treats Avoid as a 20x penalty. + int totalWeight = 0; + for( int areaIndex = 0; areaIndex < areaCount; areaIndex++ ) + { + CNavArea *currentArea = zone->m_area[areaIndex]; + if( currentArea->GetAttributes() & NAV_MESH_JUMP ) + totalWeight += 0; + else if( currentArea->GetAttributes() & NAV_MESH_AVOID ) + totalWeight += 1; + else + totalWeight += 20; + } + + if( totalWeight == 0 ) + { + assert( false && "CCSBotManager::GetRandomAreaInZone: No real areas for this zone" ); + return NULL; + } + + int randomPick = RandomInt( 1, totalWeight ); + + for( int areaIndex = 0; areaIndex < areaCount; areaIndex++ ) + { + CNavArea *currentArea = zone->m_area[areaIndex]; + if( currentArea->GetAttributes() & NAV_MESH_JUMP ) + randomPick -= 0; + else if( currentArea->GetAttributes() & NAV_MESH_AVOID ) + randomPick -= 1; + else + randomPick -= 20; + + if( randomPick <= 0 ) + return currentArea; + } + + // Won't ever get here, but the compiler will cry without it. + return zone->m_area[0]; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnServerShutdown( IGameEvent *event ) +{ + if ( !engine->IsDedicatedServer() ) + { + // Since we're a listenserver, save some config info for the next time we start up + static const char *botVars[] = + { + "bot_quota", + "bot_difficulty", + "bot_chatter", + "bot_prefix", + "bot_join_team", + "bot_defer_to_human", +#ifdef CS_SHIELD_ENABLED + "bot_allow_shield", +#endif // CS_SHIELD_ENABLED + "bot_join_after_player", + "bot_allow_rogues", + "bot_allow_pistols", + "bot_allow_shotguns", + "bot_allow_sub_machine_guns", + "bot_allow_machine_guns", + "bot_allow_rifles", + "bot_allow_snipers", + "bot_allow_grenades" + }; + + KeyValues *data = new KeyValues( "ServerConfig" ); + + // load the config data + if (data) + { + data->LoadFromFile( filesystem, "ServerConfig.vdf", "GAME" ); + for ( int i=0; i<sizeof(botVars)/sizeof(botVars[0]); ++i ) + { + const char *varName = botVars[i]; + if ( varName ) + { + ConVar *var = cvar->FindVar( varName ); + if ( var ) + { + data->SetString( varName, var->GetString() ); + } + } + } + data->SaveToFile( filesystem, "ServerConfig.vdf", "GAME" ); + data->deleteThis(); + } + return; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerFootstep( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerFootstep, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerRadio( IGameEvent *event ) +{ + // if it's an Enemy Spotted radio, update our enemy spotted timestamp + if ( event->GetInt( "slot" ) == RADIO_ENEMY_SPOTTED ) + { + // to have some idea of when a human Player has seen an enemy + SetLastSeenEnemyTimestamp(); + } + + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerRadio, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerDeath( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerDeath, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerFallDamage( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerFallDamage, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombPickedUp( IGameEvent *event ) +{ + // bomb no longer loose + SetLooseBomb( NULL ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBombPickedUp, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombPlanted( IGameEvent *event ) +{ + m_isBombPlanted = true; + m_bombPlantTimestamp = gpGlobals->curtime; + + CCSBOTMANAGER_ITERATE_BOTS( OnBombPlanted, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombBeep( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnBombBeep, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombDefuseBegin( IGameEvent *event ) +{ + m_bombDefuser = static_cast<CCSPlayer *>( UTIL_PlayerByUserId( event->GetInt( "userid" ) ) ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBombDefuseBegin, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombDefused( IGameEvent *event ) +{ + m_isBombPlanted = false; + m_bombDefuser = NULL; + + CCSBOTMANAGER_ITERATE_BOTS( OnBombDefused, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombDefuseAbort( IGameEvent *event ) +{ + m_bombDefuser = NULL; + + CCSBOTMANAGER_ITERATE_BOTS( OnBombDefuseAbort, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombExploded( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnBombExploded, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnRoundEnd( IGameEvent *event ) +{ + m_isRoundOver = true; + + CCSBOTMANAGER_ITERATE_BOTS( OnRoundEnd, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnRoundStart( IGameEvent *event ) +{ + RestartRound(); + + CCSBOTMANAGER_ITERATE_BOTS( OnRoundStart, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +static CBaseEntity * SelectSpawnSpot( const char *pEntClassName ) +{ + CBaseEntity* pSpot = NULL; + + // Find the next spawn spot. + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + + if ( pSpot == NULL ) // skip over the null point + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + + CBaseEntity *pFirstSpot = pSpot; + do + { + if ( pSpot ) + { + // check if pSpot is valid + if ( pSpot->GetAbsOrigin() == Vector( 0, 0, 0 ) ) + { + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + continue; + } + + // if so, go to pSpot + return pSpot; + } + // increment pSpot + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + } while ( pSpot != pFirstSpot ); // loop if we're not back to the start + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Pathfind from each zone to a spawn point to ensure it is valid. Assumes that every spawn can pathfind to + * every other spawn. + */ +void CCSBotManager::CheckForBlockedZones( void ) +{ + CBaseEntity *pSpot = SelectSpawnSpot( "info_player_counterterrorist" ); + if ( !pSpot ) + pSpot = SelectSpawnSpot( "info_player_terrorist" ); + + if ( !pSpot ) + return; + + Vector spawnPos = pSpot->GetAbsOrigin(); + CNavArea *spawnArea = TheNavMesh->GetNearestNavArea( spawnPos ); + if ( !spawnArea ) + return; + + ShortestPathCost costFunc; + + for( int i=0; i<m_zoneCount; ++i ) + { + if (m_zone[i].m_areaCount == 0) + continue; + + // just use the first overlapping nav area as a reasonable approximation + float dist = NavAreaTravelDistance( spawnArea, m_zone[i].m_area[0], costFunc ); + m_zone[i].m_isBlocked = (dist < 0.0f ); + + if ( cv_bot_debug.GetInt() == 5 ) + { + if ( m_zone[i].m_isBlocked ) + DevMsg( "%.1f: Zone %d, area %d (%.0f %.0f %.0f) is blocked from spawn area %d (%.0f %.0f %.0f)\n", + gpGlobals->curtime, i, m_zone[i].m_area[0]->GetID(), + m_zone[i].m_area[0]->GetCenter().x, m_zone[i].m_area[0]->GetCenter().y, m_zone[i].m_area[0]->GetCenter().z, + spawnArea->GetID(), + spawnPos.x, spawnPos.y, spawnPos.z ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnRoundFreezeEnd( IGameEvent *event ) +{ + bool reenableEvents = m_NavBlockedEvent.IsEnabled(); + + m_NavBlockedEvent.Enable( false ); // don't listen to nav_blocked events - there could be several, and we don't have bots pathing + CUtlVector< CNavArea * >& transientAreas = TheNavMesh->GetTransientAreas(); + for ( int i=0; i<transientAreas.Count(); ++i ) + { + CNavArea *area = transientAreas[i]; + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + area->UpdateBlocked(); + } + } + if ( reenableEvents ) + { + m_NavBlockedEvent.Enable( true ); + } + + CheckForBlockedZones(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnNavBlocked( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnNavBlocked, event ); + CheckForBlockedZones(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnDoorMoving( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnDoorMoving, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check all nav areas inside the breakable's extent to see if players would now fall through + */ +class CheckAreasOverlappingBreakable +{ +public: + CheckAreasOverlappingBreakable( CBaseEntity *breakable ) + { + m_breakable = breakable; + ICollideable *collideable = breakable->GetCollideable(); + collideable->WorldSpaceSurroundingBounds( &m_breakableExtent.lo, &m_breakableExtent.hi ); + + const float expand = 10.0f; + m_breakableExtent.lo += Vector( -expand, -expand, -expand ); + m_breakableExtent.hi += Vector( expand, expand, expand ); + } + + bool operator() ( CNavArea *area ) + { + Extent areaExtent; + area->GetExtent(&areaExtent); + + if (areaExtent.hi.x >= m_breakableExtent.lo.x && areaExtent.lo.x <= m_breakableExtent.hi.x && + areaExtent.hi.y >= m_breakableExtent.lo.y && areaExtent.lo.y <= m_breakableExtent.hi.y && + areaExtent.hi.z >= m_breakableExtent.lo.z && areaExtent.lo.z <= m_breakableExtent.hi.z) + { + // area overlaps the breakable + area->CheckFloor( m_breakable ); + } + + return true; + } + +private: + Extent m_breakableExtent; + CBaseEntity *m_breakable; +}; + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBreakBreakable( IGameEvent *event ) +{ + CheckAreasOverlappingBreakable collector( UTIL_EntityByIndex( event->GetInt( "entindex" ) ) ); + TheNavMesh->ForAllAreas( collector ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBreakBreakable, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBreakProp( IGameEvent *event ) +{ + CheckAreasOverlappingBreakable collector( UTIL_EntityByIndex( event->GetInt( "entindex" ) ) ); + TheNavMesh->ForAllAreas( collector ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBreakProp, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnHostageFollows( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnHostageFollows, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnHostageRescuedAll( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnHostageRescuedAll, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponFire( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponFire, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponFireOnEmpty( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponFireOnEmpty, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponReload( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponReload, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponZoom( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponZoom, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBulletImpact( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnBulletImpact, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnHEGrenadeDetonate( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnHEGrenadeDetonate, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnFlashbangDetonate( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnFlashbangDetonate, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnSmokeGrenadeDetonate( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnSmokeGrenadeDetonate, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnGrenadeBounce( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnGrenadeBounce, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Get the time remaining before the planted bomb explodes + */ +float CCSBotManager::GetBombTimeLeft( void ) const +{ + return (mp_c4timer.GetFloat() - (gpGlobals->curtime - m_bombPlantTimestamp)); +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::SetLooseBomb( CBaseEntity *bomb ) +{ + m_looseBomb = bomb; + + if (bomb) + { + m_looseBombArea = TheNavMesh->GetNearestNavArea( bomb->GetAbsOrigin() ); + } + else + { + m_looseBombArea = NULL; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if player is important to scenario (VIP, bomb carrier, etc) + */ +bool CCSBotManager::IsImportantPlayer( CCSPlayer *player ) const +{ + switch (GetScenario()) + { + case SCENARIO_DEFUSE_BOMB: + { + if (player->GetTeamNumber() == TEAM_TERRORIST && player->HasC4()) + return true; + + /// @todo TEAM_CT's defusing the bomb are important + + return false; + } + + case SCENARIO_ESCORT_VIP: + { + if (player->GetTeamNumber() == TEAM_CT && player->IsVIP()) + return true; + + return false; + } + + case SCENARIO_RESCUE_HOSTAGES: + { + /// @todo TEAM_CT's escorting hostages are important + return false; + } + } + + // everyone is equally important in a deathmatch + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return priority of player (0 = max pri) + */ +unsigned int CCSBotManager::GetPlayerPriority( CBasePlayer *player ) const +{ + const unsigned int lowestPriority = 0xFFFFFFFF; + + if (!player->IsPlayer()) + return lowestPriority; + + // human players have highest priority + if (!player->IsBot()) + return 0; + + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + + if ( !bot ) + return 0; + + // bots doing something important for the current scenario have high priority + switch (GetScenario()) + { + case SCENARIO_DEFUSE_BOMB: + { + // the bomb carrier has high priority + if (bot->GetTeamNumber() == TEAM_TERRORIST && bot->HasC4()) + return 1; + + break; + } + + case SCENARIO_ESCORT_VIP: + { + // the VIP has high priority + if (bot->GetTeamNumber() == TEAM_CT && bot->m_bIsVIP) + return 1; + + break; + } + + case SCENARIO_RESCUE_HOSTAGES: + { + // TEAM_CT's rescuing hostages have high priority + if (bot->GetTeamNumber() == TEAM_CT && bot->GetHostageEscortCount()) + return 1; + + break; + } + } + + // everyone else is ranked by their unique ID (which cannot be zero) + return 1 + bot->GetID(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns a random spawn point for the given team (no arg means use both team spawnpoints) + */ +CBaseEntity *CCSBotManager::GetRandomSpawn( int team ) const +{ + CUtlVector< CBaseEntity * > spawnSet; + CBaseEntity *spot; + + if (team == TEAM_TERRORIST || team == TEAM_MAXCOUNT) + { + // collect T spawns + for( spot = gEntList.FindEntityByClassname( NULL, "info_player_terrorist" ); + spot; + spot = gEntList.FindEntityByClassname( spot, "info_player_terrorist" ) ) + { + spawnSet.AddToTail( spot ); + } + } + + if (team == TEAM_CT || team == TEAM_MAXCOUNT) + { + // collect CT spawns + for( spot = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); + spot; + spot = gEntList.FindEntityByClassname( spot, "info_player_counterterrorist" ) ) + { + spawnSet.AddToTail( spot ); + } + } + + if (spawnSet.Count() == 0) + { + return NULL; + } + + // select one at random + int which = RandomInt( 0, spawnSet.Count()-1 ); + return spawnSet[ which ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the last time the given radio message was sent for given team + * 'teamID' can be TEAM_CT or TEAM_TERRORIST + */ +float CCSBotManager::GetRadioMessageTimestamp( RadioType event, int teamID ) const +{ + int i = (teamID == TEAM_TERRORIST) ? 0 : 1; + + if (event > RADIO_START_1 && event < RADIO_END) + return m_radioMsgTimestamp[ event - RADIO_START_1 ][ i ]; + + return 0.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the interval since the last time this message was sent + */ +float CCSBotManager::GetRadioMessageInterval( RadioType event, int teamID ) const +{ + int i = (teamID == TEAM_TERRORIST) ? 0 : 1; + + if (event > RADIO_START_1 && event < RADIO_END) + return gpGlobals->curtime - m_radioMsgTimestamp[ event - RADIO_START_1 ][ i ]; + + return 99999999.9f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the given radio message timestamp. + * 'teamID' can be TEAM_CT or TEAM_TERRORIST + */ +void CCSBotManager::SetRadioMessageTimestamp( RadioType event, int teamID ) +{ + int i = (teamID == TEAM_TERRORIST) ? 0 : 1; + + if (event > RADIO_START_1 && event < RADIO_END) + m_radioMsgTimestamp[ event - RADIO_START_1 ][ i ] = gpGlobals->curtime; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset all radio message timestamps + */ +void CCSBotManager::ResetRadioMessageTimestamps( void ) +{ + for( int t=0; t<2; ++t ) + { + for( int m=0; m<(RADIO_END - RADIO_START_1); ++m ) + m_radioMsgTimestamp[ m ][ t ] = 0.0f; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Display nav areas as they become reachable by each team + */ +void DrawOccupyTime( void ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + int r, g, b; + + if (TheCSBots()->GetElapsedRoundTime() > area->GetEarliestOccupyTime( TEAM_TERRORIST )) + { + if (TheCSBots()->GetElapsedRoundTime() > area->GetEarliestOccupyTime( TEAM_CT )) + { + r = 255; g = 0; b = 255; + } + else + { + r = 255; g = 0; b = 0; + } + } + else if (TheCSBots()->GetElapsedRoundTime() > area->GetEarliestOccupyTime( TEAM_CT )) + { + r = 0; g = 0; b = 255; + } + else + { + continue; + } + + const Vector &nw = area->GetCorner( NORTH_WEST ); + const Vector &ne = area->GetCorner( NORTH_EAST ); + const Vector &sw = area->GetCorner( SOUTH_WEST ); + const Vector &se = area->GetCorner( SOUTH_EAST ); + + NDebugOverlay::Line( nw, ne, r, g, b, true, 0.1f ); + NDebugOverlay::Line( nw, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, ne, r, g, b, true, 0.1f ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Display areas where players will likely have initial battles + */ +void DrawBattlefront( void ) +{ + const float epsilon = 1.0f; + int r = 255, g = 50, b = 0; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( fabs(area->GetEarliestOccupyTime( TEAM_TERRORIST ) - area->GetEarliestOccupyTime( TEAM_CT )) > epsilon ) + { + continue; + } + + + const Vector &nw = area->GetCorner( NORTH_WEST ); + const Vector &ne = area->GetCorner( NORTH_EAST ); + const Vector &sw = area->GetCorner( SOUTH_WEST ); + const Vector &se = area->GetCorner( SOUTH_EAST ); + + NDebugOverlay::Line( nw, ne, r, g, b, true, 0.1f ); + NDebugOverlay::Line( nw, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, ne, r, g, b, true, 0.1f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +static bool CheckAreaAgainstAllZoneAreas(CNavArea *queryArea) +{ + // A marked area means they just want to double check this one spot + int goalZoneCount = TheCSBots()->GetZoneCount(); + + for( int zoneIndex = 0; zoneIndex < goalZoneCount; zoneIndex++ ) + { + const CCSBotManager::Zone *currentZone = TheCSBots()->GetZone(zoneIndex); + + int zoneAreaCount = currentZone->m_areaCount; + for( int areaIndex = 0; areaIndex < zoneAreaCount; areaIndex++ ) + { + CNavArea *zoneArea = currentZone->m_area[areaIndex]; + // We need to be connected to every area in the zone, since we don't know what other code might pick for an area + ShortestPathCost cost; + if( NavAreaTravelDistance(queryArea, zoneArea, cost) == -1.0f ) + { + Msg( "Area #%d is disconnected from goal area #%d.\n", + queryArea->GetID(), + zoneArea->GetID() + ); + return false; + } + } + + } + return true; +} + +CON_COMMAND_F( nav_check_connectivity, "Checks to be sure every (or just the marked) nav area can get to every goal area for the map (hostages or bomb site).", FCVAR_CHEAT ) +{ + //Nav command in here since very CS specific. + + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( TheNavMesh->GetMarkedArea() ) + { + CNavArea *markedArea = TheNavMesh->GetMarkedArea(); + bool fine = CheckAreaAgainstAllZoneAreas( markedArea ); + if( fine ) + { + Msg( "Area #%d is connected to all goal areas.\n", markedArea->GetID() ); + } + } + else + { + // Otherwise, loop through every area, and make sure they can all get to the goal. + float start = engine->Time(); + FOR_EACH_VEC( TheNavAreas, nit ) + { + CheckAreaAgainstAllZoneAreas(TheNavAreas[ nit ]); + } + + float end = engine->Time(); + float time = (end - start) * 1000.0f; + Msg( "nav_check_connectivity took %2.2f ms\n", time ); + } +} + + + + diff --git a/game/server/cstrike/bot/cs_bot_manager.h b/game/server/cstrike/bot/cs_bot_manager.h new file mode 100644 index 0000000..3f1da76 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_manager.h @@ -0,0 +1,400 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef CS_CONTROL_H +#define CS_CONTROL_H + + +#include "bot_manager.h" +#include "nav_area.h" +#include "bot_util.h" +#include "bot_profile.h" +#include "cs_shareddefs.h" +#include "cs_player.h" + +extern ConVar friendlyfire; + +class CBasePlayerWeapon; + +/** + * Given one team, return the other + */ +inline int OtherTeam( int team ) +{ + return (team == TEAM_TERRORIST) ? TEAM_CT : TEAM_TERRORIST; +} + +class CCSBotManager; + +// accessor for CS-specific bots +inline CCSBotManager *TheCSBots( void ) +{ + return reinterpret_cast< CCSBotManager * >( TheBots ); +} + +//-------------------------------------------------------------------------------------------------------------- +class BotEventInterface : public IGameEventListener2 +{ +public: + virtual const char *GetEventName( void ) const = 0; +}; + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- +/** + * Macro to set up an OnEventClass() in TheCSBots. + */ +#define DECLARE_BOTMANAGER_EVENT_LISTENER( BotManagerSingleton, EventClass, EventName ) \ + public: \ + virtual void On##EventClass( IGameEvent *data ); \ + private: \ + class EventClass##Event : public BotEventInterface \ + { \ + bool m_enabled; \ + public: \ + EventClass##Event( void ) \ + { \ + gameeventmanager->AddListener( this, #EventName, true ); \ + m_enabled = true; \ + } \ + ~EventClass##Event( void ) \ + { \ + if ( m_enabled ) gameeventmanager->RemoveListener( this ); \ + } \ + virtual const char *GetEventName( void ) const \ + { \ + return #EventName; \ + } \ + void Enable( bool enable ) \ + { \ + m_enabled = enable; \ + if ( enable ) \ + gameeventmanager->AddListener( this, #EventName, true ); \ + else \ + gameeventmanager->RemoveListener( this ); \ + } \ + bool IsEnabled( void ) const { return m_enabled; } \ + void FireGameEvent( IGameEvent *event ) \ + { \ + BotManagerSingleton()->On##EventClass( event ); \ + } \ + }; \ + EventClass##Event m_##EventClass##Event; + + +//-------------------------------------------------------------------------------------------------------------- +#define DECLARE_CSBOTMANAGER_EVENT_LISTENER( EventClass, EventName ) DECLARE_BOTMANAGER_EVENT_LISTENER( TheCSBots, EventClass, EventName ) + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Macro to propogate an event from the bot manager to all bots + */ +#define CCSBOTMANAGER_ITERATE_BOTS( Callback, arg1 ) \ + { \ + for ( int idx = 1; idx <= gpGlobals->maxClients; ++idx ) \ + { \ + CBasePlayer *player = UTIL_PlayerByIndex( idx ); \ + if (player == NULL) continue; \ + if (!player->IsBot()) continue; \ + CCSBot *bot = dynamic_cast< CCSBot * >(player); \ + if ( !bot ) continue; \ + bot->Callback( arg1 ); \ + } \ + } + + +//-------------------------------------------------------------------------------------------------------------- +// +// The manager for Counter-Strike specific bots +// +class CCSBotManager : public CBotManager +{ +public: + CCSBotManager(); + + virtual CBasePlayer *AllocateBotEntity( void ); ///< factory method to allocate the appropriate entity for the bot + + virtual void ClientDisconnect( CBaseEntity *entity ); + virtual bool ClientCommand( CBasePlayer *player, const CCommand &args ); + + virtual void ServerActivate( void ); + virtual void ServerDeactivate( void ); + virtual bool ServerCommand( const char *cmd ); + bool IsServerActive( void ) const { return m_serverActive; } + + 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; ///< return priority of player (0 = max pri) + virtual bool IsImportantPlayer( CCSPlayer *player ) const; ///< return true if player is important to scenario (VIP, bomb carrier, etc) + + void ExtractScenarioData( void ); ///< search the map entities to determine the game scenario and define important zones + + // difficulty levels ----------------------------------------------------------------------------------------- + static BotDifficultyType GetDifficultyLevel( void ) + { + if (cv_bot_difficulty.GetFloat() < 0.9f) + return BOT_EASY; + if (cv_bot_difficulty.GetFloat() < 1.9f) + return BOT_NORMAL; + if (cv_bot_difficulty.GetFloat() < 2.9f) + return BOT_HARD; + + return BOT_EXPERT; + } + + // the supported game scenarios ------------------------------------------------------------------------------ + enum GameScenarioType + { + SCENARIO_DEATHMATCH, + SCENARIO_DEFUSE_BOMB, + SCENARIO_RESCUE_HOSTAGES, + SCENARIO_ESCORT_VIP + }; + GameScenarioType GetScenario( void ) const { return m_gameScenario; } + + // "zones" --------------------------------------------------------------------------------------------------- + // depending on the game mode, these are bomb zones, rescue zones, etc. + + enum { MAX_ZONES = 4 }; ///< max # of zones in a map + enum { MAX_ZONE_NAV_AREAS = 16 }; ///< max # of nav areas in a zone + struct Zone + { + CBaseEntity *m_entity; ///< the map entity + CNavArea *m_area[ MAX_ZONE_NAV_AREAS ]; ///< nav areas that overlap this zone + int m_areaCount; + Vector m_center; + bool m_isLegacy; ///< if true, use pev->origin and 256 unit radius as zone + int m_index; + bool m_isBlocked; + Extent m_extent; + }; + + const Zone *GetZone( int i ) const { return &m_zone[i]; } + const Zone *GetZone( const Vector &pos ) const; ///< return the zone that contains the given position + const Zone *GetClosestZone( const Vector &pos ) const; ///< return the closest zone to the given position + const Zone *GetClosestZone( const CBaseEntity *entity ) const; ///< return the closest zone to the given entity + int GetZoneCount( void ) const { return m_zoneCount; } + void CheckForBlockedZones( void ); + + + const Vector *GetRandomPositionInZone( const Zone *zone ) const; ///< return a random position inside the given zone + CNavArea *GetRandomAreaInZone( const Zone *zone ) const; ///< return a random area inside the given zone + + /** + * Return the zone closest to the given position, using the given cost heuristic + */ + template< typename CostFunctor > + const Zone *GetClosestZone( CNavArea *startArea, CostFunctor costFunc, float *travelDistance = NULL ) const + { + const Zone *closeZone = NULL; + float closeDist = 99999999.9f; + + if (startArea == NULL) + return NULL; + + for( int i=0; i<m_zoneCount; ++i ) + { + if (m_zone[i].m_areaCount == 0) + continue; + + if ( m_zone[i].m_isBlocked ) + continue; + + // just use the first overlapping nav area as a reasonable approximation + float dist = NavAreaTravelDistance( startArea, m_zone[i].m_area[0], costFunc ); + + if (dist >= 0.0f && dist < closeDist) + { + closeZone = &m_zone[i]; + closeDist = dist; + } + } + + if (travelDistance) + *travelDistance = closeDist; + + return closeZone; + } + + /// pick a zone at random and return it + const Zone *GetRandomZone( void ) const + { + if (m_zoneCount == 0) + return NULL; + + int i; + CUtlVector< const Zone * > unblockedZones; + for ( i=0; i<m_zoneCount; ++i ) + { + if ( m_zone[i].m_isBlocked ) + continue; + + unblockedZones.AddToTail( &(m_zone[i]) ); + } + + if ( unblockedZones.Count() == 0 ) + return NULL; + + return unblockedZones[ RandomInt( 0, unblockedZones.Count()-1 ) ]; + } + + + /// returns a random spawn point for the given team (no arg means use both team spawnpoints) + CBaseEntity *GetRandomSpawn( int team = TEAM_MAXCOUNT ) const; + + + bool IsBombPlanted( void ) const { return m_isBombPlanted; } ///< returns true if bomb has been planted + float GetBombPlantTimestamp( void ) const { return m_bombPlantTimestamp; } ///< return time bomb was planted + bool IsTimeToPlantBomb( void ) const; ///< return true if it's ok to try to plant bomb + CCSPlayer *GetBombDefuser( void ) const { return m_bombDefuser; } ///< return the player currently defusing the bomb, or NULL + float GetBombTimeLeft( void ) const; ///< get the time remaining before the planted bomb explodes + CBaseEntity *GetLooseBomb( void ) { return m_looseBomb; } ///< return the bomb if it is loose on the ground + CNavArea *GetLooseBombArea( void ) const { return m_looseBombArea; } ///< return area that bomb is in/near + void SetLooseBomb( CBaseEntity *bomb ); + + + float GetRadioMessageTimestamp( RadioType event, int teamID ) const; ///< return the last time the given radio message was sent for given team + float GetRadioMessageInterval( RadioType event, int teamID ) const; ///< return the interval since the last time this message was sent + void SetRadioMessageTimestamp( RadioType event, int teamID ); + void ResetRadioMessageTimestamps( void ); + + float GetLastSeenEnemyTimestamp( void ) const { return m_lastSeenEnemyTimestamp; } ///< return the last time anyone has seen an enemy + void SetLastSeenEnemyTimestamp( void ) { m_lastSeenEnemyTimestamp = gpGlobals->curtime; } + + float GetRoundStartTime( void ) const { return m_roundStartTimestamp; } + float GetElapsedRoundTime( void ) const { return gpGlobals->curtime - m_roundStartTimestamp; } ///< return the elapsed time since the current round began + + bool AllowRogues( void ) const { return cv_bot_allow_rogues.GetBool(); } + bool AllowPistols( void ) const { return cv_bot_allow_pistols.GetBool(); } + bool AllowShotguns( void ) const { return cv_bot_allow_shotguns.GetBool(); } + bool AllowSubMachineGuns( void ) const { return cv_bot_allow_sub_machine_guns.GetBool(); } + bool AllowRifles( void ) const { return cv_bot_allow_rifles.GetBool(); } + bool AllowMachineGuns( void ) const { return cv_bot_allow_machine_guns.GetBool(); } + bool AllowGrenades( void ) const { return cv_bot_allow_grenades.GetBool(); } + bool AllowSnipers( void ) const { return cv_bot_allow_snipers.GetBool(); } +#ifdef CS_SHIELD_ENABLED + bool AllowTacticalShield( void ) const { return cv_bot_allow_shield.GetBool(); } +#else + bool AllowTacticalShield( void ) const { return false; } +#endif // CS_SHIELD_ENABLED + + bool AllowFriendlyFireDamage( void ) const { return friendlyfire.GetBool(); } + + bool IsWeaponUseable( const CWeaponCSBase *weapon ) const; ///< return true if the bot can use this weapon + + bool IsDefenseRushing( void ) const { return m_isDefenseRushing; } ///< returns true if defense team has "decided" to rush this round + bool IsOnDefense( const CCSPlayer *player ) const; ///< return true if this player is on "defense" + bool IsOnOffense( const CCSPlayer *player ) const; ///< return true if this player is on "offense" + + bool IsRoundOver( void ) const { return m_isRoundOver; } ///< return true if the round has ended + + #define FROM_CONSOLE true + bool BotAddCommand( int team, bool isFromConsole = false, const char *profileName = NULL, CSWeaponType weaponType = WEAPONTYPE_UNKNOWN, BotDifficultyType difficulty = NUM_DIFFICULTY_LEVELS ); ///< process the "bot_add" console command + +private: + enum SkillType { LOW, AVERAGE, HIGH, RANDOM }; + + void MaintainBotQuota( void ); + + static bool m_isMapDataLoaded; ///< true if we've attempted to load map data + bool m_serverActive; ///< true between ServerActivate() and ServerDeactivate() + + GameScenarioType m_gameScenario; ///< what kind of game are we playing + + Zone m_zone[ MAX_ZONES ]; + int m_zoneCount; + + bool m_isBombPlanted; ///< true if bomb has been planted + float m_bombPlantTimestamp; ///< time bomb was planted + float m_earliestBombPlantTimestamp; ///< don't allow planting until after this time has elapsed + CCSPlayer *m_bombDefuser; ///< the player currently defusing a bomb + EHANDLE m_looseBomb; ///< will be non-NULL if bomb is loose on the ground + CNavArea *m_looseBombArea; ///< area that bomb is is/near + + bool m_isRoundOver; ///< true if the round has ended + + CountdownTimer m_checkTransientAreasTimer; ///< when elapsed, all transient nav areas should be checked for blockage + + float m_radioMsgTimestamp[ RADIO_END - RADIO_START_1 ][ 2 ]; + + float m_lastSeenEnemyTimestamp; + float m_roundStartTimestamp; ///< the time when the current round began + + bool m_isDefenseRushing; ///< whether defensive team is rushing this round or not + + // Event Handlers -------------------------------------------------------------------------------------------- + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerFootstep, player_footstep ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerRadio, player_radio ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerDeath, player_death ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerFallDamage, player_falldamage ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombPickedUp, bomb_pickup ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombPlanted, bomb_planted ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombBeep, bomb_beep ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombDefuseBegin, bomb_begindefuse ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombDefused, bomb_defused ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombDefuseAbort, bomb_abortdefuse ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombExploded, bomb_exploded ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( RoundEnd, round_end ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( RoundStart, round_start ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( RoundFreezeEnd, round_freeze_end ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( DoorMoving, door_moving ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BreakProp, break_prop ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BreakBreakable, break_breakable ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( HostageFollows, hostage_follows ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( HostageRescuedAll, hostage_rescued_all ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponFire, weapon_fire ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponFireOnEmpty, weapon_fire_on_empty ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponReload, weapon_reload ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponZoom, weapon_zoom ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BulletImpact, bullet_impact ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( HEGrenadeDetonate, hegrenade_detonate ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( FlashbangDetonate, flashbang_detonate ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( SmokeGrenadeDetonate, smokegrenade_detonate ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( GrenadeBounce, grenade_bounce ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( NavBlocked, nav_blocked ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( ServerShutdown, server_shutdown ) + + CUtlVector< BotEventInterface * > m_commonEventListeners; // These event listeners fire often, and can be disabled for performance gains when no bots are present. + bool m_eventListenersEnabled; + void EnableEventListeners( bool enable ); +}; + +inline CBasePlayer *CCSBotManager::AllocateBotEntity( void ) +{ + return static_cast<CBasePlayer *>( CreateEntityByName( "cs_bot" ) ); +} + +inline bool CCSBotManager::IsTimeToPlantBomb( void ) const +{ + return (gpGlobals->curtime >= m_earliestBombPlantTimestamp); +} + +inline const CCSBotManager::Zone *CCSBotManager::GetClosestZone( const CBaseEntity *entity ) const +{ + if (entity == NULL) + return NULL; + + Vector centroid = entity->GetAbsOrigin(); + centroid.z += HalfHumanHeight; + return GetClosestZone( centroid ); +} + +#endif diff --git a/game/server/cstrike/bot/cs_bot_nav.cpp b/game/server/cstrike/bot/cs_bot_nav.cpp new file mode 100644 index 0000000..8406101 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_nav.cpp @@ -0,0 +1,963 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "obstacle_pushaway.h" +#include "fmtstr.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +const float NearBreakableCheckDist = 20.0f; +const float FarBreakableCheckDist = 300.0f; + +#define DEBUG_BREAKABLES 0 +#define DEBUG_DOORS 0 + +//-------------------------------------------------------------------------------------------------------------- +#if DEBUG_BREAKABLES +static void DrawOutlinedQuad( const Vector &p1, + const Vector &p2, + const Vector &p3, + const Vector &p4, + int r, int g, int b, + float duration ) +{ + NDebugOverlay::Triangle( p1, p2, p3, r, g, b, 20, false, duration ); + NDebugOverlay::Triangle( p3, p4, p1, r, g, b, 20, false, duration ); + NDebugOverlay::Line( p1, p2, r, g, b, false, duration ); + NDebugOverlay::Line( p2, p3, r, g, b, false, duration ); + NDebugOverlay::Line( p3, p4, r, g, b, false, duration ); + NDebugOverlay::Line( p4, p1, r, g, b, false, duration ); +} +ConVar bot_debug_breakable_duration( "bot_debug_breakable_duration", "30" ); +#endif // DEBUG_BREAKABLES + + +//-------------------------------------------------------------------------------------------------------------- +CBaseEntity * CheckForEntitiesAlongSegment( const Vector &start, const Vector &end, const Vector &mins, const Vector &maxs, CPushAwayEnumerator *enumerator ) +{ + CBaseEntity *entity = NULL; + + Ray_t ray; + ray.Init( start, end, mins, maxs ); + + partition->EnumerateElementsAlongRay( PARTITION_ENGINE_SOLID_EDICTS, ray, false, enumerator ); + if ( enumerator->m_nAlreadyHit > 0 ) + { + entity = enumerator->m_AlreadyHit[0]; + } + +#if DEBUG_BREAKABLES + if ( entity ) + { + DrawOutlinedQuad( start + mins, start + maxs, end + maxs, end + mins, 255, 0, 0, bot_debug_breakable_duration.GetFloat() ); + } + else + { + DrawOutlinedQuad( start + mins, start + maxs, end + maxs, end + mins, 0, 255, 0, 0.1 ); + } +#endif // DEBUG_BREAKABLES + + return entity; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Look up to 'distance' units ahead on the bot's path for entities. Returns the closest one. + */ +CBaseEntity * CCSBot::FindEntitiesOnPath( float distance, CPushAwayEnumerator *enumerator, bool checkStuck ) +{ + Vector goal; + + int pathIndex = FindPathPoint( distance, &goal, NULL ); + bool isDegeneratePath = ( pathIndex == m_pathLength ); + if ( isDegeneratePath ) + { + goal = m_goalPosition; + } + goal.z += HalfHumanHeight; + + Vector mins, maxs; + mins = Vector( 0, 0, -HalfHumanWidth ); + maxs = Vector( 0, 0, HalfHumanHeight ); + + if ( distance <= NearBreakableCheckDist && m_isStuck && checkStuck ) + { + mins = Vector( -HalfHumanWidth, -HalfHumanWidth, -HalfHumanWidth ); + maxs = Vector( HalfHumanWidth, HalfHumanWidth, HalfHumanHeight ); + } + + CBaseEntity *entity = NULL; + if ( isDegeneratePath ) + { + entity = CheckForEntitiesAlongSegment( WorldSpaceCenter(), m_goalPosition + Vector( 0, 0, HalfHumanHeight ), mins, maxs, enumerator ); +#if DEBUG_BREAKABLES + if ( entity ) + { + NDebugOverlay::HorzArrow( WorldSpaceCenter(), m_goalPosition, 6, 0, 0, 255, 255, true, bot_debug_breakable_duration.GetFloat() ); + } +#endif // DEBUG_BREAKABLES + } + else + { + int startIndex = MAX( 0, m_pathIndex ); + float distanceLeft = distance; + // HACK: start with an index one lower than normal, so we can trace from the bot's location to the + // start of the path nodes. + for( int i=startIndex-1; i<m_pathLength-1; ++i ) + { + Vector start, end; + if ( i == startIndex - 1 ) + { + start = GetAbsOrigin(); + end = m_path[i+1].pos; + } + else + { + start = m_path[i].pos; + end = m_path[i+1].pos; + + if ( m_path[i+1].how == GO_LADDER_UP ) + { + // Need two checks. First we'll check along the ladder + start = m_path[i].pos; + end = m_path[i+1].ladder->m_top; + } + else if ( m_path[i].how == GO_LADDER_UP ) + { + start = m_path[i].ladder->m_top; + } + else if ( m_path[i+1].how == GO_LADDER_DOWN ) + { + // Need two checks. First we'll check along the ladder + start = m_path[i].pos; + end = m_path[i+1].ladder->m_bottom; + } + else if ( m_path[i].how == GO_LADDER_DOWN ) + { + start = m_path[i].ladder->m_bottom; + } + } + + float segmentLength = (start - end).Length(); + if ( distanceLeft - segmentLength < 0 ) + { + // scale our segment back so we don't look too far + Vector direction = end - start; + direction.NormalizeInPlace(); + + end = start + direction * distanceLeft; + } + entity = CheckForEntitiesAlongSegment( start + Vector( 0, 0, HalfHumanHeight ), end + Vector( 0, 0, HalfHumanHeight ), mins, maxs, enumerator ); + if ( entity ) + { +#if DEBUG_BREAKABLES + NDebugOverlay::HorzArrow( start, end, 4, 0, 255, 0, 255, true, bot_debug_breakable_duration.GetFloat() ); +#endif // DEBUG_BREAKABLES + break; + } + + if ( m_path[i].ladder && !IsOnLadder() && distance > NearBreakableCheckDist ) // don't try to break breakables on the other end of a ladder + break; + + distanceLeft -= segmentLength; + if ( distanceLeft < 0 ) + break; + + if ( i != startIndex - 1 && m_path[i+1].ladder ) + { + // Now we'll check from the ladder out to the endpoint + start = ( m_path[i+1].how == GO_LADDER_DOWN ) ? m_path[i+1].ladder->m_bottom : m_path[i+1].ladder->m_top; + end = m_path[i+1].pos; + + entity = CheckForEntitiesAlongSegment( start + Vector( 0, 0, HalfHumanHeight ), end + Vector( 0, 0, HalfHumanHeight ), mins, maxs, enumerator ); + if ( entity ) + { +#if DEBUG_BREAKABLES + NDebugOverlay::HorzArrow( start, end, 4, 0, 255, 0, 255, true, bot_debug_breakable_duration.GetFloat() ); +#endif // DEBUG_BREAKABLES + break; + } + } + } + } + + if ( entity && !IsVisible( entity->WorldSpaceCenter(), false, entity ) ) + return NULL; + + return entity; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::PushawayTouch( CBaseEntity *pOther ) +{ +#if DEBUG_BREAKABLES + NDebugOverlay::EntityBounds( pOther, 255, 0, 0, 127, 0.1f ); +#endif // DEBUG_BREAKABLES + + // if we're not stuck or crouched, we don't care + if ( !m_isStuck && !IsCrouching() ) + return; + + // See if it's breakable + CBaseEntity *props[1]; + CBotBreakableEnumerator enumerator( props, ARRAYSIZE( props ) ); + enumerator.EnumElement( pOther ); + + if ( enumerator.m_nAlreadyHit == 1 ) + { + // it's breakable - try to shoot it. + SetLookAt( "Breakable", pOther->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check for breakable physics props and other breakable entities. We do this here instead of catching them + * in OnTouch() because players don't collide with physics props, so OnTouch() doesn't get called. Also, + * looking ahead like this lets us anticipate when we'll need to break something, and do it before being + * stopped by it. + */ +void CCSBot::BreakablesCheck( void ) +{ +#if DEBUG_BREAKABLES + /* + // Debug code to visually mark all breakables near us + { + Ray_t ray; + Vector origin = WorldSpaceCenter(); + Vector mins( -400, -400, -400 ); + Vector maxs( 400, 400, 400 ); + ray.Init( origin, origin, mins, maxs ); + + CBaseEntity *props[40]; + CBotBreakableEnumerator enumerator( props, ARRAYSIZE( props ) ); + partition->EnumerateElementsAlongRay( PARTITION_ENGINE_SOLID_EDICTS, ray, false, &enumerator ); + for ( int i=0; i<enumerator.m_nAlreadyHit; ++i ) + { + CBaseEntity *prop = props[i]; + if ( prop && prop->m_takedamage == DAMAGE_YES ) + { + CFmtStr msg; + const char *text = msg.sprintf( "%s, %d health", prop->GetClassname(), prop->m_iHealth ); + if ( prop->m_iHealth > 200 ) + { + NDebugOverlay::EntityBounds( prop, 255, 0, 0, 10, 0.2f ); + prop->EntityText( 0, text, 0.2f, 255, 0, 0, 255 ); + } + else + { + NDebugOverlay::EntityBounds( prop, 0, 255, 0, 10, 0.2f ); + prop->EntityText( 0, text, 0.2f, 0, 255, 0, 255 ); + } + } + } + } + */ +#endif // DEBUG_BREAKABLES + + if ( IsAttacking() ) + { + // make sure we aren't running into a breakable trying to knife an enemy + if ( IsUsingKnife() && m_enemy != NULL ) + { + CBaseEntity *breakables[1]; + CBotBreakableEnumerator enumerator( breakables, ARRAYSIZE( breakables ) ); + + CBaseEntity *breakable = NULL; + Vector mins = Vector( -HalfHumanWidth, -HalfHumanWidth, -HalfHumanWidth ); + Vector maxs = Vector( HalfHumanWidth, HalfHumanWidth, HalfHumanHeight ); + breakable = CheckForEntitiesAlongSegment( WorldSpaceCenter(), m_enemy->WorldSpaceCenter(), mins, maxs, &enumerator ); + if ( breakable ) + { +#if DEBUG_BREAKABLES + NDebugOverlay::HorzArrow( WorldSpaceCenter(), m_enemy->WorldSpaceCenter(), 6, 0, 0, 255, 255, true, bot_debug_breakable_duration.GetFloat() ); +#endif // DEBUG_BREAKABLES + + // look at it (chances are we'll already be looking at it, since it's between us and our enemy) + SetLookAt( "Breakable", breakable->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + + // break it (again, don't wait: we don't have ammo, since we're using the knife, and we're looking mostly at it anyway) + PrimaryAttack(); + } + } + return; + } + + if ( !HasPath() ) + return; + + bool isNear = true; + + // Check just in front of us on the path + CBaseEntity *breakables[4]; + CBotBreakableEnumerator enumerator( breakables, ARRAYSIZE( breakables ) ); + CBaseEntity *breakable = FindEntitiesOnPath( NearBreakableCheckDist, &enumerator, true ); + + // If we don't have an object right in front of us, check a ways out + if ( !breakable ) + { + breakable = FindEntitiesOnPath( FarBreakableCheckDist, &enumerator, false ); + isNear = false; + } + + // Try to shoot a breakable we know about + if ( breakable ) + { + // look at it + SetLookAt( "Breakable", breakable->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + } + + // break it + if ( IsLookingAtSpot( PRIORITY_HIGH ) && m_lookAtSpotAttack ) + { + if ( IsUsingGrenade() || ( !isNear && IsUsingKnife() ) ) + { + EquipBestWeapon( MUST_EQUIP ); + } + else if ( GetActiveWeapon() && GetActiveWeapon()->m_flNextPrimaryAttack <= gpGlobals->curtime ) + { + bool shouldShoot = IsLookingAtPosition( m_lookAtSpot, 10.0f ); + + if ( !shouldShoot ) + { + CBaseEntity *breakables[1]; + CBotBreakableEnumerator LOSbreakable( breakables, ARRAYSIZE( breakables ) ); + + // compute the unit vector along our view + Vector aimDir = GetViewVector(); + + // trace the potential bullet's path + trace_t result; + UTIL_TraceLine( EyePosition(), EyePosition() + FarBreakableCheckDist * aimDir, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + if ( result.DidHitNonWorldEntity() ) + { + LOSbreakable.EnumElement( result.m_pEnt ); + if ( LOSbreakable.m_nAlreadyHit == 1 && LOSbreakable.m_AlreadyHit[0] == breakable ) + { + shouldShoot = true; + } + } + } + + shouldShoot = shouldShoot && !IsFriendInLineOfFire(); + + if ( shouldShoot ) + { + PrimaryAttack(); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check for doors that need +use to open. + */ +void CCSBot::DoorCheck( void ) +{ + if ( IsAttacking() && !IsUsingKnife() ) + { + // If we're attacking with a gun or nade, don't bother with doors. If we're trying to + // knife someone, we might need to open a door. + m_isOpeningDoor = false; + return; + } + + if ( !HasPath() ) + return; + + // Find any doors that need a +use to open just in front of us along the path. + CBaseEntity *doors[4]; + CBotDoorEnumerator enumerator( doors, ARRAYSIZE( doors ) ); + CBaseEntity *door = FindEntitiesOnPath( NearBreakableCheckDist, &enumerator, false ); + + if ( door ) + { + if ( !IsLookingAtSpot( PRIORITY_HIGH ) ) + { + if ( !IsOpeningDoor() ) + { + OpenDoor( door ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset the stuck-checker. + */ +void CCSBot::ResetStuckMonitor( void ) +{ + if (m_isStuck) + { + if (IsLocalPlayerWatchingMe() && cv_bot_debug.GetBool() && UTIL_GetListenServerHost()) + { + CBasePlayer *localPlayer = UTIL_GetListenServerHost(); + CSingleUserRecipientFilter filter( localPlayer ); + EmitSound( filter, localPlayer->entindex(), "Bot.StuckSound" ); + } + } + + m_isStuck = false; + m_stuckTimestamp = 0.0f; + m_stuckJumpTimer.Invalidate(); + m_avgVelIndex = 0; + m_avgVelCount = 0; + + m_areaEnteredTimestamp = gpGlobals->curtime; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Test if we have become stuck + */ +void CCSBot::StuckCheck( void ) +{ + if (m_isStuck) + { + // we are stuck - see if we have moved far enough to be considered unstuck + Vector delta = GetAbsOrigin() - m_stuckSpot; + + const float unstuckRange = 75.0f; + if (delta.IsLengthGreaterThan( unstuckRange )) + { + // we are no longer stuck + ResetStuckMonitor(); + PrintIfWatched( "UN-STUCK\n" ); + } + } + else + { + // check if we are stuck + + // compute average velocity over a short period (for stuck check) + Vector vel = GetAbsOrigin() - m_lastOrigin; + + // if we are jumping, ignore Z + if (IsJumping()) + vel.z = 0.0f; + + // cannot be Length2D, or will break ladder movement (they are only Z) + float moveDist = vel.Length(); + + float deltaT = g_BotUpdateInterval; + + 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 bots will get "stuck" when going down ladders + float stuckVel = (IsUsingLadder()) ? 10.0f : 20.0f; + + if (avgVel < stuckVel) + { + // we are stuck - note when and where we initially become stuck + m_stuckTimestamp = gpGlobals->curtime; + m_stuckSpot = GetAbsOrigin(); + m_stuckJumpTimer.Start( RandomFloat( 0.3f, 0.75f ) ); // 1.0 + + PrintIfWatched( "STUCK\n" ); + if (IsLocalPlayerWatchingMe() && cv_bot_debug.GetInt() > 0.0f && UTIL_GetListenServerHost()) + { + CBasePlayer *localPlayer = UTIL_GetListenServerHost(); + CSingleUserRecipientFilter filter( localPlayer ); + EmitSound( filter, localPlayer->entindex(), "Bot.StuckStart" ); + } + + m_isStuck = true; + } + } + } + + // always need to track this + m_lastOrigin = GetAbsOrigin(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check if we need to jump due to height change + */ +bool CCSBot::DiscontinuityJump( float ground, bool onlyJumpDown, bool mustJump ) +{ + // Don't try to jump if in the air. + if( !(GetFlags() & FL_ONGROUND) ) + { + return false; + } + + float dz = ground - GetFeetZ(); + + if (dz > StepHeight && !onlyJumpDown) + { + // dont restrict jump time when going up + if (Jump( MUST_JUMP )) + { + return true; + } + } + else if (!IsUsingLadder() && dz < -JumpHeight) + { + if (Jump( mustJump )) + { + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find "simple" ground height, treating current nav area as part of the floor + */ +bool CCSBot::GetSimpleGroundHeightWithFloor( const Vector &pos, float *height, Vector *normal ) +{ + if (TheNavMesh->GetSimpleGroundHeight( pos, height, normal )) + { + // our current nav area also serves as a ground polygon + if (m_lastKnownArea && m_lastKnownArea->IsOverlapping( pos )) + *height = MAX( (*height), m_lastKnownArea->GetZ( pos ) ); + + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Get our current radio chatter place + */ +Place CCSBot::GetPlace( void ) const +{ + if (m_lastKnownArea) + return m_lastKnownArea->GetPlace(); + + return UNDEFINED_PLACE; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move towards position, independant of view angle + */ +void CCSBot::MoveTowardsPosition( const Vector &pos ) +{ + Vector myOrigin = GetCentroid( this ); + + // + // Jump up on ledges + // Because we may not be able to get to our goal position and enter the next + // area because our extent collides with a nearby vertical ledge, make sure + // we look far enough ahead to avoid this situation. + // Can't look too far ahead, or bots will try to jump up slopes. + // + // NOTE: We need to do this frequently to catch edges at the right time + // @todo Look ahead *along path* instead of straight line + // + if ((m_lastKnownArea == NULL || !(m_lastKnownArea->GetAttributes() & NAV_MESH_NO_JUMP)) && + !IsOnLadder()) + { + float ground; + Vector aheadRay( pos.x - myOrigin.x, pos.y - myOrigin.y, 0 ); + aheadRay.NormalizeInPlace(); + + // look far ahead to allow us to smoothly jump over gaps, ledges, etc + // only jump if ground is flat at lookahead spot to avoid jumping up slopes + bool jumped = false; + if (IsRunning()) + { + const float farLookAheadRange = 80.0f; // 60 + Vector normal; + Vector stepAhead = myOrigin + farLookAheadRange * aheadRay; + stepAhead.z += HalfHumanHeight; + + if (GetSimpleGroundHeightWithFloor( stepAhead, &ground, &normal )) + { + if (normal.z > 0.9f) + jumped = DiscontinuityJump( ground, ONLY_JUMP_DOWN ); + } + } + + if (!jumped) + { + // close up jumping + const float lookAheadRange = 30.0f; // cant be less or will miss jumps over low walls + Vector stepAhead = myOrigin + lookAheadRange * aheadRay; + stepAhead.z += HalfHumanHeight; + if (GetSimpleGroundHeightWithFloor( stepAhead, &ground )) + { + jumped = DiscontinuityJump( ground ); + } + } + + if (!jumped) + { + // about to fall gap-jumping + const float lookAheadRange = 10.0f; + Vector stepAhead = myOrigin + lookAheadRange * aheadRay; + stepAhead.z += HalfHumanHeight; + if (GetSimpleGroundHeightWithFloor( stepAhead, &ground )) + { + jumped = DiscontinuityJump( ground, ONLY_JUMP_DOWN, MUST_JUMP ); + } + } + } + + + // compute our current forward and lateral vectors + float angle = EyeAngles().y; + + Vector2D dir( BotCOS(angle), BotSIN(angle) ); + Vector2D lat( -dir.y, dir.x ); + + // compute unit vector to goal position + Vector2D to( pos.x - myOrigin.x, pos.y - myOrigin.y ); + to.NormalizeInPlace(); + + // move towards the position independant of our view direction + float toProj = to.x * dir.x + to.y * dir.y; + float latProj = to.x * lat.x + to.y * lat.y; + + const float c = 0.25f; // 0.5 + if (toProj > c) + MoveForward(); + else if (toProj < -c) + MoveBackward(); + + // if we are avoiding someone via strafing, don't override + if (m_avoid != NULL) + return; + + if (latProj >= c) + StrafeLeft(); + else if (latProj <= -c) + StrafeRight(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move away from position, independant of view angle + */ +void CCSBot::MoveAwayFromPosition( const Vector &pos ) +{ + // compute our current forward and lateral vectors + float angle = EyeAngles().y; + + Vector2D dir( BotCOS(angle), BotSIN(angle) ); + Vector2D lat( -dir.y, dir.x ); + + // compute unit vector to goal position + Vector2D to( pos.x - GetAbsOrigin().x, pos.y - GetAbsOrigin().y ); + to.NormalizeInPlace(); + + // move away from the position independant of our view direction + float toProj = to.x * dir.x + to.y * dir.y; + float latProj = to.x * lat.x + to.y * lat.y; + + const float c = 0.5f; + if (toProj > c) + MoveBackward(); + else if (toProj < -c) + MoveForward(); + + if (latProj >= c) + StrafeRight(); + else if (latProj <= -c) + StrafeLeft(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Strafe (sidestep) away from position, independant of view angle + */ +void CCSBot::StrafeAwayFromPosition( const Vector &pos ) +{ + // compute our current forward and lateral vectors + float angle = EyeAngles().y; + + Vector2D dir( BotCOS(angle), BotSIN(angle) ); + Vector2D lat( -dir.y, dir.x ); + + // compute unit vector to goal position + Vector2D to( pos.x - GetAbsOrigin().x, pos.y - GetAbsOrigin().y ); + to.NormalizeInPlace(); + + float latProj = to.x * lat.x + to.y * lat.y; + + if (latProj >= 0.0f) + StrafeRight(); + else + StrafeLeft(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * For getting un-stuck + */ +void CCSBot::Wiggle( void ) +{ + if (IsCrouching()) + { + return; + } + + // for wiggling + if (m_wiggleTimer.IsElapsed()) + { + m_wiggleDirection = (NavRelativeDirType)RandomInt( 0, 3 ); + m_wiggleTimer.Start( RandomFloat( 0.3f, 0.5f ) ); // 0.3, 0.5 + } + + Vector forward, right; + EyeVectors( &forward, &right ); + + const float lookAheadRange = (m_lastKnownArea && (m_lastKnownArea->GetAttributes() & NAV_MESH_WALK)) ? 5.0f : 30.0f; + float ground; + + switch( m_wiggleDirection ) + { + case LEFT: + { + // don't move left if we will fall + Vector pos = GetAbsOrigin() - (lookAheadRange * right); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + StrafeLeft(); + } + } + break; + } + + case RIGHT: + { + // don't move right if we will fall + Vector pos = GetAbsOrigin() + (lookAheadRange * right); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + StrafeRight(); + } + } + break; + } + + case FORWARD: + { + // don't move forward if we will fall + Vector pos = GetAbsOrigin() + (lookAheadRange * forward); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + MoveForward(); + } + } + break; + } + + case BACKWARD: + { + // don't move backward if we will fall + Vector pos = GetAbsOrigin() - (lookAheadRange * forward); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + MoveBackward(); + } + } + break; + } + } + + if (m_stuckJumpTimer.IsElapsed() && m_lastKnownArea && !(m_lastKnownArea->GetAttributes() & NAV_MESH_NO_JUMP)) + { + if (Jump()) + { + m_stuckJumpTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine approach points from eye position and approach areas of current area + */ +void CCSBot::ComputeApproachPoints( void ) +{ + m_approachPointCount = 0; + + if (m_lastKnownArea == NULL) + { + return; + } + + // assume we're crouching for now + Vector eye = GetCentroid( this ); // + pev->view_ofs; // eye position + + Vector ap; + float halfWidth; + for( int i=0; i<m_lastKnownArea->GetApproachInfoCount() && m_approachPointCount < MAX_APPROACH_POINTS; ++i ) + { + const CCSNavArea::ApproachInfo *info = m_lastKnownArea->GetApproachInfo( i ); + + if (info->here.area == NULL || info->prev.area == NULL) + { + continue; + } + + // compute approach point (approach area is "info->here") + if (info->prevToHereHow <= GO_WEST) + { + info->prev.area->ComputePortal( info->here.area, (NavDirType)info->prevToHereHow, &ap, &halfWidth ); + ap.z = info->here.area->GetZ( ap ); + } + else + { + // use the area's center as an approach point + ap = info->here.area->GetCenter(); + } + + // "bend" our line of sight around corners until we can see the approach point + Vector bendPoint; + if (BendLineOfSight( eye, ap + Vector( 0, 0, HalfHumanHeight ), &bendPoint )) + { + // put point on the ground + if (TheNavMesh->GetGroundHeight( bendPoint, &bendPoint.z ) == false) + { + bendPoint.z = ap.z; + } + + m_approachPoint[ m_approachPointCount ].m_pos = bendPoint; + m_approachPoint[ m_approachPointCount ].m_area = info->here.area; + ++m_approachPointCount; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::DrawApproachPoints( void ) const +{ + for( int i=0; i<m_approachPointCount; ++i ) + { + if (TheCSBots()->GetElapsedRoundTime() >= m_approachPoint[i].m_area->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) )) + NDebugOverlay::Cross3D( m_approachPoint[i].m_pos, 10.0f, 255, 0, 255, true, 0.1f ); + else + NDebugOverlay::Cross3D( m_approachPoint[i].m_pos, 10.0f, 100, 100, 100, true, 0.1f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find the approach point that is nearest to our current path, ahead of us + */ +bool CCSBot::FindApproachPointNearestPath( Vector *pos ) +{ + if (!HasPath()) + return false; + + // make sure approach points are accurate + ComputeApproachPoints(); + + if (m_approachPointCount == 0) + return false; + + Vector target = Vector( 0, 0, 0 ), close; + float targetRangeSq = 0.0f; + bool found = false; + + int start = m_pathIndex; + int end = m_pathLength; + + // + // We dont want the strictly closest point, but the farthest approach point + // from us that is near our path + // + const float nearPathSq = 10000.0f; // (100) + + for( int i=0; i<m_approachPointCount; ++i ) + { + if (FindClosestPointOnPath( m_approachPoint[i].m_pos, start, end, &close ) == false) + continue; + + float rangeSq = (m_approachPoint[i].m_pos - close).LengthSqr(); + if (rangeSq > nearPathSq) + continue; + + if (rangeSq > targetRangeSq) + { + target = close; + targetRangeSq = rangeSq; + found = true; + } + } + + if (found) + { + *pos = target + Vector( 0, 0, HalfHumanHeight ); + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are at the/an enemy spawn right now + */ +bool CCSBot::IsAtEnemySpawn( void ) const +{ + CBaseEntity *spot; + const char *spawnName = (GetTeamNumber() == TEAM_TERRORIST) ? "info_player_counterterrorist" : "info_player_terrorist"; + + // check if we are at any of the enemy's spawn points + for( spot = gEntList.FindEntityByClassname( NULL, spawnName ); spot; spot = gEntList.FindEntityByClassname( spot, spawnName ) ) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( spot->WorldSpaceCenter() ); + if (area && GetLastKnownArea() == area) + { + return true; + } + } + + return false; +} + diff --git a/game/server/cstrike/bot/cs_bot_pathfind.cpp b/game/server/cstrike/bot/cs_bot_pathfind.cpp new file mode 100644 index 0000000..8604f9b --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_pathfind.cpp @@ -0,0 +1,2075 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.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 + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Finds a point from which we can approach a descending ladder. First it tries behind the ladder, + * then in front of ladder, based on LOS. Once we know the direction, we snap to the aproaching nav + * area. Returns true if we're approaching from behind the ladder. + */ +static bool FindDescendingLadderApproachPoint( const CNavLadder *ladder, const CNavArea *area, Vector *pos ) +{ + *pos = ladder->m_top - ladder->GetNormal() * 2.0f * HalfHumanWidth; + + trace_t result; + UTIL_TraceLine( ladder->m_top, *pos, MASK_PLAYERSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction < 1.0f) + { + *pos = ladder->m_top + ladder->GetNormal() * 2.0f * HalfHumanWidth; + + area->GetClosestPointOnArea( *pos, pos ); + } + + // Use a cross product to determine which side of the ladder 'pos' is on + Vector posToLadder = *pos - ladder->m_top; + float dot = posToLadder.Dot( ladder->GetNormal() ); + return ( dot < 0.0f ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine actual path positions bot will move between along the path + */ +bool CCSBot::ComputePathPositions( void ) +{ + if (m_pathLength == 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_pathLength; ++i ) + { + const ConnectInfo *from = &m_path[ i-1 ]; + ConnectInfo *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 = 75.0f; // 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_pathLength < MAX_PATH_LENGTH-1) + { + // copy nodes down + for( int j=m_pathLength; j>i; --j ) + m_path[j] = m_path[j-1]; + + // path is one node longer + ++m_pathLength; + + // move index ahead into the new node we just duplicated + ++i; + + m_path[i].pos.x = to->pos.x; + m_path[i].pos.y = to->pos.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 NavLadderConnectVector *pLadders = from->area->GetLadders( CNavLadder::LADDER_UP ); + int it; + for ( it = 0; it < pLadders->Count(); ++it) + { + CNavLadder *ladder = (*pLadders)[ 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 == pLadders->Count()) + { + PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + else if (to->how == GO_LADDER_DOWN) // to get to next area, must go down a ladder + { + // find our ladder + const NavLadderConnectVector *pLadders = from->area->GetLadders( CNavLadder::LADDER_DOWN ); + int it; + for ( it = 0; it < pLadders->Count(); ++it) + { + CNavLadder *ladder = (*pLadders)[ it ].ladder; + + if (ladder->m_bottomArea == to->area) + { + to->ladder = ladder; + + FindDescendingLadderApproachPoint( to->ladder, from->area, &to->pos ); + break; + } + } + + if (it == pLadders->Count()) + { + PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * If next step of path uses a ladder, prepare to traverse it + */ +void CCSBot::SetupLadderMovement( void ) +{ + if (m_pathIndex < 1 || m_pathLength == 0) + return; + + const ConnectInfo *to = &m_path[ m_pathIndex ]; + const ConnectInfo *from = &m_path[ m_pathIndex - 1 ]; + + if (to->ladder) + { + m_spotEncounter = NULL; + m_areaEnteredTimestamp = gpGlobals->curtime; + + m_pathLadder = to->ladder; + m_pathLadderTimestamp = gpGlobals->curtime; + + QAngle ladderAngles; + VectorAngles( m_pathLadder->GetNormal(), ladderAngles ); + + // to get to next area, we must traverse a ladder + if (to->how == GO_LADDER_UP) + { + m_pathLadderState = APPROACH_ASCENDING_LADDER; + m_pathLadderFaceIn = true; + PrintIfWatched( "APPROACH_ASCENDING_LADDER\n" ); + m_goalPosition = m_pathLadder->m_bottom + m_pathLadder->GetNormal() * 2.0f * HalfHumanWidth; + m_lookAheadAngle = AngleNormalizePositive( ladderAngles[ YAW ] + 180.0f ); + } + else + { + // try to mount ladder "face out" first + bool behind = FindDescendingLadderApproachPoint( m_pathLadder, from->area, &m_goalPosition ); + + if ( behind ) + { + PrintIfWatched( "APPROACH_DESCENDING_LADDER (face out)\n" ); + m_pathLadderState = APPROACH_DESCENDING_LADDER; + m_pathLadderFaceIn = false; + m_lookAheadAngle = ladderAngles[ YAW ]; + } + else + { + PrintIfWatched( "APPROACH_DESCENDING_LADDER (face in)\n" ); + m_pathLadderState = APPROACH_DESCENDING_LADDER; + m_pathLadderFaceIn = true; + m_lookAheadAngle = AngleNormalizePositive( ladderAngles[ YAW ] + 180.0f ); + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/// @todo What about ladders whose top AND bottom are messed up? +void CCSBot::ComputeLadderEndpoint( bool isAscending ) +{ + trace_t result; + Vector from, to; + + if (isAscending) + { + // find actual top in case m_pathLadder penetrates the ceiling + // trace from our chest height at m_pathLadder base + from = m_pathLadder->m_bottom + m_pathLadder->GetNormal() * HalfHumanWidth; + from.z = GetAbsOrigin().z + HalfHumanHeight; + to = m_pathLadder->m_top; + } + else + { + // find actual bottom in case m_pathLadder penetrates the floor + // trace from our chest height at m_pathLadder top + from = m_pathLadder->m_top + m_pathLadder->GetNormal() * HalfHumanWidth; + from.z = GetAbsOrigin().z + HalfHumanHeight; + to = m_pathLadder->m_bottom; + } + + UTIL_TraceLine( from, m_pathLadder->m_bottom, MASK_PLAYERSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f) + m_pathLadderEnd = to.z; + else + m_pathLadderEnd = from.z + result.fraction * (to.z - from.z); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Navigate our current ladder. Return true if we are doing ladder navigation. + * @todo Need Push() and Pop() for run/walk context to keep ladder speed contained. + */ +bool CCSBot::UpdateLadderMovement( void ) +{ + if (m_pathLadder == NULL) + return false; + + bool giveUp = false; + + // check for timeout + const float ladderTimeoutDuration = 10.0f; + if (gpGlobals->curtime - m_pathLadderTimestamp > ladderTimeoutDuration && !cv_bot_debug.GetBool()) + { + PrintIfWatched( "Ladder timeout!\n" ); + giveUp = true; + } + else if (m_pathLadderState == APPROACH_ASCENDING_LADDER || + m_pathLadderState == APPROACH_DESCENDING_LADDER || + m_pathLadderState == ASCEND_LADDER || + m_pathLadderState == DESCEND_LADDER || + m_pathLadderState == DISMOUNT_ASCENDING_LADDER || + m_pathLadderState == MOVE_TO_DESTINATION) + { + if (m_isStuck) + { + PrintIfWatched( "Giving up ladder - stuck\n" ); + giveUp = true; + } + } + + if (giveUp) + { + // jump off ladder and give up + Jump( MUST_JUMP ); + Wiggle(); + ResetStuckMonitor(); + DestroyPath(); + Run(); + return false; + } + else + { + ResetStuckMonitor(); + } + + Vector myOrigin = GetCentroid( this ); + + // check if somehow we totally missed the ladder + switch( m_pathLadderState ) + { + case MOUNT_ASCENDING_LADDER: + case MOUNT_DESCENDING_LADDER: + case ASCEND_LADDER: + case DESCEND_LADDER: + { + const float farAway = 200.0f; + const Vector &ladderPos = (m_pathLadderState == MOUNT_ASCENDING_LADDER || + m_pathLadderState == ASCEND_LADDER) ? m_pathLadder->m_bottom : m_pathLadder->m_top; + if ((ladderPos.AsVector2D() - myOrigin.AsVector2D()).IsLengthGreaterThan( farAway )) + { + PrintIfWatched( "Missed ladder\n" ); + Jump( MUST_JUMP ); + DestroyPath(); + Run(); + return false; + } + break; + } + } + + + m_areaEnteredTimestamp = gpGlobals->curtime; + + const float tolerance = 10.0f; + const float closeToGoal = 25.0f; + + switch( m_pathLadderState ) + { + case APPROACH_ASCENDING_LADDER: + { + bool approached = false; + + Vector2D d( myOrigin.x - m_goalPosition.x, myOrigin.y - m_goalPosition.y ); + + if (d.x * m_pathLadder->GetNormal().x + d.y * m_pathLadder->GetNormal().y < 0.0f) + { + Vector2D perp( -m_pathLadder->GetNormal().y, m_pathLadder->GetNormal().x ); + + if (fabs(d.x * perp.x + d.y * perp.y) < tolerance && d.Length() < closeToGoal) + approached = true; + } + + // small radius will just slow them down a little for more accuracy in hitting their spot + const float walkRange = 50.0f; + if (d.IsLengthLessThan( walkRange )) + { + Walk(); + StandUp(); + } + + if ( d.IsLengthLessThan( 100.0f ) ) + { + if ( !IsOnLadder() && (m_pathLadder->m_bottom.z - GetAbsOrigin().z > JumpCrouchHeight ) ) + { + // find yaw to directly aim at ladder + QAngle idealAngle; + VectorAngles( GetAbsVelocity(), idealAngle ); + const float angleTolerance = 15.0f; + if (AnglesAreEqual( EyeAngles().y, idealAngle.y, angleTolerance )) + { + Jump(); + } + } + } + + /// @todo Check that we are on the ladder we think we are + if (IsOnLadder()) + { + m_pathLadderState = ASCEND_LADDER; + PrintIfWatched( "ASCEND_LADDER\n" ); + + // find actual top in case m_pathLadder penetrates the ceiling + ComputeLadderEndpoint( true ); + } + else if (approached) + { + // face the m_pathLadder + m_pathLadderState = FACE_ASCENDING_LADDER; + PrintIfWatched( "FACE_ASCENDING_LADDER\n" ); + } + else + { + // move toward ladder mount point + MoveTowardsPosition( m_goalPosition ); + } + break; + } + + case APPROACH_DESCENDING_LADDER: + { + // fall check + if (GetFeetZ() <= m_pathLadder->m_bottom.z + HalfHumanHeight) + { + PrintIfWatched( "Fell from ladder.\n" ); + + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( m_pathLadder->m_bottom, &m_goalPosition ); + m_goalPosition += m_pathLadder->GetNormal() * HalfHumanWidth; + + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + else + { + bool approached = false; + + Vector2D d( myOrigin.x - m_goalPosition.x, myOrigin.y - m_goalPosition.y ); + + if (d.x * m_pathLadder->GetNormal().x + d.y * m_pathLadder->GetNormal().y > 0.0f) + { + Vector2D perp( -m_pathLadder->GetNormal().y, m_pathLadder->GetNormal().x ); + + if (fabs(d.x * perp.x + d.y * perp.y) < tolerance && d.Length() < closeToGoal) + approached = true; + } + + // if approaching ladder from the side or "ahead", walk + if (m_pathLadder->m_topBehindArea != m_lastKnownArea) + { + const float walkRange = 150.0f; + if (!IsCrouching() && d.IsLengthLessThan( walkRange )) + Walk(); + } + + /// @todo Check that we are on the ladder we think we are + if (IsOnLadder()) + { + // we slipped onto the ladder - climb it + m_pathLadderState = DESCEND_LADDER; + Run(); + PrintIfWatched( "DESCEND_LADDER\n" ); + + // find actual bottom in case m_pathLadder penetrates the floor + ComputeLadderEndpoint( false ); + } + else if (approached) + { + // face the ladder + m_pathLadderState = FACE_DESCENDING_LADDER; + PrintIfWatched( "FACE_DESCENDING_LADDER\n" ); + } + else + { + // move toward ladder mount point + MoveTowardsPosition( m_goalPosition ); + } + } + break; + } + + case FACE_ASCENDING_LADDER: + { + // find yaw to directly aim at ladder + Vector to = m_pathLadder->GetPosAtHeight(myOrigin.z) - myOrigin; + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + if (m_path[ m_pathIndex ].area == m_pathLadder->m_topForwardArea) + { + m_pathLadderDismountDir = FORWARD; + } + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topLeftArea) + { + m_pathLadderDismountDir = LEFT; + idealAngle[ YAW ] = AngleNormalizePositive( idealAngle[ YAW ] + 90.0f ); + } + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topRightArea) + { + m_pathLadderDismountDir = RIGHT; + idealAngle[ YAW ] = AngleNormalizePositive( idealAngle[ YAW ] - 90.0f ); + } + + const float angleTolerance = 5.0f; + if (AnglesAreEqual( EyeAngles().y, idealAngle.y, angleTolerance )) + { + // move toward ladder until we become "on" it + Run(); + ResetStuckMonitor(); + m_pathLadderState = MOUNT_ASCENDING_LADDER; + switch (m_pathLadderDismountDir) + { + case LEFT: PrintIfWatched( "MOUNT_ASCENDING_LADDER LEFT\n" ); break; + case RIGHT: PrintIfWatched( "MOUNT_ASCENDING_LADDER RIGHT\n" ); break; + default: PrintIfWatched( "MOUNT_ASCENDING_LADDER FORWARD\n" ); break; + } + } + break; + } + + case FACE_DESCENDING_LADDER: + { + // find yaw to directly aim at ladder + Vector to = m_pathLadder->GetPosAtHeight(myOrigin.z) - myOrigin; + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + const float angleTolerance = 5.0f; + if (AnglesAreEqual( EyeAngles().y, idealAngle.y, angleTolerance )) + { + // move toward ladder until we become "on" it + m_pathLadderState = MOUNT_DESCENDING_LADDER; + ResetStuckMonitor(); + PrintIfWatched( "MOUNT_DESCENDING_LADDER\n" ); + } + break; + } + + case MOUNT_ASCENDING_LADDER: + if (IsOnLadder()) + { + m_pathLadderState = ASCEND_LADDER; + PrintIfWatched( "ASCEND_LADDER\n" ); + + // find actual top in case m_pathLadder penetrates the ceiling + ComputeLadderEndpoint( true ); + } + + // move toward ladder mount point + if ( !IsOnLadder() && (m_pathLadder->m_bottom.z - GetAbsOrigin().z > JumpCrouchHeight ) ) + { + Jump(); + } + + switch( m_pathLadderDismountDir ) + { + case RIGHT: StrafeLeft(); break; + case LEFT: StrafeRight(); break; + default: MoveForward(); break; + } + break; + + case MOUNT_DESCENDING_LADDER: + // fall check + if (GetFeetZ() <= m_pathLadder->m_bottom.z + HalfHumanHeight) + { + PrintIfWatched( "Fell from ladder.\n" ); + + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( m_pathLadder->m_bottom, &m_goalPosition ); + m_goalPosition += m_pathLadder->GetNormal() * HalfHumanWidth; + + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + else + { + if (IsOnLadder()) + { + m_pathLadderState = DESCEND_LADDER; + PrintIfWatched( "DESCEND_LADDER\n" ); + + // find actual bottom in case m_pathLadder penetrates the floor + ComputeLadderEndpoint( false ); + } + + // move toward ladder mount point + MoveForward(); + } + break; + + case ASCEND_LADDER: + // run, so we can make our dismount jump to the side, if necessary + Run(); + + // if our destination area requires us to crouch, do it + if (m_path[ m_pathIndex ].area->GetAttributes() & NAV_MESH_CROUCH) + Crouch(); + + // did we reach the top? + if (GetFeetZ() >= m_pathLadderEnd) + { + // we reached the top - dismount + m_pathLadderState = DISMOUNT_ASCENDING_LADDER; + PrintIfWatched( "DISMOUNT_ASCENDING_LADDER\n" ); + + if (m_path[ m_pathIndex ].area == m_pathLadder->m_topForwardArea) + m_pathLadderDismountDir = FORWARD; + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topLeftArea) + m_pathLadderDismountDir = LEFT; + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topRightArea) + m_pathLadderDismountDir = RIGHT; + + m_pathLadderDismountTimestamp = gpGlobals->curtime; + } + else if (!IsOnLadder()) + { + // we fall off the ladder, repath + DestroyPath(); + return false; + } + + // move up ladder + switch( m_pathLadderDismountDir ) + { + case RIGHT: StrafeLeft(); break; + case LEFT: StrafeRight(); break; + default: MoveForward(); break; + } + break; + + case DESCEND_LADDER: + { + Run(); + float destHeight = m_pathLadderEnd; + if ( (m_path[ m_pathIndex ].area->GetAttributes() & NAV_MESH_NO_JUMP) == 0 ) + { + destHeight += HalfHumanHeight; + } + if ( !IsOnLadder() || GetFeetZ() <= destHeight ) + { + // we reached the bottom, or we fell off - dismount + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( m_pathLadder->m_bottom, &m_goalPosition ); + m_goalPosition += m_pathLadder->GetNormal() * HalfHumanWidth; + + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + + // Move down ladder + MoveForward(); + + break; + } + + case DISMOUNT_ASCENDING_LADDER: + { + if (gpGlobals->curtime - m_pathLadderDismountTimestamp >= 0.4f) + { + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( myOrigin, &m_goalPosition ); + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + + // We should already be facing the dismount point + MoveForward(); + break; + } + + case MOVE_TO_DESTINATION: + if (m_path[ m_pathIndex ].area->Contains( myOrigin )) + { + // successfully traversed ladder and reached destination area + // exit ladder state machine + PrintIfWatched( "Ladder traversed.\n" ); + m_pathLadder = NULL; + + // incrememnt path index to next step beyond this ladder + SetPathIndex( m_pathIndex+1 ); + + ClearLookAt(); + + return false; + } + + MoveTowardsPosition( m_goalPosition ); + break; + } + + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + DrawPath(); + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * 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 CCSBot::FindClosestPointOnPath( const Vector &worldPos, int startIndex, int endIndex, Vector *close ) const +{ + if (!HasPath() || 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; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * 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 CCSBot::FindOurPositionOnPath( Vector *close, bool local ) const +{ + if (!HasPath()) + return -1; + + Vector along, toFeet; + Vector feet = GetAbsOrigin(); + Vector eyes = feet + Vector( 0, 0, HalfHumanHeight ); // in case we're crouching + 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_pathIndex - 3; + if (start < 1) + start = 1; + + end = m_pathIndex + 3; + if (end > m_pathLength) + end = m_pathLength; + } + else + { + start = 1; + end = m_pathLength; + } + + 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; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Test for un-jumpable height change, or unrecoverable fall + */ +bool CCSBot::IsStraightLinePathWalkable( const Vector &goal ) const +{ +// this is causing hang-up problems when crawling thru ducts/windows that drop off into rooms (they fail the "falling" check) +return true; + + const float inc = GenerationStepSize; + + Vector feet = GetAbsOrigin(); + Vector dir = goal - feet; + float length = dir.NormalizeInPlace(); + + float lastGround; + //if (!GetSimpleGroundHeight( &pev->origin, &lastGround )) + // return false; + lastGround = feet.z; + + + float along=0.0f; + Vector pos; + float ground; + bool done = false; + while( !done ) + { + along += inc; + if (along > length) + { + along = length; + done = true; + } + + // compute step along path + pos = feet + along * dir; + + pos.z += HalfHumanHeight; + + if (!TheNavMesh->GetSimpleGroundHeight( pos, &ground )) + return false; + + // check for falling + if (ground - lastGround < -StepHeight) + return false; + + // check for unreachable jump + // use slightly shorter jump limit, to allow for some fudge room + if (ground - lastGround > JumpHeight) + return false; + + lastGround = ground; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute a point a fixed distance ahead along our path. + * Returns path index just after point. + */ +int CCSBot::FindPathPoint( float aheadRange, Vector *point, int *prevIndex ) +{ + Vector myOrigin = GetCentroid( this ); + + // 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_pathIndex; + } + + // if we are crouching, just follow the path exactly + if (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_pathLength) + index = m_pathLength-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_pathLength) + { + index = m_pathLength-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_pathLength-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, stop, or jump area, must stop (dont use ladder behind us) + if (startIndex > m_pathIndex && startIndex < m_pathLength && + (m_path[ startIndex ].ladder || m_path[ startIndex ].area->GetAttributes() & (NAV_MESH_JUMP | NAV_MESH_STOP))) + { + *point = m_path[ startIndex ].pos; + return startIndex; + } + + // we need the point just *ahead* of us + ++startIndex; + if (startIndex >= m_pathLength) + startIndex = m_pathLength-1; + + // if we hit a ladder, stop, or jump area, must stop + if (startIndex < m_pathLength && + (m_path[ startIndex ].ladder || m_path[ startIndex ].area->GetAttributes() & (NAV_MESH_JUMP | NAV_MESH_STOP))) + { + *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 = GetAbsOrigin(); + Vector eyes = feet + Vector( 0, 0, HalfHumanHeight ); + 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_pathLength; ++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_pathLength && + (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_pathLength) + afterIndex = i; + else + afterIndex = m_pathLength-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; + toPoint.x = point->x - myOrigin.x; + toPoint.y = point->y - myOrigin.y; + if (DotProduct2D( toPoint, initDir.AsVector2D() ) < 0.0f || toPoint.IsLengthLessThan( epsilon )) + { + int i; + for( i=startIndex; i<m_pathLength; ++i ) + { + toPoint.x = m_path[i].pos.x - myOrigin.x; + toPoint.y = m_path[i].pos.y - myOrigin.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_pathLength) + { + *point = GetPathEndpoint(); + startIndex = m_pathLength-1; + } + } + } + + // m_pathIndex should always be the next point on the path, even if we're not moving directly towards it + return startIndex; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the current index along the path + */ +void CCSBot::SetPathIndex( int newIndex ) +{ + m_pathIndex = MIN( newIndex, m_pathLength-1 ); + m_areaEnteredTimestamp = gpGlobals->curtime; + + if (m_path[ m_pathIndex ].ladder) + { + SetupLadderMovement(); + } + else + { + // get our "encounter spots" for this leg of the path + if (m_pathIndex < m_pathLength && m_pathIndex >= 2) + m_spotEncounter = m_path[ m_pathIndex-1 ].area->GetSpotEncounter( m_path[ m_pathIndex-2 ].area, m_path[ m_pathIndex ].area ); + else + m_spotEncounter = NULL; + + m_pathLadder = NULL; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if nearing a jump in the path + */ +bool CCSBot::IsNearJump( void ) const +{ + if (m_pathIndex == 0 || m_pathIndex >= m_pathLength) + return false; + + for( int i=m_pathIndex-1; i<m_pathIndex; ++i ) + { + if (m_path[ i ].area->GetAttributes() & NAV_MESH_JUMP) + { + float dz = m_path[ i+1 ].pos.z - m_path[ i ].pos.z; + + if (dz > 0.0f) + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return approximately how much damage will will take from the given fall height + */ +float CCSBot::GetApproximateFallDamage( float height ) const +{ + // empirically discovered height values + const float slope = 0.2178f; + const float intercept = 26.0f; + + float damage = slope * height - intercept; + + if (damage < 0.0f) + return 0.0f; + + return damage; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if a friend is between us and the given position + */ +bool CCSBot::IsFriendInTheWay( const Vector &goalPos ) +{ + // do this check less often to ease CPU burden + if (!m_avoidFriendTimer.IsElapsed()) + { + return m_isFriendInTheWay; + } + + const float avoidFriendInterval = 0.5f; + m_avoidFriendTimer.Start( avoidFriendInterval ); + + // compute ray along intended path + Vector myOrigin = GetCentroid( this ); + Vector moveDir = goalPos - myOrigin; + + // make it a unit vector + float length = moveDir.NormalizeInPlace(); + + m_isFriendInTheWay = false; + + // check if any friends are overlapping this linear path + for( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSPlayer *player = static_cast<CCSPlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (!player->IsAlive()) + continue; + + if (!player->InSameTeam( this )) + continue; + + if (player->entindex() == entindex()) + continue; + + // compute vector from us to our friend + Vector toFriend = player->GetAbsOrigin() - GetAbsOrigin(); + + // check if friend is in our "personal space" + const float personalSpace = 100.0f; + if (toFriend.IsLengthGreaterThan( personalSpace )) + continue; + + // find distance of friend along our movement path + float friendDistAlong = DotProduct( toFriend, moveDir ); + + // if friend is behind us, ignore him + if (friendDistAlong <= 0.0f) + continue; + + // constrain point to be on path segment + Vector pos; + if (friendDistAlong >= length) + pos = goalPos; + else + pos = myOrigin + friendDistAlong * moveDir; + + // check if friend overlaps our intended line of movement + const float friendRadius = 30.0f; + if ((pos - GetCentroid( player )).IsLengthLessThan( friendRadius )) + { + // friend is in our personal space and overlaps our intended line of movement + m_isFriendInTheWay = true; + break; + } + } + + return m_isFriendInTheWay; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do reflex avoidance movements if our "feelers" are touched + */ +void CCSBot::FeelerReflexAdjustment( Vector *goalPosition ) +{ + // if we are in a "precise" area, do not do feeler adjustments + if (m_lastKnownArea && m_lastKnownArea->GetAttributes() & NAV_MESH_PRECISE) + return; + + Vector dir( BotCOS( m_forwardAngle ), BotSIN( m_forwardAngle ), 0.0f ); + Vector lat( -dir.y, dir.x, 0.0f ); + + const float feelerOffset = (IsCrouching()) ? 15.0f : 20.0f; + const float feelerLengthRun = 50.0f; // 100 - too long for tight hallways (cs_747) + const float feelerLengthWalk = 30.0f; + const float feelerHeight = StepHeight + 0.1f; // if obstacle is lower than StepHeight, we'll walk right over it + + float feelerLength = (IsRunning()) ? feelerLengthRun : feelerLengthWalk; + + feelerLength = (IsCrouching()) ? 20.0f : feelerLength; + + // + // Feelers must follow floor slope + // + float ground; + Vector normal; + Vector eye = EyePosition(); + if (GetSimpleGroundHeightWithFloor( eye, &ground, &normal ) == false) + return; + + // get forward vector along floor + dir = CrossProduct( lat, normal ); + + // correct the sideways vector + lat = CrossProduct( dir, normal ); + + + Vector feet = GetAbsOrigin(); + feet.z += feelerHeight; + + Vector from = feet + feelerOffset * lat; + Vector to = from + feelerLength * dir; + + bool leftClear = IsWalkableTraceLineClear( from, to, WALK_THRU_DOORS | WALK_THRU_BREAKABLES ); + + // avoid ledges, too + // use 'from' so it doesn't interfere with legitimate gap jumping (its at our feet) + /// @todo Rethink this - it causes lots of wiggling when bots jump down from vents, etc +/* + float ground; + if (GetSimpleGroundHeightWithFloor( &from, &ground )) + { + if (GetFeetZ() - ground > JumpHeight) + leftClear = false; + } +*/ + + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + 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 ); + +/* + // avoid ledges, too + if (GetSimpleGroundHeightWithFloor( &from, &ground )) + { + if (GetFeetZ() - ground > JumpHeight) + rightClear = false; + } +*/ + + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + if (rightClear) + UTIL_DrawBeamPoints( from, to, 1, 0, 255, 0 ); + else + UTIL_DrawBeamPoints( from, to, 1, 255, 0, 0 ); + } + + const float avoidRange = (IsCrouching()) ? 150.0f : 300.0f; // 50, 300 + + if (!rightClear) + { + if (leftClear) + { + // right hit, left clear - veer left + *goalPosition = *goalPosition + avoidRange * lat; + } + } + else if (!leftClear) + { + // right clear, left hit - veer right + *goalPosition = *goalPosition - avoidRange * lat; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Allows the current nav area to make us run/walk without messing with our state + */ +bool CCSBot::IsRunning( void ) const +{ + // if we've forced running, go with it + if ( !m_mustRunTimer.IsElapsed() ) + { + return BaseClass::IsRunning(); + } + + if ( m_lastKnownArea && m_lastKnownArea->GetAttributes() & NAV_MESH_RUN ) + { + return true; + } + + if ( m_lastKnownArea && m_lastKnownArea->GetAttributes() & NAV_MESH_WALK ) + { + return false; + } + + return BaseClass::IsRunning(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move along the path. Return false if end of path reached. + */ +CCSBot::PathResult CCSBot::UpdatePathMovement( bool allowSpeedChange ) +{ + VPROF_BUDGET( "CCSBot::UpdatePathMovement", VPROF_BUDGETGROUP_NPCS ); + + if (m_pathLength == 0) + return PATH_FAILURE; + + if (cv_bot_walk.GetBool()) + Walk(); + + // + // If we are navigating a ladder, it overrides all other path movement until complete + // + if (UpdateLadderMovement()) + return PROGRESSING; + + // ladder failure can destroy the path + if (m_pathLength == 0) + return PATH_FAILURE; + + + // we are not supposed to be on a ladder - if we are, jump off + if (IsOnLadder()) + Jump( MUST_JUMP ); + + + assert( m_pathIndex < m_pathLength ); + + // + // Stop path attribute + // + if (!IsUsingLadder()) + { + // if the m_isStopping flag is set, clear our movement + // if the m_isStopping flag is set and movement is stopped, clear m_isStopping + if ( m_lastKnownArea && m_isStopping ) + { + ResetStuckMonitor(); + ClearMovement(); + + if ( GetAbsVelocity().LengthSqr() < 0.1f ) + { + m_isStopping = false; + } + else + { + return PROGRESSING; + } + } + } // end stop logic + + + // + // Check if reached the end of the path + // + bool nearEndOfPath = false; + if (m_pathIndex >= m_pathLength-1) + { + Vector toEnd = GetPathEndpoint() - GetAbsOrigin(); + Vector d = toEnd; // can't use 2D because path end may be below us (jump down) + + const float walkRange = 200.0f; + + // walk as we get close to the goal position to ensure we hit it + if (d.IsLengthLessThan( walkRange )) + { + // don't walk if crouching - too slow + if (allowSpeedChange && !IsCrouching()) + Walk(); + + // note if we are near the end of the path + const float nearEndRange = 50.0f; + if (d.IsLengthLessThan( nearEndRange )) + nearEndOfPath = true; + + const float closeEpsilon = 20.0f; + if (d.IsLengthLessThan( closeEpsilon )) + { + // reached goal position - path complete + DestroyPath(); + + /// @todo We should push and pop walk state here, in case we want to continue walking after reaching goal + if (allowSpeedChange) + Run(); + + return END_OF_PATH; + } + } + } + + + // + // To keep us moving smoothly, we will move towards + // a point farther ahead of us down our path. + // + int prevIndex = 0; // closest index on path just prior to where we are now + const float aheadRange = 300.0f; + int newIndex = FindPathPoint( aheadRange, &m_goalPosition, &prevIndex ); + + // BOTPORT: Why is prevIndex sometimes -1? + if (prevIndex < 0) + prevIndex = 0; + + // if goal position is near to us, we must be about to go around a corner - so look ahead! + Vector myOrigin = GetCentroid( this ); + const float nearCornerRange = 100.0f; + if (m_pathIndex < m_pathLength-1 && (m_goalPosition - myOrigin).IsLengthLessThan( nearCornerRange )) + { + if (!IsLookingAtSpot( PRIORITY_HIGH )) + { + ClearLookAt(); + InhibitLookAround( 0.5f ); + } + } + + // if we moved to a new node on the path, setup movement + if (newIndex > m_pathIndex) + { + SetPathIndex( newIndex ); + } + + // + // Crouching + // + if (!IsUsingLadder()) + { + // 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=prevIndex; i<m_pathLength; ++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 + // unless we are already higher than the jump area - we must've jumped already but not moved into next area + if (to->GetAttributes() & NAV_MESH_JUMP && to->GetCenter().z > GetFeetZ()) + break; + + Vector close; + to->GetClosestPointOnArea( myOrigin, &close ); + + if ((close - myOrigin).AsVector2D().IsLengthGreaterThan( crouchRange )) + break; + + if (to->GetAttributes() & NAV_MESH_CROUCH) + { + Crouch(); + didCrouch = true; + ResetStuckMonitor(); + break; + } + } + + if (!didCrouch && !IsJumping()) + { + // no crouch areas coming up + StandUp(); + } + + } // end crouching logic + + + // compute our forward facing angle + m_forwardAngle = UTIL_VecToYaw( m_goalPosition - myOrigin ); + + // + // Look farther down the path to "lead" our view around corners + // + Vector toGoal; + bool isWaitingForLadder = false; + + // if we are crouching, look towards where we are moving to negotiate tight corners + if (IsCrouching()) + { + m_lookAheadAngle = m_forwardAngle; + } + else + { + if (m_pathIndex == 0) + { + toGoal = m_path[1].pos; + } + else if (m_pathIndex < m_pathLength) + { + toGoal = m_path[ m_pathIndex ].pos - myOrigin; + + // actually aim our view farther down the path + const float lookAheadRange = 500.0f; + if (!m_path[ m_pathIndex ].ladder && + !IsNearJump() && + toGoal.AsVector2D().IsLengthLessThan( lookAheadRange )) + { + float along = toGoal.Length2D(); + int i; + for( i=m_pathIndex+1; i<m_pathLength; ++i ) + { + Vector delta = m_path[i].pos - m_path[i-1].pos; + float segmentLength = delta.Length2D(); + + if (along + segmentLength >= lookAheadRange) + { + // interpolate between points to keep look ahead point at fixed distance + float t = (lookAheadRange - along) / (segmentLength + along); + Vector target; + + if (t <= 0.0f) + target = m_path[i-1].pos; + else if (t >= 1.0f) + target = m_path[i].pos; + else + target = m_path[i-1].pos + t * delta; + + toGoal = target - myOrigin; + break; + } + + // if we are coming up to a ladder or a jump, look at it + if (m_path[i].ladder || + (m_path[i].area->GetAttributes() & NAV_MESH_JUMP) || + (m_path[i].area->GetAttributes() & NAV_MESH_PRECISE) || + (m_path[i].area->GetAttributes() & NAV_MESH_STOP)) + { + toGoal = m_path[i].pos - myOrigin; + + // if anyone is on the ladder, wait + if (m_path[i].ladder && m_path[i].ladder->IsInUse( this )) + { + isWaitingForLadder = true; + ResetStuckMonitor(); + + // if we are too close to the ladder, back off a bit + const float tooCloseRange = 100.0f; + Vector2D delta( m_path[i].ladder->m_top.x - myOrigin.x, + m_path[i].ladder->m_top.y - myOrigin.y ); + if (delta.IsLengthLessThan( tooCloseRange )) + { + MoveAwayFromPosition( m_path[i].ladder->m_top ); + } + } + + break; + } + + along += segmentLength; + } + + if (i == m_pathLength) + toGoal = GetPathEndpoint() - myOrigin; + } + } + else + { + toGoal = GetPathEndpoint() - myOrigin; + } + + m_lookAheadAngle = UTIL_VecToYaw( toGoal ); + } + + // initialize "adjusted" goal to current goal + Vector adjustedGoal = m_goalPosition; + + // + // Use short "feelers" to veer away from close-range obstacles + // Feelers come from our ankles, just above StepHeight, so we avoid short walls, too + // Don't use feelers if very near the end of the path, or about to jump + // + /// @todo Consider having feelers at several heights to deal with overhangs, etc. + if (!nearEndOfPath && !IsNearJump() && !IsJumping()) + { + FeelerReflexAdjustment( &adjustedGoal ); + } + + // draw debug visualization + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + DrawPath(); + + const Vector *pos = &m_path[ m_pathIndex ].pos; + UTIL_DrawBeamPoints( *pos, *pos + Vector( 0, 0, 50 ), 1, 255, 255, 0 ); + + UTIL_DrawBeamPoints( adjustedGoal, adjustedGoal + Vector( 0, 0, 50 ), 1, 255, 0, 255 ); + UTIL_DrawBeamPoints( myOrigin, adjustedGoal + Vector( 0, 0, 50 ), 1, 255, 0, 255 ); + } + + // dont use adjustedGoal, as it can vary wildly from the feeler adjustment + if (!IsAttacking() && IsFriendInTheWay( m_goalPosition )) + { + if (!m_isWaitingBehindFriend) + { + m_isWaitingBehindFriend = true; + + const float politeDuration = 5.0f - 3.0f * GetProfile()->GetAggression(); + m_politeTimer.Start( politeDuration ); + } + else if (m_politeTimer.IsElapsed()) + { + // we have run out of patience + m_isWaitingBehindFriend = false; + ResetStuckMonitor(); + + // repath to avoid clump of friends in the way + DestroyPath(); + } + } + else if (m_isWaitingBehindFriend) + { + // we're done waiting for our friend to move + m_isWaitingBehindFriend = false; + ResetStuckMonitor(); + } + + // + // Move along our path if there are no friends blocking our way, + // or we have run out of patience + // + if (!isWaitingForLadder && (!m_isWaitingBehindFriend || m_politeTimer.IsElapsed())) + { + // + // Move along path + // + MoveTowardsPosition( adjustedGoal ); + + // + // Stuck check + // + if (m_isStuck && !IsJumping()) + { + Wiggle(); + } + } + + // if our goal is high above us, we must have fallen + bool didFall = false; + if (m_goalPosition.z - GetFeetZ() > JumpCrouchHeight) + { + const float closeRange = 75.0f; + Vector2D to( myOrigin.x - m_goalPosition.x, myOrigin.y - m_goalPosition.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 + if (m_pathIndex < m_pathLength-1) + { + if (m_path[ m_pathIndex+1 ].pos.z - GetFeetZ() > JumpCrouchHeight) + { + // the next node is too high, too - we really did fall of the path + didFall = true; + + for ( int i=m_pathIndex; i<=m_pathIndex+1; ++i ) + { + if ( m_path[i].how == GO_LADDER_UP ) + { + // if we're going up a ladder, and we're within reach of the ladder bottom, we haven't fallen + if ( m_path[i].pos.z - GetFeetZ() <= JumpCrouchHeight ) + { + didFall = false; + break; + } + } + } + } + } + else + { + // fell trying to get to the last node in the path + didFall = true; + } + } + } + + // + // This timeout check is needed if the bot somehow slips way off + // of its path and cannot progress, but also moves around + // enough that it never becomes "stuck" + // + const float giveUpDuration = 4.0f; + if (didFall || gpGlobals->curtime - m_areaEnteredTimestamp > giveUpDuration) + { + if (didFall) + { + PrintIfWatched( "I fell off!\n" ); + if (IsLocalPlayerWatchingMe() && cv_bot_debug.GetBool() && UTIL_GetListenServerHost()) + { + CBasePlayer *localPlayer = UTIL_GetListenServerHost(); + CSingleUserRecipientFilter filter( localPlayer ); + EmitSound( filter, localPlayer->entindex(), "Bot.FellOff" ); + } + } + + // if we havent made any progress in a long time, give up + if (m_pathIndex < m_pathLength-1) + { + PrintIfWatched( "Giving up trying to get to area #%d\n", m_path[ m_pathIndex ].area->GetID() ); + } + else + { + PrintIfWatched( "Giving up trying to get to end of path\n" ); + } + + Run(); + StandUp(); + DestroyPath(); + ClearLookAt(); + + // See if we should be on a different nav area + CNavArea *area = TheNavMesh->GetNearestNavArea( GetAbsOrigin(), false, 500.0f, true ); + if (area && area != m_lastNavArea) + { + if (m_lastNavArea) + { + m_lastNavArea->DecrementPlayerCount( GetTeamNumber(), entindex() ); + } + + area->IncrementPlayerCount( GetTeamNumber(), entindex() ); + + m_lastNavArea = area; + if ( area->GetPlace() != UNDEFINED_PLACE ) + { + const char *placeName = TheNavMesh->PlaceToName( area->GetPlace() ); + if ( placeName && *placeName ) + { + Q_strncpy( m_szLastPlaceName.GetForModify(), placeName, MAX_PLACE_NAME_LENGTH ); + } + } + + // generate event + //KeyValues *event = new KeyValues( "player_entered_area" ); + //event->SetInt( "userid", GetUserID() ); + //event->SetInt( "areaid", area->GetID() ); + //gameeventmanager->FireEvent( event ); + } + + return PATH_FAILURE; + } + + return PROGRESSING; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Build trivial path to goal, assuming we are already in the same area + */ +void CCSBot::BuildTrivialPath( const Vector &goal ) +{ + Vector myOrigin = GetCentroid( this ); + + m_pathIndex = 1; + m_pathLength = 2; + + m_path[0].area = m_lastKnownArea; + m_path[0].pos = myOrigin; + m_path[0].pos.z = m_lastKnownArea->GetZ( myOrigin ); + m_path[0].ladder = NULL; + m_path[0].how = NUM_TRAVERSE_TYPES; + + m_path[1].area = m_lastKnownArea; + m_path[1].pos = goal; + m_path[1].pos.z = m_lastKnownArea->GetZ( goal ); + m_path[1].ladder = NULL; + m_path[1].how = NUM_TRAVERSE_TYPES; + + m_areaEnteredTimestamp = gpGlobals->curtime; + m_spotEncounter = NULL; + m_pathLadder = NULL; + + m_goalPosition = goal; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute shortest path to goal position via A* algorithm + * If 'goalArea' is NULL, path will get as close as it can. + */ +bool CCSBot::ComputePath( const Vector &goal, RouteType route ) +{ + VPROF_BUDGET( "CCSBot::ComputePath", VPROF_BUDGETGROUP_NPCS ); + + // + // Throttle re-pathing + // + if (!m_repathTimer.IsElapsed()) + return false; + + // randomize to distribute CPU load + m_repathTimer.Start( RandomFloat( 0.4f, 0.6f ) ); + + + DestroyPath(); + + m_pathLadder = NULL; + + CNavArea *goalArea = TheNavMesh->GetNearestNavArea( goal ); + + CNavArea *startArea = m_lastKnownArea; + if (startArea == NULL) + return false; + + // if we fell off a ledge onto an area off the mesh, we will path from the + // ledge above our heads, resulting in a path we can't follow. + Vector close; + startArea->GetClosestPointOnArea( EyePosition(), &close ); + if (close.z - GetAbsOrigin().z > JumpCrouchHeight) + { + // we can't reach our last known area - find nearest area to us + PrintIfWatched( "Last known area is above my head - resetting to nearest area.\n" ); + m_lastKnownArea = (CCSNavArea*)TheNavMesh->GetNearestNavArea( GetAbsOrigin(), false, 500.0f, true ); + if (m_lastKnownArea == NULL) + { + return false; + } + + startArea = m_lastKnownArea; + } + + // note final specific position + Vector pathEndPosition = goal; + + // make sure path end position is on the ground + if (goalArea) + pathEndPosition.z = goalArea->GetZ( pathEndPosition ); + else + TheNavMesh->GetGroundHeight( pathEndPosition, &pathEndPosition.z ); + + // if we are already in the goal area, build trivial path + if (startArea == goalArea) + { + BuildTrivialPath( pathEndPosition ); + return true; + } + + // + // Compute shortest path to goal + // + CNavArea *closestArea = NULL; + PathCost cost( this, route ); + bool pathToGoalExists = NavAreaBuildPath( startArea, goalArea, &goal, cost, &closestArea ); + + CNavArea *effectiveGoalArea = (pathToGoalExists) ? goalArea : closestArea; + + // + // Build path by following parent links + // + + // get count + int count = 0; + CNavArea *area; + for( area = effectiveGoalArea; area; area = area->GetParent() ) + ++count; + + // save room for endpoint + if (count > MAX_PATH_LENGTH-1) + count = MAX_PATH_LENGTH-1; + + if (count == 0) + return false; + + if (count == 1) + { + BuildTrivialPath( pathEndPosition ); + return true; + } + + // build path + m_pathLength = count; + for( area = effectiveGoalArea; count && area; area = area->GetParent() ) + { + --count; + m_path[ count ].area = area; + m_path[ count ].how = area->GetParentHow(); + } + + // compute path positions + if (ComputePathPositions() == false) + { + PrintIfWatched( "Error building path\n" ); + DestroyPath(); + return false; + } + + // append path end position + m_path[ m_pathLength ].area = effectiveGoalArea; + m_path[ m_pathLength ].pos = pathEndPosition; + m_path[ m_pathLength ].ladder = NULL; + m_path[ m_pathLength ].how = NUM_TRAVERSE_TYPES; + ++m_pathLength; + + // do movement setup + m_pathIndex = 1; + m_areaEnteredTimestamp = gpGlobals->curtime; + m_spotEncounter = NULL; + m_goalPosition = m_path[1].pos; + + if (m_path[1].ladder) + SetupLadderMovement(); + else + m_pathLadder = NULL; + + // find initial encounter area along this path, if we are in the early part of the round + if (IsSafe()) + { + int myTeam = GetTeamNumber(); + int enemyTeam = OtherTeam( myTeam ); + int i; + + for( i=0; i<m_pathLength; ++i ) + { + if (m_path[i].area->GetEarliestOccupyTime( myTeam ) > m_path[i].area->GetEarliestOccupyTime( enemyTeam )) + { + break; + } + } + + if (i < m_pathLength) + { + SetInitialEncounterArea( m_path[i].area ); + } + else + { + SetInitialEncounterArea( NULL ); + } + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return estimated distance left to travel along path + */ +float CCSBot::GetPathDistanceRemaining( void ) const +{ + if (!HasPath()) + return -1.0f; + + int idx = (m_pathIndex < m_pathLength) ? m_pathIndex : m_pathLength-1; + + float dist = 0.0f; + Vector prevCenter = m_path[m_pathIndex].area->GetCenter(); + + for( int i=idx+1; i<m_pathLength; ++i ) + { + dist += (m_path[i].area->GetCenter() - prevCenter).Length(); + prevCenter = m_path[i].area->GetCenter(); + } + + return dist; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw a portion of our current path for debugging. + */ +void CCSBot::DrawPath( void ) +{ + if (!HasPath()) + return; + + for( int i=1; i<m_pathLength; ++i ) + { + UTIL_DrawBeamPoints( m_path[i-1].pos, m_path[i].pos, 2, 255, 75, 0 ); + } + + Vector close; + if (FindOurPositionOnPath( &close, true ) >= 0) + { + UTIL_DrawBeamPoints( close + Vector( 0, 0, 25 ), close, 1, 0, 255, 0 ); + UTIL_DrawBeamPoints( close + Vector( 25, 0, 0 ), close + Vector( -25, 0, 0 ), 1, 0, 255, 0 ); + UTIL_DrawBeamPoints( close + Vector( 0, 25, 0 ), close + Vector( 0, -25, 0 ), 1, 0, 255, 0 ); + } +} + diff --git a/game/server/cstrike/bot/cs_bot_radio.cpp b/game/server/cstrike/bot/cs_bot_radio.cpp new file mode 100644 index 0000000..08acb4e --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_radio.cpp @@ -0,0 +1,346 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern int gmsgBotVoice; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if the radio message is an order to do something + * NOTE: "Report in" is not considered a "command" because it doesnt ask the bot to go somewhere, or change its mind + */ +bool CCSBot::IsRadioCommand( RadioType event ) const +{ + if (event == RADIO_AFFIRMATIVE || + event == RADIO_NEGATIVE || + event == RADIO_ENEMY_SPOTTED || + event == RADIO_SECTOR_CLEAR || + event == RADIO_REPORTING_IN || + event == RADIO_REPORT_IN_TEAM || + event == RADIO_ENEMY_DOWN || + event == RADIO_INVALID ) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Respond to radio commands from HUMAN players + */ +void CCSBot::RespondToRadioCommands( void ) +{ + // bots use the chatter system to respond to each other + if (m_radioSubject != NULL && m_radioSubject->IsPlayer()) + { + CCSPlayer *player = m_radioSubject; + if (player->IsBot()) + { + m_lastRadioCommand = RADIO_INVALID; + return; + } + } + + if (m_lastRadioCommand == RADIO_INVALID) + return; + + // a human player has issued a radio command + GetChatter()->ResetRadioSilenceDuration(); + + + // if we are doing something important, ignore the radio + // unless it is a "report in" request - we can do that while we continue to do other things + /// @todo Create "uninterruptable" flag + if (m_lastRadioCommand != RADIO_REPORT_IN_TEAM) + { + if (IsBusy()) + { + // consume command + m_lastRadioCommand = RADIO_INVALID; + return; + } + } + + // wait for reaction time before responding + // delay needs to be long enough for the radio message we're responding to to finish + float respondTime = 1.0f + 2.0f * GetProfile()->GetReactionTime(); + if (IsRogue()) + respondTime += 2.0f; + + if (gpGlobals->curtime - m_lastRadioRecievedTimestamp < respondTime) + return; + + // rogues won't follow commands, unless already following the player + if (!IsFollowing() && IsRogue()) + { + if (IsRadioCommand( m_lastRadioCommand )) + { + GetChatter()->Negative(); + } + + // consume command + m_lastRadioCommand = RADIO_INVALID; + return; + } + + CCSPlayer *player = m_radioSubject; + if (player == NULL) + return; + + // respond to command + bool canDo = false; + const float inhibitAutoFollowDuration = 60.0f; + switch( m_lastRadioCommand ) + { + case RADIO_REPORT_IN_TEAM: + { + GetChatter()->ReportingIn(); + break; + } + + case RADIO_FOLLOW_ME: + case RADIO_COVER_ME: + case RADIO_STICK_TOGETHER_TEAM: + case RADIO_REGROUP_TEAM: + { + if (!IsFollowing()) + { + Follow( player ); + player->AllowAutoFollow(); + canDo = true; + } + break; + } + + case RADIO_ENEMY_SPOTTED: + case RADIO_NEED_BACKUP: + case RADIO_TAKING_FIRE: + if (!IsFollowing()) + { + Follow( player ); + GetChatter()->Say( "OnMyWay" ); + player->AllowAutoFollow(); + canDo = false; + } + break; + + case RADIO_TEAM_FALL_BACK: + { + if (TryToRetreat()) + canDo = true; + break; + } + + case RADIO_HOLD_THIS_POSITION: + { + // find the leader's area + SetTask( HOLD_POSITION ); + StopFollowing(); + player->InhibitAutoFollow( inhibitAutoFollowDuration ); + Hide( TheNavMesh->GetNearestNavArea( m_radioPosition ) ); + canDo = true; + break; + } + + case RADIO_GO_GO_GO: + case RADIO_STORM_THE_FRONT: + StopFollowing(); + Hunt(); + canDo = true; + player->InhibitAutoFollow( inhibitAutoFollowDuration ); + break; + + case RADIO_GET_OUT_OF_THERE: + if (TheCSBots()->IsBombPlanted()) + { + EscapeFromBomb(); + player->InhibitAutoFollow( inhibitAutoFollowDuration ); + canDo = true; + } + break; + + case RADIO_SECTOR_CLEAR: + { + // if this is a defusal scenario, and the bomb is planted, + // and a human player cleared a bombsite, check it off our list too + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB) + { + if (GetTeamNumber() == TEAM_CT && TheCSBots()->IsBombPlanted()) + { + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( player ); + + if (zone) + { + GetGameState()->ClearBombsite( zone->m_index ); + + // if we are huting for the planted bomb, re-select bombsite + if (GetTask() == FIND_TICKING_BOMB) + Idle(); + + canDo = true; + } + } + } + break; + } + + default: + // ignore all other radio commands for now + return; + } + + if (canDo) + { + // affirmative + GetChatter()->Affirmative(); + + // if we agreed to follow a new command, put away our grenade + if (IsRadioCommand( m_lastRadioCommand ) && IsUsingGrenade()) + { + EquipBestWeapon(); + } + } + + // consume command + m_lastRadioCommand = RADIO_INVALID; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Decide if we should move to help the player, return true if we will + */ +bool CCSBot::RespondToHelpRequest( CCSPlayer *them, Place place, float maxRange ) +{ + if (IsRogue()) + return false; + + // if we're busy, ignore + if (IsBusy()) + return false; + + Vector themOrigin = GetCentroid( them ); + + // if we are too far away, ignore + if (maxRange > 0.0f) + { + // compute actual travel distance + PathCost cost(this); + float travelDistance = NavAreaTravelDistance( m_lastKnownArea, TheNavMesh->GetNearestNavArea( themOrigin ), cost ); + if (travelDistance < 0.0f) + return false; + + if (travelDistance > maxRange) + return false; + } + + + if (place == UNDEFINED_PLACE) + { + // if we have no "place" identifier, go directly to them + + // if we are already there, ignore + float rangeSq = (them->GetAbsOrigin() - GetAbsOrigin()).LengthSqr(); + const float close = 750.0f * 750.0f; + if (rangeSq < close) + return true; + + MoveTo( themOrigin, FASTEST_ROUTE ); + } + else + { + // if we are already there, ignore + if (GetPlace() == place) + return true; + + // go to where help is needed + const Vector *pos = GetRandomSpotAtPlace( place ); + if (pos) + { + MoveTo( *pos, FASTEST_ROUTE ); + } + else + { + MoveTo( themOrigin, FASTEST_ROUTE ); + } + } + + // acknowledge + GetChatter()->Say( "OnMyWay" ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Send a radio message + */ +void CCSBot::SendRadioMessage( RadioType event ) +{ + // make sure this is a radio event + if (event <= RADIO_START_1 || event >= RADIO_END) + return; + + PrintIfWatched( "%3.1f: SendRadioMessage( %s )\n", gpGlobals->curtime, RadioEventName[ event ] ); + + // note the time the message was sent + TheCSBots()->SetRadioMessageTimestamp( event, GetTeamNumber() ); + + m_lastRadioSentTimestamp = gpGlobals->curtime; + + char slot[2]; + slot[1] = '\000'; + + if (event > RADIO_START_1 && event < RADIO_START_2) + { + HandleMenu_Radio1( event - RADIO_START_1 ); + } + else if (event > RADIO_START_2 && event < RADIO_START_3) + { + HandleMenu_Radio2( event - RADIO_START_2 ); + } + else + { + HandleMenu_Radio3( event - RADIO_START_3 ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Send voice chatter. Also sends the entindex and duration for voice feedback. + */ +void CCSBot::SpeakAudio( const char *voiceFilename, float duration, int pitch ) +{ + if( !IsAlive() ) + return; + + if ( IsObserver() ) + return; + + CRecipientFilter filter; + ConstructRadioFilter( filter ); + + UserMessageBegin ( filter, "RawAudio" ); + WRITE_BYTE( pitch ); + WRITE_BYTE( entindex() ); + WRITE_FLOAT( duration ); + WRITE_STRING( voiceFilename ); + MessageEnd(); + + GetChatter()->ResetRadioSilenceDuration(); + + m_voiceEndTimestamp = gpGlobals->curtime + duration; +} + diff --git a/game/server/cstrike/bot/cs_bot_statemachine.cpp b/game/server/cstrike/bot/cs_bot_statemachine.cpp new file mode 100644 index 0000000..8cbf9aa --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_statemachine.cpp @@ -0,0 +1,693 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "cs_nav_path.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +/** + * This method is the ONLY legal way to change a bot's current state + */ +void CCSBot::SetState( BotState *state ) +{ + PrintIfWatched( "%s: SetState: %s -> %s\n", GetPlayerName(), (m_state) ? m_state->GetName() : "NULL", state->GetName() ); + + /* + if ( IsDefusingBomb() ) + { + const Vector *bombPos = GetGameState()->GetBombPosition(); + if ( bombPos != NULL ) + { + if ( TheCSBots()->GetBombDefuser() == this ) + { + if ( TheCSBots()->IsBombPlanted() ) + { + Msg( "Bot %s is switching from defusing the bomb to %s\n", + GetPlayerName(), state->GetName() ); + } + } + } + } + */ + + // if we changed state from within the special Attack state, we are no longer attacking + if (m_isAttacking) + StopAttacking(); + + if (m_state) + m_state->OnExit( this ); + + state->OnEnter( this ); + + m_state = state; + m_stateTimestamp = gpGlobals->curtime; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Idle( void ) +{ + SetTask( SEEK_AND_DESTROY ); + SetState( &m_idleState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::EscapeFromBomb( void ) +{ + SetTask( ESCAPE_FROM_BOMB ); + SetState( &m_escapeFromBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Follow( CCSPlayer *player ) +{ + if (player == NULL) + return; + + // note when we began following + if (!m_isFollowing || m_leader != player) + m_followTimestamp = gpGlobals->curtime; + + m_isFollowing = true; + m_leader = player; + + SetTask( FOLLOW ); + m_followState.SetLeader( player ); + SetState( &m_followState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Continue following our leader after finishing what we were doing + */ +void CCSBot::ContinueFollowing( void ) +{ + SetTask( FOLLOW ); + + m_followState.SetLeader( m_leader ); + + SetState( &m_followState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Stop following + */ +void CCSBot::StopFollowing( void ) +{ + m_isFollowing = false; + m_leader = NULL; + m_allowAutoFollowTime = gpGlobals->curtime + 10.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin process of rescuing hostages + */ +void CCSBot::RescueHostages( void ) +{ + SetTask( RESCUE_HOSTAGES ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Use the entity + */ +void CCSBot::UseEntity( CBaseEntity *entity ) +{ + m_useEntityState.SetEntity( entity ); + SetState( &m_useEntityState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Open the door. + * This assumes the bot is directly in front of the door with no obstructions. + * NOTE: This state is special, like Attack, in that it suspends the current behavior and returns to it when done. + */ +void CCSBot::OpenDoor( CBaseEntity *door ) +{ + m_openDoorState.SetDoor( door ); + m_isOpeningDoor = true; + m_openDoorState.OnEnter( this ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * DEPRECATED: Use TryToHide() instead. + * Move to a hiding place. + * If 'searchFromArea' is non-NULL, hiding spots are looked for from that area first. + */ +void CCSBot::Hide( CNavArea *searchFromArea, float duration, float hideRange, bool holdPosition ) +{ + DestroyPath(); + + CNavArea *source; + Vector sourcePos; + if (searchFromArea) + { + source = searchFromArea; + sourcePos = searchFromArea->GetCenter(); + } + else + { + source = m_lastKnownArea; + sourcePos = GetCentroid( this ); + } + + if (source == NULL) + { + PrintIfWatched( "Hide from area is NULL.\n" ); + Idle(); + return; + } + + m_hideState.SetSearchArea( source ); + m_hideState.SetSearchRange( hideRange ); + m_hideState.SetDuration( duration ); + m_hideState.SetHoldPosition( holdPosition ); + + // search around source area for a good hiding spot + Vector useSpot; + + const Vector *pos = FindNearbyHidingSpot( this, sourcePos, hideRange, IsSniper() ); + if (pos == NULL) + { + PrintIfWatched( "No available hiding spots.\n" ); + // hide at our current position + useSpot = GetCentroid( this ); + } + else + { + useSpot = *pos; + } + + m_hideState.SetHidingSpot( useSpot ); + + // build a path to our new hiding spot + if (ComputePath( useSpot, FASTEST_ROUTE ) == false) + { + PrintIfWatched( "Can't pathfind to hiding spot\n" ); + Idle(); + return; + } + + SetState( &m_hideState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to the given hiding place + */ +void CCSBot::Hide( const Vector &hidingSpot, float duration, bool holdPosition ) +{ + CNavArea *hideArea = TheNavMesh->GetNearestNavArea( hidingSpot ); + if (hideArea == NULL) + { + PrintIfWatched( "Hiding spot off nav mesh\n" ); + Idle(); + return; + } + + DestroyPath(); + + m_hideState.SetSearchArea( hideArea ); + m_hideState.SetSearchRange( 750.0f ); + m_hideState.SetDuration( duration ); + m_hideState.SetHoldPosition( holdPosition ); + m_hideState.SetHidingSpot( hidingSpot ); + + // build a path to our new hiding spot + if (ComputePath( hidingSpot, FASTEST_ROUTE ) == false) + { + PrintIfWatched( "Can't pathfind to hiding spot\n" ); + Idle(); + return; + } + + SetState( &m_hideState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Try to hide nearby. Return true if hiding, false if can't hide here. + * If 'searchFromArea' is non-NULL, hiding spots are looked for from that area first. + */ +bool CCSBot::TryToHide( CNavArea *searchFromArea, float duration, float hideRange, bool holdPosition, bool useNearest ) +{ + CNavArea *source; + Vector sourcePos; + if (searchFromArea) + { + source = searchFromArea; + sourcePos = searchFromArea->GetCenter(); + } + else + { + source = m_lastKnownArea; + sourcePos = GetCentroid( this ); + } + + if (source == NULL) + { + PrintIfWatched( "Hide from area is NULL.\n" ); + return false; + } + + m_hideState.SetSearchArea( source ); + m_hideState.SetSearchRange( hideRange ); + m_hideState.SetDuration( duration ); + m_hideState.SetHoldPosition( holdPosition ); + + // search around source area for a good hiding spot + const Vector *pos = FindNearbyHidingSpot( this, sourcePos, hideRange, IsSniper(), useNearest ); + if (pos == NULL) + { + PrintIfWatched( "No available hiding spots.\n" ); + return false; + } + + m_hideState.SetHidingSpot( *pos ); + + // build a path to our new hiding spot + if (ComputePath( *pos, FASTEST_ROUTE ) == false) + { + PrintIfWatched( "Can't pathfind to hiding spot\n" ); + return false; + } + + SetState( &m_hideState ); + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Retreat to a nearby hiding spot, away from enemies + */ +bool CCSBot::TryToRetreat( float maxRange, float duration ) +{ + const Vector *spot = FindNearbyRetreatSpot( this, maxRange ); + if (spot) + { + // ignore enemies for a second to give us time to hide + // reaching our hiding spot clears our disposition + IgnoreEnemies( 10.0f ); + + if (duration < 0.0f) + { + duration = RandomFloat( 3.0f, 15.0f ); + } + + StandUp(); + Run(); + Hide( *spot, duration ); + + PrintIfWatched( "Retreating to a safe spot!\n" ); + + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Hunt( void ) +{ + SetState( &m_huntState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Attack our the given victim + * NOTE: Attacking does not change our task. + */ +void CCSBot::Attack( CCSPlayer *victim ) +{ + if (victim == NULL) + return; + + // zombies never attack + if (cv_bot_zombie.GetBool()) + return; + + // cannot attack if we are reloading + if (IsReloading()) + return; + + // change enemy + SetBotEnemy( victim ); + + // + // Do not "re-enter" the attack state if we are already attacking + // + if (IsAttacking()) + return; + + // if we're holding a grenade, throw it at the victim + if (IsUsingGrenade()) + { + // throw towards their feet + ThrowGrenade( victim->GetAbsOrigin() ); + return; + } + + + // if we are currently hiding, increase our chances of crouching and holding position + if (IsAtHidingSpot()) + m_attackState.SetCrouchAndHold( (RandomFloat( 0.0f, 100.0f ) < 60.0f) ? true : false ); + else + m_attackState.SetCrouchAndHold( false ); + + //SetState( &m_attackState ); + //PrintIfWatched( "ATTACK BEGIN (reaction time = %g (+ update time), surprise time = %g, attack delay = %g)\n", + // GetProfile()->GetReactionTime(), m_surpriseDelay, GetProfile()->GetAttackDelay() ); + m_isAttacking = true; + m_attackState.OnEnter( this ); + + + Vector victimOrigin = GetCentroid( victim ); + + // cheat a bit and give the bot the initial location of its victim + m_lastEnemyPosition = victimOrigin; + m_lastSawEnemyTimestamp = gpGlobals->curtime; + m_aimSpreadTimestamp = gpGlobals->curtime; + + // compute the angle difference between where are looking, and where we need to look + Vector toEnemy = victimOrigin - GetCentroid( this ); + + QAngle idealAngle; + VectorAngles( toEnemy, idealAngle ); + + float deltaYaw = (float)fabs(m_lookYaw - idealAngle.y); + + while( deltaYaw > 180.0f ) + deltaYaw -= 360.0f; + + if (deltaYaw < 0.0f) + deltaYaw = -deltaYaw; + + // immediately aim at enemy - accuracy penalty depending on how far we must turn to aim + // accuracy is halved if we have to turn 180 degrees + float turn = deltaYaw / 180.0f; + float accuracy = GetProfile()->GetSkill() / (1.0f + turn); + + SetAimOffset( accuracy ); + + // define time when aim offset will automatically be updated + // longer time the more we had to turn (surprise) + m_aimOffsetTimestamp = gpGlobals->curtime + RandomFloat( 0.25f + turn, 1.5f ); + + // forget any look at targets we have + ClearLookAt(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Exit the Attack state + */ +void CCSBot::StopAttacking( void ) +{ + PrintIfWatched( "ATTACK END\n" ); + m_attackState.OnExit( this ); + m_isAttacking = false; + + // if we are following someone, go to the Idle state after the attack to decide whether we still want to follow + if (IsFollowing()) + { + Idle(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CCSBot::IsAttacking( void ) const +{ + return m_isAttacking; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are escaping from the bomb + */ +bool CCSBot::IsEscapingFromBomb( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_escapeFromBombState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are defusing the bomb + */ +bool CCSBot::IsDefusingBomb( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_defuseBombState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are hiding + */ +bool CCSBot::IsHiding( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_hideState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are hiding and at our hiding spot + */ +bool CCSBot::IsAtHidingSpot( void ) const +{ + if (!IsHiding()) + return false; + + return m_hideState.IsAtSpot(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return number of seconds we have been at our current hiding spot + */ +float CCSBot::GetHidingTime( void ) const +{ + if (IsHiding()) + { + return m_hideState.GetHideTime(); + } + + return 0.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are huting + */ +bool CCSBot::IsHunting( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_huntState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are in the MoveTo state + */ +bool CCSBot::IsMovingTo( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_moveToState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are buying + */ +bool CCSBot::IsBuying( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_buyState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CCSBot::IsInvestigatingNoise( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_investigateNoiseState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to potentially distant position + */ +void CCSBot::MoveTo( const Vector &pos, RouteType route ) +{ + m_moveToState.SetGoalPosition( pos ); + m_moveToState.SetRouteType( route ); + SetState( &m_moveToState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::PlantBomb( void ) +{ + SetState( &m_plantBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Bomb has been dropped - go get it + */ +void CCSBot::FetchBomb( void ) +{ + SetState( &m_fetchBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::DefuseBomb( void ) +{ + SetState( &m_defuseBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Investigate recent enemy noise + */ +void CCSBot::InvestigateNoise( void ) +{ + SetState( &m_investigateNoiseState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Buy( void ) +{ + SetState( &m_buyState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a hiding spot and wait for initial encounter with enemy team. + * Return false if can't do this behavior (ie: no hiding spots available). + */ +bool CCSBot::MoveToInitialEncounter( void ) +{ + int myTeam = GetTeamNumber(); + int enemyTeam = OtherTeam( myTeam ); + + // build a path to an enemy spawn point + CBaseEntity *enemySpawn = TheCSBots()->GetRandomSpawn( enemyTeam ); + + if (enemySpawn == NULL) + { + PrintIfWatched( "MoveToInitialEncounter: No enemy spawn points?\n" ); + return false; + } + + // build a path from us to the enemy spawn + CCSNavPath path; + PathCost cost( this, FASTEST_ROUTE ); + path.Compute( WorldSpaceCenter(), enemySpawn->GetAbsOrigin(), cost ); + + if (!path.IsValid()) + { + PrintIfWatched( "MoveToInitialEncounter: Pathfind failed.\n" ); + return false; + } + + // find battlefront area where teams will first meet along this path + int i; + for( i=0; i<path.GetSegmentCount(); ++i ) + { + if (path[i]->area->GetEarliestOccupyTime( myTeam ) > path[i]->area->GetEarliestOccupyTime( enemyTeam )) + { + break; + } + } + + if (i == path.GetSegmentCount()) + { + PrintIfWatched( "MoveToInitialEncounter: Can't find battlefront!\n" ); + return false; + } + + /// @todo Remove this evil side-effect + SetInitialEncounterArea( path[i]->area ); + + // find a hiding spot on our side of the battlefront that has LOS to it + const float maxRange = 1500.0f; + const HidingSpot *spot = FindInitialEncounterSpot( this, path[i]->area->GetCenter(), path[i]->area->GetEarliestOccupyTime( enemyTeam ), maxRange, IsSniper() ); + + if (spot == NULL) + { + PrintIfWatched( "MoveToInitialEncounter: Can't find a hiding spot\n" ); + return false; + } + + float timeToWait = path[i]->area->GetEarliestOccupyTime( enemyTeam ) - spot->GetArea()->GetEarliestOccupyTime( myTeam ); + float minWaitTime = 4.0f * GetProfile()->GetAggression() + 3.0f; + if (timeToWait < minWaitTime) + { + timeToWait = minWaitTime; + } + + Hide( spot->GetPosition(), timeToWait ); + + return true; +} + diff --git a/game/server/cstrike/bot/cs_bot_update.cpp b/game/server/cstrike/bot/cs_bot_update.cpp new file mode 100644 index 0000000..d57af0a --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_update.cpp @@ -0,0 +1,1211 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_bot.h" +#include "fmtstr.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------------------------------------- +float CCSBot::GetMoveSpeed( void ) +{ + return 250.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Lightweight maintenance, invoked frequently + */ +void CCSBot::Upkeep( void ) +{ + VPROF_BUDGET( "CCSBot::Upkeep", VPROF_BUDGETGROUP_NPCS ); + + if (TheNavMesh->IsGenerating() || !IsAlive()) + return; + + // If bot_flipout is on, then generate some random commands. + if ( cv_bot_flipout.GetBool() ) + { + int val = RandomInt( 0, 2 ); + if ( val == 0 ) + MoveForward(); + else if ( val == 1 ) + MoveBackward(); + + val = RandomInt( 0, 2 ); + if ( val == 0 ) + StrafeLeft(); + else if ( val == 1 ) + StrafeRight(); + + if ( RandomInt( 0, 5 ) == 0 ) + Jump( true ); + + val = RandomInt( 0, 2 ); + if ( val == 0 ) + Crouch(); + else ( val == 1 ); + StandUp(); + + return; + } + + // BOTPORT: Remove this nasty hack + m_eyePosition = EyePosition(); + + Vector myOrigin = GetCentroid( this ); + + // aiming must be smooth - update often + if (IsAimingAtEnemy()) + { + UpdateAimOffset(); + + // aim at enemy, if he's still alive + if (m_enemy != NULL && m_enemy->IsAlive()) + { + Vector enemyOrigin = GetCentroid( m_enemy ); + + if (m_isEnemyVisible) + { + // + // Enemy is visible - determine which part of him to shoot at + // + const float sharpshooter = 0.8f; + VisiblePartType aimAtPart; + + if (IsUsingMachinegun()) + { + // spray the big machinegun at the enemy's gut + aimAtPart = GUT; + } + else if (IsUsing( WEAPON_AWP ) || IsUsingShotgun()) + { + // these weapons are best aimed at the chest + aimAtPart = GUT; + } + else if (GetProfile()->GetSkill() > 0.5f && IsActiveWeaponRecoilHigh() ) + { + // sprayin' and prayin' - aim at the gut since we're not going to be accurate + aimAtPart = GUT; + } + else if (GetProfile()->GetSkill() < sharpshooter) + { + // low skill bots don't go for headshots + aimAtPart = GUT; + } + else + { + // high skill - aim for the head + aimAtPart = HEAD; + } + + if (IsEnemyPartVisible( aimAtPart )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), aimAtPart ); + } + else + { + // desired part is blocked - aim at whatever part is visible + if (IsEnemyPartVisible( GUT )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), GUT ); + } + else if (IsEnemyPartVisible( HEAD )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), HEAD ); + } + else if (IsEnemyPartVisible( LEFT_SIDE )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), LEFT_SIDE ); + } + else if (IsEnemyPartVisible( RIGHT_SIDE )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), RIGHT_SIDE ); + } + else // FEET + { + m_aimSpot = GetPartPosition( GetBotEnemy(), FEET ); + } + } + + // high skill bots lead the target a little to compensate for update tick latency + /* + if (false && GetProfile()->GetSkill() > 0.5f) + { + const float k = 1.0f; + m_aimSpot += k * g_flBotCommandInterval * (m_enemy->GetAbsVelocity() - GetAbsVelocity()); + } + */ + + } + else + { + // aim where we last saw enemy - but bend the ray so we dont point directly into walls + // if we put this back, make sure you only bend the ray ONCE and keep the bent spot - dont continually recompute + //BendLineOfSight( m_eyePosition, m_lastEnemyPosition, &m_aimSpot ); + m_aimSpot = m_lastEnemyPosition; + } + + // add in aim error + m_aimSpot.x += m_aimOffset.x; + m_aimSpot.y += m_aimOffset.y; + m_aimSpot.z += m_aimOffset.z; + + Vector to = m_aimSpot - EyePositionConst(); + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + // adjust aim angle for recoil, based on bot skill + const QAngle &punchAngles = GetPunchAngle(); + idealAngle -= punchAngles * GetProfile()->GetSkill(); + + SetLookAngles( idealAngle.y, idealAngle.x ); + } + } + else + { + if (m_lookAtSpotClearIfClose) + { + // dont look at spots just in front of our face - it causes erratic view rotation + const float tooCloseRange = 100.0f; + if ((m_lookAtSpot - myOrigin).IsLengthLessThan( tooCloseRange )) + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + } + + switch( m_lookAtSpotState ) + { + case NOT_LOOKING_AT_SPOT: + { + // look ahead + SetLookAngles( m_lookAheadAngle, 0.0f ); + break; + } + + case LOOK_TOWARDS_SPOT: + { + UpdateLookAt(); + if (IsLookingAtPosition( m_lookAtSpot, m_lookAtSpotAngleTolerance )) + { + m_lookAtSpotState = LOOK_AT_SPOT; + m_lookAtSpotTimestamp = gpGlobals->curtime; + } + break; + } + + case LOOK_AT_SPOT: + { + UpdateLookAt(); + + if (m_lookAtSpotDuration >= 0.0f && gpGlobals->curtime - m_lookAtSpotTimestamp > m_lookAtSpotDuration) + { + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + m_lookAtSpotDuration = 0.0f; + } + break; + } + } + + // have view "drift" very slowly, so view looks "alive" + if (!IsUsingSniperRifle()) + { + float driftAmplitude = 2.0f; + if (IsBlind()) + { + driftAmplitude = 5.0f; + } + + m_lookYaw += driftAmplitude * BotCOS( 33.0f * gpGlobals->curtime ); + m_lookPitch += driftAmplitude * BotSIN( 13.0f * gpGlobals->curtime ); + } + } + + // view angles can change quickly + UpdateLookAngles(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Heavyweight processing, invoked less often + */ +void CCSBot::Update( void ) +{ + VPROF_BUDGET( "CCSBot::Update", VPROF_BUDGETGROUP_NPCS ); + + // If bot_flipout is on, then we only do stuff in Upkeep(). + if ( cv_bot_flipout.GetBool() ) + return; + + Vector myOrigin = GetCentroid( this ); + + // if we are spectating, get into the game + if (GetTeamNumber() == 0) + { + HandleCommand_JoinTeam( m_desiredTeam ); + int desiredClass = GetProfile()->GetSkin(); + if ( m_desiredTeam == TEAM_CT && desiredClass ) + { + desiredClass = FIRST_CT_CLASS + desiredClass - 1; + } + else if ( m_desiredTeam == TEAM_TERRORIST && desiredClass ) + { + desiredClass = FIRST_T_CLASS + desiredClass - 1; + } + HandleCommand_JoinClass( desiredClass ); + return; + } + + + // update our radio chatter + // need to allow bots to finish their chatter even if they are dead + GetChatter()->Update(); + + // check if we are dead + if (!IsAlive()) + { + // remember that we died + m_diedLastRound = true; + + BotDeathThink(); + return; + } + + // the bot is alive and in the game at this point + m_hasJoined = true; + + // + // Debug beam rendering + // + + if (cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + DebugDisplay(); + } + + if (cv_bot_stop.GetBool()) + return; + + // check if we are stuck + StuckCheck(); + + // Check for physics props and other breakables in our way that we can break + BreakablesCheck(); + + // Check for useable doors in our way that we need to open + DoorCheck(); + + // update travel distance to all players (this is an optimization) + UpdateTravelDistanceToAllPlayers(); + + // if our current 'noise' was heard a long time ago, forget it + const float rememberNoiseDuration = 20.0f; + if (m_noiseTimestamp > 0.0f && gpGlobals->curtime - m_noiseTimestamp > rememberNoiseDuration) + { + ForgetNoise(); + } + + // where are we + if (!m_currentArea || !m_currentArea->Contains( myOrigin )) + { + m_currentArea = (CCSNavArea *)TheNavMesh->GetNavArea( myOrigin ); + } + + // track the last known area we were in + if (m_currentArea && m_currentArea != m_lastKnownArea) + { + m_lastKnownArea = m_currentArea; + + OnEnteredNavArea( m_currentArea ); + } + + // keep track of how long we have been motionless + const float stillSpeed = 10.0f; + if (GetAbsVelocity().IsLengthLessThan( stillSpeed )) + { + m_stillTimer.Start(); + } + else + { + m_stillTimer.Invalidate(); + } + + // if we're blind, retreat! + if (IsBlind()) + { + if (m_blindFire) + { + PrimaryAttack(); + } + } + + UpdatePanicLookAround(); + + // + // Enemy acquisition and attack initiation + // + + // take a snapshot and update our reaction time queue + UpdateReactionQueue(); + + // "threat" may be the same as our current enemy + CCSPlayer *threat = GetRecognizedEnemy(); + if (threat) + { + Vector threatOrigin = GetCentroid( threat ); + + // adjust our personal "safe" time + AdjustSafeTime(); + + BecomeAlert(); + + const float selfDefenseRange = 500.0f; // 750.0f; + const float farAwayRange = 2000.0f; + + // + // Decide if we should attack + // + bool doAttack = false; + switch( GetDisposition() ) + { + case IGNORE_ENEMIES: + { + // never attack + doAttack = false; + break; + } + + case SELF_DEFENSE: + { + // attack if fired on + doAttack = (IsPlayerLookingAtMe( threat, 0.99f ) && DidPlayerJustFireWeapon( threat )); + + // attack if enemy very close + if (!doAttack) + { + doAttack = (myOrigin - threatOrigin).IsLengthLessThan( selfDefenseRange ); + } + + break; + } + + case ENGAGE_AND_INVESTIGATE: + case OPPORTUNITY_FIRE: + { + if ((myOrigin - threatOrigin).IsLengthGreaterThan( farAwayRange )) + { + // enemy is very far away - wait to take our shot until he is closer + // unless we are a sniper or he is shooting at us + if (IsSniper()) + { + // snipers love far away targets + doAttack = true; + } + else + { + // attack if fired on + doAttack = (IsPlayerLookingAtMe( threat, 0.99f ) && DidPlayerJustFireWeapon( threat )); + } + } + else + { + // normal combat range + doAttack = true; + } + + break; + } + } + + // if we aren't attacking but we are being attacked, retaliate + if (!doAttack && !IsAttacking() && GetDisposition() != IGNORE_ENEMIES) + { + const float recentAttackDuration = 1.0f; + if (GetTimeSinceAttacked() < recentAttackDuration) + { + // we may not be attacking our attacker, but at least we're not just taking it + // (since m_attacker isn't reaction-time delayed, we can't directly use it) + doAttack = true; + PrintIfWatched( "Ouch! Retaliating!\n" ); + } + } + + if (doAttack) + { + if (!IsAttacking() || threat != GetBotEnemy()) + { + if (IsUsingKnife() && IsHiding()) + { + // if hiding with a knife, wait until threat is close + const float knifeAttackRange = 250.0f; + if ((GetAbsOrigin() - threat->GetAbsOrigin()).IsLengthLessThan( knifeAttackRange )) + { + Attack( threat ); + } + } + else + { + Attack( threat ); + } + } + } + else + { + // dont attack, but keep track of nearby enemies + SetBotEnemy( threat ); + m_isEnemyVisible = true; + } + + TheCSBots()->SetLastSeenEnemyTimestamp(); + } + + // + // Validate existing enemy, if any + // + if (m_enemy != NULL) + { + if (IsAwareOfEnemyDeath()) + { + // we have noticed that our enemy has died + m_enemy = NULL; + m_isEnemyVisible = false; + } + else + { + // check LOS to current enemy (chest & head), in case he's dead (GetNearestEnemy() only returns live players) + // note we're not checking FOV - once we've acquired an enemy (which does check FOV), assume we know roughly where he is + if (IsVisible( m_enemy, false, &m_visibleEnemyParts )) + { + m_isEnemyVisible = true; + m_lastSawEnemyTimestamp = gpGlobals->curtime; + m_lastEnemyPosition = GetCentroid( m_enemy ); + } + else + { + m_isEnemyVisible = false; + } + + // check if enemy died + if (m_enemy->IsAlive()) + { + m_enemyDeathTimestamp = 0.0f; + m_isLastEnemyDead = false; + } + else if (m_enemyDeathTimestamp == 0.0f) + { + // note time of death (to allow bots to overshoot for a time) + m_enemyDeathTimestamp = gpGlobals->curtime; + m_isLastEnemyDead = true; + } + } + } + else + { + m_isEnemyVisible = false; + } + + + // if we have seen an enemy recently, keep an eye on him if we can + if (!IsBlind() && !IsLookingAtSpot(PRIORITY_UNINTERRUPTABLE) ) + { + const float seenRecentTime = 3.0f; + if (m_enemy != NULL && GetTimeSinceLastSawEnemy() < seenRecentTime) + { + AimAtEnemy(); + } + else + { + StopAiming(); + } + } + else if( IsAimingAtEnemy() ) + { + StopAiming(); + } + + // + // Hack to fire while retreating + /// @todo Encapsulate aiming and firing on enemies separately from current task + // + if (GetDisposition() == IGNORE_ENEMIES) + { + FireWeaponAtEnemy(); + } + + // toss grenades + LookForGrenadeTargets(); + + // process grenade throw state machine + UpdateGrenadeThrow(); + + // avoid enemy grenades + AvoidEnemyGrenades(); + + + // check if our weapon is totally out of ammo + // or if we no longer feel "safe", equip our weapon + if (!IsSafe() && !IsUsingGrenade() && IsActiveWeaponOutOfAmmo()) + { + EquipBestWeapon(); + } + + /// @todo This doesn't work if we are restricted to just knives and sniper rifles because we cant use the rifle at close range + if (!IsSafe() && !IsUsingGrenade() && IsUsingKnife() && !IsEscapingFromBomb()) + { + EquipBestWeapon(); + } + + // if we haven't seen an enemy in awhile, and we switched to our pistol during combat, + // switch back to our primary weapon (if it still has ammo left) + const float safeRearmTime = 5.0f; + if (!IsReloading() && IsUsingPistol() && !IsPrimaryWeaponEmpty() && GetTimeSinceLastSawEnemy() > safeRearmTime) + { + EquipBestWeapon(); + } + + // reload our weapon if we must + ReloadCheck(); + + // equip silencer + SilencerCheck(); + + // listen to the radio + RespondToRadioCommands(); + + // make way + const float avoidTime = 0.33f; + if (gpGlobals->curtime - m_avoidTimestamp < avoidTime && m_avoid != NULL) + { + StrafeAwayFromPosition( GetCentroid( m_avoid ) ); + } + else + { + m_avoid = NULL; + } + + // if we're using a sniper rifle and are no longer attacking, stop looking thru scope + if (!IsAtHidingSpot() && !IsAttacking() && IsUsingSniperRifle() && IsUsingScope()) + { + SecondaryAttack(); + } + + if (!IsBlind()) + { + // check encounter spots + UpdatePeripheralVision(); + + // watch for snipers + if (CanSeeSniper() && !HasSeenSniperRecently()) + { + GetChatter()->SpottedSniper(); + + const float sniperRecentInterval = 20.0f; + m_sawEnemySniperTimer.Start( sniperRecentInterval ); + } + + // + // Update gamestate + // + if (m_bomber != NULL) + GetChatter()->SpottedBomber( GetBomber() ); + + if (CanSeeLooseBomb()) + GetChatter()->SpottedLooseBomb( TheCSBots()->GetLooseBomb() ); + } + + // + // Scenario interrupts + // + switch (TheCSBots()->GetScenario()) + { + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + // flee if the bomb is ready to blow and we aren't defusing it or attacking and we know where the bomb is + // (aggressive players wait until its almost too late) + float gonnaBlowTime = 8.0f - (2.0f * GetProfile()->GetAggression()); + + // if we have a defuse kit, can wait longer + if (m_bHasDefuser) + gonnaBlowTime *= 0.66f; + + if (!IsEscapingFromBomb() && // we aren't already escaping the bomb + TheCSBots()->IsBombPlanted() && // is the bomb planted + GetGameState()->IsPlantedBombLocationKnown() && // we know where the bomb is + TheCSBots()->GetBombTimeLeft() < gonnaBlowTime && // is the bomb about to explode + !IsDefusingBomb() && // we aren't defusing the bomb + !IsAttacking()) // we aren't in the midst of a firefight + { + EscapeFromBomb(); + break; + } + + break; + } + + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + if (GetTeamNumber() == TEAM_CT) + { + UpdateHostageEscortCount(); + } + else + { + // Terrorists have imperfect information on status of hostages + unsigned char status = GetGameState()->ValidateHostagePositions(); + + if (status & CSGameState::HOSTAGES_ALL_GONE) + { + GetChatter()->HostagesTaken(); + Idle(); + } + else if (status & CSGameState::HOSTAGE_GONE) + { + GetGameState()->HostageWasTaken(); + Idle(); + } + } + break; + } + } + + + // + // Follow nearby humans if our co-op is high and we have nothing else to do + // If we were just following someone, don't auto-follow again for a short while to + // give us a chance to do something else. + // + const float earliestAutoFollowTime = 5.0f; + const float minAutoFollowTeamwork = 0.4f; + if (cv_bot_auto_follow.GetBool() && + TheCSBots()->GetElapsedRoundTime() > earliestAutoFollowTime && + GetProfile()->GetTeamwork() > minAutoFollowTeamwork && + CanAutoFollow() && + !IsBusy() && + !IsFollowing() && + !IsBlind() && + !GetGameState()->IsAtPlantedBombsite()) + { + + // chance of following is proportional to teamwork attribute + if (GetProfile()->GetTeamwork() > RandomFloat( 0.0f, 1.0f )) + { + CCSPlayer *leader = GetClosestVisibleHumanFriend(); + if (leader && leader->IsAutoFollowAllowed()) + { + // count how many bots are already following this player + const float maxFollowCount = 2; + if (GetBotFollowCount( leader ) < maxFollowCount) + { + const float autoFollowRange = 300.0f; + Vector leaderOrigin = GetCentroid( leader ); + if ((leaderOrigin - myOrigin).IsLengthLessThan( autoFollowRange )) + { + CNavArea *leaderArea = TheNavMesh->GetNavArea( leaderOrigin ); + if (leaderArea) + { + PathCost cost( this, FASTEST_ROUTE ); + float travelRange = NavAreaTravelDistance( GetLastKnownArea(), leaderArea, cost ); + if (travelRange >= 0.0f && travelRange < autoFollowRange) + { + // follow this human + Follow( leader ); + PrintIfWatched( "Auto-Following %s\n", leader->GetPlayerName() ); + + if (CSGameRules()->IsCareer()) + { + GetChatter()->Say( "FollowingCommander", 10.0f ); + } + else + { + GetChatter()->Say( "FollowingSir", 10.0f ); + } + } + } + } + } + } + } + else + { + // we decided not to follow, don't re-check for a duration + m_allowAutoFollowTime = gpGlobals->curtime + 15.0f + (1.0f - GetProfile()->GetTeamwork()) * 30.0f; + } + } + + if (IsFollowing()) + { + // if we are following someone, make sure they are still alive + CBaseEntity *leader = m_leader; + if (leader == NULL || !leader->IsAlive()) + { + StopFollowing(); + } + + // decide whether to continue following them + const float highTeamwork = 0.85f; + if (GetProfile()->GetTeamwork() < highTeamwork) + { + float minFollowDuration = 15.0f; + if (GetFollowDuration() > minFollowDuration + 40.0f * GetProfile()->GetTeamwork()) + { + // we are bored of following our leader + StopFollowing(); + PrintIfWatched( "Stopping following - bored\n" ); + } + } + } + + + // + // Execute state machine + // + if (m_isOpeningDoor) + { + // opening doors takes precedence over attacking because DoorCheck() will stop opening doors if + // we don't have a knife out. + m_openDoorState.OnUpdate( this ); + + if (m_openDoorState.IsDone()) + { + // open door behavior is finished, return to previous behavior context + m_openDoorState.OnExit( this ); + m_isOpeningDoor = false; + } + } + else if (m_isAttacking) + { + m_attackState.OnUpdate( this ); + } + else + { + m_state->OnUpdate( this ); + } + + // do wait behavior + if (!IsAttacking() && IsWaiting()) + { + ResetStuckMonitor(); + ClearMovement(); + } + + // don't move while reloading unless we see an enemy + if (IsReloading() && !m_isEnemyVisible) + { + ResetStuckMonitor(); + ClearMovement(); + } + + // if we get too far ahead of the hostages we are escorting, wait for them + if (!IsAttacking() && m_inhibitWaitingForHostageTimer.IsElapsed()) + { + const float waitForHostageRange = 500.0f; + if ((GetTask() == COLLECT_HOSTAGES || GetTask() == RESCUE_HOSTAGES) && GetRangeToFarthestEscortedHostage() > waitForHostageRange) + { + if (!m_isWaitingForHostage) + { + // just started waiting + m_isWaitingForHostage = true; + m_waitForHostageTimer.Start( 10.0f ); + } + else + { + // we've been waiting + if (m_waitForHostageTimer.IsElapsed()) + { + // give up waiting for awhile + m_isWaitingForHostage = false; + m_inhibitWaitingForHostageTimer.Start( 3.0f ); + } + else + { + // keep waiting + ResetStuckMonitor(); + ClearMovement(); + } + } + } + } + + // remember our prior safe time status + m_wasSafe = IsSafe(); +} + + +//-------------------------------------------------------------------------------------------------------------- +class DrawTravelTime +{ +public: + DrawTravelTime( const CCSBot *me ) + { + m_me = me; + } + + bool operator() ( CBasePlayer *player ) + { + if (player->IsAlive() && !m_me->InSameTeam( player )) + { + CFmtStr msg; + player->EntityText( 0, + msg.sprintf( "%3.0f", m_me->GetTravelDistanceToPlayer( (CCSPlayer *)player ) ), + 0.1f ); + + + if (m_me->DidPlayerJustFireWeapon( ToCSPlayer( player ) )) + { + player->EntityText( 1, "BANG!", 0.1f ); + } + } + + return true; + } + + const CCSBot *m_me; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Render bot debug info + */ +void CCSBot::DebugDisplay( void ) const +{ + const float duration = 0.15f; + CFmtStr msg; + + NDebugOverlay::ScreenText( 0.5f, 0.34f, msg.sprintf( "Skill: %d%%", (int)(100.0f * GetProfile()->GetSkill()) ), 255, 255, 255, 150, duration ); + + if ( m_pathLadder ) + { + NDebugOverlay::ScreenText( 0.5f, 0.36f, msg.sprintf( "Ladder: %d", m_pathLadder->GetID() ), 255, 255, 255, 150, duration ); + } + + // show safe time + float safeTime = GetSafeTimeRemaining(); + if (safeTime > 0.0f) + { + NDebugOverlay::ScreenText( 0.5f, 0.38f, msg.sprintf( "SafeTime: %3.2f", safeTime ), 255, 255, 255, 150, duration ); + } + + // show if blind + if (IsBlind()) + { + NDebugOverlay::ScreenText( 0.5f, 0.38f, msg.sprintf( "<<< BLIND >>>" ), 255, 255, 255, 255, duration ); + } + + // show if alert + if (IsAlert()) + { + NDebugOverlay::ScreenText( 0.5f, 0.38f, msg.sprintf( "ALERT" ), 255, 0, 0, 255, duration ); + } + + // show if panicked + if (IsPanicking()) + { + NDebugOverlay::ScreenText( 0.5f, 0.36f, msg.sprintf( "PANIC" ), 255, 255, 0, 255, duration ); + } + + // show behavior variables + if (m_isAttacking) + { + NDebugOverlay::ScreenText( 0.5f, 0.4f, msg.sprintf( "ATTACKING: %s", GetBotEnemy()->GetPlayerName() ), 255, 0, 0, 255, duration ); + } + else + { + NDebugOverlay::ScreenText( 0.5f, 0.4f, msg.sprintf( "State: %s", m_state->GetName() ), 255, 255, 0, 255, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.42f, msg.sprintf( "Task: %s", GetTaskName() ), 0, 255, 0, 255, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.44f, msg.sprintf( "Disposition: %s", GetDispositionName() ), 100, 100, 255, 255, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.46f, msg.sprintf( "Morale: %s", GetMoraleName() ), 0, 200, 200, 255, duration ); + } + + // show look at status + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT) + { + const char *string = msg.sprintf( "LookAt: %s (%s)", m_lookAtDesc, (m_lookAtSpotState == LOOK_TOWARDS_SPOT) ? "LOOK_TOWARDS_SPOT" : "LOOK_AT_SPOT" ); + + NDebugOverlay::ScreenText( 0.5f, 0.60f, string, 255, 255, 0, 150, duration ); + } + + NDebugOverlay::ScreenText( 0.5f, 0.62f, msg.sprintf( "Steady view = %s", HasViewBeenSteady( 0.2f ) ? "YES" : "NO" ), 255, 255, 0, 150, duration ); + + + // show friend/foes I know of + NDebugOverlay::ScreenText( 0.5f, 0.64f, msg.sprintf( "Nearby friends = %d", m_nearbyFriendCount ), 100, 255, 100, 150, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.66f, msg.sprintf( "Nearby enemies = %d", m_nearbyEnemyCount ), 255, 100, 100, 150, duration ); + + if ( m_lastNavArea ) + { + NDebugOverlay::ScreenText( 0.5f, 0.68f, msg.sprintf( "Nav Area: %d (%s)", m_lastNavArea->GetID(), m_szLastPlaceName.Get() ), 255, 255, 255, 150, duration ); + /* + if ( cv_bot_traceview.GetBool() ) + { + if ( m_currentArea ) + { + NDebugOverlay::Line( GetAbsOrigin(), m_currentArea->GetCenter(), 0, 255, 0, true, 0.1f ); + } + else if ( m_lastKnownArea ) + { + NDebugOverlay::Line( GetAbsOrigin(), m_lastKnownArea->GetCenter(), 255, 0, 0, true, 0.1f ); + } + else if ( m_lastNavArea ) + { + NDebugOverlay::Line( GetAbsOrigin(), m_lastNavArea->GetCenter(), 0, 0, 255, true, 0.1f ); + } + } + */ + } + + // show debug message history + float y = 0.8f; + const float lineHeight = 0.02f; + const float fadeAge = 7.0f; + const float maxAge = 10.0f; + for( int i=0; i<TheBots->GetDebugMessageCount(); ++i ) + { + const CBotManager::DebugMessage *msg = TheBots->GetDebugMessage( i ); + + if (msg->m_age.GetElapsedTime() < maxAge) + { + int alpha = 255; + + if (msg->m_age.GetElapsedTime() > fadeAge) + { + alpha *= (1.0f - (msg->m_age.GetElapsedTime() - fadeAge) / (maxAge - fadeAge)); + } + + NDebugOverlay::ScreenText( 0.5f, y, msg->m_string, 255, 255, 255, alpha, duration ); + y += lineHeight; + } + } + + // show noises + const Vector *noisePos = GetNoisePosition(); + if (noisePos) + { + const float size = 25.0f; + NDebugOverlay::VertArrow( *noisePos + Vector( 0, 0, size ), *noisePos, size/4.0f, 255, 255, 0, 0, true, duration ); + } + + // show aim spot + if (IsAimingAtEnemy()) + { + NDebugOverlay::Cross3D( m_aimSpot, 5.0f, 255, 0, 0, true, duration ); + } + + + + if (IsHiding()) + { + // show approach points + DrawApproachPoints(); + } + else + { + // show encounter spot data + if (false && m_spotEncounter) + { + NDebugOverlay::Line( m_spotEncounter->path.from, m_spotEncounter->path.to, 0, 150, 150, true, 0.1f ); + + Vector dir = m_spotEncounter->path.to - m_spotEncounter->path.from; + float length = dir.NormalizeInPlace(); + + const SpotOrder *order; + Vector along; + + FOR_EACH_VEC( m_spotEncounter->spots, it ) + { + order = &m_spotEncounter->spots[ it ]; + + // ignore spots the enemy could not have possibly reached yet + if (order->spot->GetArea()) + { + if (TheCSBots()->GetElapsedRoundTime() < order->spot->GetArea()->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) )) + { + continue; + } + } + + along = m_spotEncounter->path.from + order->t * length * dir; + + NDebugOverlay::Line( along, order->spot->GetPosition(), 0, 255, 255, true, 0.1f ); + } + } + } + + // show aim targets + if (false) + { + CStudioHdr *pStudioHdr = const_cast< CCSBot *>( this )->GetModelPtr(); + if ( !pStudioHdr ) + return; + + mstudiohitboxset_t *set = pStudioHdr->pHitboxSet( const_cast< CCSBot *>( this )->GetHitboxSet() ); + if ( !set ) + return; + + Vector position, forward, right, up; + QAngle angles; + char buffer[16]; + + for ( int i = 0; i < set->numhitboxes; i++ ) + { + mstudiobbox_t *pbox = set->pHitbox( i ); + + const_cast< CCSBot *>( this )->GetBonePosition( pbox->bone, position, angles ); + + AngleVectors( angles, &forward, &right, &up ); + + NDebugOverlay::BoxAngles( position, pbox->bbmin, pbox->bbmax, angles, 255, 0, 0, 0, 0.1f ); + + const float size = 5.0f; + NDebugOverlay::Line( position, position + size * forward, 255, 255, 0, true, 0.1f ); + NDebugOverlay::Line( position, position + size * right, 255, 0, 0, true, 0.1f ); + NDebugOverlay::Line( position, position + size * up, 0, 255, 0, true, 0.1f ); + + Q_snprintf( buffer, 16, "%d", i ); + if (i == 12) + { + // in local bone space + const float headForwardOffset = 4.0f; + const float headRightOffset = 2.0f; + position += headForwardOffset * forward + headRightOffset * right; + } + NDebugOverlay::Text( position, buffer, true, 0.1f ); + } + } + + + /* + const QAngle &angles = const_cast< CCSBot *>( this )->GetPunchAngle(); + NDebugOverlay::ScreenText( 0.3f, 0.66f, msg.sprintf( "Punch angle pitch = %3.2f", angles.x ), 255, 255, 0, 150, duration ); + */ + + DrawTravelTime drawTravelTime( this ); + ForEachPlayer( drawTravelTime ); + +/* + // show line of fire + if ((cv_bot_traceview.GetInt() == 100 && IsLocalPlayerWatchingMe()) || cv_bot_traceview.GetInt() == 101) + { + if (!IsFriendInLineOfFire()) + { + Vector vecAiming = GetViewVector(); + Vector vecSrc = EyePositionConst(); + + if (GetTeamNumber() == TEAM_TERRORIST) + UTIL_DrawBeamPoints( vecSrc, vecSrc + 2000.0f * vecAiming, 1, 255, 0, 0 ); + else + UTIL_DrawBeamPoints( vecSrc, vecSrc + 2000.0f * vecAiming, 1, 0, 50, 255 ); + } + } + + // show path navigation data + if (cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe()) + { + Vector from = EyePositionConst(); + + const float size = 50.0f; + //Vector arrow( size * cos( m_forwardAngle * M_PI/180.0f ), size * sin( m_forwardAngle * M_PI/180.0f ), 0.0f ); + Vector arrow( size * (float)cos( m_lookAheadAngle * M_PI/180.0f ), + size * (float)sin( m_lookAheadAngle * M_PI/180.0f ), + 0.0f ); + + UTIL_DrawBeamPoints( from, from + arrow, 1, 0, 255, 255 ); + } + + if (cv_bot_show_nav.GetBool() && m_lastKnownArea) + { + m_lastKnownArea->DrawConnectedAreas(); + } + */ + + + if (IsAttacking()) + { + const float crossSize = 2.0f; + CCSPlayer *enemy = GetBotEnemy(); + if (enemy) + { + NDebugOverlay::Cross3D( GetPartPosition( enemy, GUT ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, HEAD ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, FEET ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, LEFT_SIDE ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, RIGHT_SIDE ), crossSize, 0, 255, 0, true, 0.1f ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Periodically compute shortest path distance to each player. + * NOTE: Travel distance is NOT symmetric between players A and B. Each much be computed separately. + */ +void CCSBot::UpdateTravelDistanceToAllPlayers( void ) +{ + const unsigned char numPhases = 3; + + if (m_updateTravelDistanceTimer.IsElapsed()) + { + ShortestPathCost pathCost; + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CCSPlayer *player = static_cast< CCSPlayer * >( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (FNullEnt( player->edict() )) + continue; + + if (!player->IsPlayer()) + continue; + + if (!player->IsAlive()) + continue; + + // skip friends for efficiency + if (player->InSameTeam( this )) + continue; + + int which = player->entindex() % MAX_PLAYERS; + + // if player is very far away, update every third time (on phase 0) + const float veryFarAway = 4000.0f; + if (m_playerTravelDistance[ which ] < 0.0f || m_playerTravelDistance[ which ] > veryFarAway) + { + if (m_travelDistancePhase != 0) + continue; + } + else + { + // if player is far away, update two out of three times (on phases 1 and 2) + const float farAway = 2000.0f; + if (m_playerTravelDistance[ which ] > farAway && m_travelDistancePhase == 0) + continue; + } + + // if player is fairly close, update often + m_playerTravelDistance[ which ] = NavAreaTravelDistance( EyePosition(), player->EyePosition(), pathCost ); + } + + // throttle the computation frequency + const float checkInterval = 1.0f; + m_updateTravelDistanceTimer.Start( checkInterval ); + + // round-robin the phases + ++m_travelDistancePhase; + if (m_travelDistancePhase >= numPhases) + { + m_travelDistancePhase = 0; + } + } +} diff --git a/game/server/cstrike/bot/cs_bot_vision.cpp b/game/server/cstrike/bot/cs_bot_vision.cpp new file mode 100644 index 0000000..a7e8500 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_vision.cpp @@ -0,0 +1,1834 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "datacache/imdlcache.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Used to update view angles to stay on a ladder + */ +inline float StayOnLadderLine( CCSBot *me, const CNavLadder *ladder ) +{ + // determine our facing + NavDirType faceDir = AngleToDirection( me->EyeAngles().y ); + + const float stiffness = 1.0f; + + // move toward ladder mount point + switch( faceDir ) + { + case NORTH: + return stiffness * (ladder->m_top.x - me->GetAbsOrigin().x); + + case SOUTH: + return -stiffness * (ladder->m_top.x - me->GetAbsOrigin().x); + + case WEST: + return -stiffness * (ladder->m_top.y - me->GetAbsOrigin().y); + + case EAST: + return stiffness * (ladder->m_top.y - me->GetAbsOrigin().y); + } + + return 0.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::ComputeLadderAngles( float *yaw, float *pitch ) +{ + if ( !yaw || !pitch ) + return; + + Vector myOrigin = GetCentroid( this ); + + // set yaw to aim at ladder + Vector to = m_pathLadder->GetPosAtHeight(myOrigin.z) - myOrigin; + float idealYaw = UTIL_VecToYaw( to ); + + Vector faceDir = (m_pathLadderFaceIn) ? -m_pathLadder->GetNormal() : m_pathLadder->GetNormal(); + QAngle faceAngles; + VectorAngles( faceDir, faceAngles ); + + const float lookAlongLadderRange = 50.0f; + const float ladderPitchUpApproach = -30.0f; + const float ladderPitchUpTraverse = -60.0f; // -80 + const float ladderPitchDownApproach = 0.0f; + const float ladderPitchDownTraverse = 80.0f; + + // adjust pitch to look up/down ladder as we ascend/descend + switch( m_pathLadderState ) + { + case APPROACH_ASCENDING_LADDER: + { + Vector to = m_goalPosition - myOrigin; + *yaw = idealYaw; + + if (to.IsLengthLessThan( lookAlongLadderRange )) + *pitch = ladderPitchUpApproach; + break; + } + + case APPROACH_DESCENDING_LADDER: + { + Vector to = m_goalPosition - myOrigin; + *yaw = idealYaw; + + if (to.IsLengthLessThan( lookAlongLadderRange )) + *pitch = ladderPitchDownApproach; + break; + } + + case FACE_ASCENDING_LADDER: + if ( m_pathLadderDismountDir == LEFT ) + { + *yaw = AngleNormalizePositive( idealYaw + 90.0f ); + } + else if ( m_pathLadderDismountDir == RIGHT ) + { + *yaw = AngleNormalizePositive( idealYaw - 90.0f ); + } + else + { + *yaw = idealYaw; + } + *pitch = ladderPitchUpApproach; + break; + + case FACE_DESCENDING_LADDER: + *yaw = idealYaw; + *pitch = ladderPitchDownApproach; + break; + + case MOUNT_ASCENDING_LADDER: + case ASCEND_LADDER: + if ( m_pathLadderDismountDir == LEFT ) + { + *yaw = AngleNormalizePositive( idealYaw + 90.0f ); + } + else if ( m_pathLadderDismountDir == RIGHT ) + { + *yaw = AngleNormalizePositive( idealYaw - 90.0f ); + } + else + { + *yaw = faceAngles[ YAW ] + StayOnLadderLine( this, m_pathLadder ); + } + *pitch = ( m_pathLadderState == ASCEND_LADDER ) ? ladderPitchUpTraverse : ladderPitchUpApproach; + break; + + case MOUNT_DESCENDING_LADDER: + case DESCEND_LADDER: + *yaw = faceAngles[ YAW ] + StayOnLadderLine( this, m_pathLadder ); + *pitch = ( m_pathLadderState == DESCEND_LADDER ) ? ladderPitchDownTraverse : ladderPitchDownApproach; + break; + + case DISMOUNT_ASCENDING_LADDER: + if ( m_pathLadderDismountDir == LEFT ) + { + *yaw = AngleNormalizePositive( faceAngles[ YAW ] + 90.0f ); + } + else if ( m_pathLadderDismountDir == RIGHT ) + { + *yaw = AngleNormalizePositive( faceAngles[ YAW ] - 90.0f ); + } + else + { + *yaw = faceAngles[ YAW ]; + } + break; + + case DISMOUNT_DESCENDING_LADDER: + *yaw = faceAngles[ YAW ]; + break; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move actual view angles towards desired ones. + * This is the only place v_angle is altered. + * @todo Make stiffness and turn rate constants timestep invariant. + */ +void CCSBot::UpdateLookAngles( void ) +{ + VPROF_BUDGET( "CCSBot::UpdateLookAngles", VPROF_BUDGETGROUP_NPCS ); + + const float deltaT = g_BotUpkeepInterval; + float maxAccel; + float stiffness; + float damping; + + // If mimicing the player, don't modify the view angles. + if ( bot_mimic.GetInt() ) + return; + + // springs are stiffer when attacking, so we can track and move between targets better + if (IsAttacking()) + { + stiffness = 300.0f; + damping = 30.0f; // 20 + maxAccel = 3000.0f; // 4000 + } + else + { + stiffness = 200.0f; + damping = 25.0f; + maxAccel = 3000.0f; + } + + // these may be overridden by ladder logic + float useYaw = m_lookYaw; + float usePitch = m_lookPitch; + + // + // Ladders require precise movement, therefore we need to look at the + // ladder as we approach and ascend/descend it. + // If we are on a ladder, we need to look up or down to traverse it - override pitch in this case. + // + // If we're trying to break something, though, we actually need to look at it before we can + // look at the ladder + // + if ( IsUsingLadder() && !(IsLookingAtSpot( PRIORITY_HIGH ) && m_lookAtSpotAttack) ) + { + ComputeLadderAngles( &useYaw, &usePitch ); + } + + // get current view angles + QAngle viewAngles = EyeAngles(); + + // + // Yaw + // + float angleDiff = AngleNormalize( useYaw - viewAngles.y ); + + /* + * m_forwardAngle is unreliable. Need to simulate mouse sliding & centering + if (!IsAttacking()) + { + // do not allow rotation through our reverse facing angle - go the "long" way instead + float toCurrent = AngleNormalize( pev->v_angle.y - m_forwardAngle ); + float toDesired = AngleNormalize( useYaw - m_forwardAngle ); + + // if angle differences are different signs, they cross the forward facing + if (toCurrent * toDesired < 0.0f) + { + // if the sum of the angles is greater than 180, turn the "long" way around + if (abs( toCurrent - toDesired ) >= 180.0f) + { + if (angleDiff > 0.0f) + angleDiff -= 360.0f; + else + angleDiff += 360.0f; + } + } + } + */ + + // if almost at target angle, snap to it + const float onTargetTolerance = 1.0f; // 3 + if (angleDiff < onTargetTolerance && angleDiff > -onTargetTolerance) + { + m_lookYawVel = 0.0f; + viewAngles.y = useYaw; + } + else + { + // simple angular spring/damper + float accel = stiffness * angleDiff - damping * m_lookYawVel; + + // limit rate + if (accel > maxAccel) + accel = maxAccel; + else if (accel < -maxAccel) + accel = -maxAccel; + + m_lookYawVel += deltaT * accel; + viewAngles.y += deltaT * m_lookYawVel; + + // keep track of how long our view remains steady + const float steadyYaw = 1000.0f; + if (fabs( accel ) > steadyYaw) + { + m_viewSteadyTimer.Start(); + } + } + + // + // Pitch + // Actually, this is negative pitch. + // + angleDiff = usePitch - viewAngles.x; + + angleDiff = AngleNormalize( angleDiff ); + + + if (false && angleDiff < onTargetTolerance && angleDiff > -onTargetTolerance) + { + m_lookPitchVel = 0.0f; + viewAngles.x = usePitch; + } + else + { + // simple angular spring/damper + // double the stiffness since pitch is only +/- 90 and yaw is +/- 180 + float accel = 2.0f * stiffness * angleDiff - damping * m_lookPitchVel; + + // limit rate + if (accel > maxAccel) + accel = maxAccel; + else if (accel < -maxAccel) + accel = -maxAccel; + + m_lookPitchVel += deltaT * accel; + viewAngles.x += deltaT * m_lookPitchVel; + + // keep track of how long our view remains steady + const float steadyPitch = 1000.0f; + if (fabs( accel ) > steadyPitch) + { + m_viewSteadyTimer.Start(); + } + } + + //PrintIfWatched( "yawVel = %g, pitchVel = %g\n", m_lookYawVel, m_lookPitchVel ); + + // limit range - avoid gimbal lock + if (viewAngles.x < -89.0f) + viewAngles.x = -89.0f; + else if (viewAngles.x > 89.0f) + viewAngles.x = 89.0f; + + // update view angles + SnapEyeAngles( viewAngles ); + + // if our weapon is zooming, our view is not steady + if (IsWaitingForZoom()) + { + m_viewSteadyTimer.Start(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we can see the point + */ +bool CCSBot::IsVisible( const Vector &pos, bool testFOV, const CBaseEntity *ignore ) const +{ + VPROF_BUDGET( "CCSBot::IsVisible( pos )", VPROF_BUDGETGROUP_NPCS ); + + // we can't see anything if we're blind + if (IsBlind()) + return false; + + // is it in my general viewcone? + if (testFOV && !(const_cast<CCSBot *>(this)->FInViewCone( pos ))) + return false; + + // check line of sight against smoke + if (TheCSBots()->IsLineBlockedBySmoke( EyePositionConst(), pos )) + return false; + + // check line of sight + // Must include CONTENTS_MONSTER to pick up all non-brush objects like barrels + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( ignore, COLLISION_GROUP_NONE ); + UTIL_TraceLine( EyePositionConst(), pos, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + if (result.fraction != 1.0f) + return false; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we can see any part of the player + * Check parts in order of importance. Return the first part seen in "visPart" if it is non-NULL. + */ +bool CCSBot::IsVisible( CCSPlayer *player, bool testFOV, unsigned char *visParts ) const +{ + VPROF_BUDGET( "CCSBot::IsVisible( player )", VPROF_BUDGETGROUP_NPCS ); + + // optimization - assume if center is not in FOV, nothing is + // we're using WorldSpaceCenter instead of GUT so we can skip GetPartPosition below - that's + // the most expensive part of this, and if we can skip it, so much the better. + if (testFOV && !(const_cast<CCSBot *>(this)->FInViewCone( player->WorldSpaceCenter() ))) + { + return false; + } + + unsigned char testVisParts = NONE; + + // check gut + Vector partPos = GetPartPosition( player, GUT ); + + // finish gut check + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= GUT; + } + + + // check top of head + partPos = GetPartPosition( player, HEAD ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= HEAD; + } + + // check feet + partPos = GetPartPosition( player, FEET ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= FEET; + } + + // check "edges" + partPos = GetPartPosition( player, LEFT_SIDE ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= LEFT_SIDE; + } + + partPos = GetPartPosition( player, RIGHT_SIDE ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= RIGHT_SIDE; + } + + if (visParts) + *visParts = testVisParts; + + if (testVisParts) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Interesting part positions + */ +CCSBot::PartInfo CCSBot::m_partInfo[ MAX_PLAYERS ]; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute part positions from bone location. + */ +void CCSBot::ComputePartPositions( CCSPlayer *player ) +{ + const int headBox = 12; + const int gutBox = 9; + const int leftElbowBox = 14; + const int rightElbowBox = 17; + //const int hipBox = 0; + //const int leftFootBox = 4; + //const int rightFootBox = 8; + const int maxBoxIndex = rightElbowBox; + + VPROF_BUDGET( "CCSBot::ComputePartPositions", VPROF_BUDGETGROUP_NPCS ); + + // which PartInfo corresponds to the given player + PartInfo *info = &m_partInfo[ player->entindex() % MAX_PLAYERS ]; + + // always compute feet, since it doesn't rely on bones + info->m_feetPos = player->GetAbsOrigin(); + info->m_feetPos.z += 5.0f; + + // get bone positions for interesting points on the player + MDLCACHE_CRITICAL_SECTION(); + CStudioHdr *studioHdr = player->GetModelPtr(); + if (studioHdr) + { + mstudiohitboxset_t *set = studioHdr->pHitboxSet( player->GetHitboxSet() ); + if (set && maxBoxIndex < set->numhitboxes) + { + QAngle angles; + mstudiobbox_t *box; + + // gut + box = set->pHitbox( gutBox ); + player->GetBonePosition( box->bone, info->m_gutPos, angles ); + + // head + box = set->pHitbox( headBox ); + player->GetBonePosition( box->bone, info->m_headPos, angles ); + + Vector forward, right; + AngleVectors( angles, &forward, &right, NULL ); + + // in local bone space + const float headForwardOffset = 4.0f; + const float headRightOffset = 2.0f; + info->m_headPos += headForwardOffset * forward + headRightOffset * right; + + /// @todo Fix this hack - lower the head target because it's a bit too high for the current T model + info->m_headPos.z -= 2.0f; + + + // left side + box = set->pHitbox( leftElbowBox ); + player->GetBonePosition( box->bone, info->m_leftSidePos, angles ); + + // right side + box = set->pHitbox( rightElbowBox ); + player->GetBonePosition( box->bone, info->m_rightSidePos, angles ); + + return; + } + } + + + // default values if bones are not available + info->m_headPos = GetCentroid( player ); + info->m_gutPos = info->m_headPos; + info->m_leftSidePos = info->m_headPos; + info->m_rightSidePos = info->m_headPos; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return world space position of given part on player. + * Uses hitboxes to get accurate positions. + * @todo Optimize by computing once for each player and storing. + */ +const Vector &CCSBot::GetPartPosition( CCSPlayer *player, VisiblePartType part ) const +{ + VPROF_BUDGET( "CCSBot::GetPartPosition", VPROF_BUDGETGROUP_NPCS ); + + // which PartInfo corresponds to the given player + PartInfo *info = &m_partInfo[ player->entindex() % MAX_PLAYERS ]; + + if (gpGlobals->framecount > info->m_validFrame) + { + // update part positions + const_cast< CCSBot * >( this )->ComputePartPositions( player ); + info->m_validFrame = gpGlobals->framecount; + } + + // return requested part position + switch( part ) + { + default: + { + AssertMsg( false, "GetPartPosition: Invalid part" ); + // fall thru to GUT + } + + case GUT: + return info->m_gutPos; + + case HEAD: + return info->m_headPos; + + case FEET: + return info->m_feetPos; + + case LEFT_SIDE: + return info->m_leftSidePos; + + case RIGHT_SIDE: + return info->m_rightSidePos; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update desired view angles to point towards m_lookAtSpot + */ +void CCSBot::UpdateLookAt( void ) +{ + Vector to = m_lookAtSpot - EyePositionConst(); + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + //Vector idealAngle = UTIL_VecToAngles( to ); + //idealAngle.x = 360.0f - idealAngle.x; + + SetLookAngles( idealAngle.y, idealAngle.x ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Look at the given point in space for the given duration (-1 means forever) + */ +void CCSBot::SetLookAt( const char *desc, const Vector &pos, PriorityType pri, float duration, bool clearIfClose, float angleTolerance, bool attack ) +{ + if (IsBlind()) + return; + + // if currently looking at a point in space with higher priority, ignore this request + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && m_lookAtSpotPriority > pri) + return; + + // if already looking at this spot, just extend the time + const float tolerance = 10.0f; + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && VectorsAreEqual( pos, m_lookAtSpot, tolerance )) + { + m_lookAtSpotDuration = duration; + + if (m_lookAtSpotPriority < pri) + m_lookAtSpotPriority = pri; + } + else + { + // look at new spot + m_lookAtSpot = pos; + m_lookAtSpotState = LOOK_TOWARDS_SPOT; + m_lookAtSpotDuration = duration; + m_lookAtSpotPriority = pri; + } + + m_lookAtSpotAngleTolerance = angleTolerance; + m_lookAtSpotClearIfClose = clearIfClose; + m_lookAtDesc = desc; + m_lookAtSpotAttack = attack; + + PrintIfWatched( "%3.1f SetLookAt( %s ), duration = %f\n", gpGlobals->curtime, desc, duration ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Block all "look at" and "look around" behavior for given duration - just look ahead + */ +void CCSBot::InhibitLookAround( float duration ) +{ + m_inhibitLookAroundTimestamp = gpGlobals->curtime + duration; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update enounter spot timestamps, etc + */ +void CCSBot::UpdatePeripheralVision() +{ + VPROF_BUDGET( "CCSBot::UpdatePeripheralVision", VPROF_BUDGETGROUP_NPCS ); + + const float peripheralUpdateInterval = 0.29f; // if we update at 10Hz, this ensures we test once every three + if (gpGlobals->curtime - m_peripheralTimestamp < peripheralUpdateInterval) + return; + + m_peripheralTimestamp = gpGlobals->curtime; + + if (m_spotEncounter) + { + // check LOS to all spots in case we see them with our "peripheral vision" + const SpotOrder *spotOrder; + Vector pos; + + FOR_EACH_VEC( m_spotEncounter->spots, it ) + { + spotOrder = &m_spotEncounter->spots[ it ]; + + const Vector &spotPos = spotOrder->spot->GetPosition(); + + pos.x = spotPos.x; + pos.y = spotPos.y; + pos.z = spotPos.z + HalfHumanHeight; + + if (!IsVisible( pos, CHECK_FOV )) + continue; + + // can see hiding spot, remember when we saw it last + SetHidingSpotCheckTimestamp( spotOrder->spot ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update the "looking around" behavior. + */ +void CCSBot::UpdateLookAround( bool updateNow ) +{ + VPROF_BUDGET( "CCSBot::UpdateLookAround", VPROF_BUDGETGROUP_NPCS ); + + // + // If we recently saw an enemy, look towards where we last saw them + // Unless we can hear them moving, in which case look towards the noise + // + const float closeRange = 500.0f; + if (!IsNoiseHeard() || GetNoiseRange() > closeRange) + { + const float recentThreatTime = 1.0f; // 0.25f; + if (!IsLookingAtSpot( PRIORITY_MEDIUM ) && gpGlobals->curtime - m_lastSawEnemyTimestamp < recentThreatTime) + { + ClearLookAt(); + + Vector spot = m_lastEnemyPosition; + + // find enemy position on the ground + if (TheNavMesh->GetSimpleGroundHeight( m_lastEnemyPosition, &spot.z )) + { + spot.z += HalfHumanHeight; + SetLookAt( "Last Enemy Position", spot, PRIORITY_MEDIUM, RandomFloat( 2.0f, 3.0f ), true ); + return; + } + } + } + + // + // Look at nearby enemy noises + // + if (UpdateLookAtNoise()) + return; + + + // check if looking around has been inhibited + // Moved inhibit to allow high priority enemy lookats to still occur + if (gpGlobals->curtime < m_inhibitLookAroundTimestamp) + return; + + // + // If we are hiding (or otherwise standing still), watch all approach points leading into this region + // + const float minStillTime = 2.0f; + if (IsAtHidingSpot() || IsNotMoving( minStillTime )) + { + // update approach points + const float recomputeApproachPointTolerance = 50.0f; + if ((m_approachPointViewPosition - GetAbsOrigin()).IsLengthGreaterThan( recomputeApproachPointTolerance )) + { + ComputeApproachPoints(); + m_approachPointViewPosition = GetAbsOrigin(); + } + + // if we're sniping, zoom in to watch our approach points + if (IsUsingSniperRifle()) + { + // low skill bots don't pre-zoom + if (GetProfile()->GetSkill() > 0.4f) + { + if (!IsViewMoving()) + { + float range = ComputeWeaponSightRange(); + AdjustZoom( range ); + } + else + { + // zoom out + if (GetZoomLevel() != NO_ZOOM) + SecondaryAttack(); + } + } + } + + if (m_lastKnownArea == NULL) + return; + + if (gpGlobals->curtime < m_lookAroundStateTimestamp) + return; + + // if we're sniping, switch look-at spots less often + if (IsUsingSniperRifle()) + m_lookAroundStateTimestamp = gpGlobals->curtime + RandomFloat( 5.0f, 10.0f ); + else + m_lookAroundStateTimestamp = gpGlobals->curtime + RandomFloat( 1.0f, 2.0f ); // 0.5, 1.0 + + + #define MAX_APPROACHES 16 + Vector validSpot[ MAX_APPROACHES ]; + int validSpotCount = 0; + + Vector *earlySpot = NULL; + float earliest = 999999.9f; + + for( int i=0; i<m_approachPointCount; ++i ) + { + float spotTime = m_approachPoint[i].m_area->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) ); + + // ignore approach areas the enemy could not have possibly reached yet + if (TheCSBots()->GetElapsedRoundTime() >= spotTime) + { + validSpot[ validSpotCount++ ] = m_approachPoint[i].m_pos; + } + else + { + // keep track of earliest spot we can see in case we get there very early + if (spotTime < earliest) + { + earlySpot = &m_approachPoint[i].m_pos; + earliest = spotTime; + } + } + } + + Vector spot; + + if (validSpotCount) + { + int which = RandomInt( 0, validSpotCount-1 ); + spot = validSpot[ which ]; + } + else if (earlySpot) + { + // all of the spots we can see can't be reached yet by the enemy - look at the earliest spot + spot = *earlySpot; + } + else + { + return; + } + + // don't look at the floor, look roughly at chest level + /// @todo If this approach point is very near, this will cause us to aim up in the air if were crouching + spot.z += HalfHumanHeight; + + SetLookAt( "Approach Point (Hiding)", spot, PRIORITY_LOW ); + + return; + } + + // + // Glance at "encouter spots" as we move past them + // + if (m_spotEncounter) + { + // + // Check encounter spots + // + if (!IsSafe() && !IsLookingAtSpot( PRIORITY_LOW )) + { + // allow a short time to look where we're going + if (gpGlobals->curtime < m_spotCheckTimestamp) + return; + + /// @todo Use skill parameter instead of accuracy + + // lower skills have exponentially longer delays + float asleep = (1.0f - GetProfile()->GetSkill()); + asleep *= asleep; + asleep *= asleep; + + m_spotCheckTimestamp = gpGlobals->curtime + asleep * RandomFloat( 10.0f, 30.0f ); + + + // figure out how far along the path segment we are + Vector delta = m_spotEncounter->path.to - m_spotEncounter->path.from; + float length = delta.Length(); + float adx = (float)fabs(delta.x); + float ady = (float)fabs(delta.y); + float t; + Vector myOrigin = GetCentroid( this ); + + if (adx > ady) + t = (myOrigin.x - m_spotEncounter->path.from.x) / delta.x; + else + t = (myOrigin.y - m_spotEncounter->path.from.y) / delta.y; + + // advance parameter a bit so we "lead" our checks + const float leadCheckRange = 50.0f; + t += leadCheckRange / length; + + if (t < 0.0f) + t = 0.0f; + else if (t > 1.0f) + t = 1.0f; + + // collect the unchecked spots so far + #define MAX_DANGER_SPOTS 16 + HidingSpot *dangerSpot[MAX_DANGER_SPOTS]; + int dangerSpotCount = 0; + int dangerIndex = 0; + + const float checkTime = 10.0f; + const SpotOrder *spotOrder; + FOR_EACH_VEC( m_spotEncounter->spots, it ) + { + spotOrder = &(m_spotEncounter->spots[ it ]); + + // if we have seen this spot recently, we don't need to look at it + if (gpGlobals->curtime - GetHidingSpotCheckTimestamp( spotOrder->spot ) <= checkTime) + continue; + + if (spotOrder->t > t) + break; + + // ignore spots the enemy could not have possibly reached yet + if (spotOrder->spot->GetArea()) + { + if (TheCSBots()->GetElapsedRoundTime() < spotOrder->spot->GetArea()->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) )) + { + continue; + } + } + + dangerSpot[ dangerIndex++ ] = spotOrder->spot; + if (dangerIndex >= MAX_DANGER_SPOTS) + dangerIndex = 0; + if (dangerSpotCount < MAX_DANGER_SPOTS) + ++dangerSpotCount; + } + + if (dangerSpotCount) + { + // pick one of the spots at random + int which = RandomInt( 0, dangerSpotCount-1 ); + + // glance at the spot for minimum time + SetLookAt( "Encounter Spot", dangerSpot[which]->GetPosition() + Vector( 0, 0, HalfHumanHeight ), PRIORITY_LOW, 0.2f, true, 10.0f ); + + // immediately mark it as "checked", so we don't check it again + // if we get distracted before we check it - that's the way it goes + SetHidingSpotCheckTimestamp( dangerSpot[which] ); + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * "Bend" our line of sight around corners until we can "see" the point. + */ +bool CCSBot::BendLineOfSight( const Vector &eye, const Vector &target, Vector *bend, float angleLimit ) const +{ + VPROF_BUDGET( "CCSBot::BendLineOfSight", VPROF_BUDGETGROUP_NPCS ); + + bool doDebug = false; + const float debugDuration = 0.04f; + if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + NDebugOverlay::Line( eye, target, 255, 255, 255, true, debugDuration ); + + // if we can directly see the point, use it + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( this, COLLISION_GROUP_NONE ); + UTIL_TraceLine( eye, target, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + if (result.fraction == 1.0f && !result.startsolid) + { + // can directly see point, no bending needed + *bend = target; + return true; + } + + // "bend" our line of sight until we can see the approach point + Vector to = target - eye; + float startAngle = UTIL_VecToYaw( to ); + float length = to.Length2D(); + to.NormalizeInPlace(); + + struct Color3 + { + int r, g, b; + }; + const int colorCount = 6; + Color3 colorSet[ colorCount ] = + { + { 255, 0, 0 }, + { 0, 255, 0 }, + { 0, 0, 255 }, + { 255, 255, 0 }, + { 0, 255, 255 }, + { 255, 0, 255 }, + }; + + int color = 0; + + // optiming assumption - previous rays cast "shadow" on subsequent rays since they already + // enumerated visible space along their length. + // We should do a dot product and compute the exact length, but since the angular changes + // are incremental, using the direct length should be close enough. + float priorVisibleLength[2] = { 0.0f, 0.0f }; + + float angleInc = 5.0f; + for( float angle = angleInc; angle <= angleLimit; angle += angleInc ) + { + // check both sides at this angle offset + for( int side=0; side<2; ++side ) + { + float actualAngle = (side) ? (startAngle + angle) : (startAngle - angle); + + float dx = cos( 3.141592f * actualAngle / 180.0f ); + float dy = sin( 3.141592f * actualAngle / 180.0f ); + + // compute rotated point ray endpoint + Vector rotPoint( eye.x + length * dx, eye.y + length * dy, target.z ); + + // check LOS to find length to test along ray + UTIL_TraceLine( eye, rotPoint, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + + // if this ray started in an obstacle, skip it + if (result.startsolid) + { + continue; + } + + Vector ray = rotPoint - eye; + float rayLength = ray.NormalizeInPlace(); + float visibleLength = rayLength * result.fraction; + + if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + NDebugOverlay::Line( eye, eye + visibleLength * ray, colorSet[color].r, colorSet[color].g, colorSet[color].b, true, debugDuration ); + } + + // step along ray, checking if point is visible from ray point + const float bendStepSize = 50.0f; + + // start from point that prior rays couldn't see + float startLength = priorVisibleLength[ side ]; + + for( float bendLength=startLength; bendLength <= visibleLength; bendLength += bendStepSize ) + { + // compute point along ray + Vector bendPoint = eye + bendLength * ray; + + // check if we can see approach point from this bend point + UTIL_TraceLine( bendPoint, target, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + + if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + NDebugOverlay::Line( bendPoint, result.endpos, colorSet[color].r/2, colorSet[color].g/2, colorSet[color].b/2, true, debugDuration ); + } + + if (result.fraction == 1.0f && !result.startsolid) + { + // target is visible from this bend point on the ray - use this point on the ray as our point + + // keep "bent" point at correct height along line of sight + bendPoint.z = eye.z + bendLength * to.z; + + *bend = bendPoint; + + return true; + } + } + + priorVisibleLength[ side ] = visibleLength; + + ++color; + if (color >= colorCount) + { + color = 0; + } + } // side + } + + // bending rays didn't help - still can't see the point + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we "notice" given player + * @todo Increase chance if player is rotating + * @todo Decrease chance as nears edge of FOV + */ +bool CCSBot::IsNoticable( const CCSPlayer *player, unsigned char visParts ) const +{ + // if this player has just fired his weapon, we notice him + if (DidPlayerJustFireWeapon( player )) + { + return true; + } + + float deltaT = m_attentionInterval.GetElapsedTime(); + + // all chances are specified in terms of a standard "quantum" of time + // in which a normal person would notice something + const float noticeQuantum = 0.25f; + + // determine percentage of player that is visible + float coverRatio = 0.0f; + + if (visParts & GUT) + { + const float chance = 40.0f; + coverRatio += chance; + } + + if (visParts & HEAD) + { + const float chance = 10.0f; + coverRatio += chance; + } + + if (visParts & LEFT_SIDE) + { + const float chance = 20.0f; + coverRatio += chance; + } + + if (visParts & RIGHT_SIDE) + { + const float chance = 20.0f; + coverRatio += chance; + } + + if (visParts & FEET) + { + const float chance = 10.0f; + coverRatio += chance; + } + + + // compute range modifier - farther away players are harder to notice, depeding on what they are doing + float range = (player->GetAbsOrigin() - GetAbsOrigin()).Length(); + const float closeRange = 300.0f; + const float farRange = 1000.0f; + + float rangeModifier; + if (range < closeRange) + { + rangeModifier = 0.0f; + } + else if (range > farRange) + { + rangeModifier = 1.0f; + } + else + { + rangeModifier = (range - closeRange)/(farRange - closeRange); + } + + + // harder to notice when crouched + bool isCrouching = (player->GetFlags() & FL_DUCKING); + + + // moving players are easier to spot + float playerSpeedSq = player->GetAbsVelocity().LengthSqr(); + const float runSpeed = 200.0f; + const float walkSpeed = 30.0f; + float farChance, closeChance; + if (playerSpeedSq > runSpeed * runSpeed) + { + // running players are always easy to spot (must be standing to run) + return true; + } + else if (playerSpeedSq > walkSpeed * walkSpeed) + { + // walking players are less noticable far away + if (isCrouching) + { + closeChance = 90.0f; + farChance = 60.0f; + } + else // standing + { + closeChance = 100.0f; + farChance = 75.0f; + } + } + else + { + // motionless players are hard to notice + if (isCrouching) + { + // crouching and motionless - very tough to notice + closeChance = 80.0f; + farChance = 5.0f; // takes about three seconds to notice (50% chance) + } + else // standing + { + closeChance = 100.0f; + farChance = 10.0f; + } + } + + // combine posture, speed, and range chances + float dispositionChance = closeChance + (farChance - closeChance) * rangeModifier; + + // determine actual chance of noticing player + float noticeChance = dispositionChance * coverRatio/100.0f; + + // scale by skill level + noticeChance *= (0.5f + 0.5f * GetProfile()->GetSkill()); + + // if we are alert, our chance of noticing is much higher + if (IsAlert()) + { + const float alertBonus = 50.0f; + noticeChance += alertBonus; + } + + // scale by time quantum + noticeChance *= deltaT / noticeQuantum; + + // there must always be a chance of detecting the enemy + const float minChance = 0.1f; + if (noticeChance < minChance) + { + noticeChance = minChance; + } + + //PrintIfWatched( "Notice chance = %3.2f\n", noticeChance ); + + return (RandomFloat( 0.0f, 100.0f ) < noticeChance); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return most dangerous threat in my field of view (feeds into reaction time queue). + * @todo Account for lighting levels, cover, and distance to see if we notice enemy + */ +CCSPlayer *CCSBot::FindMostDangerousThreat( void ) +{ + VPROF_BUDGET( "CCSBot::FindMostDangerousThreat", VPROF_BUDGETGROUP_NPCS ); + + if (IsBlind()) + { + return NULL; + } + + enum { MAX_THREATS = 16 }; // maximum number of simulataneously attendable threats + struct CloseInfo + { + CCSPlayer *enemy; + float range; + } + threat[ MAX_THREATS ]; + int threatCount = 0; + + int prevIndex = m_enemyQueueIndex - 1; + if ( prevIndex < 0 ) + prevIndex = MAX_ENEMY_QUEUE - 1; + CCSPlayer *currentThreat = m_enemyQueue[ prevIndex ].player; + + m_bomber = NULL; + m_isEnemySniperVisible = false; + + m_closestVisibleFriend = NULL; + float closeFriendRange = 99999999999.9f; + + m_closestVisibleHumanFriend = NULL; + float closeHumanFriendRange = 99999999999.9f; + + CCSPlayer *sniperThreat = NULL; + float sniperThreatRange = 99999999999.9f; + bool sniperThreatIsFacingMe = false; + + const float lookingAtMeTolerance = 0.7071f; + + int i; + + { + VPROF_BUDGET( "CCSBot::Collect Threats", VPROF_BUDGETGROUP_NPCS ); + + for( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBaseEntity *entity = UTIL_PlayerByIndex( i ); + + if (entity == NULL) + continue; + + // is it a player? + if (!entity->IsPlayer()) + continue; + + CCSPlayer *player = static_cast<CCSPlayer *>( entity ); + + // ignore self + if (player->entindex() == entindex()) + continue; + + // is it alive? + if (!player->IsAlive()) + continue; + + // is it an enemy? + if (player->InSameTeam( this )) + { + // keep track of nearby friends - use less exact visibility check + if (IsVisible( entity->WorldSpaceCenter(), false, this )) + { + // update watch timestamp + int idx = player->entindex(); + m_watchInfo[idx].timestamp = gpGlobals->curtime; + m_watchInfo[idx].isEnemy = false; + + // keep track of our closest friend + Vector to = GetAbsOrigin() - player->GetAbsOrigin(); + float rangeSq = to.LengthSqr(); + if (rangeSq < closeFriendRange) + { + m_closestVisibleFriend = player; + closeFriendRange = rangeSq; + } + + // keep track of our closest human friend + if (!player->IsBot() && rangeSq < closeHumanFriendRange) + { + m_closestVisibleHumanFriend = player; + closeHumanFriendRange = rangeSq; + } + } + + continue; + } + + // check if this enemy is fully or partially visible + unsigned char visParts; + if (!IsVisible( player, CHECK_FOV, &visParts )) + continue; + + // do we notice this enemy? (always notice current enemy) + if (player != currentThreat) + { + if (!IsNoticable( player, visParts )) + { + continue; + } + } + + // update watch timestamp + int idx = player->entindex(); + m_watchInfo[idx].timestamp = gpGlobals->curtime; + m_watchInfo[idx].isEnemy = true; + + // note if we see the bomber + if (player->HasC4()) + { + m_bomber = player; + } + + // keep track of all visible threats + Vector d = GetAbsOrigin() - player->GetAbsOrigin(); + float distSq = d.LengthSqr(); + + // track enemy sniper threats + if (IsSniperRifle( player->GetActiveCSWeapon() )) + { + m_isEnemySniperVisible = true; + + // keep track of the most dangerous sniper we see + if (sniperThreat) + { + if (IsPlayerLookingAtMe( player, lookingAtMeTolerance )) + { + if (sniperThreatIsFacingMe) + { + // several snipers are facing us - keep closest + if (distSq < sniperThreatRange) + { + sniperThreat = player; + sniperThreatRange = distSq; + sniperThreatIsFacingMe = true; + } + } + else + { + // even if this sniper is farther away, keep it because he's aiming at us + sniperThreat = player; + sniperThreatRange = distSq; + sniperThreatIsFacingMe = true; + } + } + else + { + // this sniper is not looking at us, only consider it if we dont have a sniper facing us + if (!sniperThreatIsFacingMe && distSq < sniperThreatRange) + { + sniperThreat = player; + sniperThreatRange = distSq; + } + } + } + else + { + // first sniper we see + sniperThreat = player; + sniperThreatRange = distSq; + sniperThreatIsFacingMe = IsPlayerLookingAtMe( player, lookingAtMeTolerance ); + } + } + + + { + VPROF_BUDGET( "CCSBot::Sort Threats", VPROF_BUDGETGROUP_NPCS ); + + // maintain set of visible threats, sorted by increasing distance + if (threatCount == 0) + { + threat[0].enemy = player; + threat[0].range = distSq; + threatCount = 1; + } + else + { + // find insertion point + int j; + for( j=0; j<threatCount; ++j ) + { + if (distSq < threat[j].range) + break; + } + + // shift lower half down a notch + for( int k=threatCount-1; k>=j; --k ) + threat[k+1] = threat[k]; + + // insert threat into sorted list + threat[j].enemy = player; + threat[j].range = distSq; + + if (threatCount < MAX_THREATS) + ++threatCount; + } + } + } + } + + + { + VPROF_BUDGET( "CCSBot::Count nearby Friends & Enemies", VPROF_BUDGETGROUP_NPCS ); + + // track the maximum enemy and friend counts we've seen recently + int prevEnemies = m_nearbyEnemyCount; + m_nearbyEnemyCount = 0; + m_nearbyFriendCount = 0; + for( i=0; i<MAX_PLAYERS; ++i ) + { + if (m_watchInfo[i].timestamp <= 0.0f) + continue; + + const float recentTime = 3.0f; + if (gpGlobals->curtime - m_watchInfo[i].timestamp < recentTime) + { + if (m_watchInfo[i].isEnemy) + ++m_nearbyEnemyCount; + else + ++m_nearbyFriendCount; + } + } + + // note when we saw this batch of enemies + if (prevEnemies == 0 && m_nearbyEnemyCount > 0) + { + m_firstSawEnemyTimestamp = gpGlobals->curtime; + } + } + + + { + VPROF_BUDGET( "CCSBot::Track enemy Place", VPROF_BUDGETGROUP_NPCS ); + + // + // Track the place where we saw most of our enemies + // + struct PlaceRank + { + unsigned int place; + int count; + }; + static PlaceRank placeRank[ MAX_PLACES_PER_MAP ]; + int locCount = 0; + + PlaceRank common; + common.place = 0; + common.count = 0; + + for( i=0; i<threatCount; ++i ) + { + // find the area the player/bot is standing on + CNavArea *area; + CCSBot *bot = dynamic_cast<CCSBot *>(threat[i].enemy); + if (bot && bot->IsBot()) + { + area = bot->GetLastKnownArea(); + } + else + { + Vector enemyOrigin = GetCentroid( threat[i].enemy ); + area = TheNavMesh->GetNearestNavArea( enemyOrigin ); + } + + if (area == NULL) + continue; + + unsigned int threatLoc = area->GetPlace(); + if (!threatLoc) + continue; + + // if place is already in set, increment count + int j; + for( j=0; j<locCount; ++j ) + if (placeRank[j].place == threatLoc) + break; + + if (j == locCount) + { + // new place + if (locCount < MAX_PLACES_PER_MAP) + { + placeRank[ locCount ].place = threatLoc; + placeRank[ locCount ].count = 1; + + if (common.count == 0) + common = placeRank[locCount]; + + ++locCount; + } + } + else + { + // others are in that place, increment + ++placeRank[j].count; + + // keep track of the most common place + if (placeRank[j].count > common.count) + common = placeRank[j]; + } + } + + // remember most common place + m_enemyPlace = common.place; + } + + + { + VPROF_BUDGET( "CCSBot::Select Threat", VPROF_BUDGETGROUP_NPCS ); + + if (threatCount == 0) + return NULL; + + // if we can still see our current threat, keep it + // unless a new one is much closer + bool sawCloserThreat = false; + bool sawCurrentThreat = false; + int t; + for( t=0; t<threatCount; ++t ) + { + if ( threat[t].enemy == currentThreat ) + { + sawCurrentThreat = true; + } + else if ( threat[t].enemy != currentThreat && + IsSignificantlyCloser( threat[t].enemy, currentThreat ) ) + { + sawCloserThreat = true; + } + } + + if ( sawCurrentThreat && !sawCloserThreat ) + { + return currentThreat; + } + + // if we are a sniper and we see a sniper threat, attack it unless + // there are other close enemies facing me + if (IsSniper() && sniperThreat) + { + const float closeCombatRange = 500.0f; + + for( t=0; t<threatCount; ++t ) + { + if (threat[t].range < closeCombatRange && IsPlayerLookingAtMe( threat[t].enemy, lookingAtMeTolerance )) + { + return threat[t].enemy; + } + } + + return sniperThreat; + } + + // otherwise, find the closest threat that is looking at me + for( t=0; t<threatCount; ++t ) + { + if (IsPlayerLookingAtMe( threat[t].enemy, lookingAtMeTolerance )) + { + return threat[t].enemy; + } + } + } + + + // return closest threat + return threat[0].enemy; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update our reaction time queue + */ +void CCSBot::UpdateReactionQueue( void ) +{ + VPROF_BUDGET( "CCSBot::UpdateReactionQueue", VPROF_BUDGETGROUP_NPCS ); + + // zombies dont see any threats + if (cv_bot_zombie.GetBool()) + return; + + // find biggest threat at this instant + CCSPlayer *threat = FindMostDangerousThreat(); + + // reset timer + m_attentionInterval.Start(); + + + int now = m_enemyQueueIndex; + + // store a snapshot of its state at the end of the reaction time queue + if (threat) + { + m_enemyQueue[ now ].player = threat; + m_enemyQueue[ now ].isReloading = threat->IsReloading(); + m_enemyQueue[ now ].isProtectedByShield = threat->IsProtectedByShield(); + } + else + { + m_enemyQueue[ now ].player = NULL; + m_enemyQueue[ now ].isReloading = false; + m_enemyQueue[ now ].isProtectedByShield = false; + } + + // queue is round-robin + ++m_enemyQueueIndex; + if (m_enemyQueueIndex >= MAX_ENEMY_QUEUE) + m_enemyQueueIndex = 0; + + if (m_enemyQueueCount < MAX_ENEMY_QUEUE) + ++m_enemyQueueCount; + + // clamp reaction time to enemy queue size + float reactionTime = GetProfile()->GetReactionTime() - g_BotUpdateInterval; + float maxReactionTime = (MAX_ENEMY_QUEUE * g_BotUpdateInterval) - 0.01f; + if (reactionTime > maxReactionTime) + reactionTime = maxReactionTime; + + // "rewind" time back to our reaction time + int reactionTimeSteps = (int)((reactionTime / g_BotUpdateInterval) + 0.5f); + + int i = now - reactionTimeSteps; + if (i < 0) + i += MAX_ENEMY_QUEUE; + + m_enemyQueueAttendIndex = (unsigned char)i; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the most dangerous threat we are "conscious" of + */ +CCSPlayer *CCSBot::GetRecognizedEnemy( void ) +{ + if (m_enemyQueueAttendIndex >= m_enemyQueueCount || IsBlind()) + { + return NULL; + } + + return m_enemyQueue[ m_enemyQueueAttendIndex ].player; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the enemy we are "conscious" of is reloading + */ +bool CCSBot::IsRecognizedEnemyReloading( void ) +{ + if (m_enemyQueueAttendIndex >= m_enemyQueueCount) + return false; + + return m_enemyQueue[ m_enemyQueueAttendIndex ].isReloading; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the enemy we are "conscious" of is hiding behind a shield + */ +bool CCSBot::IsRecognizedEnemyProtectedByShield( void ) +{ + if (m_enemyQueueAttendIndex >= m_enemyQueueCount) + return false; + + return m_enemyQueue[ m_enemyQueueAttendIndex ].isProtectedByShield; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return distance to closest enemy we are "conscious" of + */ +float CCSBot::GetRangeToNearestRecognizedEnemy( void ) +{ + const CCSPlayer *enemy = GetRecognizedEnemy(); + + if (enemy) + return (GetAbsOrigin() - enemy->GetAbsOrigin()).Length(); + + return 99999999.9f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Blind the bot for the given duration + */ +void CCSBot::Blind( float holdTime, float fadeTime, float startingAlpha ) +{ + PrintIfWatched( "Blinded: holdTime = %3.2f, fadeTime = %3.2f, alpha = %3.2f\n", holdTime, fadeTime, startingAlpha ); + + // if we were only blinded a little bit, shake it off + const float mildBlindTime = 3.0f; + if (holdTime < mildBlindTime) + { + Wait( 0.75f * holdTime ); + BecomeAlert(); + BaseClass::Blind( holdTime, fadeTime, startingAlpha ); + return; + } + + + // if blinded while in combat - then spray and pray! + m_blindFire = IsAttacking(); + + // retreat + // do this first, so spot selection happens before IsBlind() is set + const float hideRange = 400.0f; + TryToRetreat( hideRange ); + + PrintIfWatched( "I'm blind!\n" ); + + if (RandomFloat( 0.0f, 100.0f ) < 33.3f) + { + GetChatter()->Say( "Blinded", 1.0f ); + } + + // no longer safe + AdjustSafeTime(); + + // decide which way to move while blind + m_blindMoveDir = static_cast<NavRelativeDirType>( RandomInt( 1, NUM_RELATIVE_DIRECTIONS-1 ) ); + + // if we're defusing, don't give up + if (IsDefusingBomb()) + { + return; + } + + // can't see to aim at enemy + StopAiming(); + + // dont override "facing away" behavior unless we are going to spray and pray + if (m_blindFire) + { + ClearLookAt(); + + // just look straight ahead while blind + Vector forward; + EyeVectors( &forward ); + SetLookAt( "Blind", EyePosition() + 10000.0f * forward, PRIORITY_UNINTERRUPTABLE, holdTime + 0.5f * fadeTime ); + } + + StopWaiting(); + BecomeAlert(); + + BaseClass::Blind( holdTime, fadeTime, startingAlpha ); +} + + +//-------------------------------------------------------------------------------------------------------------- +class CheckLookAt +{ +public: + CheckLookAt( const CCSBot *me, bool testFOV ) + { + m_me = me; + m_testFOV = testFOV; + } + + bool operator() ( CBasePlayer *player ) + { + if (!m_me->IsEnemy( player )) + return true; + + if (m_testFOV && !(const_cast< CCSBot * >(m_me)->FInViewCone( player->WorldSpaceCenter() ))) + return true; + + if (!m_me->IsPlayerLookingAtMe( player )) + return true; + + if (m_me->IsVisible( (CCSPlayer *)player )) + return false; + + return true; + } + + const CCSBot *m_me; + bool m_testFOV; +}; + +/** + * Return true if any enemy I have LOS to is looking directly at me + * @todo Use reaction time pipeline + */ +bool CCSBot::IsAnyVisibleEnemyLookingAtMe( bool testFOV ) const +{ + CheckLookAt checkLookAt( this, testFOV ); + return (ForEachPlayer( checkLookAt ) == false) ? true : false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do panic behavior + */ +void CCSBot::UpdatePanicLookAround( void ) +{ + if (m_panicTimer.IsElapsed()) + { + return; + } + + if (IsEnemyVisible()) + { + StopPanicking(); + return; + } + + if (HasLookAtTarget()) + { + // wait until we finish our current look at + return; + } + + // select a spot somewhere behind us to look at as we search for our attacker + const QAngle &eyeAngles = EyeAngles(); + + QAngle newAngles; + newAngles.x = RandomFloat( -30.0f, 30.0f ); + + // Look directly behind at a random offset in a 90 window. + float yaw = RandomFloat( 135.0f, 225.0f ); + newAngles.y = eyeAngles.y + yaw; + newAngles.z = 0.0f; + + Vector forward; + AngleVectors( newAngles, &forward ); + + Vector spot; + spot = EyePosition() + 1000.0f * forward; + + SetLookAt( "Panic", spot, PRIORITY_HIGH, 0.0f ); + PrintIfWatched( "Panic yaw angle = %3.2f\n", newAngles.y ); +} diff --git a/game/server/cstrike/bot/cs_bot_weapon.cpp b/game/server/cstrike/bot/cs_bot_weapon.cpp new file mode 100644 index 0000000..4c737f5 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_weapon.cpp @@ -0,0 +1,1363 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "basecsgrenade_projectile.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Fire our active weapon towards our current enemy + * NOTE: Aiming our weapon is handled in RunBotUpkeep() + */ +void CCSBot::FireWeaponAtEnemy( void ) +{ + if (cv_bot_dont_shoot.GetBool()) + { + return; + } + + CBasePlayer *enemy = GetBotEnemy(); + if (enemy == NULL) + { + return; + } + + Vector myOrigin = GetCentroid( this ); + + if (IsUsingSniperRifle()) + { + // if we're using a sniper rifle, don't fire until we are standing still, are zoomed in, and not rapidly moving our view + if (!IsNotMoving() || IsWaitingForZoom() || !HasViewBeenSteady( GetProfile()->GetReactionTime() ) ) + { + return; + } + } + + if (gpGlobals->curtime > m_fireWeaponTimestamp && + GetTimeSinceAcquiredCurrentEnemy() >= GetProfile()->GetAttackDelay() && + !IsSurprised()) + { + if (!(IsRecognizedEnemyProtectedByShield() && IsPlayerFacingMe( enemy )) && // don't shoot at enemies behind shields + !IsReloading() && + !IsActiveWeaponClipEmpty() && + //gpGlobals->curtime > m_reacquireTimestamp && + IsEnemyVisible()) + { + // we have a clear shot - pull trigger if we are aiming at enemy + Vector toAimSpot = m_aimSpot - EyePosition(); + float rangeToEnemy = toAimSpot.NormalizeInPlace(); + + if ( IsUsingSniperRifle() ) + { + // check our accuracy versus our target distance + float fProjectedSpread = rangeToEnemy * GetActiveCSWeapon()->GetInaccuracy(); + float fRequiredSpread = IsUsing( WEAPON_AWP ) ? 50.0f : 25.0f; // AWP will kill with any hit + if ( fProjectedSpread > fRequiredSpread ) + return; + } + + // get actual view direction vector + Vector aimDir = GetViewVector(); + + float onTarget = DotProduct( toAimSpot, aimDir ); + + // aim more precisely with a sniper rifle + // because rifles' bullets spray, don't have to be very precise + const float halfSize = (IsUsingSniperRifle()) ? HalfHumanWidth : 2.0f * HalfHumanWidth; + + // aiming tolerance depends on how close the target is - closer targets subtend larger angles + float aimTolerance = (float)cos( atan( halfSize / rangeToEnemy ) ); + + if (onTarget > aimTolerance) + { + bool doAttack; + + // if friendly fire is on, don't fire if a teammate is blocking our line of fire + if (TheCSBots()->AllowFriendlyFireDamage()) + { + if (IsFriendInLineOfFire()) + doAttack = false; + else + doAttack = true; + } + else + { + // fire freely + doAttack = true; + } + + if (doAttack) + { + // if we are using a knife, only swing it if we're close + if (IsUsingKnife()) + { + const float knifeRange = 75.0f; // 50 + if (rangeToEnemy < knifeRange) + { + // since we've given ourselves away - run! + ForceRun( 5.0f ); + + // if our prey is facing away, backstab him! + if (!IsPlayerFacingMe( enemy )) + { + SecondaryAttack(); + } + else + { + // randomly choose primary and secondary attacks with knife + const float knifeStabChance = 33.3f; + if (RandomFloat( 0, 100 ) < knifeStabChance) + SecondaryAttack(); + else + PrimaryAttack(); + } + } + } + else + { + PrimaryAttack(); + } + } + + if (IsUsingPistol()) + { + // high-skill bots fire their pistols quickly at close range + const float closePistolRange = 360.0f; + if (GetProfile()->GetSkill() > 0.75f && rangeToEnemy < closePistolRange) + { + // fire as fast as possible + m_fireWeaponTimestamp = 0.0f; + } + else + { + // fire somewhat quickly + m_fireWeaponTimestamp = RandomFloat( 0.15f, 0.4f ); + } + } + else // not using a pistol + { + const float sprayRange = 400.0f; + if (GetProfile()->GetSkill() < 0.5f || rangeToEnemy < sprayRange || IsUsingMachinegun()) + { + // spray 'n pray if enemy is close, or we're not that good, or we're using the big machinegun + m_fireWeaponTimestamp = 0.0f; + } + else + { + const float distantTargetRange = 800.0f; + if (!IsUsingSniperRifle() && rangeToEnemy > distantTargetRange) + { + // if very far away, fire slowly for better accuracy + m_fireWeaponTimestamp = RandomFloat( 0.3f, 0.7f ); + } + else + { + // fire short bursts for accuracy + m_fireWeaponTimestamp = RandomFloat( 0.15f, 0.25f ); // 0.15, 0.5 + } + } + } + + // subtract system latency + m_fireWeaponTimestamp -= g_BotUpdateInterval; + + m_fireWeaponTimestamp += gpGlobals->curtime; + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the current aim offset using given accuracy (1.0 = perfect aim, 0.0f = terrible aim) + */ +void CCSBot::SetAimOffset( float accuracy ) +{ + // if our accuracy is less than perfect, it will improve as we "focus in" while not rotating our view + if (accuracy < 1.0f) + { + // if we moved our view, reset our "focus" mechanism + if (IsViewMoving( 100.0f )) + m_aimSpreadTimestamp = gpGlobals->curtime; + + // focusTime is the time it takes for a bot to "focus in" for very good aim, from 2 to 5 seconds + const float focusTime = MAX( 5.0f * (1.0f - accuracy), 2.0f ); + float focusInterval = gpGlobals->curtime - m_aimSpreadTimestamp; + + float focusAccuracy = focusInterval / focusTime; + + // limit how much "focus" will help + const float maxFocusAccuracy = 0.75f; + if (focusAccuracy > maxFocusAccuracy) + focusAccuracy = maxFocusAccuracy; + + accuracy = MAX( accuracy, focusAccuracy ); + } + + //PrintIfWatched( "Accuracy = %4.3f\n", accuracy ); + + // aim error increases with distance, such that actual crosshair error stays about the same + float range = (m_lastEnemyPosition - EyePosition()).Length(); + float maxOffset = (GetFOV()/GetDefaultFOV()) * 0.05f * range; // 0.1 + float error = maxOffset * (1.0f - accuracy); + + m_aimOffsetGoal.x = RandomFloat( -error, error ); + m_aimOffsetGoal.y = RandomFloat( -error, error ); + m_aimOffsetGoal.z = RandomFloat( -error, error ); + + // define time when aim offset will automatically be updated + m_aimOffsetTimestamp = gpGlobals->curtime + RandomFloat( 0.25f, 1.0f ); // 0.25, 1.5f +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Wiggle aim error based on GetProfile()->GetSkill() + */ +void CCSBot::UpdateAimOffset( void ) +{ + if (gpGlobals->curtime >= m_aimOffsetTimestamp) + { + SetAimOffset( GetProfile()->GetSkill() ); + } + + // move current offset towards goal offset + Vector d = m_aimOffsetGoal - m_aimOffset; + const float stiffness = 0.1f; + m_aimOffset.x += stiffness * d.x; + m_aimOffset.y += stiffness * d.y; + m_aimOffset.z += stiffness * d.z; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Change our zoom level to be appropriate for the given range. + * Return true if the zoom level changed. + */ +bool CCSBot::AdjustZoom( float range ) +{ + bool adjustZoom = false; + + if (IsUsingSniperRifle()) + { + const float sniperZoomRange = 150.0f; // NOTE: This must be less than sniperMinRange in AttackState + const float sniperFarZoomRange = 1500.0f; + + // if range is too close, don't zoom + if (range <= sniperZoomRange) + { + // zoom out + if (GetZoomLevel() != NO_ZOOM) + { + adjustZoom = true; + } + } + else if (range < sniperFarZoomRange) + { + // maintain low zoom + if (GetZoomLevel() != LOW_ZOOM) + { + adjustZoom = true; + } + } + else + { + // maintain high zoom + if (GetZoomLevel() != HIGH_ZOOM) + { + adjustZoom = true; + } + } + } + else + { + // zoom out + if (GetZoomLevel() != NO_ZOOM) + { + adjustZoom = true; + } + } + + if (adjustZoom) + { + SecondaryAttack(); + + // pause after zoom to allow "eyes" to refocus +// m_zoomTimer.Start( 0.25f + (1.0f - GetProfile()->GetSkill()) ); + m_zoomTimer.Start( 0.25f ); + } + + return adjustZoom; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if using the specific weapon + */ +bool CCSBot::IsUsing( CSWeaponID weaponID ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon == NULL) + return false; + + if (weapon->IsA( weaponID )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we are using a weapon with a removable silencer + */ +bool CCSBot::DoesActiveWeaponHaveSilencer( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon == NULL) + return false; + + if (weapon->IsA( WEAPON_M4A1 ) || weapon->IsA( WEAPON_USP )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are using a sniper rifle + */ +bool CCSBot::IsUsingSniperRifle( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && IsSniperRifle( weapon )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we have a sniper rifle in our inventory + */ +bool CCSBot::IsSniper( void ) const +{ + CWeaponCSBase *weapon = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + + if (weapon && IsSniperRifle( weapon )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are actively sniping (moving to sniper spot or settled in) + */ +bool CCSBot::IsSniping( void ) const +{ + if (GetTask() == MOVE_TO_SNIPER_SPOT || GetTask() == SNIPING) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are using a shotgun + */ +bool CCSBot::IsUsingShotgun( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon == NULL) + return false; + + return weapon->IsKindOf(WEAPONTYPE_SHOTGUN); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if using the big 'ol machinegun + */ +bool CCSBot::IsUsingMachinegun( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && weapon->IsA( WEAPON_M249 )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if primary weapon doesn't exist or is totally out of ammo + */ +bool CCSBot::IsPrimaryWeaponEmpty( void ) const +{ + CWeaponCSBase *weapon = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + + if (weapon == NULL) + return true; + + // check if gun has any ammo left + if (weapon->HasAnyAmmo()) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if pistol doesn't exist or is totally out of ammo + */ +bool CCSBot::IsPistolEmpty( void ) const +{ + CWeaponCSBase *weapon = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_PISTOL ) ); + + if (weapon == NULL) + return true; + + // check if gun has any ammo left + if (weapon->HasAnyAmmo()) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip the given item + */ +bool CCSBot::DoEquip( CWeaponCSBase *weapon ) +{ + if (weapon == NULL) + return false; + + // check if weapon has any ammo left + if (!weapon->HasAnyAmmo()) + return false; + + // equip it + SelectItem( weapon->GetClassname() ); + m_equipTimer.Start(); + + return true; +} + + +// throttle how often equipping is allowed +const float minEquipInterval = 5.0f; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip the best weapon we are carrying that has ammo + */ +void CCSBot::EquipBestWeapon( bool mustEquip ) +{ + // throttle how often equipping is allowed + if (!mustEquip && m_equipTimer.GetElapsedTime() < minEquipInterval) + return; + + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheBots ); + + CWeaponCSBase *primary = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + if (primary) + { + CSWeaponType weaponClass = primary->GetCSWpnData().m_WeaponType; + + if ((ctrl->AllowShotguns() && weaponClass == WEAPONTYPE_SHOTGUN) || + (ctrl->AllowMachineGuns() && weaponClass == WEAPONTYPE_MACHINEGUN) || + (ctrl->AllowRifles() && weaponClass == WEAPONTYPE_RIFLE) || + (ctrl->AllowShotguns() && weaponClass == WEAPONTYPE_SHOTGUN) || + (ctrl->AllowSnipers() && weaponClass == WEAPONTYPE_SNIPER_RIFLE) || + (ctrl->AllowSubMachineGuns() && weaponClass == WEAPONTYPE_SUBMACHINEGUN)) + { + if (DoEquip( primary )) + return; + } + } + + if (ctrl->AllowPistols()) + { + if (DoEquip( static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_PISTOL ) ) )) + return; + } + + // always have a knife + EquipKnife(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip our pistol + */ +void CCSBot::EquipPistol( void ) +{ + // throttle how often equipping is allowed + if (m_equipTimer.GetElapsedTime() < minEquipInterval) + return; + + if (TheCSBots()->AllowPistols() && !IsUsingPistol()) + { + CWeaponCSBase *pistol = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_PISTOL ) ); + DoEquip( pistol ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip the knife + */ +void CCSBot::EquipKnife( void ) +{ + if (!IsUsingKnife()) + { + SelectItem( "weapon_knife" ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we have a grenade in our inventory + */ +bool CCSBot::HasGrenade( void ) const +{ + CWeaponCSBase *grenade = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_GRENADES ) ); + return (grenade) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip a grenade, return false if we cant + */ +bool CCSBot::EquipGrenade( bool noSmoke ) +{ + // snipers don't use grenades + if (IsSniper()) + return false; + + if (IsUsingGrenade()) + return true; + + if (HasGrenade()) + { + CWeaponCSBase *grenade = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_GRENADES ) ); + + if (noSmoke && grenade->IsA( WEAPON_SMOKEGRENADE )) + return false; + + SelectItem( grenade->GetClassname() ); + + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we have knife equipped + */ +bool CCSBot::IsUsingKnife( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && weapon->IsA( WEAPON_KNIFE )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we have pistol equipped + */ +bool CCSBot::IsUsingPistol( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && weapon->IsPistol()) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we have a grenade equipped + */ +bool CCSBot::IsUsingGrenade( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (!weapon) + return false; + + if (weapon->IsA( WEAPON_FLASHBANG ) || + weapon->IsA( WEAPON_SMOKEGRENADE ) || + weapon->IsA( WEAPON_HEGRENADE )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin the process of throwing the grenade + */ +void CCSBot::ThrowGrenade( const Vector &target ) +{ + if (IsUsingGrenade() && m_grenadeTossState == NOT_THROWING && !IsOnLadder()) + { + m_grenadeTossState = START_THROW; + m_tossGrenadeTimer.Start( 2.0f ); + + const float angleTolerance = 3.0f; + SetLookAt( "GrenadeThrow", target, PRIORITY_UNINTERRUPTABLE, 4.0f, false, angleTolerance ); + + Wait( RandomFloat( 2.0f, 4.0f ) ); + + if (cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + NDebugOverlay::Cross3D( target, 25.0f, 255, 125, 0, true, 3.0f ); + } + + PrintIfWatched( "%3.2f: Grenade: START_THROW\n", gpGlobals->curtime ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if our weapon can attack + */ +bool CCSBot::CanActiveWeaponFire( void ) const +{ + return ( GetActiveWeapon() && GetActiveWeapon()->m_flNextPrimaryAttack <= gpGlobals->curtime ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find spot to throw grenade ahead of us and "around the corner" along our path + */ +bool CCSBot::FindGrenadeTossPathTarget( Vector *pos ) +{ + if (!HasPath()) + return false; + + // find farthest point we can see on the path + int i; + for( i=m_pathIndex; i<m_pathLength; ++i ) + { + if (!FVisible( m_path[i].pos + Vector( 0, 0, HalfHumanHeight ) )) + break; + } + + if (i == m_pathIndex) + return false; + + // find exact spot where we lose sight + Vector dir = m_path[i].pos - m_path[i-1].pos; + float length = dir.NormalizeInPlace(); + + const float inc = 25.0f; + Vector p; + Vector visibleSpot = m_path[i-1].pos; + for( float t = 0.0f; t<length; t += inc ) + { + p = m_path[i-1].pos + t * dir; + p.z += HalfHumanHeight; + if (!FVisible( p )) + break; + + visibleSpot = p; + } + + // massage the location a bit + visibleSpot.z += 10.0f; + + const float bufferRange = 50.0f; + + trace_t result; + Vector check; + + // check +X + check = visibleSpot + Vector( 999.9f, 0, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = result.endpos.x - visibleSpot.x; + if (range < bufferRange) + { + visibleSpot.x = result.endpos.x - bufferRange; + } + } + + // check -X + check = visibleSpot + Vector( -999.9f, 0, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = visibleSpot.x - result.endpos.x; + if (range < bufferRange) + { + visibleSpot.x = result.endpos.x + bufferRange; + } + } + + // check +Y + check = visibleSpot + Vector( 0, 999.9f, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = result.endpos.y - visibleSpot.y; + if (range < bufferRange) + { + visibleSpot.y = result.endpos.y - bufferRange; + } + } + + // check -Y + check = visibleSpot + Vector( 0, -999.9f, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = visibleSpot.y - result.endpos.y; + if (range < bufferRange) + { + visibleSpot.y = result.endpos.y + bufferRange; + } + } + + *pos = visibleSpot; + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Look for grenade throw targets and throw the grenade + */ +void CCSBot::LookForGrenadeTargets( void ) +{ + if (!IsUsingGrenade() || IsThrowingGrenade()) + { + return; + } + + const CNavArea *tossArea = GetInitialEncounterArea(); + if (tossArea == NULL) + { + return; + } + + int enemyTeam = OtherTeam( GetTeamNumber() ); + + // check if we should put our grenade away + if (tossArea->GetEarliestOccupyTime( enemyTeam ) > gpGlobals->curtime) + { + EquipBestWeapon( MUST_EQUIP ); + return; + } + + // throw grenades at initial encounter area + Vector tossTarget = Vector( 0, 0, 0 ); + if (!tossArea->IsVisible( EyePosition(), &tossTarget )) + { + return; + } + + + CWeaponCSBase *weapon = GetActiveCSWeapon(); + if (weapon && weapon->IsA( WEAPON_SMOKEGRENADE )) + { + // don't worry so much about smokes + ThrowGrenade( tossTarget ); + PrintIfWatched( "Throwing smoke grenade!" ); + SetInitialEncounterArea( NULL ); + return; + } + else // explosive and flashbang grenades + { + // initial encounter area is visible, wait to throw until timing is right + + const float leadTime = 1.5f; + float enemyTime = tossArea->GetEarliestOccupyTime( enemyTeam ); + if (enemyTime - TheCSBots()->GetElapsedRoundTime() > leadTime) + { + // don't throw yet + return; + } + + + Vector to = tossTarget - EyePosition(); + float range = to.Length(); + + const float slope = 0.2f; // 0.25f; + float tossHeight = slope * range; + + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( this, COLLISION_GROUP_NONE ); + + const float heightInc = tossHeight / 10.0f; + Vector target; + float safeSpace = tossHeight / 2.0f; + + // Build a box to sweep along the ray when looking for obstacles + const Vector& eyePosition = EyePosition(); + Vector mins = VEC_HULL_MIN; + Vector maxs = VEC_HULL_MAX; + mins.z = 0; + maxs.z = heightInc; + + + // find low and high bounds of toss window + float low = 0.0f; + float high = tossHeight + safeSpace; + bool gotLow = false; + float lastH = 0.0f; + for( float h = 0.0f; h < 3.0f * tossHeight; h += heightInc ) + { + target = tossTarget + Vector( 0, 0, h ); + + // make sure toss line is clear + + QAngle angles( 0, 0, 0 ); + Ray_t ray; + ray.Init( eyePosition, target, mins, maxs ); + enginetrace->TraceRay( ray, MASK_VISIBLE_AND_NPCS | CONTENTS_GRATE, &traceFilter, &result ); + if (result.fraction == 1.0f) + { + //NDebugOverlay::SweptBox( eyePosition, target, mins, maxs, angles, 0, 0, 255, 40, 10.0f ); + + // line is clear + if (!gotLow) + { + low = h; + gotLow = true; + } + } + else + { + //NDebugOverlay::SweptBox( eyePosition, target, mins, maxs, angles, 255, 0, 0, 5, 10.0f ); + + // line is blocked + if (gotLow) + { + high = lastH; + break; + } + } + + lastH = h; + } + + if (gotLow) + { + // throw grenade into toss window + if (tossHeight < low) + { + if (low + safeSpace > high) + { + // narrow window + tossHeight = (high + low)/2.0f; + } + else + { + tossHeight = low + safeSpace; + } + } + else if (tossHeight > high - safeSpace) + { + if (high - safeSpace < low) + { + // narrow window + tossHeight = (high + low)/2.0f; + } + else + { + tossHeight = high - safeSpace; + } + } + + ThrowGrenade( tossTarget + Vector( 0, 0, tossHeight ) ); + SetInitialEncounterArea( NULL ); + return; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +class FOVClearOfFriends +{ +public: + FOVClearOfFriends( CCSBot *me ) + { + m_me = me; + } + + bool operator() ( CBasePlayer *player ) + { + if (player == m_me || !player->IsAlive()) + return true; + + if (m_me->InSameTeam( player )) + { + Vector to = player->EyePosition() - m_me->EyePosition(); + to.NormalizeInPlace(); + + Vector forward; + m_me->EyeVectors( &forward ); + + if (DotProduct( to, forward ) > 0.95f) + { + if (m_me->IsVisible( (CCSPlayer *)player )) + { + // we see a friend in our FOV + return false; + } + } + } + + return true; + } + + CCSBot *m_me; +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Process the grenade throw state machine + */ +void CCSBot::UpdateGrenadeThrow( void ) +{ + switch( m_grenadeTossState ) + { + case START_THROW: + { + if (m_tossGrenadeTimer.IsElapsed()) + { + // something prevented the throw - give up + EquipBestWeapon( MUST_EQUIP ); + ClearLookAt(); + m_grenadeTossState = NOT_THROWING; + PrintIfWatched( "%3.2f: Grenade: THROW FAILED\n", gpGlobals->curtime ); + return; + } + + if (m_lookAtSpotState == LOOK_AT_SPOT) + { + // don't throw if there are friends ahead of us + FOVClearOfFriends fovClear( this ); + if (ForEachPlayer( fovClear )) + { + m_grenadeTossState = FINISH_THROW; + m_tossGrenadeTimer.Start( 1.0f ); + PrintIfWatched( "%3.2f: Grenade: FINISH_THROW\n", gpGlobals->curtime ); + } + else + { + PrintIfWatched( "%3.2f: Grenade: Friend is in the way...\n", gpGlobals->curtime ); + } + } + + // hold in the trigger and be ready to throw + PrimaryAttack(); + + break; + } + + case FINISH_THROW: + { + // throw the grenade and hold our aiming line for a moment + if (m_tossGrenadeTimer.IsElapsed()) + { + ClearLookAt(); + + m_grenadeTossState = NOT_THROWING; + PrintIfWatched( "%3.2f: Grenade: THROW COMPLETE\n", gpGlobals->curtime ); + } + break; + } + + default: + { + if (IsUsingGrenade()) + { + // pull the pin + PrimaryAttack(); + } + break; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +class GrenadeResponse +{ +public: + GrenadeResponse( CCSBot *me ) + { + m_me = me; + } + + bool operator() ( ActiveGrenade *ag ) const + { + const float retreatRange = 300.0f; + const float hideTime = 1.0f; + + // do we see this grenade + if (m_me->IsVisible( ag->GetPosition(), CHECK_FOV, (CBaseEntity *)ag->GetEntity() )) + { + // we see it + if (ag->IsSmoke()) + { + // ignore smokes + return true; + } + + Vector velDir = ag->GetEntity()->GetAbsVelocity(); + float grenadeSpeed = velDir.NormalizeInPlace(); + const float atRestSpeed = 50.0f; + + const float aboutToBlow = 0.5f; + if (ag->IsFlashbang() && ag->GetEntity()->m_flDetonateTime - gpGlobals->curtime < aboutToBlow) + { + // turn away from flashbangs about to explode + QAngle eyeAngles = m_me->EyeAngles(); + + float yaw = RandomFloat( 100.0f, 135.0f ); + eyeAngles.y += (RandomFloat( -1.0f, 1.0f ) < 0.0f) ? (-yaw) : yaw; + + Vector forward; + AngleVectors( eyeAngles, &forward ); + + Vector away = m_me->EyePosition() - 1000.0f * forward; + const float duration = 2.0f; + + m_me->ClearLookAt(); + m_me->SetLookAt( "Avoid Flashbang", away, PRIORITY_UNINTERRUPTABLE, duration ); + + m_me->StopAiming(); + + return false; + } + + + // flee from grenades if close by or thrown towards us + const float throwDangerRange = 750.0f; + const float nearDangerRange = 300.0f; + Vector to = ag->GetPosition() - m_me->GetAbsOrigin(); + float range = to.NormalizeInPlace(); + if (range > throwDangerRange) + { + return true; + } + + if (grenadeSpeed > atRestSpeed) + { + // grenade is moving + if (DotProduct( to, velDir ) >= -0.5f) + { + // going away from us + return true; + } + + m_me->PrintIfWatched( "Retreating from a grenade thrown towards me!\n" ); + } + else if (range < nearDangerRange) + { + // grenade has come to rest near us + m_me->PrintIfWatched( "Retreating from a grenade that landed near me!\n" ); + } + + // retreat! + m_me->TryToRetreat( retreatRange, hideTime ); + + return false; + } + + return true; + } + + CCSBot *m_me; +}; + +/** + * React to enemy grenades we see + */ +void CCSBot::AvoidEnemyGrenades( void ) +{ + // low skill bots dont avoid grenades + if (GetProfile()->GetSkill() < 0.5) + { + return; + } + + if (IsAvoidingGrenade()) + { + // already avoiding one + return; + } + + // low skill bots don't avoid grenades + if (GetProfile()->GetSkill() < 0.6f) + { + return; + } + + GrenadeResponse respond( this ); + if (TheBots->ForEachGrenade( respond ) == false) + { + const float avoidTime = 4.0f; + m_isAvoidingGrenade.Start( avoidTime ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reload our weapon if we must + */ +void CCSBot::ReloadCheck( void ) +{ + const float safeReloadWaitTime = 3.0f; + const float reloadAmmoRatio = 0.6f; + + // don't bother to reload if there are no enemies left + if (GetEnemiesRemaining() == 0) + return; + + if (IsDefusingBomb() || IsReloading()) + return; + + if (IsActiveWeaponClipEmpty()) + { + // high-skill players switch to pistol instead of reloading during combat + if (GetProfile()->GetSkill() > 0.5f && IsAttacking()) + { + if (!GetActiveCSWeapon()->IsPistol() && !IsPistolEmpty()) + { + // switch to pistol instead of reloading + EquipPistol(); + return; + } + } + } + else if (GetTimeSinceLastSawEnemy() > safeReloadWaitTime && GetActiveWeaponAmmoRatio() <= reloadAmmoRatio) + { + // high-skill players use all their ammo and switch to pistol instead of reloading during combat + if (GetProfile()->GetSkill() > 0.5f && IsAttacking()) + return; + } + else + { + // do not need to reload + return; + } + + // don't reload the AWP until it is totally out of ammo + if (IsUsing( WEAPON_AWP ) && !IsActiveWeaponClipEmpty()) + return; + + Reload(); + + // move to cover to reload if there are enemies nearby + if (GetNearbyEnemyCount()) + { + // avoid enemies while reloading (above 0.75 skill always hide to reload) + const float hideChance = 25.0f + 100.0f * GetProfile()->GetSkill(); + + if (!IsHiding() && RandomFloat( 0, 100 ) < hideChance) + { + const float safeTime = 5.0f; + if (GetTimeSinceLastSawEnemy() < safeTime) + { + PrintIfWatched( "Retreating to a safe spot to reload!\n" ); + const Vector *spot = FindNearbyRetreatSpot( this, 1000.0f ); + if (spot) + { + // ignore enemies for a second to give us time to hide + // reaching our hiding spot clears our disposition + IgnoreEnemies( 10.0f ); + + Run(); + StandUp(); + Hide( *spot, 0.0f ); + } + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Silence/unsilence our weapon if we must + */ +void CCSBot::SilencerCheck( void ) +{ + const float safeSilencerWaitTime = 3.5f; // longer than reload check because reloading should take precedence + + if (IsDefusingBomb() || IsReloading() || IsAttacking()) + return; + + // M4A1 and USP are the only weapons with removable silencers + if (!DoesActiveWeaponHaveSilencer()) + return; + + if (GetTimeSinceLastSawEnemy() < safeSilencerWaitTime) + return; + + // don't touch the silencer if there are enemies nearby + if (GetNearbyEnemyCount() == 0) + { + CWeaponCSBase *weapon = GetActiveCSWeapon(); + if (weapon == NULL) + return; + + bool isSilencerOn = weapon->IsSilenced(); + + if ( weapon->m_flNextSecondaryAttack >= gpGlobals->curtime ) + return; + + // equip silencer if we want to and we don't have a shield. + if ( isSilencerOn != (GetProfile()->PrefersSilencer() || GetProfile()->GetSkill() > 0.7f) && !HasShield() ) + { + PrintIfWatched( "%s silencer!\n", (isSilencerOn) ? "Unequipping" : "Equipping" ); + weapon->SecondaryAttack(); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when in contact with a CBaseCombatWeapon + */ +bool CCSBot::BumpWeapon( CBaseCombatWeapon *pWeapon ) +{ + CWeaponCSBase *droppedGun = dynamic_cast< CWeaponCSBase* >( pWeapon ); + + // right now we only care about primary weapons on the ground + if ( droppedGun && droppedGun->GetSlot() == WEAPON_SLOT_RIFLE ) + { + CWeaponCSBase *myGun = dynamic_cast< CWeaponCSBase* >( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + + // if the gun on the ground is the same one we have, dont bother + if ( myGun && droppedGun->GetWeaponID() != myGun->GetWeaponID() ) + { + // if we don't have a weapon preference, give up + if ( GetProfile()->HasPrimaryPreference() ) + { + // don't change weapons if we've seen enemies recently + const float safeTime = 2.5f; + if ( GetTimeSinceLastSawEnemy() >= safeTime ) + { + // we have a primary weapon - drop it if the one on the ground is better + for( int i = 0; i < GetProfile()->GetWeaponPreferenceCount(); ++i ) + { + CSWeaponID prefID = GetProfile()->GetWeaponPreference( i ); + + if (!IsPrimaryWeapon( prefID )) + continue; + + // if the gun we are using is more desirable, give up + if ( prefID == myGun->GetWeaponID() ) + break; + + if ( prefID == droppedGun->GetWeaponID() ) + { + // the gun on the ground is better than the one we have - drop our gun + DropRifle(); + break; + } + } + } + } + } + } + + return BaseClass::BumpWeapon( droppedGun ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if a friend is in our weapon's way + * @todo Check more rays for safety. + */ +bool CCSBot::IsFriendInLineOfFire( void ) +{ + // compute the unit vector along our view + Vector aimDir = GetViewVector(); + + // trace the bullet's path + trace_t result; + UTIL_TraceLine( EyePosition(), EyePosition() + 10000.0f * aimDir, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.DidHitNonWorldEntity()) + { + CBaseEntity *victim = result.m_pEnt; + + if (victim && victim->IsPlayer() && victim->IsAlive()) + { + CBasePlayer *player = static_cast<CBasePlayer *>( victim ); + + if (player->InSameTeam( this )) + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return line-of-sight distance to obstacle along weapon fire ray + * @todo Re-use this computation with IsFriendInLineOfFire() + */ +float CCSBot::ComputeWeaponSightRange( void ) +{ + // compute the unit vector along our view + Vector aimDir = GetViewVector(); + + // trace the bullet's path + trace_t result; + UTIL_TraceLine( EyePosition(), EyePosition() + 10000.0f * aimDir, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + return (EyePosition() - result.endpos).Length(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the given player just fired their weapon + */ +bool CCSBot::DidPlayerJustFireWeapon( const CCSPlayer *player ) const +{ + // if this player has just fired his weapon, we notice him + CWeaponCSBase *weapon = player->GetActiveCSWeapon(); + return (weapon && !weapon->IsSilenced() && weapon->m_flNextPrimaryAttack > gpGlobals->curtime); +} + diff --git a/game/server/cstrike/bot/cs_bot_weapon_id.cpp b/game/server/cstrike/bot/cs_bot_weapon_id.cpp new file mode 100644 index 0000000..21c423e --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_weapon_id.cpp @@ -0,0 +1,22 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael Booth ([email protected]), 2003 +// Author: Matthew D. Campbell ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------- +// +// Temporary solution until we have time to build something more elegant +// Very nasty - need to keep in sync with the buy aliases +// NOTE: Array must be NULL terminated +// diff --git a/game/server/cstrike/bot/cs_gamestate.cpp b/game/server/cstrike/bot/cs_gamestate.cpp new file mode 100644 index 0000000..61f537b --- /dev/null +++ b/game/server/cstrike/bot/cs_gamestate.cpp @@ -0,0 +1,767 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Encapsulation of the current scenario/game state. Allows each bot imperfect knowledge. +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "KeyValues.h" + +#include "cs_bot.h" +#include "cs_gamestate.h" +#include "cs_simple_hostage.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +CSGameState::CSGameState( CCSBot *owner ) +{ + m_owner = owner; + m_isRoundOver = false; + + m_bombState = MOVING; + m_lastSawBomber.Invalidate(); + m_lastSawLooseBomb.Invalidate(); + m_isPlantedBombPosKnown = false; + m_plantedBombsite = UNKNOWN; + + m_bombsiteCount = 0; + m_bombsiteSearchIndex = 0; + + for( int i=0; i<MAX_HOSTAGES; ++i ) + { + m_hostage[i].hostage = NULL; + m_hostage[i].isValid = false; + m_hostage[i].isAlive = false; + m_hostage[i].isFree = true; + m_hostage[i].knownPos = Vector( 0, 0, 0 ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset at round start + */ +void CSGameState::Reset( void ) +{ + m_isRoundOver = false; + + // bomb ----------------------------------------------------------------------- + m_bombState = MOVING; + m_lastSawBomber.Invalidate(); + m_lastSawLooseBomb.Invalidate(); + m_isPlantedBombPosKnown = false; + m_plantedBombsite = UNKNOWN; + + m_bombsiteCount = TheCSBots()->GetZoneCount(); + + int i; + for( i=0; i<m_bombsiteCount; ++i ) + { + m_isBombsiteClear[i] = false; + m_bombsiteSearchOrder[i] = i; + } + + // shuffle the bombsite search order + // allows T's to plant at random site, and TEAM_CT's to search in a random order + // NOTE: VS6 std::random_shuffle() doesn't work well with an array of two elements (most maps) + for( i=0; i < m_bombsiteCount; ++i ) + { + int swap = m_bombsiteSearchOrder[i]; + int rnd = RandomInt( i, m_bombsiteCount-1 ); + m_bombsiteSearchOrder[i] = m_bombsiteSearchOrder[ rnd ]; + m_bombsiteSearchOrder[ rnd ] = swap; + } + + m_bombsiteSearchIndex = 0; + + // hostage --------------------------------------------------------------------- + InitializeHostageInfo(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnHostageRescuedAll( IGameEvent *event ) +{ + m_allHostagesRescued = true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnRoundEnd( IGameEvent *event ) +{ + m_isRoundOver = true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnRoundStart( IGameEvent *event ) +{ + Reset(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnBombPlanted( IGameEvent *event ) +{ + // change state - the event is announced to everyone + SetBombState( PLANTED ); + + CBasePlayer *plantingPlayer = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + + // Terrorists always know where the bomb is + if (m_owner->GetTeamNumber() == TEAM_TERRORIST && plantingPlayer) + { + UpdatePlantedBomb( plantingPlayer->GetAbsOrigin() ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnBombDefused( IGameEvent *event ) +{ + // change state - the event is announced to everyone + SetBombState( DEFUSED ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnBombExploded( IGameEvent *event ) +{ + // change state - the event is announced to everyone + SetBombState( EXPLODED ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * True if round has been won or lost (but not yet reset) + */ +bool CSGameState::IsRoundOver( void ) const +{ + return m_isRoundOver; +} + +//-------------------------------------------------------------------------------------------------------------- +void CSGameState::SetBombState( BombState state ) +{ + // if state changed, reset "last seen" timestamps + if (m_bombState != state) + { + m_bombState = state; + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CSGameState::UpdateLooseBomb( const Vector &pos ) +{ + m_looseBombPos = pos; + m_lastSawLooseBomb.Reset(); + + // we saw the loose bomb, update our state + SetBombState( LOOSE ); +} + +//-------------------------------------------------------------------------------------------------------------- +float CSGameState::TimeSinceLastSawLooseBomb( void ) const +{ + return m_lastSawLooseBomb.GetElapsedTime(); +} + +//-------------------------------------------------------------------------------------------------------------- +bool CSGameState::IsLooseBombLocationKnown( void ) const +{ + if (m_bombState != LOOSE) + return false; + + return (m_lastSawLooseBomb.HasStarted()) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +void CSGameState::UpdateBomber( const Vector &pos ) +{ + m_bomberPos = pos; + m_lastSawBomber.Reset(); + + // we saw the bomber, update our state + SetBombState( MOVING ); +} + +//-------------------------------------------------------------------------------------------------------------- +float CSGameState::TimeSinceLastSawBomber( void ) const +{ + return m_lastSawBomber.GetElapsedTime(); +} + +//-------------------------------------------------------------------------------------------------------------- +bool CSGameState::IsPlantedBombLocationKnown( void ) const +{ + if (m_bombState != PLANTED) + return false; + + return m_isPlantedBombPosKnown; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the zone index of the planted bombsite, or UNKNOWN + */ +int CSGameState::GetPlantedBombsite( void ) const +{ + if (m_bombState != PLANTED) + return UNKNOWN; + + return m_plantedBombsite; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are currently in the bombsite where the bomb is planted + */ +bool CSGameState::IsAtPlantedBombsite( void ) const +{ + if (m_bombState != PLANTED) + return false; + + Vector myOrigin = GetCentroid( m_owner ); + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( myOrigin ); + + if (zone) + { + return (m_plantedBombsite == zone->m_index); + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the zone index of the next bombsite to search + */ +int CSGameState::GetNextBombsiteToSearch( void ) +{ + if (m_bombsiteCount <= 0) + return 0; + + int i; + + // return next non-cleared bombsite index + for( i=m_bombsiteSearchIndex; i<m_bombsiteCount; ++i ) + { + int z = m_bombsiteSearchOrder[i]; + if (!m_isBombsiteClear[z]) + { + m_bombsiteSearchIndex = i; + return z; + } + } + + // all the bombsites are clear, someone must have been mistaken - start search over + for( i=0; i<m_bombsiteCount; ++i ) + m_isBombsiteClear[i] = false; + m_bombsiteSearchIndex = 0; + + return GetNextBombsiteToSearch(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns position of bomb in its various states (moving, loose, planted), + * or NULL if we don't know where the bomb is + */ +const Vector *CSGameState::GetBombPosition( void ) const +{ + switch( m_bombState ) + { + case MOVING: + { + if (!m_lastSawBomber.HasStarted()) + return NULL; + + return &m_bomberPos; + } + + case LOOSE: + { + if (IsLooseBombLocationKnown()) + return &m_looseBombPos; + + return NULL; + } + + case PLANTED: + { + if (IsPlantedBombLocationKnown()) + return &m_plantedBombPos; + + return NULL; + } + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * We see the planted bomb at 'pos' + */ +void CSGameState::UpdatePlantedBomb( const Vector &pos ) +{ + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( pos ); + + if (zone == NULL) + { + CONSOLE_ECHO( "ERROR: Bomb planted outside of a zone!\n" ); + m_plantedBombsite = UNKNOWN; + } + else + { + m_plantedBombsite = zone->m_index; + } + + m_plantedBombPos = pos; + m_isPlantedBombPosKnown = true; + SetBombState( PLANTED ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Someone told us where the bomb is planted + */ +void CSGameState::MarkBombsiteAsPlanted( int zoneIndex ) +{ + m_plantedBombsite = zoneIndex; + SetBombState( PLANTED ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Someone told us a bombsite is clear + */ +void CSGameState::ClearBombsite( int zoneIndex ) +{ + if (zoneIndex >= 0 && zoneIndex < m_bombsiteCount) + m_isBombsiteClear[ zoneIndex ] = true; +} + +//-------------------------------------------------------------------------------------------------------------- +bool CSGameState::IsBombsiteClear( int zoneIndex ) const +{ + if (zoneIndex >= 0 && zoneIndex < m_bombsiteCount) + return m_isBombsiteClear[ zoneIndex ]; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Initialize our knowledge of the number and location of hostages + */ +void CSGameState::InitializeHostageInfo( void ) +{ + m_hostageCount = 0; + m_allHostagesRescued = false; + m_haveSomeHostagesBeenTaken = false; + + for( int i=0; i<g_Hostages.Count(); ++i ) + { + m_hostage[ m_hostageCount ].hostage = g_Hostages[i]; + m_hostage[ m_hostageCount ].knownPos = g_Hostages[i]->GetAbsOrigin(); + m_hostage[ m_hostageCount ].isValid = true; + m_hostage[ m_hostageCount ].isAlive = true; + m_hostage[ m_hostageCount ].isFree = true; + ++m_hostageCount; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest free and live hostage + * If we are a CT this information is perfect. + * Otherwise, this is based on our individual memory of the game state. + * If NULL is returned, we don't think there are any hostages left, or we dont know where they are. + * NOTE: a T can remember a hostage who has died. knowPos will be filled in, but NULL will be + * returned, since CHostages get deleted when they die. + */ +CHostage *CSGameState::GetNearestFreeHostage( Vector *knowPos ) const +{ + if (m_owner == NULL) + return NULL; + + CNavArea *startArea = m_owner->GetLastKnownArea(); + if (startArea == NULL) + return NULL; + + CHostage *close = NULL; + Vector closePos( 0, 0, 0 ); + float closeDistance = 9999999999.9f; + + for( int i=0; i<m_hostageCount; ++i ) + { + CHostage *hostage = m_hostage[i].hostage; + Vector hostagePos; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // we know exactly where the hostages are, and if they are alive + if (!m_hostage[i].hostage || !m_hostage[i].hostage->IsValid()) + continue; + + if (m_hostage[i].hostage->IsFollowingSomeone()) + continue; + + hostagePos = m_hostage[i].hostage->GetAbsOrigin(); + } + else + { + // use our memory of where we think the hostages are + if (m_hostage[i].isValid == false) + continue; + + hostagePos = m_hostage[i].knownPos; + } + + CNavArea *hostageArea = TheNavMesh->GetNearestNavArea( hostagePos ); + if (hostageArea) + { + ShortestPathCost cost; + float travelDistance = NavAreaTravelDistance( startArea, hostageArea, cost ); + + if (travelDistance >= 0.0f && travelDistance < closeDistance) + { + closeDistance = travelDistance; + closePos = hostagePos; + close = hostage; + } + } + } + + // return where we think the hostage is + if (knowPos && close) + *knowPos = closePos; + + return close; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the location of a "free" hostage, or NULL if we dont know of any + */ +const Vector *CSGameState::GetRandomFreeHostagePosition( void ) const +{ + if (m_owner == NULL) + return NULL; + + static Vector freePos[ MAX_HOSTAGES ]; + int freeCount = 0; + + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // we know exactly where the hostages are, and if they are alive + if (!info->hostage || !info->hostage->IsAlive()) + continue; + + // escorted hostages are not "free" + if (info->hostage->IsFollowingSomeone()) + continue; + + freePos[ freeCount++ ] = info->hostage->GetAbsOrigin(); + } + else + { + // use our memory of where we think the hostages are + if (info->isValid == false) + continue; + + freePos[ freeCount++ ] = info->knownPos; + } + } + + if (freeCount) + { + return &freePos[ RandomInt( 0, freeCount-1 ) ]; + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * If we can see any of the positions where we think a hostage is, validate it + * Return status of any changes (a hostage died or was moved) + */ +unsigned char CSGameState::ValidateHostagePositions( void ) +{ + // limit how often we validate + if (!m_validateInterval.IsElapsed()) + return NO_CHANGE; + + const float validateInterval = 0.5f; + m_validateInterval.Start( validateInterval ); + + + // check the status of hostages + unsigned char status = NO_CHANGE; + + int i; + int startValidCount = 0; + for( i=0; i<m_hostageCount; ++i ) + if (m_hostage[i].isValid) + ++startValidCount; + + for( i=0; i<m_hostageCount; ++i ) + { + HostageInfo *info = &m_hostage[i]; + + if (!info->hostage ) + continue; + + // if we can see a hostage, update our knowledge of it + Vector pos = info->hostage->GetAbsOrigin() + Vector( 0, 0, HalfHumanHeight ); + if (m_owner->IsVisible( pos, CHECK_FOV )) + { + if (info->hostage->IsAlive()) + { + // live hostage + + // if hostage is being escorted by a CT, we don't "see" it, we see the CT + if (info->hostage->IsFollowingSomeone()) + { + info->isValid = false; + } + else + { + info->knownPos = info->hostage->GetAbsOrigin(); + info->isValid = true; + } + } + else + { + // dead hostage + + // if we thought it was alive, this is news to us + if (info->isAlive) + status |= HOSTAGE_DIED; + + info->isAlive = false; + info->isValid = false; + } + + continue; + } + + // if we dont know where this hostage is, nothing to validate + if (!info->isValid) + continue; + + // can't directly see this hostage + // check line of sight to where we think this hostage is, to see if we noticed that is has moved + pos = info->knownPos + Vector( 0, 0, HalfHumanHeight ); + if (m_owner->IsVisible( pos, CHECK_FOV )) + { + // we can see where we thought the hostage was - verify it is still there and alive + + if (!info->hostage->IsValid()) + { + // since we have line of sight to an invalid hostage, it must be dead + // discovered that hostage has been killed + status |= HOSTAGE_DIED; + info->isAlive = false; + info->isValid = false; + continue; + } + + if (info->hostage->IsFollowingSomeone()) + { + // discovered the hostage has been taken + status |= HOSTAGE_GONE; + info->isValid = false; + continue; + } + + const float tolerance = 50.0f; + if ((info->hostage->GetAbsOrigin() - info->knownPos).IsLengthGreaterThan( tolerance )) + { + // discovered that hostage has been moved + status |= HOSTAGE_GONE; + info->isValid = false; + continue; + } + } + } + + int endValidCount = 0; + for( i=0; i<m_hostageCount; ++i ) + if (m_hostage[i].isValid) + ++endValidCount; + + if (endValidCount == 0 && startValidCount > 0) + { + // we discovered all the hostages are gone + status &= ~HOSTAGE_GONE; + status |= HOSTAGES_ALL_GONE; + } + + return status; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the nearest visible free hostage + * Since we can actually see any hostage we return, we know its actual position + */ +CHostage *CSGameState::GetNearestVisibleFreeHostage( void ) const +{ + CHostage *close = NULL; + float closeRangeSq = 999999999.9f; + float rangeSq; + + Vector pos; + Vector myOrigin = GetCentroid( m_owner ); + + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if ( !info->hostage ) + continue; + + // if the hostage is dead or rescued, its not free + if (!info->hostage->IsAlive()) + continue; + + // if this hostage is following someone, its not free + if (info->hostage->IsFollowingSomeone()) + continue; + + /// @todo Use travel distance here + pos = info->hostage->GetAbsOrigin(); + rangeSq = (pos - myOrigin).LengthSqr(); + + if (rangeSq < closeRangeSq) + { + if (!m_owner->IsVisible( pos )) + continue; + + close = info->hostage; + closeRangeSq = rangeSq; + } + } + + return close; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if there are no free hostages + */ +bool CSGameState::AreAllHostagesBeingRescued( void ) const +{ + // if the hostages have all been rescued, they are not being rescued any longer + if (m_allHostagesRescued) + return false; + + bool isAllDead = true; + + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // CT's have perfect knowledge via their radar + if (info->hostage && info->hostage->IsValid()) + { + if (!info->hostage->IsFollowingSomeone()) + return false; + + isAllDead = false; + } + } + else + { + if (info->isValid && info->isAlive) + return false; + + if (info->isAlive) + isAllDead = false; + } + } + + // if all of the remaining hostages are dead, they arent being rescued + if (isAllDead) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * All hostages have been rescued or are dead + */ +bool CSGameState::AreAllHostagesGone( void ) const +{ + if (m_allHostagesRescued) + return true; + + // do we know that all the hostages are dead + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // CT's have perfect knowledge via their radar + if (info->hostage && info->hostage->IsAlive()) + return false; + } + else + { + if (info->isValid && info->isAlive) + return false; + } + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Someone told us all the hostages are gone + */ +void CSGameState::AllHostagesGone( void ) +{ + for( int i=0; i<m_hostageCount; ++i ) + m_hostage[i].isValid = false; +} + diff --git a/game/server/cstrike/bot/cs_gamestate.h b/game/server/cstrike/bot/cs_gamestate.h new file mode 100644 index 0000000..79bb8d2 --- /dev/null +++ b/game/server/cstrike/bot/cs_gamestate.h @@ -0,0 +1,151 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef _GAME_STATE_H_ +#define _GAME_STATE_H_ + + +#include "bot_util.h" + + +class CHostage; +class CCSBot; + +/** + * This class represents the game state as known by a particular bot + */ +class CSGameState +{ +public: + CSGameState( CCSBot *owner ); + + void Reset( void ); + + // Event handling + void OnHostageRescuedAll( IGameEvent *event ); + void OnRoundEnd( IGameEvent *event ); + void OnRoundStart( IGameEvent *event ); + void OnBombPlanted( IGameEvent *event ); + void OnBombDefused( IGameEvent *event ); + void OnBombExploded( IGameEvent *event ); + + bool IsRoundOver( void ) const; ///< true if round has been won or lost (but not yet reset) + + // bomb defuse scenario ----------------------------------------------------------------------------- + + enum BombState + { + MOVING, ///< being carried by a Terrorist + LOOSE, ///< loose on the ground somewhere + PLANTED, ///< planted and ticking + DEFUSED, ///< the bomb has been defused + EXPLODED ///< the bomb has exploded + }; + + bool IsBombMoving( void ) const { return (m_bombState == MOVING); } + bool IsBombLoose( void ) const { return (m_bombState == LOOSE); } + bool IsBombPlanted( void ) const { return (m_bombState == PLANTED); } + bool IsBombDefused( void ) const { return (m_bombState == DEFUSED); } + bool IsBombExploded( void ) const { return (m_bombState == EXPLODED); } + + void UpdateLooseBomb( const Vector &pos ); ///< we see the loose bomb + float TimeSinceLastSawLooseBomb( void ) const; ///< how long has is been since we saw the loose bomb + bool IsLooseBombLocationKnown( void ) const; ///< do we know where the loose bomb is + + void UpdateBomber( const Vector &pos ); ///< we see the bomber + float TimeSinceLastSawBomber( void ) const; ///< how long has is been since we saw the bomber + + void UpdatePlantedBomb( const Vector &pos ); ///< we see the planted bomb + bool IsPlantedBombLocationKnown( void ) const; ///< do we know where the bomb was planted + void MarkBombsiteAsPlanted( int zoneIndex ); ///< mark bombsite as the location of the planted bomb + + enum { UNKNOWN = -1 }; + int GetPlantedBombsite( void ) const; ///< return the zone index of the planted bombsite, or UNKNOWN + bool IsAtPlantedBombsite( void ) const; ///< return true if we are currently in the bombsite where the bomb is planted + + int GetNextBombsiteToSearch( void ); ///< return the zone index of the next bombsite to search + bool IsBombsiteClear( int zoneIndex ) const; ///< return true if given bombsite has been cleared + void ClearBombsite( int zoneIndex ); ///< mark bombsite as clear + + const Vector *GetBombPosition( void ) const; ///< return where we think the bomb is, or NULL if we don't know + + // hostage rescue scenario ------------------------------------------------------------------------ + CHostage *GetNearestFreeHostage( Vector *knowPos = NULL ) const; ///< return the closest free hostage, and where we think it is (knowPos) + const Vector *GetRandomFreeHostagePosition( void ) const; + bool AreAllHostagesBeingRescued( void ) const; ///< return true if there are no free hostages + bool AreAllHostagesGone( void ) const; ///< all hostages have been rescued or are dead + void AllHostagesGone( void ); ///< someone told us all the hostages are gone + bool HaveSomeHostagesBeenTaken( void ) const ///< return true if one or more hostages have been moved by the CT's + { + return m_haveSomeHostagesBeenTaken; + } + void HostageWasTaken( void ) ///< someone told us a CT is talking to a hostage + { + m_haveSomeHostagesBeenTaken = true; + } + + CHostage *GetNearestVisibleFreeHostage( void ) const; + + enum ValidateStatusType + { + NO_CHANGE = 0x00, + HOSTAGE_DIED = 0x01, + HOSTAGE_GONE = 0x02, + HOSTAGES_ALL_GONE = 0x04 + }; + unsigned char ValidateHostagePositions( void ); ///< update our knowledge with what we currently see - returns bitflag events + +private: + CCSBot *m_owner; ///< who owns this gamestate + + bool m_isRoundOver; ///< true if round is over, but no yet reset + + // bomb defuse scenario --------------------------------------------------------------------------- + void SetBombState( BombState state ); + BombState GetBombState( void ) const { return m_bombState; } + + BombState m_bombState; ///< what we think the bomb is doing + + IntervalTimer m_lastSawBomber; + Vector m_bomberPos; + + IntervalTimer m_lastSawLooseBomb; + Vector m_looseBombPos; + + bool m_isBombsiteClear[ CCSBotManager::MAX_ZONES ]; ///< corresponds to zone indices in CCSBotManager + int m_bombsiteSearchOrder[ CCSBotManager::MAX_ZONES ]; ///< randomized order of bombsites to search + int m_bombsiteCount; + int m_bombsiteSearchIndex; ///< the next step in the search + + int m_plantedBombsite; ///< zone index of the bombsite where the planted bomb is + + bool m_isPlantedBombPosKnown; ///< if true, we know the exact location of the bomb + Vector m_plantedBombPos; + + // hostage rescue scenario ------------------------------------------------------------------------ + struct HostageInfo + { + CHandle<CHostage> hostage; + Vector knownPos; + bool isValid; + bool isAlive; + bool isFree; ///< not being escorted by a CT + } + m_hostage[ MAX_HOSTAGES ]; + int m_hostageCount; ///< number of hostages left in map + CountdownTimer m_validateInterval; + + CBaseEntity *GetNearestHostage( void ) const; ///< return the closest live hostage + void InitializeHostageInfo( void ); ///< initialize our knowledge of the number and location of hostages + + bool m_allHostagesRescued; + bool m_haveSomeHostagesBeenTaken; ///< true if a hostage has been moved by a CT (and we've seen it) +}; + +#endif // _GAME_STATE_ diff --git a/game/server/cstrike/bot/states/cs_bot_attack.cpp b/game/server/cstrike/bot/states/cs_bot_attack.cpp new file mode 100644 index 0000000..1f44b6c --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_attack.cpp @@ -0,0 +1,710 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin attacking + */ +void AttackState::OnEnter( CCSBot *me ) +{ + CBasePlayer *enemy = me->GetBotEnemy(); + + // store our posture when the attack began + me->PushPostureContext(); + + me->DestroyPath(); + + // if we are using a knife, try to sneak up on the enemy + if (enemy && me->IsUsingKnife() && !me->IsPlayerFacingMe( enemy )) + me->Walk(); + else + me->Run(); + + me->GetOffLadder(); + me->ResetStuckMonitor(); + + m_repathTimer.Invalidate(); + m_haveSeenEnemy = me->IsEnemyVisible(); + m_nextDodgeStateTimestamp = 0.0f; + m_firstDodge = true; + m_isEnemyHidden = false; + m_reacquireTimestamp = 0.0f; + + m_pinnedDownTimestamp = gpGlobals->curtime + RandomFloat( 7.0f, 10.0f ); + + m_shieldToggleTimestamp = gpGlobals->curtime + RandomFloat( 2.0f, 10.0f ); + m_shieldForceOpen = false; + + // if we encountered someone while escaping, grab our weapon and fight! + if (me->IsEscapingFromBomb()) + me->EquipBestWeapon(); + + if (me->IsUsingKnife()) + { + // can't crouch and hold with a knife + m_crouchAndHold = false; + me->StandUp(); + } + else if (me->CanSeeSniper() && !me->IsSniper()) + { + // don't sit still if we see a sniper! + m_crouchAndHold = false; + me->StandUp(); + } + else + { + // decide whether to crouch where we are, or run and gun (if we havent already - see CCSBot::Attack()) + if (!m_crouchAndHold) + { + if (enemy) + { + const float crouchFarRange = 750.0f; + float crouchChance; + + // more likely to crouch if using sniper rifle or if enemy is far away + if (me->IsUsingSniperRifle()) + crouchChance = 50.0f; + else if ((GetCentroid( me ) - GetCentroid( enemy )).IsLengthGreaterThan( crouchFarRange )) + crouchChance = 50.0f; + else + crouchChance = 20.0f * (1.0f - me->GetProfile()->GetAggression()); + + if (RandomFloat( 0.0f, 100.0f ) < crouchChance) + { + // make sure we can still see if we crouch + trace_t result; + + Vector origin = GetCentroid( me ); + if (!me->IsCrouching()) + { + // we are standing, adjust for lower crouch origin + origin.z -= 20.0f; + } + + UTIL_TraceLine( origin, enemy->EyePosition(), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f) + { + m_crouchAndHold = true; + } + } + } + } + + if (m_crouchAndHold) + { + me->Crouch(); + me->PrintIfWatched( "Crouch and hold attack!\n" ); + } + } + + m_scopeTimestamp = 0; + m_didAmbushCheck = false; + + float skill = me->GetProfile()->GetSkill(); + + // tendency to dodge is proportional to skill + float dodgeChance = 80.0f * skill; + + // high skill bots always dodge if outnumbered, or they see a sniper + if (skill > 0.5f && (me->IsOutnumbered() || me->CanSeeSniper())) + { + dodgeChance = 100.0f; + } + + m_shouldDodge = (RandomFloat( 0, 100 ) <= dodgeChance); + + + // decide whether we might bail out of this fight + m_isCoward = (RandomFloat( 0, 100 ) > 100.0f * me->GetProfile()->GetAggression()); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * When we are done attacking, this is invoked + */ +void AttackState::StopAttacking( CCSBot *me ) +{ + if (me->GetTask() == CCSBot::SNIPING) + { + // stay in our hiding spot + me->Hide( me->GetLastKnownArea(), -1.0f, 50.0f ); + } + else + { + me->StopAttacking(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do dodge behavior + */ +void AttackState::Dodge( CCSBot *me ) +{ + // + // Dodge. + // If sniping or crouching, stand still. + // + if (m_shouldDodge && !me->IsUsingSniperRifle() && !m_crouchAndHold) + { + CBasePlayer *enemy = me->GetBotEnemy(); + if (enemy == NULL) + { + return; + } + + Vector toEnemy = enemy->GetAbsOrigin() - me->GetAbsOrigin(); + float range = toEnemy.Length(); + + const float hysterisRange = 125.0f; // (+/-) m_combatRange + + float minRange = me->GetCombatRange() - hysterisRange; + float maxRange = me->GetCombatRange() + hysterisRange; + + if (me->IsUsingKnife()) + { + // dodge when far away if armed only with a knife + maxRange = 999999.9f; + } + + // move towards (or away from) enemy if we are using a knife, behind a corner, or we aren't very skilled + if (me->GetProfile()->GetSkill() < 0.66f || !me->IsEnemyVisible()) + { + if (range > maxRange) + me->MoveForward(); + else if (range < minRange) + me->MoveBackward(); + } + + // don't dodge if enemy is facing away + const float dodgeRange = 2000.0f; + if (!me->CanSeeSniper() && (range > dodgeRange || !me->IsPlayerFacingMe( enemy ))) + { + m_dodgeState = STEADY_ON; + m_nextDodgeStateTimestamp = 0.0f; + } + else if (gpGlobals->curtime >= m_nextDodgeStateTimestamp) + { + int next; + + // high-skill bots keep moving and don't jump if they see a sniper + if (me->GetProfile()->GetSkill() > 0.5f && me->CanSeeSniper()) + { + // juke back and forth + if (m_firstDodge) + { + next = (RandomInt( 0, 100 ) < 50) ? SLIDE_RIGHT : SLIDE_LEFT; + } + else + { + next = (m_dodgeState == SLIDE_LEFT) ? SLIDE_RIGHT : SLIDE_LEFT; + } + } + else + { + // select next dodge state that is different that our current one + do + { + // low-skill bots may jump when first engaging the enemy (if they are moving) + const float jumpChance = 33.3f; + if (m_firstDodge && me->GetProfile()->GetSkill() < 0.5f && RandomFloat( 0, 100 ) < jumpChance && !me->IsNotMoving()) + next = RandomInt( 0, NUM_ATTACK_STATES-1 ); + else + next = RandomInt( 0, NUM_ATTACK_STATES-2 ); + } + while( !m_firstDodge && next == m_dodgeState ); + } + + m_dodgeState = (DodgeStateType)next; + m_nextDodgeStateTimestamp = gpGlobals->curtime + RandomFloat( 0.3f, 1.0f ); + m_firstDodge = false; + } + + + Vector forward, right; + me->EyeVectors( &forward, &right ); + + const float lookAheadRange = 30.0f; + float ground; + + switch( m_dodgeState ) + { + case STEADY_ON: + { + break; + } + + case SLIDE_LEFT: + { + // don't move left if we will fall + Vector pos = me->GetAbsOrigin() - (lookAheadRange * right); + + if (me->GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (me->GetAbsOrigin().z - ground < StepHeight) + { + me->StrafeLeft(); + } + } + break; + } + + case SLIDE_RIGHT: + { + // don't move left if we will fall + Vector pos = me->GetAbsOrigin() + (lookAheadRange * right); + + if (me->GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (me->GetAbsOrigin().z - ground < StepHeight) + { + me->StrafeRight(); + } + } + break; + } + + case JUMP: + { + if (me->m_isEnemyVisible) + { + me->Jump(); + } + break; + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Perform attack behavior + */ +void AttackState::OnUpdate( CCSBot *me ) +{ + // can't be stuck while attacking + me->ResetStuckMonitor(); + + // if we somehow ended up with the C4 or a grenade in our hands, grab our weapon! + CWeaponCSBase *weapon = me->GetActiveCSWeapon(); + if (weapon) + { + if (weapon->GetWeaponID() == WEAPON_C4 || + weapon->GetWeaponID() == WEAPON_HEGRENADE || + weapon->GetWeaponID() == WEAPON_FLASHBANG || + weapon->GetWeaponID() == WEAPON_SMOKEGRENADE) + { + me->EquipBestWeapon(); + } + } + + CBasePlayer *enemy = me->GetBotEnemy(); + if (enemy == NULL) + { + StopAttacking( me ); + return; + } + + Vector myOrigin = GetCentroid( me ); + Vector enemyOrigin = GetCentroid( enemy ); + + // keep track of whether we have seen our enemy at least once yet + if (!m_haveSeenEnemy) + m_haveSeenEnemy = me->IsEnemyVisible(); + + + // + // Retreat check + // Do not retreat if the enemy is too close + // + if (m_retreatTimer.IsElapsed()) + { + // If we've been fighting this battle for awhile, we're "pinned down" and + // need to do something else. + // If we are outnumbered, retreat. + // If we see a sniper and we aren't a sniper, retreat. + + bool isPinnedDown = (gpGlobals->curtime > m_pinnedDownTimestamp); + + if (isPinnedDown || + (me->CanSeeSniper() && !me->IsSniper()) || + (me->IsOutnumbered() && m_isCoward) || + (me->OutnumberedCount() >= 2 && me->GetProfile()->GetAggression() < 1.0f)) + { + // only retreat if at least one of them is aiming at me + if (me->IsAnyVisibleEnemyLookingAtMe( CHECK_FOV )) + { + // tell our teammates our plight + if (isPinnedDown) + me->GetChatter()->PinnedDown(); + else if (!me->CanSeeSniper()) + me->GetChatter()->Scared(); + + m_retreatTimer.Start( RandomFloat( 3.0f, 15.0f ) ); + + // try to retreat + if (me->TryToRetreat()) + { + // if we are a sniper, equip our pistol so we can fire while retreating + /* + if (me->IsUsingSniperRifle()) + { + // wait a moment to allow one last shot + me->Wait( 0.5f ); + //me->EquipPistol(); + } + */ + + // request backup if outnumbered + if (me->IsOutnumbered()) + { + me->GetChatter()->NeedBackup(); + } + } + else + { + me->PrintIfWatched( "I want to retreat, but no safe spots nearby!\n" ); + } + } + } + } + + // + // Knife fighting + // We need to pathfind right to the enemy to cut him + // + if (me->IsUsingKnife()) + { + // can't crouch and hold with a knife + m_crouchAndHold = false; + me->StandUp(); + + // if we are using a knife and our prey is looking towards us, run at him + if (me->IsPlayerFacingMe( enemy )) + { + me->ForceRun( 5.0f ); + me->Hurry( 10.0f ); + } + + // slash our victim + me->FireWeaponAtEnemy(); + + // if toe to toe with our enemy, don't dodge, just slash + const float slashRange = 70.0f; + if ((enemy->GetAbsOrigin() - me->GetAbsOrigin()).IsLengthGreaterThan( slashRange )) + { + const float repathInterval = 0.5f; + + // if our victim has moved, repath + bool repath = false; + if (me->HasPath()) + { + const float repathRange = 100.0f; // 50 + if ((me->GetPathEndpoint() - enemy->GetAbsOrigin()).IsLengthGreaterThan( repathRange )) + { + repath = true; + } + } + else + { + repath = true; + } + + if (repath && m_repathTimer.IsElapsed()) + { + Vector enemyPos = enemy->GetAbsOrigin() + Vector( 0, 0, HalfHumanHeight ); + me->ComputePath( enemyPos, FASTEST_ROUTE ); + m_repathTimer.Start( repathInterval ); + } + + // move towards victim + if (me->UpdatePathMovement( NO_SPEED_CHANGE ) != CCSBot::PROGRESSING) + { + me->DestroyPath(); + } + } + + return; + } + + + + // + // Simple shield usage + // + if (me->HasShield()) + { + if (me->IsEnemyVisible() && !m_shieldForceOpen) + { + if (!me->IsRecognizedEnemyReloading() && !me->IsReloading() && me->IsPlayerLookingAtMe( enemy )) + { + // close up - enemy is pointing his gun at us + if (!me->IsProtectedByShield()) + me->SecondaryAttack(); + } + else + { + // enemy looking away or reloading his weapon - open up and shoot him + if (me->IsProtectedByShield()) + me->SecondaryAttack(); + } + } + else + { + // can't see enemy, open up + if (me->IsProtectedByShield()) + me->SecondaryAttack(); + } + + if (gpGlobals->curtime > m_shieldToggleTimestamp) + { + m_shieldToggleTimestamp = gpGlobals->curtime + RandomFloat( 0.5, 2.0f ); + + // toggle shield force open + m_shieldForceOpen = !m_shieldForceOpen; + } + } + + + // check if our weapon range is bad and we should switch to pistol + if (me->IsUsingSniperRifle()) + { + // if we have a sniper rifle and our enemy is too close, switch to pistol + const float sniperMinRange = 160.0f; // NOTE: Must be larger than NO_ZOOM range in AdjustZoom() + if ((enemyOrigin - myOrigin).IsLengthLessThan( sniperMinRange )) + me->EquipPistol(); + } + else if (me->IsUsingShotgun()) + { + // if we have a shotgun equipped and enemy is too far away, switch to pistol + const float shotgunMaxRange = 600.0f; + if ((enemyOrigin - myOrigin).IsLengthGreaterThan( shotgunMaxRange )) + me->EquipPistol(); + } + + // if we're sniping, look through the scope - need to do this here in case a reload resets our scope + if (me->IsUsingSniperRifle()) + { + // for Scouts and AWPs, we need to wait for zoom to resume + if (me->m_bResumeZoom) + { + m_scopeTimestamp = gpGlobals->curtime; + return; + } + + Vector toAimSpot3D = me->m_aimSpot - myOrigin; + float targetRange = toAimSpot3D.Length(); + + // dont adjust zoom level if we're already zoomed in - just fire + if (me->GetZoomLevel() == CCSBot::NO_ZOOM && me->AdjustZoom( targetRange )) + m_scopeTimestamp = gpGlobals->curtime; + + const float waitScopeTime = 0.3f + me->GetProfile()->GetReactionTime(); + if (gpGlobals->curtime - m_scopeTimestamp < waitScopeTime) + { + // force us to wait until zoomed in before firing + return; + } + } + + // see if we "notice" that our prey is dead + if (me->IsAwareOfEnemyDeath()) + { + // let team know if we killed the last enemy + if (me->GetLastVictimID() == enemy->entindex() && me->GetNearbyEnemyCount() <= 1) + { + me->GetChatter()->KilledMyEnemy( enemy->entindex() ); + + // if there are other enemies left, wait a moment - they usually come in groups + if (me->GetEnemiesRemaining()) + { + me->Wait( RandomFloat( 1.0f, 3.0f ) ); + } + } + + StopAttacking( me ); + return; + } + + float notSeenEnemyTime = gpGlobals->curtime - me->GetLastSawEnemyTimestamp(); + + // if we haven't seen our enemy for a moment, continue on if we dont want to fight, or decide to ambush if we do + if (!me->IsEnemyVisible()) + { + // attend to nearby enemy gunfire + if (notSeenEnemyTime > 0.5f && me->CanHearNearbyEnemyGunfire()) + { + // give up the attack, since we didn't want it in the first place + StopAttacking( me ); + + const Vector *pos = me->GetNoisePosition(); + if (pos) + { + me->SetLookAt( "Nearby enemy gunfire", *pos, PRIORITY_HIGH, 0.0f ); + me->PrintIfWatched( "Checking nearby threatening enemy gunfire!\n" ); + return; + } + } + + // check if we have lost track of our enemy during combat + if (notSeenEnemyTime > 0.25f) + { + m_isEnemyHidden = true; + } + + + if (notSeenEnemyTime > 0.1f) + { + if (me->GetDisposition() == CCSBot::ENGAGE_AND_INVESTIGATE) + { + // decide whether we should hide and "ambush" our enemy + if (m_haveSeenEnemy && !m_didAmbushCheck) + { + float hideChance = 33.3f; + + if (RandomFloat( 0.0, 100.0f ) < hideChance) + { + float ambushTime = RandomFloat( 3.0f, 15.0f ); + + // hide in ambush nearby + /// @todo look towards where we know enemy is + const Vector *spot = FindNearbyRetreatSpot( me, 200.0f ); + if (spot) + { + me->IgnoreEnemies( 1.0f ); + + me->Run(); + me->StandUp(); + me->Hide( *spot, ambushTime, true ); + return; + } + } + + // don't check again + m_didAmbushCheck = true; + } + } + else + { + // give up the attack, since we didn't want it in the first place + StopAttacking( me ); + return; + } + } + } + else + { + // we can see the enemy again - reset our ambush check + m_didAmbushCheck = false; + + // if the enemy is coming out of hiding, we need time to react + if (m_isEnemyHidden) + { + m_reacquireTimestamp = gpGlobals->curtime + me->GetProfile()->GetReactionTime(); + m_isEnemyHidden = false; + } + } + + + // if we haven't seen our enemy for a long time, chase after them + float chaseTime = 2.0f + 2.0f * (1.0f - me->GetProfile()->GetAggression()); + + // if we are sniping, be very patient + if (me->IsUsingSniperRifle()) + chaseTime += 3.0f; + else if (me->IsCrouching()) // if we are crouching, be a little patient + chaseTime += 1.0f; + + // if we can't see the enemy, and have either seen him but currently lost sight of him, + // or haven't yet seen him, chase after him (unless we are a sniper) + if (!me->IsEnemyVisible() && (notSeenEnemyTime > chaseTime || !m_haveSeenEnemy)) + { + // snipers don't chase their prey - they wait for their prey to come to them + if (me->GetTask() == CCSBot::SNIPING) + { + StopAttacking( me ); + return; + } + else + { + // move to last known position of enemy + me->SetTask( CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION, enemy ); + me->MoveTo( me->GetLastKnownEnemyPosition() ); + return; + } + } + + + // if we can't see our enemy at the moment, and were shot by + // a different visible enemy, engage them instead + const float hurtRecentlyTime = 3.0f; + if (!me->IsEnemyVisible() && + me->GetTimeSinceAttacked() < hurtRecentlyTime && + me->GetAttacker() && + me->GetAttacker() != me->GetBotEnemy()) + { + // if we can see them, attack, otherwise panic + if (me->IsVisible( me->GetAttacker(), CHECK_FOV )) + { + me->Attack( me->GetAttacker() ); + me->PrintIfWatched( "Switching targets to retaliate against new attacker!\n" ); + } + /* + * Rethink this + else + { + me->Panic( me->GetAttacker() ); + me->PrintIfWatched( "Panicking from crossfire while attacking!\n" ); + } + */ + + return; + } + + if (true || gpGlobals->curtime > m_reacquireTimestamp) + me->FireWeaponAtEnemy(); + + + // do dodge behavior + Dodge( me ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Finish attack + */ +void AttackState::OnExit( CCSBot *me ) +{ + me->PrintIfWatched( "AttackState:OnExit()\n" ); + + m_crouchAndHold = false; + + // clear any noises we heard during battle + me->ForgetNoise(); + me->ResetStuckMonitor(); + + // resume our original posture + me->PopPostureContext(); + + // put shield away + if (me->IsProtectedByShield()) + me->SecondaryAttack(); + + + //me->StopAiming(); +} + diff --git a/game/server/cstrike/bot/states/cs_bot_buy.cpp b/game/server/cstrike/bot/states/cs_bot_buy.cpp new file mode 100644 index 0000000..de06e8b --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_buy.cpp @@ -0,0 +1,690 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +ConVar bot_loadout( "bot_loadout", "", FCVAR_CHEAT, "bots are given these items at round start" ); +ConVar bot_randombuy( "bot_randombuy", "0", FCVAR_CHEAT, "should bots ignore their prefered weapons and just buy weapons at random?" ); + +//-------------------------------------------------------------------------------------------------------------- +/** + * Debug command to give a named weapon + */ +void CCSBot::GiveWeapon( const char *weaponAlias ) +{ + const char *translatedAlias = GetTranslatedWeaponAlias( weaponAlias ); + + char wpnName[128]; + Q_snprintf( wpnName, sizeof( wpnName ), "weapon_%s", translatedAlias ); + WEAPON_FILE_INFO_HANDLE hWpnInfo = LookupWeaponInfoSlot( wpnName ); + if ( hWpnInfo == GetInvalidWeaponInfoHandle() ) + { + return; + } + + CCSWeaponInfo *pWeaponInfo = dynamic_cast< CCSWeaponInfo* >( GetFileWeaponInfoFromHandle( hWpnInfo ) ); + if ( !pWeaponInfo ) + { + return; + } + + if ( !Weapon_OwnsThisType( wpnName ) ) + { + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( pWeaponInfo->iSlot ); + if ( pWeapon ) + { + if ( pWeaponInfo->iSlot == WEAPON_SLOT_PISTOL ) + { + DropPistol(); + } + else if ( pWeaponInfo->iSlot == WEAPON_SLOT_RIFLE ) + { + DropRifle(); + } + } + } + + GiveNamedItem( wpnName ); +} + +//-------------------------------------------------------------------------------------------------------------- +static bool HasDefaultPistol( CCSBot *me ) +{ + CWeaponCSBase *pistol = (CWeaponCSBase *)me->Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + + if (pistol == NULL) + return false; + + if (me->GetTeamNumber() == TEAM_TERRORIST && pistol->IsA( WEAPON_GLOCK )) + return true; + + if (me->GetTeamNumber() == TEAM_CT && pistol->IsA( WEAPON_USP )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Buy weapons, armor, etc. + */ +void BuyState::OnEnter( CCSBot *me ) +{ + m_retries = 0; + m_prefRetries = 0; + m_prefIndex = 0; + + const char *cheatWeaponString = bot_loadout.GetString(); + if ( cheatWeaponString && *cheatWeaponString ) + { + m_doneBuying = false; // we're going to be given weapons - ignore the eco limit + } + else + { + // check if we are saving money for the next round + if (me->m_iAccount < cv_bot_eco_limit.GetFloat()) + { + me->PrintIfWatched( "Saving money for next round.\n" ); + m_doneBuying = true; + } + else + { + m_doneBuying = false; + } + } + + m_isInitialDelay = true; + + // this will force us to stop holding live grenade + me->EquipBestWeapon( MUST_EQUIP ); + + m_buyDefuseKit = false; + m_buyShield = false; + + if (me->GetTeamNumber() == TEAM_CT) + { + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB) + { + // CT's sometimes buy defuse kits in the bomb scenario (except in career mode, where the player should defuse) + if (CSGameRules()->IsCareer() == false) + { + const float buyDefuseKitChance = 100.0f * (me->GetProfile()->GetSkill() + 0.2f); + if (RandomFloat( 0.0f, 100.0f ) < buyDefuseKitChance) + { + m_buyDefuseKit = true; + } + } + } + + // determine if we want a tactical shield + if (!me->HasPrimaryWeapon() && TheCSBots()->AllowTacticalShield()) + { + if (me->m_iAccount > 2500) + { + if (me->m_iAccount < 4000) + m_buyShield = (RandomFloat( 0, 100.0f ) < 33.3f) ? true : false; + else + m_buyShield = (RandomFloat( 0, 100.0f ) < 10.0f) ? true : false; + } + } + } + + if (TheCSBots()->AllowGrenades()) + { + m_buyGrenade = (RandomFloat( 0.0f, 100.0f ) < 33.3f) ? true : false; + } + else + { + m_buyGrenade = false; + } + + + m_buyPistol = false; + if (TheCSBots()->AllowPistols()) + { + // check if we have a pistol + if (me->Weapon_GetSlot( WEAPON_SLOT_PISTOL )) + { + // if we have our default pistol, think about buying a different one + if (HasDefaultPistol( me )) + { + // if everything other than pistols is disallowed, buy a pistol + if (TheCSBots()->AllowShotguns() == false && + TheCSBots()->AllowSubMachineGuns() == false && + TheCSBots()->AllowRifles() == false && + TheCSBots()->AllowMachineGuns() == false && + TheCSBots()->AllowTacticalShield() == false && + TheCSBots()->AllowSnipers() == false) + { + m_buyPistol = (RandomFloat( 0, 100 ) < 75.0f); + } + else if (me->m_iAccount < 1000) + { + // if we're low on cash, buy a pistol + m_buyPistol = (RandomFloat( 0, 100 ) < 75.0f); + } + else + { + m_buyPistol = (RandomFloat( 0, 100 ) < 33.3f); + } + } + } + else + { + // we dont have a pistol - buy one + m_buyPistol = true; + } + } +} + + +enum WeaponType +{ + PISTOL, + SHOTGUN, + SUB_MACHINE_GUN, + RIFLE, + MACHINE_GUN, + SNIPER_RIFLE, + GRENADE, + + NUM_WEAPON_TYPES +}; + +struct BuyInfo +{ + WeaponType type; + bool preferred; ///< more challenging bots prefer these weapons + const char *buyAlias; ///< the buy alias for this equipment +}; + +#define PRIMARY_WEAPON_BUY_COUNT 13 +#define SECONDARY_WEAPON_BUY_COUNT 3 + +/** + * These tables MUST be kept in sync with the CT and T buy aliases + */ + +static BuyInfo primaryWeaponBuyInfoCT[ PRIMARY_WEAPON_BUY_COUNT ] = +{ + { SHOTGUN, false, "m3" }, // WEAPON_M3 + { SHOTGUN, false, "xm1014" }, // WEAPON_XM1014 + { SUB_MACHINE_GUN, false, "tmp" }, // WEAPON_TMP + { SUB_MACHINE_GUN, false, "mp5navy" }, // WEAPON_MP5N + { SUB_MACHINE_GUN, false, "ump45" }, // WEAPON_UMP45 + { SUB_MACHINE_GUN, false, "p90" }, // WEAPON_P90 + { RIFLE, true, "famas" }, // WEAPON_FAMAS + { SNIPER_RIFLE, false, "scout" }, // WEAPON_SCOUT + { RIFLE, true, "m4a1" }, // WEAPON_M4A1 + { RIFLE, false, "aug" }, // WEAPON_AUG + { SNIPER_RIFLE, true, "sg550" }, // WEAPON_SG550 + { SNIPER_RIFLE, true, "awp" }, // WEAPON_AWP + { MACHINE_GUN, false, "m249" } // WEAPON_M249 +}; + +static BuyInfo secondaryWeaponBuyInfoCT[ SECONDARY_WEAPON_BUY_COUNT ] = +{ +// { PISTOL, false, "glock" }, +// { PISTOL, false, "usp" }, + { PISTOL, true, "p228" }, + { PISTOL, true, "deagle" }, + { PISTOL, true, "fn57" } +}; + + +static BuyInfo primaryWeaponBuyInfoT[ PRIMARY_WEAPON_BUY_COUNT ] = +{ + { SHOTGUN, false, "m3" }, // WEAPON_M3 + { SHOTGUN, false, "xm1014" }, // WEAPON_XM1014 + { SUB_MACHINE_GUN, false, "mac10" }, // WEAPON_MAC10 + { SUB_MACHINE_GUN, false, "mp5navy" }, // WEAPON_MP5N + { SUB_MACHINE_GUN, false, "ump45" }, // WEAPON_UMP45 + { SUB_MACHINE_GUN, false, "p90" }, // WEAPON_P90 + { RIFLE, true, "galil" }, // WEAPON_GALIL + { RIFLE, true, "ak47" }, // WEAPON_AK47 + { SNIPER_RIFLE, false, "scout" }, // WEAPON_SCOUT + { RIFLE, true, "sg552" }, // WEAPON_SG552 + { SNIPER_RIFLE, true, "awp" }, // WEAPON_AWP + { SNIPER_RIFLE, true, "g3sg1" }, // WEAPON_G3SG1 + { MACHINE_GUN, false, "m249" } // WEAPON_M249 +}; + +static BuyInfo secondaryWeaponBuyInfoT[ SECONDARY_WEAPON_BUY_COUNT ] = +{ +// { PISTOL, false, "glock" }, +// { PISTOL, false, "usp" }, + { PISTOL, true, "p228" }, + { PISTOL, true, "deagle" }, + { PISTOL, true, "elites" } +}; + +/** + * Given a weapon alias, return the kind of weapon it is + */ +inline WeaponType GetWeaponType( const char *alias ) +{ + int i; + + for( i=0; i<PRIMARY_WEAPON_BUY_COUNT; ++i ) + { + if (!stricmp( alias, primaryWeaponBuyInfoCT[i].buyAlias )) + return primaryWeaponBuyInfoCT[i].type; + + if (!stricmp( alias, primaryWeaponBuyInfoT[i].buyAlias )) + return primaryWeaponBuyInfoT[i].type; + } + + for( i=0; i<SECONDARY_WEAPON_BUY_COUNT; ++i ) + { + if (!stricmp( alias, secondaryWeaponBuyInfoCT[i].buyAlias )) + return secondaryWeaponBuyInfoCT[i].type; + + if (!stricmp( alias, secondaryWeaponBuyInfoT[i].buyAlias )) + return secondaryWeaponBuyInfoT[i].type; + } + + return NUM_WEAPON_TYPES; +} + + + + +//-------------------------------------------------------------------------------------------------------------- +void BuyState::OnUpdate( CCSBot *me ) +{ + char cmdBuffer[256]; + + // wait for a Navigation Mesh + if (!TheNavMesh->IsLoaded()) + return; + + // apparently we cant buy things in the first few seconds, so wait a bit + if (m_isInitialDelay) + { + const float waitToBuyTime = 0.25f; + if (gpGlobals->curtime - me->GetStateTimestamp() < waitToBuyTime) + return; + + m_isInitialDelay = false; + } + + // if we're done buying and still in the freeze period, wait + if (m_doneBuying) + { + if (CSGameRules()->IsMultiplayer() && CSGameRules()->IsFreezePeriod()) + { + // make sure we're locked and loaded + me->EquipBestWeapon( MUST_EQUIP ); + me->Reload(); + me->ResetStuckMonitor(); + return; + } + + me->Idle(); + return; + } + + // If we're supposed to buy a specific weapon for debugging, do so and then bail + const char *cheatWeaponString = bot_loadout.GetString(); + if ( cheatWeaponString && *cheatWeaponString ) + { + CUtlVector<char*, CUtlMemory<char*> > loadout; + Q_SplitString( cheatWeaponString, " ", loadout ); + for ( int i=0; i<loadout.Count(); ++i ) + { + const char *item = loadout[i]; + if ( FStrEq( item, "vest" ) ) + { + me->GiveNamedItem( "item_kevlar" ); + } + else if ( FStrEq( item, "vesthelm" ) ) + { + me->GiveNamedItem( "item_assaultsuit" ); + } + else if ( FStrEq( item, "defuser" ) ) + { + if ( me->GetTeamNumber() == TEAM_CT ) + { + me->GiveDefuser(); + } + } + else if ( FStrEq( item, "nvgs" ) ) + { + me->m_bHasNightVision = true; + } + else if ( FStrEq( item, "primammo" ) ) + { + me->AttemptToBuyAmmo( 0 ); + } + else if ( FStrEq( item, "secammo" ) ) + { + me->AttemptToBuyAmmo( 1 ); + } + else + { + me->GiveWeapon( item ); + } + } + m_doneBuying = true; + return; + } + + + if (!me->IsInBuyZone()) + { + m_doneBuying = true; + CONSOLE_ECHO( "%s bot spawned outside of a buy zone (%d, %d, %d)\n", + (me->GetTeamNumber() == TEAM_CT) ? "CT" : "Terrorist", + (int)me->GetAbsOrigin().x, + (int)me->GetAbsOrigin().y, + (int)me->GetAbsOrigin().z ); + return; + } + + // try to buy some weapons + const float buyInterval = 0.02f; + if (gpGlobals->curtime - me->GetStateTimestamp() > buyInterval) + { + me->m_stateTimestamp = gpGlobals->curtime; + + bool isPreferredAllDisallowed = true; + + // try to buy our preferred weapons first + if (m_prefIndex < me->GetProfile()->GetWeaponPreferenceCount() && bot_randombuy.GetBool() == false ) + { + // need to retry because sometimes first buy fails?? + const int maxPrefRetries = 2; + if (m_prefRetries >= maxPrefRetries) + { + // try to buy next preferred weapon + ++m_prefIndex; + m_prefRetries = 0; + return; + } + + int weaponPreference = me->GetProfile()->GetWeaponPreference( m_prefIndex ); + + // don't buy it again if we still have one from last round + char weaponPreferenceName[32]; + Q_snprintf( weaponPreferenceName, sizeof(weaponPreferenceName), "weapon_%s", me->GetProfile()->GetWeaponPreferenceAsString( m_prefIndex ) ); + if( me->Weapon_OwnsThisType(weaponPreferenceName) )//Prefs and buyalias use the short version, this uses the long + { + // done with buying preferred weapon + m_prefIndex = 9999; + return; + } + + if (me->HasShield() && weaponPreference == WEAPON_SHIELDGUN) + { + // done with buying preferred weapon + m_prefIndex = 9999; + return; + } + + const char *buyAlias = NULL; + + if (weaponPreference == WEAPON_SHIELDGUN) + { + if (TheCSBots()->AllowTacticalShield()) + buyAlias = "shield"; + } + else + { + buyAlias = WeaponIDToAlias( weaponPreference ); + WeaponType type = GetWeaponType( buyAlias ); + switch( type ) + { + case PISTOL: + if (!TheCSBots()->AllowPistols()) + buyAlias = NULL; + break; + + case SHOTGUN: + if (!TheCSBots()->AllowShotguns()) + buyAlias = NULL; + break; + + case SUB_MACHINE_GUN: + if (!TheCSBots()->AllowSubMachineGuns()) + buyAlias = NULL; + break; + + case RIFLE: + if (!TheCSBots()->AllowRifles()) + buyAlias = NULL; + break; + + case MACHINE_GUN: + if (!TheCSBots()->AllowMachineGuns()) + buyAlias = NULL; + break; + + case SNIPER_RIFLE: + if (!TheCSBots()->AllowSnipers()) + buyAlias = NULL; + break; + } + } + + if (buyAlias) + { + Q_snprintf( cmdBuffer, 256, "buy %s\n", buyAlias ); + + CCommand args; + args.Tokenize( cmdBuffer ); + me->ClientCommand( args ); + + me->PrintIfWatched( "Tried to buy preferred weapon %s.\n", buyAlias ); + isPreferredAllDisallowed = false; + } + + ++m_prefRetries; + + // bail out so we dont waste money on other equipment + // unless everything we prefer has been disallowed, then buy at random + if (isPreferredAllDisallowed == false) + return; + } + + // if we have no preferred primary weapon (or everything we want is disallowed), buy at random + if (!me->HasPrimaryWeapon() && (isPreferredAllDisallowed || !me->GetProfile()->HasPrimaryPreference())) + { + if (m_buyShield) + { + // buy a shield + CCommand args; + args.Tokenize( "buy shield" ); + me->ClientCommand( args ); + + me->PrintIfWatched( "Tried to buy a shield.\n" ); + } + else + { + // build list of allowable weapons to buy + BuyInfo *masterPrimary = (me->GetTeamNumber() == TEAM_TERRORIST) ? primaryWeaponBuyInfoT : primaryWeaponBuyInfoCT; + BuyInfo *stockPrimary[ PRIMARY_WEAPON_BUY_COUNT ]; + int stockPrimaryCount = 0; + + // dont choose sniper rifles as often + const float sniperRifleChance = 50.0f; + bool wantSniper = (RandomFloat( 0, 100 ) < sniperRifleChance) ? true : false; + + if ( bot_randombuy.GetBool() ) + { + wantSniper = true; + } + + for( int i=0; i<PRIMARY_WEAPON_BUY_COUNT; ++i ) + { + if ((masterPrimary[i].type == SHOTGUN && TheCSBots()->AllowShotguns()) || + (masterPrimary[i].type == SUB_MACHINE_GUN && TheCSBots()->AllowSubMachineGuns()) || + (masterPrimary[i].type == RIFLE && TheCSBots()->AllowRifles()) || + (masterPrimary[i].type == SNIPER_RIFLE && TheCSBots()->AllowSnipers() && wantSniper) || + (masterPrimary[i].type == MACHINE_GUN && TheCSBots()->AllowMachineGuns())) + { + stockPrimary[ stockPrimaryCount++ ] = &masterPrimary[i]; + } + } + + if (stockPrimaryCount) + { + // buy primary weapon if we don't have one + int which; + + // on hard difficulty levels, bots try to buy preferred weapons on the first pass + if (m_retries == 0 && TheCSBots()->GetDifficultyLevel() >= BOT_HARD && bot_randombuy.GetBool() == false ) + { + // count up available preferred weapons + int prefCount = 0; + for( which=0; which<stockPrimaryCount; ++which ) + if (stockPrimary[which]->preferred) + ++prefCount; + + if (prefCount) + { + int whichPref = RandomInt( 0, prefCount-1 ); + for( which=0; which<stockPrimaryCount; ++which ) + if (stockPrimary[which]->preferred && whichPref-- == 0) + break; + } + else + { + // no preferred weapons available, just pick randomly + which = RandomInt( 0, stockPrimaryCount-1 ); + } + } + else + { + which = RandomInt( 0, stockPrimaryCount-1 ); + } + + Q_snprintf( cmdBuffer, 256, "buy %s\n", stockPrimary[ which ]->buyAlias ); + + CCommand args; + args.Tokenize( cmdBuffer ); + me->ClientCommand( args ); + + me->PrintIfWatched( "Tried to buy %s.\n", stockPrimary[ which ]->buyAlias ); + } + } + } + + + // + // If we now have a weapon, or have tried for too long, we're done + // + if (me->HasPrimaryWeapon() || m_retries++ > 5) + { + // primary ammo + CCommand args; + if (me->HasPrimaryWeapon()) + { + args.Tokenize( "buy primammo" ); + me->ClientCommand( args ); + } + + // buy armor last, to make sure we bought a weapon first + args.Tokenize( "buy vesthelm" ); + me->ClientCommand( args ); + args.Tokenize( "buy vest" ); + me->ClientCommand( args ); + + // pistols - if we have no preferred pistol, buy at random + if (TheCSBots()->AllowPistols() && !me->GetProfile()->HasPistolPreference()) + { + if (m_buyPistol) + { + int which = RandomInt( 0, SECONDARY_WEAPON_BUY_COUNT-1 ); + + const char *what = NULL; + + if (me->GetTeamNumber() == TEAM_TERRORIST) + what = secondaryWeaponBuyInfoT[ which ].buyAlias; + else + what = secondaryWeaponBuyInfoCT[ which ].buyAlias; + + Q_snprintf( cmdBuffer, 256, "buy %s\n", what ); + args.Tokenize( cmdBuffer ); + me->ClientCommand( args ); + + + // only buy one pistol + m_buyPistol = false; + } + + // make sure we have enough pistol ammo + args.Tokenize( "buy secammo" ); + me->ClientCommand( args ); + } + + // buy a grenade if we wish, and we don't already have one + if (m_buyGrenade && !me->HasGrenade()) + { + if (UTIL_IsTeamAllBots( me->GetTeamNumber() )) + { + // only allow Flashbangs if everyone on the team is a bot (dont want to blind our friendly humans) + float rnd = RandomFloat( 0, 100 ); + + if (rnd < 10) + { + args.Tokenize( "buy smokegrenade" ); + me->ClientCommand( args ); // smoke grenade + } + else if (rnd < 35) + { + args.Tokenize( "buy flashbang" ); + me->ClientCommand( args ); // flashbang + } + else + { + args.Tokenize( "buy hegrenade" ); + me->ClientCommand( args ); // he grenade + } + } + else + { + if (RandomFloat( 0, 100 ) < 10) + { + args.Tokenize( "buy smokegrenade" ); // smoke grenade + me->ClientCommand( args ); + } + else + { + args.Tokenize( "buy hegrenade" ); // he grenade + me->ClientCommand( args ); + } + } + } + + if (m_buyDefuseKit) + { + args.Tokenize( "buy defuser" ); + me->ClientCommand( args ); + } + + m_doneBuying = true; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void BuyState::OnExit( CCSBot *me ) +{ + me->ResetStuckMonitor(); + me->EquipBestWeapon(); +} + diff --git a/game/server/cstrike/bot/states/cs_bot_defuse_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_defuse_bomb.cpp new file mode 100644 index 0000000..3614bcb --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_defuse_bomb.cpp @@ -0,0 +1,82 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin defusing the bomb + */ +void DefuseBombState::OnEnter( CCSBot *me ) +{ + me->Crouch(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + me->GetChatter()->Say( "DefusingBomb" ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Defuse the bomb + */ +void DefuseBombState::OnUpdate( CCSBot *me ) +{ + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + + if (bombPos == NULL) + { + me->PrintIfWatched( "In Defuse state, but don't know where the bomb is!\n" ); + me->Idle(); + return; + } + + // look at the bomb + me->SetLookAt( "Defuse bomb", *bombPos, PRIORITY_HIGH ); + + // defuse... + me->UseEnvironment(); + + if (gpGlobals->curtime - me->GetStateTimestamp() > 1.0f) + { + // if we missed starting the defuse, give up + if (TheCSBots()->GetBombDefuser() == NULL) + { + me->PrintIfWatched( "Failed to start defuse, giving up\n" ); + me->Idle(); + return; + } + else if (TheCSBots()->GetBombDefuser() != me) + { + // if someone else got the defuse, give up + me->PrintIfWatched( "Someone else started defusing, giving up\n" ); + me->Idle(); + return; + } + } + + // if bomb has been defused, give up + if (!TheCSBots()->IsBombPlanted()) + { + me->Idle(); + return; + } +} + +//-------------------------------------------------------------------------------------------------------------- +void DefuseBombState::OnExit( CCSBot *me ) +{ + me->StandUp(); + me->ResetStuckMonitor(); + me->SetTask( CCSBot::SEEK_AND_DESTROY ); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + me->ClearLookAt(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_escape_from_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_escape_from_bomb.cpp new file mode 100644 index 0000000..9abeb2b --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_escape_from_bomb.cpp @@ -0,0 +1,67 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Escape from the bomb. + */ +void EscapeFromBombState::OnEnter( CCSBot *me ) +{ + me->StandUp(); + me->Run(); + me->DestroyPath(); + me->EquipKnife(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Escape from the bomb. + */ +void EscapeFromBombState::OnUpdate( CCSBot *me ) +{ + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + + // if we don't know where the bomb is, we shouldn't be in this state + if (bombPos == NULL) + { + me->Idle(); + return; + } + + // grab our knife to move quickly + me->EquipKnife(); + + // look around + me->UpdateLookAround(); + + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + // we have no path, or reached the end of one - create a new path far away from the bomb + FarAwayFromPositionFunctor func( *bombPos ); + CNavArea *goalArea = FindMinimumCostArea( me->GetLastKnownArea(), func ); + + // if this fails, we'll try again next time + me->ComputePath( goalArea->GetCenter(), FASTEST_ROUTE ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Escape from the bomb. + */ +void EscapeFromBombState::OnExit( CCSBot *me ) +{ + me->EquipBestWeapon(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_fetch_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_fetch_bomb.cpp new file mode 100644 index 0000000..6e78019 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_fetch_bomb.cpp @@ -0,0 +1,69 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to the bomb on the floor and pick it up + */ +void FetchBombState::OnEnter( CCSBot *me ) +{ + me->DestroyPath(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to the bomb on the floor and pick it up + */ +void FetchBombState::OnUpdate( CCSBot *me ) +{ + if (me->HasC4()) + { + me->PrintIfWatched( "I picked up the bomb\n" ); + me->Idle(); + return; + } + + + CBaseEntity *bomb = TheCSBots()->GetLooseBomb(); + if (bomb) + { + if (!me->HasPath()) + { + // build a path to the bomb + if (me->ComputePath( bomb->GetAbsOrigin() ) == false) + { + me->PrintIfWatched( "Fetch bomb pathfind failed\n" ); + + // go Hunt instead of Idle to prevent continuous re-pathing to inaccessible bomb + me->Hunt(); + return; + } + } + } + else + { + // someone picked up the bomb + me->PrintIfWatched( "Someone else picked up the bomb.\n" ); + me->Idle(); + return; + } + + // look around + me->UpdateLookAround(); + + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + me->Idle(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_follow.cpp b/game/server/cstrike/bot/states/cs_bot_follow.cpp new file mode 100644 index 0000000..c8d4976 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_follow.cpp @@ -0,0 +1,366 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Follow our leader + */ +void FollowState::OnEnter( CCSBot *me ) +{ + me->StandUp(); + me->Run(); + me->DestroyPath(); + + m_isStopped = false; + m_stoppedTimestamp = 0.0f; + + // to force immediate repath + m_lastLeaderPos.x = -99999999.9f; + m_lastLeaderPos.y = -99999999.9f; + m_lastLeaderPos.z = -99999999.9f; + + m_lastSawLeaderTime = 0; + + // set re-pathing frequency + m_repathInterval.Invalidate(); + + m_isSneaking = false; + + m_walkTime.Invalidate(); + m_isAtWalkSpeed = false; + + m_leaderMotionState = INVALID; + m_idleTimer.Start( RandomFloat( 2.0f, 5.0f ) ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine the leader's motion state by tracking his speed + */ +void FollowState::ComputeLeaderMotionState( float leaderSpeed ) +{ + // walk = 130, run = 250 + const float runWalkThreshold = 140.0f; + const float walkStopThreshold = 10.0f; // 120.0f; + LeaderMotionStateType prevState = m_leaderMotionState; + if (leaderSpeed > runWalkThreshold) + { + m_leaderMotionState = RUNNING; + m_isAtWalkSpeed = false; + } + else if (leaderSpeed > walkStopThreshold) + { + // track when began to walk + if (!m_isAtWalkSpeed) + { + m_walkTime.Start(); + m_isAtWalkSpeed = true; + } + + const float minWalkTime = 0.25f; + if (m_walkTime.GetElapsedTime() > minWalkTime) + { + m_leaderMotionState = WALKING; + } + } + else + { + m_leaderMotionState = STOPPED; + m_isAtWalkSpeed = false; + } + + // track time spent in this motion state + if (prevState != m_leaderMotionState) + { + m_leaderMotionStateTime.Start(); + m_waitTime = RandomFloat( 1.0f, 3.0f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor to collect all areas in the forward direction of the given player within a radius + */ +class FollowTargetCollector +{ +public: + FollowTargetCollector( CBasePlayer *player ) + { + m_player = player; + + Vector playerVel = player->GetAbsVelocity(); + m_forward.x = playerVel.x; + m_forward.y = playerVel.y; + float speed = m_forward.NormalizeInPlace(); + + Vector playerOrigin = GetCentroid( player ); + + const float walkSpeed = 100.0f; + if (speed < walkSpeed) + { + m_cutoff.x = playerOrigin.x; + m_cutoff.y = playerOrigin.y; + m_forward.x = 0.0f; + m_forward.y = 0.0f; + } + else + { + const float k = 1.5f; // 2.0f; + float trimSpeed = MIN( speed, 200.0f ); + m_cutoff.x = playerOrigin.x + k * trimSpeed * m_forward.x; + m_cutoff.y = playerOrigin.y + k * trimSpeed * m_forward.y; + } + + m_targetAreaCount = 0; + } + + enum { MAX_TARGET_AREAS = 128 }; + + bool operator() ( CNavArea *area ) + { + if (m_targetAreaCount >= MAX_TARGET_AREAS) + return false; + + // only use two-way connections + if (!area->GetParent() || area->IsConnected( area->GetParent(), NUM_DIRECTIONS )) + { + if (m_forward.IsZero()) + { + m_targetArea[ m_targetAreaCount++ ] = area; + } + else + { + // collect areas in the direction of the player's forward motion + Vector2D to( area->GetCenter().x - m_cutoff.x, area->GetCenter().y - m_cutoff.y ); + to.NormalizeInPlace(); + + //if (DotProduct( to, m_forward ) > 0.7071f) + if ((to.x * m_forward.x + to.y * m_forward.y) > 0.7071f) + m_targetArea[ m_targetAreaCount++ ] = area; + } + } + + return (m_targetAreaCount < MAX_TARGET_AREAS); + } + + + CBasePlayer *m_player; + Vector2D m_forward; + Vector2D m_cutoff; + + CNavArea *m_targetArea[ MAX_TARGET_AREAS ]; + int m_targetAreaCount; +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Follow our leader + * @todo Clean up this nasty mess + */ +void FollowState::OnUpdate( CCSBot *me ) +{ + // if we lost our leader, give up + if (m_leader == NULL || !m_leader->IsAlive()) + { + me->Idle(); + return; + } + + // if we are carrying the bomb and at a bombsite, plant + if (me->HasC4() && me->IsAtBombsite()) + { + // plant it + me->SetTask( CCSBot::PLANT_BOMB ); + me->PlantBomb(); + + // radio to the team + me->GetChatter()->PlantingTheBomb( me->GetPlace() ); + + return; + } + + // look around + me->UpdateLookAround(); + + // if we are moving, we are not idle + if (me->IsNotMoving() == false) + m_idleTimer.Start( RandomFloat( 2.0f, 5.0f ) ); + + // compute the leader's speed + Vector leaderVel = m_leader->GetAbsVelocity(); + float leaderSpeed = Vector2D( leaderVel.x, leaderVel.y ).Length(); + + // determine our leader's movement state + ComputeLeaderMotionState( leaderSpeed ); + + // track whether we can see the leader + bool isLeaderVisible; + Vector leaderOrigin = GetCentroid( m_leader ); + if (me->IsVisible( leaderOrigin )) + { + m_lastSawLeaderTime = gpGlobals->curtime; + isLeaderVisible = true; + } + else + { + isLeaderVisible = false; + } + + + // determine whether we should sneak or not + const float farAwayRange = 750.0f; + Vector myOrigin = GetCentroid( me ); + if ((leaderOrigin - myOrigin).IsLengthGreaterThan( farAwayRange )) + { + // far away from leader - run to catch up + m_isSneaking = false; + } + else if (isLeaderVisible) + { + // if we see leader walking and we are nearby, walk + if (m_leaderMotionState == WALKING) + m_isSneaking = true; + + // if we are sneaking and our leader starts running, stop sneaking + if (m_isSneaking && m_leaderMotionState == RUNNING) + m_isSneaking = false; + } + + // if we haven't seen the leader for a long time, run + const float longTime = 20.0f; + if (gpGlobals->curtime - m_lastSawLeaderTime > longTime) + m_isSneaking = false; + + if (m_isSneaking) + me->Walk(); + else + me->Run(); + + + bool repath = false; + + // if the leader has stopped, hide nearby + const float nearLeaderRange = 250.0f; + if (!me->HasPath() && m_leaderMotionState == STOPPED && m_leaderMotionStateTime.GetElapsedTime() > m_waitTime) + { + // throttle how often this check occurs + m_waitTime += RandomFloat( 1.0f, 3.0f ); + + // the leader has stopped - if we are close to him, take up a hiding spot + if ((leaderOrigin - myOrigin).IsLengthLessThan( nearLeaderRange )) + { + const float hideRange = 250.0f; + if (me->TryToHide( NULL, -1.0f, hideRange, false, USE_NEAREST )) + { + me->ResetStuckMonitor(); + return; + } + } + } + + // if we have been idle for awhile, move + if (m_idleTimer.IsElapsed()) + { + repath = true; + + // always walk when we move such a short distance + m_isSneaking = true; + } + + // if our leader has moved, repath (don't repath if leading is stopping) + if (leaderSpeed > 100.0f && m_leaderMotionState != STOPPED) + { + repath = true; + } + + // move along our path + if (me->UpdatePathMovement( NO_SPEED_CHANGE ) != CCSBot::PROGRESSING) + { + me->DestroyPath(); + } + + // recompute our path if necessary + if (repath && m_repathInterval.IsElapsed() && !me->IsOnLadder()) + { + // recompute our path to keep us near our leader + m_lastLeaderPos = leaderOrigin; + + me->ResetStuckMonitor(); + + const float runSpeed = 200.0f; + + const float collectRange = (leaderSpeed > runSpeed) ? 600.0f : 400.0f; // 400, 200 + FollowTargetCollector collector( m_leader ); + SearchSurroundingAreas( TheNavMesh->GetNearestNavArea( m_lastLeaderPos ), m_lastLeaderPos, collector, collectRange ); + + if (cv_bot_debug.GetBool()) + { + for( int i=0; i<collector.m_targetAreaCount; ++i ) + collector.m_targetArea[i]->Draw( /*255, 0, 0, 2*/ ); + } + + // move to one of the collected areas + if (collector.m_targetAreaCount) + { + CNavArea *target = NULL; + Vector targetPos; + + // if we are idle, pick a random area + if (m_idleTimer.IsElapsed()) + { + target = collector.m_targetArea[ RandomInt( 0, collector.m_targetAreaCount-1 ) ]; + targetPos = target->GetCenter(); + me->PrintIfWatched( "%4.1f: Bored. Repathing to a new nearby area\n", gpGlobals->curtime ); + } + else + { + me->PrintIfWatched( "%4.1f: Repathing to stay with leader.\n", gpGlobals->curtime ); + + // find closest area to where we are + CNavArea *area; + float closeRangeSq = 9999999999.9f; + Vector close; + + for( int a=0; a<collector.m_targetAreaCount; ++a ) + { + area = collector.m_targetArea[a]; + + area->GetClosestPointOnArea( myOrigin, &close ); + + float rangeSq = (myOrigin - close).LengthSqr(); + if (rangeSq < closeRangeSq) + { + target = area; + targetPos = close; + closeRangeSq = rangeSq; + } + } + } + + if (target == NULL || me->ComputePath( target->GetCenter(), FASTEST_ROUTE ) == false) + me->PrintIfWatched( "Pathfind to leader failed.\n" ); + + // throttle how often we repath + m_repathInterval.Start( 0.5f ); + + m_idleTimer.Reset(); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void FollowState::OnExit( CCSBot *me ) +{ +} diff --git a/game/server/cstrike/bot/states/cs_bot_hide.cpp b/game/server/cstrike/bot/states/cs_bot_hide.cpp new file mode 100644 index 0000000..9afd1e8 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_hide.cpp @@ -0,0 +1,549 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin moving to a nearby hidey-hole. + * NOTE: Do not forget this state may include a very long "move-to" time to get to our hidey spot! + */ +void HideState::OnEnter( CCSBot *me ) +{ + m_isAtSpot = false; + m_isLookingOutward = false; + + // if duration is "infinite", set it to a reasonably long time to prevent infinite camping + if (m_duration < 0.0f) + { + m_duration = RandomFloat( 30.0f, 60.0f ); + } + + // decide whether to "ambush" or not - never set to false so as not to override external setting + if (RandomFloat( 0.0f, 100.0f ) < 50.0f) + { + m_isHoldingPosition = true; + } + + // if we are holding position, decide for how long + if (m_isHoldingPosition) + { + m_holdPositionTime = RandomFloat( 3.0f, 10.0f ); + } + else + { + m_holdPositionTime = 0.0f; + } + + m_heardEnemy = false; + m_firstHeardEnemyTime = 0.0f; + m_retry = 0; + + if (me->IsFollowing()) + { + m_leaderAnchorPos = GetCentroid( me->GetFollowLeader() ); + } + + // if we are a sniper, we need to periodically pause while we retreat to squeeze off a shot or two + if (me->IsSniper()) + { + // start off paused to allow a final shot before retreating + m_isPaused = false; + m_pauseTimer.Invalidate(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a nearby hidey-hole. + * NOTE: Do not forget this state may include a very long "move-to" time to get to our hidey spot! + */ +void HideState::OnUpdate( CCSBot *me ) +{ + Vector myOrigin = GetCentroid( me ); + + // wait until finished reloading to leave hide state + if (!me->IsReloading()) + { + // if we are momentarily hiding while following someone, check to see if he has moved on + if (me->IsFollowing()) + { + CCSPlayer *leader = static_cast<CCSPlayer *>( static_cast<CBaseEntity *>( me->GetFollowLeader() ) ); + Vector leaderOrigin = GetCentroid( leader ); + + // BOTPORT: Determine walk/run velocity thresholds + float runThreshold = 200.0f; + if (leader->GetAbsVelocity().IsLengthGreaterThan( runThreshold )) + { + // leader is running, stay with him + me->Follow( leader ); + return; + } + + // if leader has moved, stay with him + const float followRange = 250.0f; + if ((m_leaderAnchorPos - leaderOrigin).IsLengthGreaterThan( followRange )) + { + me->Follow( leader ); + return; + } + } + + // if we see a nearby buddy in combat, join him + /// @todo - Perhaps tie in to TakeDamage(), so it works for human players, too + + // + // Scenario logic + // + switch( TheCSBots()->GetScenario() ) + { + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + if (me->GetTeamNumber() == TEAM_CT) + { + // if we are just holding position (due to a radio order) and the bomb has just planted, go defuse it + if (me->GetTask() == CCSBot::HOLD_POSITION && + TheCSBots()->IsBombPlanted() && + TheCSBots()->GetBombPlantTimestamp() > me->GetStateTimestamp()) + { + me->Idle(); + return; + } + + // if we are guarding the defuser and he dies/gives up, stop hiding (to choose another defuser) + if (me->GetTask() == CCSBot::GUARD_BOMB_DEFUSER && TheCSBots()->GetBombDefuser() == NULL) + { + me->Idle(); + return; + } + + // if we are guarding the loose bomb and it is picked up, stop hiding + if (me->GetTask() == CCSBot::GUARD_LOOSE_BOMB && TheCSBots()->GetLooseBomb() == NULL) + { + me->GetChatter()->TheyPickedUpTheBomb(); + me->Idle(); + return; + } + + // if we are guarding a bombsite and the bomb is dropped and we hear about it, stop guarding + if (me->GetTask() == CCSBot::GUARD_BOMB_ZONE && me->GetGameState()->IsLooseBombLocationKnown()) + { + me->Idle(); + return; + } + + // if we are guarding (bombsite, initial encounter, etc) and the bomb is planted, go defuse it + if (me->IsDoingScenario() && me->GetTask() != CCSBot::GUARD_BOMB_DEFUSER && TheCSBots()->IsBombPlanted()) + { + me->Idle(); + return; + } + + } + else // TERRORIST + { + // if we are near the ticking bomb and someone starts defusing it, attack! + if (TheCSBots()->GetBombDefuser()) + { + Vector defuserOrigin = GetCentroid( TheCSBots()->GetBombDefuser() ); + Vector toDefuser = defuserOrigin - myOrigin; + + const float hearDefuseRange = 2000.0f; + if (toDefuser.IsLengthLessThan( hearDefuseRange )) + { + // if we are nearby, attack, otherwise move to the bomb (which will cause us to attack when we see defuser) + if (me->CanSeePlantedBomb()) + { + me->Attack( TheCSBots()->GetBombDefuser() ); + } + else + { + me->MoveTo( defuserOrigin, FASTEST_ROUTE ); + me->InhibitLookAround( 10.0f ); + } + + return; + } + } + } + break; + } + + //-------------------------------------------------------------------------------------------------- + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + // if we're guarding the hostages and they all die or are taken, do something else + if (me->GetTask() == CCSBot::GUARD_HOSTAGES) + { + if (me->GetGameState()->AreAllHostagesBeingRescued() || me->GetGameState()->AreAllHostagesGone()) + { + me->Idle(); + return; + } + } + else if (me->GetTask() == CCSBot::GUARD_HOSTAGE_RESCUE_ZONE) + { + // if we stumble across a hostage, guard it + CHostage *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + // we see a free hostage, guard it + Vector hostageOrigin = GetCentroid( hostage ); + CNavArea *area = TheNavMesh->GetNearestNavArea( hostageOrigin ); + if (area) + { + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm guarding hostages I found\n" ); + // don't chatter here - he'll tell us when he's in his hiding spot + return; + } + } + } + } + } + + + bool isSettledInSniper = (me->IsSniper() && m_isAtSpot) ? true : false; + + // only investigate noises if we are initiating attacks, and we aren't a "settled in" sniper + // dont investigate noises if we are reloading + if (!me->IsReloading() && + !isSettledInSniper && + me->GetDisposition() == CCSBot::ENGAGE_AND_INVESTIGATE) + { + // if we are holding position, and have heard the enemy nearby, investigate after our hold time is up + if (m_isHoldingPosition && m_heardEnemy && (gpGlobals->curtime - m_firstHeardEnemyTime > m_holdPositionTime)) + { + /// @todo We might need to remember specific location of last enemy noise here + me->InvestigateNoise(); + return; + } + + // investigate nearby enemy noises + if (me->HeardInterestingNoise()) + { + // if we are holding position, check if enough time has elapsed since we first heard the enemy + if (m_isAtSpot && m_isHoldingPosition) + { + if (!m_heardEnemy) + { + // first time we heard the enemy + m_heardEnemy = true; + m_firstHeardEnemyTime = gpGlobals->curtime; + me->PrintIfWatched( "Heard enemy, holding position for %f2.1 seconds...\n", m_holdPositionTime ); + } + } + else + { + // not holding position - investigate enemy noise + me->InvestigateNoise(); + return; + } + } + } + } // end reloading check + + // look around + me->UpdateLookAround(); + + // if we are at our hiding spot, crouch and wait + if (m_isAtSpot) + { + me->ResetStuckMonitor(); + + CNavArea *area = TheNavMesh->GetNavArea( m_hidingSpot ); + if ( !area || !( area->GetAttributes() & NAV_MESH_STAND ) ) + { + me->Crouch(); + } + + // check if duration has expired + if (m_hideTimer.IsElapsed()) + { + if (me->GetTask() == CCSBot::GUARD_LOOSE_BOMB) + { + // if we're guarding the loose bomb, continue to guard it but pick a new spot + me->Hide( TheCSBots()->GetLooseBombArea() ); + return; + } + else if (me->GetTask() == CCSBot::GUARD_BOMB_ZONE) + { + // if we're guarding a bombsite, continue to guard it but pick a new spot + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( myOrigin ); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + me->Hide( area ); + return; + } + } + } + else if (me->GetTask() == CCSBot::GUARD_HOSTAGE_RESCUE_ZONE) + { + // if we're guarding a rescue zone, continue to guard this or another rescue zone + if (me->GuardRandomZone()) + { + me->SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + me->PrintIfWatched( "Continuing to guard hostage rescue zones\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->GetChatter()->GuardingHostageEscapeZone( IS_PLAN ); + return; + } + } + + me->Idle(); + return; + } + +/* + // if we are watching for an approaching noisy enemy, anticipate and fire before they round the corner + /// @todo Need to check if we are looking at an ENEMY_NOISE here + const float veryCloseNoise = 250.0f; + if (me->IsLookingAtSpot() && me->GetNoiseRange() < veryCloseNoise) + { + // fire! + me->PrimaryAttack(); + me->PrintIfWatched( "Firing at anticipated enemy coming around the corner!\n" ); + } +*/ + + // if we have a shield, hide behind it + if (me->HasShield() && !me->IsProtectedByShield()) + me->SecondaryAttack(); + + // while sitting at our hiding spot, if we are being attacked but can't see our attacker, move somewhere else + const float hurtRecentlyTime = 1.0f; + if (!me->IsEnemyVisible() && me->GetTimeSinceAttacked() < hurtRecentlyTime) + { + me->Idle(); + return; + } + + // encourage the human player + if (!me->IsDoingScenario()) + { + if (me->GetTeamNumber() == TEAM_CT) + { + if (me->GetTask() == CCSBot::GUARD_BOMB_ZONE && + me->IsAtHidingSpot() && + TheCSBots()->IsBombPlanted()) + { + if (me->GetNearbyEnemyCount() == 0) + { + const float someTime = 30.0f; + const float littleTime = 11.0; + + if (TheCSBots()->GetBombTimeLeft() > someTime) + me->GetChatter()->Encourage( "BombsiteSecure", RandomFloat( 10.0f, 15.0f ) ); + else if (TheCSBots()->GetBombTimeLeft() > littleTime) + me->GetChatter()->Encourage( "WaitingForHumanToDefuseBomb", RandomFloat( 5.0f, 8.0f ) ); + else + me->GetChatter()->Encourage( "WaitingForHumanToDefuseBombPanic", RandomFloat( 3.0f, 4.0f ) ); + } + } + + if (me->GetTask() == CCSBot::GUARD_HOSTAGES && me->IsAtHidingSpot()) + { + if (me->GetNearbyEnemyCount() == 0) + { + CHostage *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + me->GetChatter()->Encourage( "WaitingForHumanToRescueHostages", RandomFloat( 10.0f, 15.0f ) ); + } + } + } + } + } + } + else + { + // we are moving to our hiding spot + + // snipers periodically pause and fire while retreating + if (me->IsSniper() && me->IsEnemyVisible()) + { + if (m_isPaused) + { + if (m_pauseTimer.IsElapsed()) + { + // get moving + m_isPaused = false; + m_pauseTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + } + else + { + me->Wait( 0.2f ); + } + } + else + { + if (m_pauseTimer.IsElapsed()) + { + // pause for a moment + m_isPaused = true; + m_pauseTimer.Start( RandomFloat( 0.5f, 1.5f ) ); + } + } + } + + // if a Player is using this hiding spot, give up + float range; + CCSPlayer *camper = static_cast<CCSPlayer *>( UTIL_GetClosestPlayer( m_hidingSpot, &range ) ); + + const float closeRange = 75.0f; + if (camper && camper != me && range < closeRange && me->IsVisible( camper, CHECK_FOV )) + { + // player is in our hiding spot + me->PrintIfWatched( "Someone's in my hiding spot - picking another...\n" ); + + const int maxRetries = 3; + if (m_retry++ >= maxRetries) + { + me->PrintIfWatched( "Can't find a free hiding spot, giving up.\n" ); + me->Idle(); + return; + } + + // pick another hiding spot near where we were planning on hiding + me->Hide( TheNavMesh->GetNavArea( m_hidingSpot ) ); + + return; + } + + Vector toSpot; + toSpot.x = m_hidingSpot.x - myOrigin.x; + toSpot.y = m_hidingSpot.y - myOrigin.y; + toSpot.z = m_hidingSpot.z - me->GetFeetZ(); // use feet location + range = toSpot.Length(); + + // look outwards as we get close to our hiding spot + if (!me->IsEnemyVisible() && !m_isLookingOutward) + { + const float lookOutwardRange = 200.0f; + const float nearSpotRange = 10.0f; + if (range < lookOutwardRange && range > nearSpotRange) + { + m_isLookingOutward = true; + + toSpot.x /= range; + toSpot.y /= range; + toSpot.z /= range; + + me->SetLookAt( "Face outward", me->EyePosition() - 1000.0f * toSpot, PRIORITY_HIGH, 3.0f ); + } + } + + const float atDist = 20.0f; + if (range < atDist) + { + //------------------------------------- + // Just reached our hiding spot + // + m_isAtSpot = true; + m_hideTimer.Start( m_duration ); + + // make sure our approach points are valid, since we'll be watching them + me->ComputeApproachPoints(); + me->ClearLookAt(); + + // ready our weapon and prepare to attack + me->EquipBestWeapon( me->IsUsingGrenade() ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + + // if we are a sniper, update our task + if (me->GetTask() == CCSBot::MOVE_TO_SNIPER_SPOT) + { + me->SetTask( CCSBot::SNIPING ); + } + else if (me->GetTask() == CCSBot::GUARD_INITIAL_ENCOUNTER) + { + const float campChatterChance = 20.0f; + if (RandomFloat( 0, 100 ) < campChatterChance) + { + me->GetChatter()->Say( "WaitingHere" ); + } + } + + + // determine which way to look + trace_t result; + float outAngle = 0.0f; + float outAngleRange = 0.0f; + for( float angle = 0.0f; angle < 360.0f; angle += 45.0f ) + { + UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 1000.0f * Vector( BotCOS(angle), BotSIN(angle), 0.0f ), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &result ); + + if (result.fraction > outAngleRange) + { + outAngle = angle; + outAngleRange = result.fraction; + } + } + + me->SetLookAheadAngle( outAngle ); + + } + + // move to hiding spot + if (me->UpdatePathMovement() != CCSBot::PROGRESSING && !m_isAtSpot) + { + // we couldn't get to our hiding spot - pick another + me->PrintIfWatched( "Can't get to my hiding spot - finding another...\n" ); + + // search from hiding spot, since we know it was valid + const Vector *pos = FindNearbyHidingSpot( me, m_hidingSpot, m_range, me->IsSniper() ); + if (pos == NULL) + { + // no available hiding spots + me->PrintIfWatched( "No available hiding spots - hiding where I'm at.\n" ); + + // hide where we are + m_hidingSpot.x = myOrigin.x; + m_hidingSpot.x = myOrigin.y; + m_hidingSpot.z = me->GetFeetZ(); + } + else + { + m_hidingSpot = *pos; + } + + // build a path to our new hiding spot + if (me->ComputePath( m_hidingSpot, FASTEST_ROUTE ) == false) + { + me->PrintIfWatched( "Can't pathfind to hiding spot\n" ); + me->Idle(); + return; + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void HideState::OnExit( CCSBot *me ) +{ + m_isHoldingPosition = false; + + me->StandUp(); + me->ResetStuckMonitor(); + //me->ClearLookAt(); + me->ClearApproachPoints(); + + // if we have a shield, put it away + if (me->HasShield() && me->IsProtectedByShield()) + me->SecondaryAttack(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_hunt.cpp b/game/server/cstrike/bot/states/cs_bot_hunt.cpp new file mode 100644 index 0000000..9ca6c90 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_hunt.cpp @@ -0,0 +1,234 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin the hunt + */ +void HuntState::OnEnter( CCSBot *me ) +{ + // lurking death + if (me->IsUsingKnife() && me->IsWellPastSafe() && !me->IsHurrying()) + me->Walk(); + else + me->Run(); + + + me->StandUp(); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + me->SetTask( CCSBot::SEEK_AND_DESTROY ); + + me->DestroyPath(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Hunt down our enemies + */ +void HuntState::OnUpdate( CCSBot *me ) +{ + // if we've been hunting for a long time, drop into Idle for a moment to + // select something else to do + const float huntingTooLongTime = 30.0f; + if (gpGlobals->curtime - me->GetStateTimestamp() > huntingTooLongTime) + { + // stop being a rogue and do the scenario, since there must not be many enemies left to hunt + me->PrintIfWatched( "Giving up hunting.\n" ); + me->SetRogue( false ); + me->Idle(); + return; + } + + // scenario logic + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB) + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + // if we have the bomb and it's time to plant, or we happen to be in a bombsite and it seems safe, do it + if (me->HasC4()) + { + const float safeTime = 3.0f; + + if (TheCSBots()->IsTimeToPlantBomb() || + (me->IsAtBombsite() && gpGlobals->curtime - me->GetLastSawEnemyTimestamp() > safeTime)) + { + me->Idle(); + return; + } + } + + // if we notice the bomb lying on the ground, go get it + if (me->NoticeLooseBomb()) + { + me->FetchBomb(); + return; + } + + // if bomb has been planted, and we hear it, move to a hiding spot near the bomb and watch it + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (!me->IsRogue() && me->GetGameState()->IsBombPlanted() && bombPos) + { + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + return; + } + } + else // CT + { + if (!me->IsRogue() && me->CanSeeLooseBomb()) + { + // if we are near the loose bomb and can see it, hide nearby and guard it + me->SetTask( CCSBot::GUARD_LOOSE_BOMB ); + me->Hide( TheCSBots()->GetLooseBombArea() ); + me->GetChatter()->GuardingLooseBomb( TheCSBots()->GetLooseBomb() ); + return; + } + else if (TheCSBots()->IsBombPlanted()) + { + // rogues will defuse a bomb, but not guard the defuser + if (!me->IsRogue() || !TheCSBots()->GetBombDefuser()) + { + // search for the planted bomb to defuse + me->Idle(); + return; + } + } + } + } + else if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_RESCUE_HOSTAGES) + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + if (me->GetGameState()->AreAllHostagesBeingRescued()) + { + // all hostages are being rescued, head them off at the escape zones + if (me->GuardRandomZone()) + { + me->SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + me->PrintIfWatched( "Trying to beat them to an escape zone!\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->GetChatter()->GuardingHostageEscapeZone( IS_PLAN ); + return; + } + } + + // if safe time is up, and we stumble across a hostage, guard it + if (!me->IsRogue() && !me->IsSafe()) + { + CHostage *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ); + if (area) + { + // we see a free hostage, guard it + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm guarding hostages\n" ); + me->GetChatter()->GuardingHostages( area->GetPlace(), IS_PLAN ); + return; + } + } + } + } + } + + // listen for enemy noises + if (me->HeardInterestingNoise()) + { + me->InvestigateNoise(); + return; + } + + // look around + me->UpdateLookAround(); + + // if we have reached our destination area, pick a new one + // if our path fails, pick a new one + if (me->GetLastKnownArea() == m_huntArea || me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + // pick a new hunt area + const float earlyGameTime = 45.0f; + if (TheCSBots()->GetElapsedRoundTime() < earlyGameTime && !me->HasVisitedEnemySpawn()) + { + // in the early game, rush the enemy spawn + CBaseEntity *enemySpawn = TheCSBots()->GetRandomSpawn( OtherTeam( me->GetTeamNumber() ) ); + + //ADRIAN: REVISIT + if ( enemySpawn ) + { + m_huntArea = TheNavMesh->GetNavArea( enemySpawn->WorldSpaceCenter() ); + } + } + else + { + m_huntArea = NULL; + float oldest = 0.0f; + + int areaCount = 0; + const float minSize = 150.0f; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + ++areaCount; + + // skip the small areas + Extent extent; + area->GetExtent(&extent); + if (extent.hi.x - extent.lo.x < minSize || extent.hi.y - extent.lo.y < minSize) + continue; + + // keep track of the least recently cleared area + float age = gpGlobals->curtime - area->GetClearedTimestamp( me->GetTeamNumber()-1 ); + if (age > oldest) + { + oldest = age; + m_huntArea = area; + } + } + + // if all the areas were too small, pick one at random + int which = RandomInt( 0, areaCount-1 ); + + areaCount = 0; + FOR_EACH_VEC( TheNavAreas, hit ) + { + m_huntArea = TheNavAreas[ hit ]; + + if (which == areaCount) + break; + + --which; + } + } + + if (m_huntArea) + { + // create a new path to a far away area of the map + me->ComputePath( m_huntArea->GetCenter() ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Done hunting + */ +void HuntState::OnExit( CCSBot *me ) +{ +} diff --git a/game/server/cstrike/bot/states/cs_bot_idle.cpp b/game/server/cstrike/bot/states/cs_bot_idle.cpp new file mode 100644 index 0000000..64e14d2 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_idle.cpp @@ -0,0 +1,887 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// range for snipers to select a hiding spot +const float sniperHideRange = 2000.0f; + +//-------------------------------------------------------------------------------------------------------------- +/** + * The Idle state. + * We never stay in the Idle state - it is a "home base" for the state machine that + * does various checks to determine what we should do next. + */ +void IdleState::OnEnter( CCSBot *me ) +{ + me->DestroyPath(); + me->SetBotEnemy( NULL ); + + // lurking death + if (me->IsUsingKnife() && me->IsWellPastSafe() && !me->IsHurrying()) + me->Walk(); + + // + // Since Idle assigns tasks, we assume that coming back to Idle means our task is complete + // + me->SetTask( CCSBot::SEEK_AND_DESTROY ); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine what we should do next + */ +void IdleState::OnUpdate( CCSBot *me ) +{ + // all other states assume GetLastKnownArea() is valid, ensure that it is + if (me->GetLastKnownArea() == NULL && me->StayOnNavMesh() == false) + return; + + // zombies never leave the Idle state + if (cv_bot_zombie.GetBool()) + { + me->ResetStuckMonitor(); + return; + } + + // if we are in the early "safe" time, grab a knife or grenade + if (me->IsSafe()) + { + // if we have a grenade, use it + if (!me->EquipGrenade()) + { + // high-skill bots run with the knife, unless using the Scout (which moves faster) + if (me->GetProfile()->GetSkill() > 0.33f && !me->IsUsing( WEAPON_SCOUT )) + { + me->EquipKnife(); + } + } + } + + // if round is over, hunt + if (me->GetGameState()->IsRoundOver()) + { + // if we are escorting hostages, try to get to the rescue zone + if (me->GetHostageEscortCount()) + { + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me, FASTEST_ROUTE ) ); + const Vector *zonePos = TheCSBots()->GetRandomPositionInZone( zone ); + + if (zonePos) + { + me->SetTask( CCSBot::RESCUE_HOSTAGES ); + me->Run(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + me->MoveTo( *zonePos, FASTEST_ROUTE ); + me->PrintIfWatched( "Trying to rescue hostages at the end of the round\n" ); + return; + } + } + + me->Hunt(); + return; + } + + const float defenseSniperCampChance = 75.0f; + const float offenseSniperCampChance = 10.0f; + + // if we were following someone, continue following them + if (me->IsFollowing()) + { + me->ContinueFollowing(); + return; + } + + // + // Scenario logic + // + switch (TheCSBots()->GetScenario()) + { + //====================================================================================================== + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + // if this is a bomb game and we have the bomb, go plant it + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + if (me->GetGameState()->IsBombPlanted()) + { + if (me->GetGameState()->GetPlantedBombsite() != CSGameState::UNKNOWN) + { + // T's always know where the bomb is - go defend it + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( me->GetGameState()->GetPlantedBombsite() ); + if (zone) + { + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + + Place place = TheNavMesh->GetPlace( zone->m_center ); + if (place != UNDEFINED_PLACE) + { + // pick a random hiding spot in this place + const Vector *spot = FindRandomHidingSpot( me, place, me->IsSniper() ); + if (spot) + { + me->Hide( *spot ); + return; + } + } + + // hide nearby + me->Hide( TheNavMesh->GetNearestNavArea( zone->m_center ) ); + return; + } + } + else + { + // ask our teammates where the bomb is + me->GetChatter()->RequestBombLocation(); + + // we dont know where the bomb is - we must search the bombsites + int zoneIndex = me->GetGameState()->GetNextBombsiteToSearch(); + + // move to bombsite - if we reach it, we'll update its cleared status, causing us to select another + const Vector *pos = TheCSBots()->GetRandomPositionInZone( TheCSBots()->GetZone( zoneIndex ) ); + if (pos) + { + me->SetTask( CCSBot::FIND_TICKING_BOMB ); + me->MoveTo( *pos ); + return; + } + } + } + else if (me->HasC4()) + { + // if we're at a bomb site, plant the bomb + if (me->IsAtBombsite()) + { + // plant it + me->SetTask( CCSBot::PLANT_BOMB ); + me->PlantBomb(); + + // radio to the team + me->GetChatter()->PlantingTheBomb( me->GetPlace() ); + + return; + } + else if (TheCSBots()->IsTimeToPlantBomb()) + { + // move to the closest bomb site + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me ) ); + if (zone) + { + // pick a random spot within the bomb zone + const Vector *pos = TheCSBots()->GetRandomPositionInZone( zone ); + if (pos) + { + // move to bombsite + me->SetTask( CCSBot::PLANT_BOMB ); + me->Run(); + me->MoveTo( *pos ); + + return; + } + } + } + } + else + { + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // small chance of sniper camping on offense, if we aren't carrying the bomb + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + + // if the bomb is loose (on the ground), go get it + if (me->NoticeLooseBomb()) + { + me->FetchBomb(); + return; + } + + // if bomb has been planted, and we hear it, move to a hiding spot near the bomb and guard it + if (!me->IsRogue() && me->GetGameState()->IsBombPlanted() && me->GetGameState()->GetBombPosition()) + { + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + + if (bombPos) + { + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + return; + } + } + } + } + else // CT ------------------------------------------------------------------------------------------ + { + if (me->GetGameState()->IsBombPlanted()) + { + // if the bomb has been planted, attempt to defuse it + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (bombPos) + { + // if someone is defusing the bomb, guard them + if (TheCSBots()->GetBombDefuser()) + { + if (!me->IsRogue()) + { + me->SetTask( CCSBot::GUARD_BOMB_DEFUSER ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + return; + } + } + else if (me->IsDoingScenario()) + { + // move to the bomb and defuse it + me->SetTask( CCSBot::DEFUSE_BOMB ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->MoveTo( *bombPos ); + return; + } + else + { + // we're not allowed to defuse, guard the bomb zone + me->SetTask( CCSBot::GUARD_BOMB_ZONE ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + else if (me->GetGameState()->GetPlantedBombsite() != CSGameState::UNKNOWN) + { + // we know which bombsite, but not exactly where the bomb is, go there + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( me->GetGameState()->GetPlantedBombsite() ); + if (zone) + { + if (me->IsDoingScenario()) + { + me->SetTask( CCSBot::DEFUSE_BOMB ); + me->MoveTo( zone->m_center ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + else + { + // we're not allowed to defuse, guard the bomb zone + me->SetTask( CCSBot::GUARD_BOMB_ZONE ); + me->Hide( TheNavMesh->GetNavArea( zone->m_center ) ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + else + { + // we dont know where the bomb is - we must search the bombsites + + // find closest un-cleared bombsite + const CCSBotManager::Zone *zone = NULL; + float travelDistance = 9999999.9f; + + for( int z=0; z<TheCSBots()->GetZoneCount(); ++z ) + { + if (TheCSBots()->GetZone(z)->m_areaCount == 0) + continue; + + // don't check bombsites that have been cleared + if (me->GetGameState()->IsBombsiteClear( z )) + continue; + + // just use the first overlapping nav area as a reasonable approximation + ShortestPathCost cost = ShortestPathCost(); + float dist = NavAreaTravelDistance( me->GetLastKnownArea(), + TheNavMesh->GetNearestNavArea( TheCSBots()->GetZone(z)->m_center ), + cost ); + + if (dist >= 0.0f && dist < travelDistance) + { + zone = TheCSBots()->GetZone(z); + travelDistance = dist; + } + } + + + if (zone) + { + const float farAwayRange = 2000.0f; + if (travelDistance > farAwayRange) + { + zone = NULL; + } + } + + // if closest bombsite is "far away", pick one at random + if (zone == NULL) + { + int zoneIndex = me->GetGameState()->GetNextBombsiteToSearch(); + zone = TheCSBots()->GetZone( zoneIndex ); + } + + // move to bombsite - if we reach it, we'll update its cleared status, causing us to select another + if (zone) + { + const Vector *pos = TheCSBots()->GetRandomPositionInZone( zone ); + if (pos) + { + me->SetTask( CCSBot::FIND_TICKING_BOMB ); + me->MoveTo( *pos ); + return; + } + } + } + AssertMsg( 0, "A CT bot doesn't know what to do while the bomb is planted!\n" ); + } + + + // if we have a sniper rifle, we like to camp, whether rogue or not + if (me->IsSniper() && !me->IsSafe()) + { + if (RandomFloat( 0, 100 ) <= defenseSniperCampChance) + { + CNavArea *snipingArea = NULL; + + // if the bomb is loose, snipe near it + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (me->GetGameState()->IsLooseBombLocationKnown() && bombPos) + { + snipingArea = TheNavMesh->GetNearestNavArea( *bombPos ); + me->PrintIfWatched( "Sniping near loose bomb\n" ); + } + else + { + // snipe bomb zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + snipingArea = TheCSBots()->GetRandomAreaInZone( zone ); + me->PrintIfWatched( "Sniping near bombsite\n" ); + } + } + + if (snipingArea) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( snipingArea, -1.0, sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // rogues just hunt, unless they want to snipe + // if the whole team has decided to rush, hunt + // if we know the bomb is dropped, hunt for enemies and the loose bomb + if (me->IsRogue() || TheCSBots()->IsDefenseRushing() || me->GetGameState()->IsLooseBombLocationKnown()) + { + me->Hunt(); + return; + } + + // the lower our morale gets, the more we want to camp the bomb zone(s) + // only decide to camp at the start of the round, or if we haven't seen anything for a long time + if (me->IsSafe() || me->HasNotSeenEnemyForLongTime()) + { + float guardBombsiteChance = -34.0f * me->GetMorale(); + + if (RandomFloat( 0.0f, 100.0f ) < guardBombsiteChance) + { + float guardRange = 500.0f + 100.0f * (me->GetMorale() + 3); + + // guard bomb zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + me->PrintIfWatched( "I'm guarding a bombsite\n" ); + me->GetChatter()->GuardingBombsite( area->GetPlace() ); + me->SetTask( CCSBot::GUARD_BOMB_ZONE ); + me->Hide( area, -1.0, guardRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + } + } + + break; + } + + //====================================================================================================== + case CCSBotManager::SCENARIO_ESCORT_VIP: + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + // if we have a sniper rifle, we like to camp, whether rogue or not + if (me->IsSniper()) + { + if (RandomFloat( 0, 100 ) <= defenseSniperCampChance) + { + // snipe escape zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( area, -1.0, sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping near escape zone\n" ); + return; + } + } + } + } + + // rogues just hunt, unless they want to snipe + // if the whole team has decided to rush, hunt + if (me->IsRogue() || TheCSBots()->IsDefenseRushing()) + break; + + // the lower our morale gets, the more we want to camp the escape zone(s) + float guardEscapeZoneChance = -34.0f * me->GetMorale(); + + if (RandomFloat( 0.0f, 100.0f ) < guardEscapeZoneChance) + { + // guard escape zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + // guard the escape zone - stay closer if our morale is low + me->SetTask( CCSBot::GUARD_VIP_ESCAPE_ZONE ); + me->PrintIfWatched( "I'm guarding an escape zone\n" ); + + float escapeGuardRange = 750.0f + 250.0f * (me->GetMorale() + 3); + me->Hide( area, -1.0, escapeGuardRange ); + + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + } + else // CT + { + if (me->m_bIsVIP) + { + // if early in round, pick a random zone, otherwise pick closest zone + const float earlyTime = 20.0f; + const CCSBotManager::Zone *zone = NULL; + + if (TheCSBots()->GetElapsedRoundTime() < earlyTime) + { + // pick random zone + zone = TheCSBots()->GetRandomZone(); + } + else + { + // pick closest zone + zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me ) ); + } + + if (zone) + { + // pick a random spot within the escape zone + const Vector *pos = TheCSBots()->GetRandomPositionInZone( zone ); + if (pos) + { + // move to escape zone + me->SetTask( CCSBot::VIP_ESCAPE ); + me->Run(); + me->MoveTo( *pos ); + + // tell team to follow + const float repeatTime = 30.0f; + if (me->GetFriendsRemaining() && + TheCSBots()->GetRadioMessageInterval( RADIO_FOLLOW_ME, me->GetTeamNumber() ) > repeatTime) + me->SendRadioMessage( RADIO_FOLLOW_ME ); + return; + } + } + } + else + { + // small chance of sniper camping on offense, if we aren't VIP + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + } + } + break; + } + + //====================================================================================================== + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + bool campHostages; + + // if we are in early game, camp the hostages + if (me->IsSafe()) + { + campHostages = true; + } + else if (me->GetGameState()->HaveSomeHostagesBeenTaken() || me->GetGameState()->AreAllHostagesBeingRescued()) + { + campHostages = false; + } + else + { + // later in the game, camp either hostages or escape zone + const float campZoneChance = 100.0f * (TheCSBots()->GetElapsedRoundTime() - me->GetSafeTime())/120.0f; + + campHostages = (RandomFloat( 0, 100 ) > campZoneChance) ? true : false; + } + + + // if we have a sniper rifle, we like to camp, whether rogue or not + if (me->IsSniper()) + { + // the at start of the round, snipe the initial rush + if (me->IsSafe()) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm sniping an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + + if (RandomFloat( 0, 100 ) <= defenseSniperCampChance) + { + const Vector *hostagePos = me->GetGameState()->GetRandomFreeHostagePosition(); + if (hostagePos && campHostages) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->PrintIfWatched( "Sniping near hostages\n" ); + me->Hide( TheNavMesh->GetNearestNavArea( *hostagePos ), -1.0, sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + else + { + // camp the escape zone(s) + if (me->GuardRandomZone( sniperHideRange )) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->PrintIfWatched( "Sniping near a rescue zone\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + } + + // if safe time is up, and we stumble across a hostage, guard it + if (!me->IsSafe() && !me->IsRogue()) + { + CBaseEntity *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + // we see a free hostage, guard it + CNavArea *area = TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ); + if (area) + { + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm guarding hostages I found\n" ); + // don't chatter here - he'll tell us when he's in his hiding spot + return; + } + } + } + + + // decide if we want to hunt, or guard + const float huntChance = 70.0f + 25.0f * me->GetMorale(); + + // rogues just hunt, unless they want to snipe + // if the whole team has decided to rush, hunt + if (me->GetFriendsRemaining()) + { + if (me->IsRogue() || TheCSBots()->IsDefenseRushing() || RandomFloat( 0, 100 ) < huntChance) + { + me->Hunt(); + return; + } + } + + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + + // decide whether to camp the hostages or the escape zones + const Vector *hostagePos = me->GetGameState()->GetRandomFreeHostagePosition(); + if (hostagePos && campHostages) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( *hostagePos ); + if (area) + { + // guard the hostages - stay closer to hostages if our morale is low + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->PrintIfWatched( "I'm guarding hostages\n" ); + + float hostageGuardRange = 750.0f + 250.0f * (me->GetMorale() + 3); // 2000 + me->Hide( area, -1.0, hostageGuardRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + + if (RandomFloat( 0, 100 ) < 50) + me->GetChatter()->GuardingHostages( area->GetPlace(), IS_PLAN ); + + return; + } + } + + // guard rescue zone(s) + if (me->GuardRandomZone()) + { + me->SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + me->PrintIfWatched( "I'm guarding a rescue zone\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->GetChatter()->GuardingHostageEscapeZone( IS_PLAN ); + return; + } + } + else // CT --------------------------------------------------------------------------------- + { + // only decide to do something else if we aren't already rescuing hostages + if (!me->GetHostageEscortCount()) + { + // small chance of sniper camping on offense + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + + if (me->GetFriendsRemaining() && !me->GetHostageEscortCount()) + { + // rogues just hunt, unless all friends are dead + // if we have friends left, we might go hunting instead of hostage rescuing + const float huntChance = 33.3f; + if (me->IsRogue() || RandomFloat( 0.0f, 100.0f ) < huntChance) + { + me->Hunt(); + return; + } + } + } + + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // look for free hostages - CT's have radar so they know where hostages are at all times + CHostage *hostage = me->GetGameState()->GetNearestFreeHostage(); + + // if we are not allowed to do the scenario, guard the hostages to clear the area for the human(s) + if (!me->IsDoingScenario()) + { + if (hostage) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ); + if (area) + { + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm securing the hostages for a human to rescue\n" ); + return; + } + } + + me->Hunt(); + return; + } + + + bool fetchHostages = false; + bool rescueHostages = false; + const CCSBotManager::Zone *zone = NULL; + me->SetGoalEntity( NULL ); + + // if we are escorting hostages, determine where to take them + if (me->GetHostageEscortCount()) + zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me, FASTEST_ROUTE ) ); + + // if we are escorting hostages and there are more hostages to rescue, + // determine whether it's faster to rescue the ones we have, or go get the remaining ones + if (hostage) + { + Vector hostageOrigin = GetCentroid( hostage ); + + if (zone) + { + PathCost cost( me, FASTEST_ROUTE ); + float toZone = NavAreaTravelDistance( me->GetLastKnownArea(), zone->m_area[0], cost ); + float toHostage = NavAreaTravelDistance( me->GetLastKnownArea(), TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ), cost ); + + if (toHostage < 0.0f) + { + rescueHostages = true; + } + else + { + if (toZone < toHostage) + rescueHostages = true; + else + fetchHostages = true; + } + } + else + { + fetchHostages = true; + } + } + else if (zone) + { + rescueHostages = true; + } + + + if (fetchHostages) + { + // go get hostages + me->SetTask( CCSBot::COLLECT_HOSTAGES ); + me->Run(); + me->SetGoalEntity( hostage ); + me->ResetWaitForHostagePatience(); + + // if we already have some hostages, move to the others by the quickest route + RouteType route = (me->GetHostageEscortCount()) ? FASTEST_ROUTE : SAFEST_ROUTE; + me->MoveTo( GetCentroid( hostage ), route ); + + me->PrintIfWatched( "I'm collecting hostages\n" ); + return; + } + + const Vector *zonePos = TheCSBots()->GetRandomPositionInZone( zone ); + if (rescueHostages && zonePos) + { + me->SetTask( CCSBot::RESCUE_HOSTAGES ); + me->Run(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + me->MoveTo( *zonePos, FASTEST_ROUTE ); + me->PrintIfWatched( "I'm rescuing hostages\n" ); + me->GetChatter()->EscortingHostages(); + return; + } + } + break; + } + + default: // deathmatch + { + // sniping check + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + break; + } + } + + // if we have nothing special to do, go hunting for enemies + me->Hunt(); +} + diff --git a/game/server/cstrike/bot/states/cs_bot_investigate_noise.cpp b/game/server/cstrike/bot/states/cs_bot_investigate_noise.cpp new file mode 100644 index 0000000..9aa3dd0 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_investigate_noise.cpp @@ -0,0 +1,139 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move towards currently heard noise + */ +void InvestigateNoiseState::AttendCurrentNoise( CCSBot *me ) +{ + if (!me->IsNoiseHeard() && me->GetNoisePosition()) + return; + + // remember where the noise we heard was + m_checkNoisePosition = *me->GetNoisePosition(); + + // tell our teammates (unless the noise is obvious, like gunfire) + if (me->IsWellPastSafe() && me->HasNotSeenEnemyForLongTime() && me->GetNoisePriority() != PRIORITY_HIGH) + me->GetChatter()->HeardNoise( *me->GetNoisePosition() ); + + // figure out how to get to the noise + me->PrintIfWatched( "Attending to noise...\n" ); + me->ComputePath( m_checkNoisePosition, FASTEST_ROUTE ); + + const float minAttendTime = 3.0f; + const float maxAttendTime = 10.0f; + m_minTimer.Start( RandomFloat( minAttendTime, maxAttendTime ) ); + + // consume the noise + me->ForgetNoise(); +} + +//-------------------------------------------------------------------------------------------------------------- +void InvestigateNoiseState::OnEnter( CCSBot *me ) +{ + AttendCurrentNoise( me ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * @todo Use TravelDistance instead of distance... + */ +void InvestigateNoiseState::OnUpdate( CCSBot *me ) +{ + Vector myOrigin = GetCentroid( me ); + + // keep an ear out for closer noises... + if (m_minTimer.IsElapsed()) + { + const float nearbyRange = 500.0f; + if (me->HeardInterestingNoise() && me->GetNoiseRange() < nearbyRange) + { + // new sound is closer + AttendCurrentNoise( me ); + } + } + + + // if the pathfind fails, give up + if (!me->HasPath()) + { + me->Idle(); + return; + } + + // look around + me->UpdateLookAround(); + + // get distance remaining on our path until we reach the source of the noise + float range = me->GetPathDistanceRemaining(); + + if (me->IsUsingKnife()) + { + if (me->IsHurrying()) + me->Run(); + else + me->Walk(); + } + else + { + const float closeToNoiseRange = 1500.0f; + if (range < closeToNoiseRange) + { + // if we dont have many friends left, or we are alone, and we are near noise source, sneak quietly + if ((me->GetNearbyFriendCount() == 0 || me->GetFriendsRemaining() <= 2) && !me->IsHurrying()) + { + me->Walk(); + } + else + { + me->Run(); + } + } + else + { + me->Run(); + } + } + + + // if we can see the noise position and we're close enough to it and looking at it, + // we don't need to actually move there (it's checked enough) + const float closeRange = 500.0f; + if (range < closeRange) + { + if (me->IsVisible( m_checkNoisePosition, CHECK_FOV )) + { + // can see noise position + me->PrintIfWatched( "Noise location is clear.\n" ); + me->ForgetNoise(); + me->Idle(); + return; + } + } + + // move towards noise + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + me->Idle(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +void InvestigateNoiseState::OnExit( CCSBot *me ) +{ + // reset to run mode in case we were sneaking about + me->Run(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_move_to.cpp b/game/server/cstrike/bot/states/cs_bot_move_to.cpp new file mode 100644 index 0000000..eafba40 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_move_to.cpp @@ -0,0 +1,366 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" +#include "cs_gamerules.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a potentially far away position. + */ +void MoveToState::OnEnter( CCSBot *me ) +{ + if (me->IsUsingKnife() && me->IsWellPastSafe() && !me->IsHurrying()) + { + me->Walk(); + } + else + { + me->Run(); + } + + + // if we need to find the bomb, get there as quick as we can + RouteType route; + switch (me->GetTask()) + { + case CCSBot::FIND_TICKING_BOMB: + case CCSBot::DEFUSE_BOMB: + case CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION: + route = FASTEST_ROUTE; + break; + + default: + route = SAFEST_ROUTE; + break; + } + + // build path to, or nearly to, goal position + me->ComputePath( m_goalPosition, route ); + + m_radioedPlan = false; + m_askedForCover = false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a potentially far away position. + */ +void MoveToState::OnUpdate( CCSBot *me ) +{ + Vector myOrigin = GetCentroid( me ); + + // assume that we are paying attention and close enough to know our enemy died + if (me->GetTask() == CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION) + { + /// @todo Account for reaction time so we take some time to realized the enemy is dead + CBasePlayer *victim = static_cast<CBasePlayer *>( me->GetTaskEntity() ); + if (victim == NULL || !victim->IsAlive()) + { + me->PrintIfWatched( "The enemy I was chasing was killed - giving up.\n" ); + me->Idle(); + return; + } + } + + // look around + me->UpdateLookAround(); + + // + // Scenario logic + // + switch (TheCSBots()->GetScenario()) + { + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + // if the bomb has been planted, find it + // NOTE: This task is used by both CT and T's to find the bomb + if (me->GetTask() == CCSBot::FIND_TICKING_BOMB) + { + if (!me->GetGameState()->IsBombPlanted()) + { + // the bomb is not planted - give up this task + me->Idle(); + return; + } + + if (me->GetGameState()->GetPlantedBombsite() != CSGameState::UNKNOWN) + { + // we know where the bomb is planted, stop searching + me->Idle(); + return; + } + + // check off bombsites that we explore or happen to stumble into + for( int z=0; z<TheCSBots()->GetZoneCount(); ++z ) + { + // don't re-check zones + if (me->GetGameState()->IsBombsiteClear( z )) + continue; + + if (TheCSBots()->GetZone(z)->m_extent.Contains( myOrigin )) + { + // note this bombsite is clear + me->GetGameState()->ClearBombsite( z ); + + if (me->GetTeamNumber() == TEAM_CT) + { + // tell teammates this bombsite is clear + me->GetChatter()->BombsiteClear( z ); + } + + // find another zone to check + me->Idle(); + + return; + } + } + + // move to a bombsite + break; + } + + + if (me->GetTeamNumber() == TEAM_CT) + { + if (me->GetGameState()->IsBombPlanted()) + { + switch( me->GetTask() ) + { + case CCSBot::DEFUSE_BOMB: + { + // if we are near the bombsite and there is time left, sneak in (unless all enemies are dead) + if (me->GetEnemiesRemaining()) + { + const float plentyOfTime = 15.0f; + if (TheCSBots()->GetBombTimeLeft() > plentyOfTime) + { + // get distance remaining on our path until we reach the bombsite + float range = me->GetPathDistanceRemaining(); + + const float closeRange = 1500.0f; + if (range < closeRange) + { + me->Walk(); + } + else + { + me->Run(); + } + } + } + else + { + // everyone is dead - run! + me->Run(); + } + + // if we are trying to defuse the bomb, and someone has started defusing, guard them instead + if (me->CanSeePlantedBomb() && TheCSBots()->GetBombDefuser()) + { + me->GetChatter()->Say( "CoveringFriend" ); + me->Idle(); + return; + } + + + // if we are near the bomb, defuse it (if we are reloading, don't try to defuse until we finish) + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (bombPos && !me->IsReloading()) + { + const float defuseRange = 100.0f; // 50 + if ((*bombPos - me->EyePosition()).IsLengthLessThan( defuseRange )) + { + // make sure we can see the bomb + if (me->IsVisible( *bombPos )) + { + me->DefuseBomb(); + return; + } + } + } + + break; + } + + default: + { + // we need to find the bomb + me->Idle(); + return; + } + } + } + } + else // TERRORIST + { + if (me->GetTask() == CCSBot::PLANT_BOMB ) + { + if ( me->GetFriendsRemaining() ) + { + // if we are about to plant, radio for cover + if (!m_askedForCover) + { + const float nearPlantSite = 50.0f; + if (me->IsAtBombsite() && me->GetPathDistanceRemaining() < nearPlantSite) + { + // radio to the team + me->GetChatter()->PlantingTheBomb( me->GetPlace() ); + m_askedForCover = true; + } + + // after we have started to move to the bombsite, tell team we're going to plant, and where + // don't do this if we have already radioed that we are starting to plant + if (!m_radioedPlan) + { + const float radioTime = 2.0f; + if (gpGlobals->curtime - me->GetStateTimestamp() > radioTime) + { + // radio to the team if we're more than 10 seconds (2400 units) out + const float nearPlantSite = 2400.0f; + if ( me->GetPathDistanceRemaining() >= nearPlantSite ) + { + me->GetChatter()->GoingToPlantTheBomb( TheNavMesh->GetPlace( m_goalPosition ) ); + } + m_radioedPlan = true; + } + } + } + } + } + } + break; + } + + //-------------------------------------------------------------------------------------------------- + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + if (me->GetTask() == CCSBot::COLLECT_HOSTAGES) + { + // + // Since CT's have a radar, they can directly look at the actual hostage state + // + + // check if someone else collected our hostage, or the hostage died or was rescued + CHostage *hostage = static_cast<CHostage *>( me->GetGoalEntity() ); + if (hostage == NULL || !hostage->IsValid() || hostage->IsFollowingSomeone()) + { + me->Idle(); + return; + } + + Vector hostageOrigin = GetCentroid( hostage ); + + // if our hostage has moved, repath + const float repathToleranceSq = 75.0f * 75.0f; + float error = (hostageOrigin - m_goalPosition).LengthSqr(); + if (error > repathToleranceSq) + { + m_goalPosition = hostageOrigin; + me->ComputePath( m_goalPosition, SAFEST_ROUTE ); + } + + /// @todo Generalize ladder priorities over other tasks + if (!me->IsUsingLadder()) + { + Vector pos = hostage->EyePosition(); + Vector to = pos - me->EyePosition(); // "Use" checks from eye position, so we should too + + // look at the hostage as we approach + const float watchHostageRange = 100.0f; + if (to.IsLengthLessThan( watchHostageRange )) + { + me->SetLookAt( "Hostage", pos, PRIORITY_LOW, 0.5f ); + + // randomly move just a bit to avoid infinite use loops from bad hostage placement + NavRelativeDirType dir = (NavRelativeDirType)RandomInt( 0, 3 ); + switch( dir ) + { + case LEFT: me->StrafeLeft(); break; + case RIGHT: me->StrafeRight(); break; + case FORWARD: me->MoveForward(); break; + case BACKWARD: me->MoveBackward(); break; + } + + // check if we are close enough to the hostage to talk to him + const float useRange = PLAYER_USE_RADIUS - 10.0f; // shave off a fudge factor to make sure we're within range + if (to.IsLengthLessThan( useRange )) + { + me->UseEntity( me->GetGoalEntity() ); + return; + } + } + } + } + else if (me->GetTask() == CCSBot::RESCUE_HOSTAGES) + { + // periodically check if we lost all our hostages + if (me->GetHostageEscortCount() == 0) + { + // lost our hostages - go get 'em + me->Idle(); + return; + } + } + + break; + } + } + + + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + // reached destination + switch( me->GetTask() ) + { + case CCSBot::PLANT_BOMB: + // if we are at bombsite with the bomb, plant it + if (me->IsAtBombsite() && me->HasC4()) + { + me->PlantBomb(); + return; + } + break; + + case CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION: + { + CBasePlayer *victim = static_cast<CBasePlayer *>( me->GetTaskEntity() ); + if (victim && victim->IsAlive()) + { + // if we got here and haven't re-acquired the enemy, we lost him + BotStatement *say = new BotStatement( me->GetChatter(), REPORT_ENEMY_LOST, 8.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "LostEnemy" ) ); + say->SetStartTime( gpGlobals->curtime + RandomFloat( 3.0f, 5.0f ) ); + + me->GetChatter()->AddStatement( say ); + } + break; + } + } + + // default behavior when destination is reached + me->Idle(); + return; + } +} + +//-------------------------------------------------------------------------------------------------------------- +void MoveToState::OnExit( CCSBot *me ) +{ + // reset to run in case we were walking near our goal position + me->Run(); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + //me->StopAiming(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_open_door.cpp b/game/server/cstrike/bot/states/cs_bot_open_door.cpp new file mode 100644 index 0000000..a945a6c --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_open_door.cpp @@ -0,0 +1,94 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), April 2005 + +#include "cbase.h" +#include "cs_bot.h" +#include "BasePropDoor.h" +#include "doors.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//------------------------------------------------------------------------------------------------- +/** + * Face the door and open it. + * NOTE: This state assumes we are standing in range of the door to be opened, with no obstructions. + */ +void OpenDoorState::OnEnter( CCSBot *me ) +{ + m_isDone = false; + m_timeout.Start( 1.0f ); +} + + +//------------------------------------------------------------------------------------------------- +void OpenDoorState::SetDoor( CBaseEntity *door ) +{ + CBaseDoor *funcDoor = dynamic_cast< CBaseDoor * >(door); + if ( funcDoor ) + { + m_funcDoor = funcDoor; + return; + } + + CBasePropDoor *propDoor = dynamic_cast< CBasePropDoor * >(door); + if ( propDoor ) + { + m_propDoor = propDoor; + return; + } +} + + +//------------------------------------------------------------------------------------------------- +void OpenDoorState::OnUpdate( CCSBot *me ) +{ + me->ResetStuckMonitor(); + + // wait for door to swing open before leaving state + if (m_timeout.IsElapsed()) + { + m_isDone = true; + return; + } + + // look at the door + Vector pos; + bool isDoorMoving = false; + if ( m_funcDoor ) + { + pos = m_funcDoor->WorldSpaceCenter(); + isDoorMoving = m_funcDoor->m_toggle_state == TS_GOING_UP || m_funcDoor->m_toggle_state == TS_GOING_DOWN; + } + else + { + pos = m_propDoor->WorldSpaceCenter(); + isDoorMoving = m_propDoor->IsDoorOpening() || m_propDoor->IsDoorClosing(); + } + + me->SetLookAt( "Open door", pos, PRIORITY_HIGH ); + + // if we are looking at the door, "use" it and exit + if (me->IsLookingAtPosition( pos )) + { + me->UseEnvironment(); + } +} + + +//------------------------------------------------------------------------------------------------- +void OpenDoorState::OnExit( CCSBot *me ) +{ + me->ClearLookAt(); + me->ResetStuckMonitor(); +} + + + diff --git a/game/server/cstrike/bot/states/cs_bot_plant_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_plant_bomb.cpp new file mode 100644 index 0000000..fe45a3d --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_plant_bomb.cpp @@ -0,0 +1,79 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Plant the bomb. + */ +void PlantBombState::OnEnter( CCSBot *me ) +{ + me->Crouch(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + + // look at the floor +// Vector down( myOrigin.x, myOrigin.y, -1000.0f ); + + float yaw = me->EyeAngles().y; + Vector2D dir( BotCOS(yaw), BotSIN(yaw) ); + Vector myOrigin = GetCentroid( me ); + + Vector down( myOrigin.x + 10.0f * dir.x, myOrigin.y + 10.0f * dir.y, me->GetFeetZ() ); + me->SetLookAt( "Plant bomb on floor", down, PRIORITY_HIGH ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Plant the bomb. + */ +void PlantBombState::OnUpdate( CCSBot *me ) +{ + CBaseCombatWeapon *gun = me->GetActiveWeapon(); + bool holdingC4 = false; + if (gun) + { + if (FStrEq( gun->GetClassname(), "weapon_c4" )) + holdingC4 = true; + } + + // if we aren't holding the C4, grab it, otherwise plant it + if (holdingC4) + me->PrimaryAttack(); + else + me->SelectItem( "weapon_c4" ); + + // if we no longer have the C4, we've successfully planted + if (!me->HasC4()) + { + // move to a hiding spot and watch the bomb + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + me->Hide(); + } + + // if we time out, it's because we slipped into a non-plantable area + const float timeout = 5.0f; + if (gpGlobals->curtime - me->GetStateTimestamp() > timeout) + me->Idle(); +} + +//-------------------------------------------------------------------------------------------------------------- +void PlantBombState::OnExit( CCSBot *me ) +{ + // equip our rifle (in case we were interrupted while holding C4) + me->EquipBestWeapon(); + me->StandUp(); + me->ResetStuckMonitor(); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + me->ClearLookAt(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_use_entity.cpp b/game/server/cstrike/bot/states/cs_bot_use_entity.cpp new file mode 100644 index 0000000..591b364 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_use_entity.cpp @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +/** + * Face the entity and "use" it + * NOTE: This state assumes we are standing in range of the entity to be used, with no obstructions. + */ +void UseEntityState::OnEnter( CCSBot *me ) +{ +} + +void UseEntityState::OnUpdate( CCSBot *me ) +{ + // in the very rare situation where two or more bots "used" a hostage at the same time, + // one bot will fail and needs to time out of this state + const float useTimeout = 5.0f; + if (me->GetStateTimestamp() - gpGlobals->curtime > useTimeout) + { + me->Idle(); + return; + } + + // look at the entity + Vector pos = m_entity->EyePosition(); + me->SetLookAt( "Use entity", pos, PRIORITY_HIGH ); + + // if we are looking at the entity, "use" it and exit + if (me->IsLookingAtPosition( pos )) + { + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_RESCUE_HOSTAGES && + me->GetTeamNumber() == TEAM_CT && + me->GetTask() == CCSBot::COLLECT_HOSTAGES) + { + // we are collecting a hostage, assume we were successful - the update check will correct us if we weren't + me->IncreaseHostageEscortCount(); + } + + me->UseEnvironment(); + me->Idle(); + } +} + +void UseEntityState::OnExit( CCSBot *me ) +{ + me->ClearLookAt(); + me->ResetStuckMonitor(); +} + + + diff --git a/game/server/cstrike/cs_autobuy.cpp b/game/server/cstrike/cs_autobuy.cpp new file mode 100644 index 0000000..e7ed336 --- /dev/null +++ b/game/server/cstrike/cs_autobuy.cpp @@ -0,0 +1,49 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Data for Autobuy and Rebuy +// +//=============================================================================// + +#include "cbase.h" +#include "cs_autobuy.h" + +// Weapon class information for each weapon including the class name and the buy command alias. +AutoBuyInfoStruct g_autoBuyInfo[] = +{ + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_RIFLE), "galil", "weapon_galil" }, // galil + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_RIFLE), "ak47", "weapon_ak47" }, // ak47 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SNIPERRIFLE), "scout", "weapon_scout" }, // scout + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_RIFLE), "sg552", "weapon_sg552" }, // sg552 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SNIPERRIFLE), "awp", "weapon_awp" }, // awp + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SNIPERRIFLE), "g3sg1", "weapon_g3sg1" }, // g3sg1 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_RIFLE), "famas", "weapon_famas" }, // famas + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_RIFLE), "m4a1", "weapon_m4a1" }, // m4a1 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_RIFLE), "aug", "weapon_aug" }, // aug + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SNIPERRIFLE), "sg550", "weapon_sg550" }, // sg550 + { (AutoBuyClassType)(AUTOBUYCLASS_SECONDARY | AUTOBUYCLASS_PISTOL), "glock", "weapon_glock" }, // glock + { (AutoBuyClassType)(AUTOBUYCLASS_SECONDARY | AUTOBUYCLASS_PISTOL), "usp", "weapon_usp" }, // usp + { (AutoBuyClassType)(AUTOBUYCLASS_SECONDARY | AUTOBUYCLASS_PISTOL), "p228", "weapon_p228" }, // p228 + { (AutoBuyClassType)(AUTOBUYCLASS_SECONDARY | AUTOBUYCLASS_PISTOL), "deagle", "weapon_deagle" }, // deagle + { (AutoBuyClassType)(AUTOBUYCLASS_SECONDARY | AUTOBUYCLASS_PISTOL), "elite", "weapon_elite" }, // elites + { (AutoBuyClassType)(AUTOBUYCLASS_SECONDARY | AUTOBUYCLASS_PISTOL), "fn57", "weapon_fiveseven" }, // fn57 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SHOTGUN), "m3", "weapon_m3" }, // m3 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SHOTGUN), "xm1014", "weapon_xm1014" }, // xm1014 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SMG), "mac10", "weapon_mac10" }, // mac10 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SMG), "tmp", "weapon_tmp" }, // tmp + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SMG), "mp5navy", "weapon_mp5navy" }, // mp5navy + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SMG), "ump45", "weapon_ump45" }, // ump45 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SMG), "p90", "weapon_p90" }, // p90 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_MACHINEGUN), "m249", "weapon_m249" }, // m249 + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_AMMO), "primammo", "primammo" }, // primammo + { (AutoBuyClassType)(AUTOBUYCLASS_SECONDARY | AUTOBUYCLASS_AMMO), "secammo", "secammo" }, // secmmo + { (AutoBuyClassType)(AUTOBUYCLASS_ARMOR), "vest", "item_kevlar" }, // vest + { (AutoBuyClassType)(AUTOBUYCLASS_ARMOR), "vesthelm", "item_assaultsuit" }, // vesthelm + { (AutoBuyClassType)(AUTOBUYCLASS_GRENADE), "flashbang", "weapon_flashbang" }, // flash + { (AutoBuyClassType)(AUTOBUYCLASS_GRENADE), "hegrenade", "weapon_hegrenade" }, // hegren + { (AutoBuyClassType)(AUTOBUYCLASS_GRENADE), "smokegrenade", "weapon_smokegrenade" }, // sgren + { (AutoBuyClassType)(AUTOBUYCLASS_NIGHTVISION), "nvgs", "nvgs" }, // nvgs + { (AutoBuyClassType)(AUTOBUYCLASS_DEFUSER), "defuser", "defuser" }, // defuser + { (AutoBuyClassType)(AUTOBUYCLASS_PRIMARY | AUTOBUYCLASS_SHIELD), "shield", "shield" }, // shield + + { (AutoBuyClassType)0, "", "" } // last one, must be at end. +}; diff --git a/game/server/cstrike/cs_autobuy.h b/game/server/cstrike/cs_autobuy.h new file mode 100644 index 0000000..9fc8346 --- /dev/null +++ b/game/server/cstrike/cs_autobuy.h @@ -0,0 +1,53 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Headers and defines for Autobuy and Rebuy +// +//=============================================================================// + +/** + * Weapon classes as used by the AutoBuy + * Has to be different that the previous ones because these are bitmasked values as a weapon can be from + * more than one class. This also includes all the classes of equipment that a player can buy. + */ +enum AutoBuyClassType +{ + AUTOBUYCLASS_PRIMARY = 1, + AUTOBUYCLASS_SECONDARY = 2, + AUTOBUYCLASS_AMMO = 4, + AUTOBUYCLASS_ARMOR = 8, + AUTOBUYCLASS_DEFUSER = 16, + AUTOBUYCLASS_PISTOL = 32, + AUTOBUYCLASS_SMG = 64, + AUTOBUYCLASS_RIFLE = 128, + AUTOBUYCLASS_SNIPERRIFLE = 256, + AUTOBUYCLASS_SHOTGUN = 512, + AUTOBUYCLASS_MACHINEGUN = 1024, + AUTOBUYCLASS_GRENADE = 2048, + AUTOBUYCLASS_NIGHTVISION = 4096, + AUTOBUYCLASS_SHIELD = 8192, +}; + +struct AutoBuyInfoStruct +{ + AutoBuyClassType m_class; + const char *m_command; + const char *m_classname; +}; + +struct RebuyStruct +{ + char m_szPrimaryWeapon[64]; //"weapon_" string of the primary weapon + char m_szSecondaryWeapon[64]; //"weapon_" string of the secondary weapon + + int m_primaryAmmo; // number of rounds the player had (not including rounds in the gun) + int m_secondaryAmmo; // number of rounds the player had (not including rounds in the gun) + int m_heGrenade; // number of grenades to buy + int m_flashbang; // number of grenades to buy + int m_smokeGrenade; // number of grenades to buy + int m_armor; // 0, 1, or 2 (0 = none, 1 = vest, 2 = vest + helmet) + + bool m_defuser; // do we want a defuser + bool m_nightVision; // do we want night vision +}; + +extern AutoBuyInfoStruct g_autoBuyInfo[]; diff --git a/game/server/cstrike/cs_bot_temp.cpp b/game/server/cstrike/cs_bot_temp.cpp new file mode 100644 index 0000000..b154911 --- /dev/null +++ b/game/server/cstrike/cs_bot_temp.cpp @@ -0,0 +1,554 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Basic BOT handling. +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "cs_player.h" +#include "in_buttons.h" +#include "movehelper_server.h" +#include "team.h" +#include "cs_gamerules.h" +#include "client.h" + + +void Bot_Think( CCSPlayer *pBot ); + +ConVar bot_forcefireweapon( "bot_forcefireweapon", "", 0, "Force bots with the specified weapon to fire." ); +ConVar bot_forceattack2( "bot_forceattack2", "0", 0, "When firing, use attack2." ); +ConVar bot_forceattackon( "bot_forceattackon", "0", 0, "When firing, don't tap fire, hold it down." ); +ConVar bot_flipout( "bot_flipout", "0", 0, "When on, all bots fire their guns." ); +ConVar bot_mimic( "bot_mimic", "0", 0, "Bot uses usercmd of player by index." ); +static ConVar bot_mimic_yaw_offset( "bot_mimic_yaw_offset", "0", 0, "Offsets the bot yaw." ); + +static int BotNumber = 1; +static int g_iNextBotTeam = -1; + + +typedef struct +{ + bool backwards; + + float nextturntime; + bool lastturntoright; + + float nextstrafetime; + float sidemove; + + QAngle forwardAngle; + QAngle lastAngles; + + int m_WantedTeam; + float m_flJoinTeamTime; + + bool m_bTempBot; // Is this slot a dump temp bot or a real bot? +} botdata_t; + +static botdata_t g_BotData[ MAX_PLAYERS ]; + +//----------------------------------------------------------------------------- +// Purpose: Create a new Bot and put it in the game. +// Output : Pointer to the new Bot, or NULL if there's no free clients. +//----------------------------------------------------------------------------- +CBasePlayer *BotPutInServer( bool bFrozen, int iTeam ) +{ + g_iNextBotTeam = iTeam; + + char botname[ 64 ]; + Q_snprintf( botname, sizeof( botname ), "Bot%02i", BotNumber ); + + edict_t *pEdict = engine->CreateFakeClient( botname ); + + if (!pEdict) + { + Msg( "Failed to create Bot.\n"); + return NULL; + } + + // Allocate a CBasePlayer for the bot, and call spawn + //ClientPutInServer( pEdict, botname ); + //ClientActive( pEdict, false ); + + CCSPlayer *pPlayer = ((CCSPlayer *)CBaseEntity::Instance( pEdict )); + pPlayer->ClearFlags(); + pPlayer->AddFlag( FL_CLIENT | FL_FAKECLIENT ); + + if ( bFrozen ) + pPlayer->AddEFlags( EFL_BOT_FROZEN ); + + if ( iTeam == -1 ) + iTeam = ( pPlayer->entindex() & 1 ) ? TEAM_TERRORIST : TEAM_CT; + + botdata_t *pData = &g_BotData[pPlayer->entindex()-1]; + pData->m_WantedTeam = iTeam; + pData->m_flJoinTeamTime = gpGlobals->curtime + 0.3; + pData->m_bTempBot = true; + + BotNumber++; + return pPlayer; +} + +bool IsTempBot( CBaseEntity *pEnt ) +{ + if ( !pEnt ) + return false; + + if ( !(pEnt->GetFlags() & FL_FAKECLIENT) ) + return false; + + int i = pEnt->entindex(); + if ( i >= 1 && i < MAX_PLAYERS ) + return g_BotData[i-1].m_bTempBot; + else + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Run through all the Bots in the game and let them think. +//----------------------------------------------------------------------------- +void Bot_RunAll( void ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + + if ( IsTempBot( pPlayer ) ) + { + Bot_Think( pPlayer ); + } + } +} + +bool RunMimicCommand( CUserCmd& cmd ) +{ + if ( bot_mimic.GetInt() <= 0 ) + return false; + + if ( bot_mimic.GetInt() > gpGlobals->maxClients ) + return false; + + + CBasePlayer *pPlayer = UTIL_PlayerByIndex( bot_mimic.GetInt() ); + if ( !pPlayer ) + return false; + + if ( !pPlayer->GetLastUserCommand() ) + return false; + + cmd = *pPlayer->GetLastUserCommand(); + cmd.viewangles[YAW] += bot_mimic_yaw_offset.GetFloat(); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Simulates a single frame of movement for a player +// Input : *fakeclient - +// *viewangles - +// forwardmove - +// sidemove - +// upmove - +// buttons - +// impulse - +// msec - +// Output : virtual void +//----------------------------------------------------------------------------- +static void RunPlayerMove( CCSPlayer *fakeclient, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, unsigned short buttons, byte impulse, float frametime ) +{ + if ( !fakeclient ) + return; + + CUserCmd cmd; + + // Store off the globals.. they're gonna get whacked + float flOldFrametime = gpGlobals->frametime; + float flOldCurtime = gpGlobals->curtime; + + float flTimeBase = gpGlobals->curtime; + fakeclient->SetTimeBase( flTimeBase ); + + Q_memset( &cmd, 0, sizeof( cmd ) ); + + if ( !RunMimicCommand( cmd ) ) + { + VectorCopy( viewangles, cmd.viewangles ); + cmd.forwardmove = forwardmove; + cmd.sidemove = sidemove; + cmd.upmove = upmove; + cmd.buttons = buttons; + cmd.impulse = impulse; + cmd.random_seed = random->RandomInt( 0, 0x7fffffff ); + } + + MoveHelperServer()->SetHost( fakeclient ); + fakeclient->PlayerRunCommand( &cmd, MoveHelperServer() ); + + // save off the last good usercmd + fakeclient->SetLastUserCommand( cmd ); + + // Clear out any fixangle that has been set + fakeclient->pl.fixangle = FIXANGLE_NONE; + + // Restore the globals.. + gpGlobals->frametime = flOldFrametime; + gpGlobals->curtime = flOldCurtime; +} + +//----------------------------------------------------------------------------- +// Purpose: Run this Bot's AI for one frame. +//----------------------------------------------------------------------------- +void Bot_Think( CCSPlayer *pBot ) +{ + // Make sure we stay being a bot + pBot->AddFlag( FL_FAKECLIENT ); + + botdata_t *botdata = &g_BotData[ pBot->entindex() - 1 ]; + + float forwardmove = 0.0; + float sidemove = botdata->sidemove; + float upmove = 0.0; + unsigned short buttons = 0; + byte impulse = 0; + float frametime = gpGlobals->frametime; + + if ( pBot->GetTeamNumber() == TEAM_UNASSIGNED && gpGlobals->curtime > botdata->m_flJoinTeamTime ) + { + pBot->HandleCommand_JoinTeam( botdata->m_WantedTeam ); + } + else if ( pBot->GetTeamNumber() != TEAM_UNASSIGNED && pBot->PlayerClass() == CS_CLASS_NONE ) + { + // If they're on a team but haven't picked a class, choose a random class.. + pBot->HandleCommand_JoinClass( 0 ); + } + else + { + QAngle vecViewAngles; + vecViewAngles = pBot->GetLocalAngles(); + + // Create some random values + if ( pBot->IsAlive() && (pBot->GetSolid() == SOLID_BBOX) ) + { + trace_t trace; + + // Stop when shot + if ( !pBot->IsEFlagSet(EFL_BOT_FROZEN) ) + { + if ( pBot->m_iHealth == 100 ) + { + forwardmove = 600 * ( botdata->backwards ? -1 : 1 ); + if ( botdata->sidemove != 0.0f ) + { + forwardmove *= random->RandomFloat( 0.1, 1.0f ); + } + } + else + { + forwardmove = 0; + } + } + + // Only turn if I haven't been hurt + if ( !pBot->IsEFlagSet(EFL_BOT_FROZEN) && pBot->m_iHealth == 100 ) + { + Vector vecEnd; + Vector forward; + + QAngle angle; + float angledelta = 15.0; + + int maxtries = (int)360.0/angledelta; + + if ( botdata->lastturntoright ) + { + angledelta = -angledelta; + } + + angle = pBot->GetLocalAngles(); + + Vector vecSrc; + while ( --maxtries >= 0 ) + { + AngleVectors( angle, &forward, NULL, NULL ); + + vecSrc = pBot->GetLocalOrigin() + Vector( 0, 0, 36 ); + + vecEnd = vecSrc + forward * 10; + + UTIL_TraceHull( vecSrc, vecEnd, VEC_HULL_MIN_SCALED( pBot ), VEC_HULL_MAX_SCALED( pBot ), + MASK_PLAYERSOLID, pBot, COLLISION_GROUP_NONE, &trace ); + + if ( trace.fraction == 1.0 ) + { + //if ( gpGlobals->curtime < botdata->nextturntime ) + //{ + break; + //} + } + + angle.y += angledelta; + + if ( angle.y > 180 ) + angle.y -= 360; + else if ( angle.y < -180 ) + angle.y += 360; + + botdata->nextturntime = gpGlobals->curtime + 2.0; + botdata->lastturntoright = random->RandomInt( 0, 1 ) == 0 ? true : false; + + botdata->forwardAngle = angle; + botdata->lastAngles = angle; + + } + + + /* + if ( gpGlobals->curtime >= botdata->nextstrafetime ) + { + botdata->nextstrafetime = gpGlobals->curtime + 1.0f; + + if ( random->RandomInt( 0, 5 ) == 0 ) + { + botdata->sidemove = -600.0f + 1200.0f * random->RandomFloat( 0, 2 ); + } + else + { + botdata->sidemove = 0; + } + sidemove = botdata->sidemove; + + if ( random->RandomInt( 0, 20 ) == 0 ) + { + botdata->backwards = true; + } + else + { + botdata->backwards = false; + } + } + */ + + pBot->SetLocalAngles( angle ); + vecViewAngles = angle; + } + + // If bots are being forced to fire a weapon, see if I have it + else if ( bot_forcefireweapon.GetString() ) + { + CBaseCombatWeapon *pWeapon = pBot->Weapon_OwnsThisType( bot_forcefireweapon.GetString() ); + if ( pWeapon ) + { + // Switch to it if we don't have it out + CBaseCombatWeapon *pActiveWeapon = pBot->GetActiveWeapon(); + + // Switch? + if ( pActiveWeapon != pWeapon ) + { + pBot->Weapon_Switch( pWeapon ); + } + else + { + // Start firing + // Some weapons require releases, so randomise firing + if ( bot_forceattackon.GetBool() || (RandomFloat(0.0,1.0) > 0.5) ) + { + buttons |= bot_forceattack2.GetBool() ? IN_ATTACK2 : IN_ATTACK; + } + } + } + } + + if ( bot_flipout.GetInt() ) + { + if ( bot_forceattackon.GetBool() || (RandomFloat(0.0,1.0) > 0.5) ) + { + buttons |= bot_forceattack2.GetBool() ? IN_ATTACK2 : IN_ATTACK; + } + } + } + else + { + // Wait for Reinforcement wave + if ( !pBot->IsAlive() ) + { + // Try hitting my buttons occasionally + if ( random->RandomInt( 0, 100 ) > 80 ) + { + // Respawn the bot + if ( random->RandomInt( 0, 1 ) == 0 ) + { + buttons |= IN_JUMP; + } + else + { + buttons = 0; + } + } + } + } + + if ( bot_flipout.GetInt() >= 2 ) + { + + QAngle angOffset = RandomAngle( -1, 1 ); + + botdata->lastAngles += angOffset; + + for ( int i = 0 ; i < 2; i++ ) + { + if ( fabs( botdata->lastAngles[ i ] - botdata->forwardAngle[ i ] ) > 15.0f ) + { + if ( botdata->lastAngles[ i ] > botdata->forwardAngle[ i ] ) + { + botdata->lastAngles[ i ] = botdata->forwardAngle[ i ] + 15; + } + else + { + botdata->lastAngles[ i ] = botdata->forwardAngle[ i ] - 15; + } + } + } + + botdata->lastAngles[ 2 ] = 0; + + pBot->SetLocalAngles( botdata->lastAngles ); + } + } + + pBot->SetPunchAngle( QAngle( 0, 0, 0 ) ); + RunPlayerMove( pBot, pBot->GetLocalAngles(), forwardmove, sidemove, upmove, buttons, impulse, frametime ); +} + + + +// Handler for the "bot" command. +CON_COMMAND_F( "bot_old", "Add a bot.", FCVAR_CHEAT ) +{ + // Disable the CS bots, otherwise they'll interfere with the bot code here. + //extern bool g_bEnableCSBots; + //g_bEnableCSBots = false; + + CCSPlayer *pPlayer = CCSPlayer::Instance( UTIL_GetCommandClientIndex() ); + + // The bot command uses switches like command-line switches. + // -count <count> tells how many bots to spawn. + // -team <index> selects the bot's team. Default is -1 which chooses randomly. + // Note: if you do -team !, then it + // -class <index> selects the bot's class. Default is -1 which chooses randomly. + // -frozen prevents the bots from running around when they spawn in. + + int count = args.FindArgInt( "-count", 1 ); + count = clamp( count, 1, 16 ); + + int iTeam = -1; + const char *pVal = args.FindArg( "-team" ); + if ( pVal ) + { + if ( pVal[0] == '!' ) + { + if ( pPlayer->GetTeamNumber() == TEAM_TERRORIST ) + iTeam = TEAM_CT; + else + iTeam = TEAM_TERRORIST; + } + else if ( pVal[0] == 't' || pVal[0] == 'T' ) + { + iTeam = TEAM_TERRORIST; + } + else if ( pVal[0] == 'c' || pVal[0] == 'C' ) + { + iTeam = TEAM_CT; + } + else + { + iTeam = atoi( pVal ); + if ( iTeam == 1 ) + iTeam = TEAM_TERRORIST; + else + iTeam = TEAM_CT; + } + } + + // Look at -frozen. + bool bFrozen = !!args.FindArg( "-frozen" ); + + // Ok, spawn all the bots. + while ( --count >= 0 ) + { + extern CBasePlayer *BotPutInServer( bool bFrozen, int iTeam ); + BotPutInServer( bFrozen, iTeam ); + } +} + + +// Handle the "PossessBot" command. +void PossessBot_f( const CCommand &args ) +{ + CCSPlayer *pPlayer = CCSPlayer::Instance( UTIL_GetCommandClientIndex() ); + if ( !pPlayer ) + return; + + // Put the local player in control of this bot. + if ( args.ArgC() != 2 ) + { + Warning( "PossessBot <client index>\n" ); + return; + } + + int iBotClient = atoi( args[1] ); + int iBotEnt = iBotClient + 1; + + if ( iBotClient < 0 || + iBotClient >= gpGlobals->maxClients || + pPlayer->entindex() == iBotEnt ) + { + Warning( "PossessBot <client index>\n" ); + } + else + { + edict_t *pPlayerData = pPlayer->edict(); + edict_t *pBotData = engine->PEntityOfEntIndex( iBotEnt ); + if ( pBotData && pBotData ) + { + // SWAP EDICTS + + // Backup things we don't want to swap. + edict_t oldPlayerData = *pPlayerData; + edict_t oldBotData = *pBotData; + + // Swap edicts. + edict_t tmp = *pPlayerData; + *pPlayerData = *pBotData; + *pBotData = tmp; + + // Restore things we didn't want to swap. + //pPlayerData->m_EntitiesTouched = oldPlayerData.m_EntitiesTouched; + //pBotData->m_EntitiesTouched = oldBotData.m_EntitiesTouched; + + CBaseEntity *pPlayerBaseEnt = CBaseEntity::Instance( pPlayerData ); + CBaseEntity *pBotBaseEnt = CBaseEntity::Instance( pBotData ); + + // Make the other a bot and make the player not a bot. + pPlayerBaseEnt->RemoveFlag( FL_FAKECLIENT ); + pBotBaseEnt->AddFlag( FL_FAKECLIENT ); + + + // Point the CBaseEntities at the right players. + pPlayerBaseEnt->NetworkProp()->SetEdict( pPlayerData ); + pBotBaseEnt->NetworkProp()->SetEdict( pBotData ); + + // Freeze the bot. + pBotBaseEnt->AddEFlags( EFL_BOT_FROZEN ); + } + } +} + + +ConCommand cc_PossessBot( "PossessBot", PossessBot_f, "Toggle. Possess a bot.\n\tArguments: <bot client number>", FCVAR_CHEAT ); + diff --git a/game/server/cstrike/cs_bot_temp.h b/game/server/cstrike/cs_bot_temp.h new file mode 100644 index 0000000..b7dfdb5 --- /dev/null +++ b/game/server/cstrike/cs_bot_temp.h @@ -0,0 +1,17 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef CS_BOT_TEMP_H +#define CS_BOT_TEMP_H +#ifdef _WIN32 +#pragma once +#endif + + +extern ConVar bot_mimic; + + +#endif // CS_BOT_TEMP_H diff --git a/game/server/cstrike/cs_client.cpp b/game/server/cstrike/cs_client.cpp new file mode 100644 index 0000000..b7f025e --- /dev/null +++ b/game/server/cstrike/cs_client.cpp @@ -0,0 +1,205 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +/* + +===== tf_client.cpp ======================================================== + + HL2 client/server game specific stuff + +*/ + +#include "cbase.h" +#include "player.h" +#include "gamerules.h" +#include "entitylist.h" +#include "physics.h" +#include "game.h" +#include "ai_network.h" +#include "ai_node.h" +#include "ai_hull.h" +#include "shake.h" +#include "player_resource.h" +#include "engine/IEngineSound.h" +#include "cs_player.h" +#include "cs_gamerules.h" +#include "cs_bot.h" +#include "tier0/vprof.h" +#include "teamplayroundbased_gamerules.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +extern CBaseEntity *FindPickerEntity( CBasePlayer *pPlayer ); + +extern bool g_fGameOver; + + +void FinishClientPutInServer( CCSPlayer *pPlayer ) +{ + pPlayer->InitialSpawn(); + pPlayer->Spawn(); + + if (!pPlayer->IsBot()) + { + // When the player first joins the server, they + pPlayer->m_iNumSpawns = 0; + pPlayer->m_takedamage = DAMAGE_NO; + pPlayer->pl.deadflag = true; + pPlayer->m_lifeState = LIFE_DEAD; + pPlayer->AddEffects( EF_NODRAW ); + pPlayer->ChangeTeam( TEAM_UNASSIGNED ); + pPlayer->SetThink( NULL ); + pPlayer->AddAccount( CSGameRules()->GetStartMoney() ); + + // Move them to the first intro camera. + pPlayer->MoveToNextIntroCamera(); + pPlayer->SetMoveType( MOVETYPE_NONE ); + } + + + char sName[128]; + Q_strncpy( sName, pPlayer->GetPlayerName(), sizeof( sName ) ); + + // First parse the name and remove any %'s + for ( char *pApersand = sName; pApersand != NULL && *pApersand != 0; pApersand++ ) + { + // Replace it with a space + if ( *pApersand == '%' ) + *pApersand = ' '; + } + + // notify other clients of player joining the game + UTIL_ClientPrintAll( HUD_PRINTNOTIFY, "#Game_connected", sName[0] != 0 ? sName : "<unconnected>" ); +} + +/* +=========== +ClientPutInServer + +called each time a player is spawned into the game +============ +*/ +void ClientPutInServer( edict_t *pEdict, const char *playername ) +{ + // Allocate a CBaseTFPlayer for pev, and call spawn + CCSPlayer *pPlayer = CCSPlayer::CreatePlayer( "player", pEdict ); + + pPlayer->SetPlayerName( playername ); +} + + +void ClientActive( edict_t *pEdict, bool bLoadGame ) +{ + // Can't load games in CS! + Assert( !bLoadGame ); + + CCSPlayer *pPlayer = ToCSPlayer( CBaseEntity::Instance( pEdict ) ); + FinishClientPutInServer( pPlayer ); + + CSingleUserRecipientFilter user( pPlayer ); + user.MakeReliable(); + + // send the 4 end of match conditions. long frag limit, long max rounds, long rounds needed won, and long time + UserMessageBegin( user, "MatchEndConditions" ); + WRITE_LONG( fraglimit.GetInt() ); + WRITE_LONG( mp_maxrounds.GetInt() ); + WRITE_LONG( mp_winlimit.GetInt() ); + WRITE_LONG( mp_timelimit.GetInt() ); + MessageEnd(); +} + + +/* +=============== +const char *GetGameDescription() + +Returns the descriptive name of this .dll. E.g., Half-Life, or Team Fortress 2 +=============== +*/ +const char *GetGameDescription() +{ + if ( g_pGameRules ) // this function may be called before the world has spawned, and the game rules initialized + return g_pGameRules->GetGameDescription(); + else + return "Counter-Strike: Source"; +} + + +//----------------------------------------------------------------------------- +// Purpose: Precache game-specific models & sounds +//----------------------------------------------------------------------------- +void ClientGamePrecache( void ) +{ + // Materials used by the client effects + CBaseEntity::PrecacheModel( "sprites/white.vmt" ); + CBaseEntity::PrecacheModel( "sprites/physbeam.vmt" ); + + // Legacy temp ents sounds + CBaseEntity::PrecacheScriptSound( "Bounce.PistolShell" ); + CBaseEntity::PrecacheScriptSound( "Bounce.RifleShell" ); + CBaseEntity::PrecacheScriptSound( "Bounce.ShotgunShell" ); + +// Moved to pure_server_minimal.txt +// // Flashbang-related files +// engine->ForceExactFile( "sprites/white.vmt" ); +// engine->ForceExactFile( "sprites/white.vtf" ); +// engine->ForceExactFile( "vgui/white.vmt" ); +// engine->ForceExactFile( "vgui/white.vtf" ); +// engine->ForceExactFile( "effects/flashbang.vmt" ); +// engine->ForceExactFile( "effects/flashbang_white.vmt" ); +// +// // Smoke grenade-related files +// engine->ForceExactFile( "particle/particle_smokegrenade1.vmt" ); +// engine->ForceExactFile( "particle/particle_smokegrenade.vtf" ); +// +// // Sniper scope +// engine->ForceExactFile( "sprites/scope_arc.vmt" ); +// engine->ForceExactFile( "sprites/scope_arc.vtf" ); +// +// // DSP presets - don't want people avoiding the deafening + ear ring +// engine->ForceExactFile( "scripts/dsp_presets.txt" ); +} + + +// called by ClientKill and DeadThink +void respawn( CBaseEntity *pEdict, bool fCopyCorpse ) +{ + if (gpGlobals->coop || gpGlobals->deathmatch) + { + if ( fCopyCorpse ) + { + // make a copy of the dead body for appearances sake + dynamic_cast< CBasePlayer* >( pEdict )->CreateCorpse(); + } + + // respawn player + pEdict->Spawn(); + } + else + { // restart the entire server + engine->ServerCommand("reload\n"); + } +} + +void GameStartFrame( void ) +{ + VPROF( "GameStartFrame" ); + + if ( g_fGameOver ) + return; + + gpGlobals->teamplay = teamplay.GetInt() ? true : false; +} + +//========================================================= +// instantiate the proper game rules object +//========================================================= +void InstallGameRules() +{ + CreateGameRulesObject( "CCSGameRules" ); +} diff --git a/game/server/cstrike/cs_client.h b/game/server/cstrike/cs_client.h new file mode 100644 index 0000000..a56f2c4 --- /dev/null +++ b/game/server/cstrike/cs_client.h @@ -0,0 +1,19 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef CS_CLIENT_H +#define CS_CLIENT_H +#ifdef _WIN32 +#pragma once +#endif + + +void respawn( CBaseEntity *pEdict, bool fCopyCorpse ); + +void FinishClientPutInServer( CCSPlayer *pPlayer ); + + +#endif // CS_CLIENT_H diff --git a/game/server/cstrike/cs_eventlog.cpp b/game/server/cstrike/cs_eventlog.cpp new file mode 100644 index 0000000..095f6f2 --- /dev/null +++ b/game/server/cstrike/cs_eventlog.cpp @@ -0,0 +1,321 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "../EventLog.h" +#include "team.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#define LOG_DETAIL_ENEMY_ATTACKS 0x01 +#define LOG_DETAIL_TEAMMATE_ATTACKS 0x02 + +ConVar mp_logdetail( "mp_logdetail", "0", FCVAR_NONE, "Logs attacks. Values are: 0=off, 1=enemy, 2=teammate, 3=both)", true, 0.0f, true, 3.0f ); + +class CCSEventLog : public CEventLog +{ +private: + typedef CEventLog BaseClass; + +public: + bool PrintEvent( IGameEvent *event ) // override virtual function + { + if ( !PrintCStrikeEvent( event ) ) // allow CS to override logging + { + return BaseClass::PrintEvent( event ); + } + else + { + return true; + } + } + + bool Init() + { + BaseClass::Init(); + + // listen to CS events + ListenForGameEvent( "round_end" ); + ListenForGameEvent( "round_start" ); + ListenForGameEvent( "bomb_pickup" ); + ListenForGameEvent( "bomb_begindefuse" ); + ListenForGameEvent( "bomb_dropped" ); + ListenForGameEvent( "bomb_defused" ); + ListenForGameEvent( "bomb_planted" ); + ListenForGameEvent( "hostage_rescued" ); + ListenForGameEvent( "hostage_killed" ); + ListenForGameEvent( "hostage_follows" ); + ListenForGameEvent( "player_hurt" ); + + return true; + } + +protected: + + bool PrintCStrikeEvent( IGameEvent *event ) // print Mod specific logs + { + const char *eventName = event->GetName(); + + // messages that don't have a user associated to them + if ( !Q_strncmp( eventName, "round_end", Q_strlen("round_end") ) ) + { + const int winner = event->GetInt( "winner" ); + const int reason = event->GetInt( "reason" ); + const char *msg = event->GetString( "message" ); + msg++; // remove the '#' char + + switch( reason ) + { + case Game_Commencing: + UTIL_LogPrintf( "World triggered \"Game_Commencing\"\n" ); + return true; + break; + } + + CTeam *ct = GetGlobalTeam( TEAM_CT ); + CTeam *ter = GetGlobalTeam( TEAM_TERRORIST ); + Assert( ct && ter ); + + switch ( winner ) + { + case WINNER_CT: + UTIL_LogPrintf( "Team \"%s\" triggered \"%s\" (CT \"%i\") (T \"%i\")\n", ct->GetName(), msg, ct->GetScore(), ter->GetScore() ); + break; + case WINNER_TER: + UTIL_LogPrintf( "Team \"%s\" triggered \"%s\" (CT \"%i\") (T \"%i\")\n", ter->GetName(), msg, ct->GetScore(), ter->GetScore() ); + break; + case WINNER_DRAW: + default: + UTIL_LogPrintf( "World triggered \"%s\" (CT \"%i\") (T \"%i\")\n", msg, ct->GetScore(), ter->GetScore() ); + break; + } + + UTIL_LogPrintf( "Team \"CT\" scored \"%i\" with \"%i\" players\n", ct->GetScore(), ct->GetNumPlayers() ); + UTIL_LogPrintf( "Team \"TERRORIST\" scored \"%i\" with \"%i\" players\n", ter->GetScore(), ter->GetNumPlayers() ); + + UTIL_LogPrintf("World triggered \"Round_End\"\n"); + return true; + } + else if ( !Q_strncmp( eventName, "server_", strlen("server_")) ) + { + return false; // ignore server_ messages + } + + const int userid = event->GetInt( "userid" ); + CBasePlayer *pPlayer = UTIL_PlayerByUserId( userid ); + if ( !pPlayer ) + { + return false; + } + + if ( FStrEq( eventName, "player_hurt" ) ) + { + const int attackerid = event->GetInt("attacker" ); + const char *weapon = event->GetString( "weapon" ); + CBasePlayer *pAttacker = UTIL_PlayerByUserId( attackerid ); + if ( !pAttacker ) + { + return false; + } + + bool isTeamAttack = ( (pPlayer->GetTeamNumber() == pAttacker->GetTeamNumber() ) && (pPlayer != pAttacker) ); + int detail = mp_logdetail.GetInt(); + if ( ( isTeamAttack && ( detail & LOG_DETAIL_TEAMMATE_ATTACKS ) ) || + ( !isTeamAttack && ( detail & LOG_DETAIL_ENEMY_ATTACKS ) ) ) + { + int hitgroup = event->GetInt( "hitgroup" ); + const char *hitgroupStr = "GENERIC"; + switch ( hitgroup ) + { + case HITGROUP_GENERIC: + hitgroupStr = "generic"; + break; + case HITGROUP_HEAD: + hitgroupStr = "head"; + break; + case HITGROUP_CHEST: + hitgroupStr = "chest"; + break; + case HITGROUP_STOMACH: + hitgroupStr = "stomach"; + break; + case HITGROUP_LEFTARM: + hitgroupStr = "left arm"; + break; + case HITGROUP_RIGHTARM: + hitgroupStr = "right arm"; + break; + case HITGROUP_LEFTLEG: + hitgroupStr = "left leg"; + break; + case HITGROUP_RIGHTLEG: + hitgroupStr = "right leg"; + break; + } + + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" attacked \"%s<%i><%s><%s>\" with \"%s\" (damage \"%d\") (damage_armor \"%d\") (health \"%d\") (armor \"%d\") (hitgroup \"%s\")\n", + pAttacker->GetPlayerName(), + attackerid, + pAttacker->GetNetworkIDString(), + pAttacker->GetTeam()->GetName(), + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + pPlayer->GetTeam()->GetName(), + weapon, + event->GetInt( "dmg_health" ), + event->GetInt( "dmg_armor" ), + event->GetInt( "health" ), + event->GetInt( "armor" ), + hitgroupStr ); + } + return true; + } + else if ( !Q_strncmp( eventName, "player_death", Q_strlen("player_death") ) ) + { + const int attackerid = event->GetInt("attacker" ); + const char *weapon = event->GetString( "weapon" ); + const bool headShot = (event->GetInt( "headshot" ) == 1); + CBasePlayer *pAttacker = UTIL_PlayerByUserId( attackerid ); + + if ( pPlayer == pAttacker ) + { + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" committed suicide with \"%s\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + pPlayer->GetTeam()->GetName(), + weapon + ); + } + else if ( pAttacker ) + { + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" killed \"%s<%i><%s><%s>\" with \"%s\"%s\n", + pAttacker->GetPlayerName(), + attackerid, + pAttacker->GetNetworkIDString(), + pAttacker->GetTeam()->GetName(), + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + pPlayer->GetTeam()->GetName(), + weapon, + headShot ? " (headshot)":"" + ); + } + else + { + // killed by the world + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" committed suicide with \"world\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + pPlayer->GetTeam()->GetName() + ); + } + return true; + } + else if ( !Q_strncmp( eventName, "round_start", Q_strlen("round_start") ) ) + { + UTIL_LogPrintf("World triggered \"Round_Start\"\n"); + return true; + } + else if ( !Q_strncmp( eventName, "hostage_follows", Q_strlen("hostage_follows") ) ) + { + UTIL_LogPrintf( "\"%s<%i><%s><CT>\" triggered \"Touched_A_Hostage\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString() + ); + return true; + } + else if ( !Q_strncmp( eventName, "hostage_killed", Q_strlen("hostage_killed") ) ) + { + UTIL_LogPrintf( "\"%s<%i><%s><%s>\" triggered \"Killed_A_Hostage\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + pPlayer->GetTeam()->GetName() + ); + return true; + } + else if ( !Q_strncmp( eventName, "hostage_rescued", Q_strlen("hostage_rescued") ) ) + { + UTIL_LogPrintf("\"%s<%i><%s><CT>\" triggered \"Rescued_A_Hostage\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString() + ); + return true; + } + else if ( !Q_strncmp( eventName, "bomb_planted", Q_strlen("bomb_planted") ) ) + { + UTIL_LogPrintf("\"%s<%i><%s><TERRORIST>\" triggered \"Planted_The_Bomb\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString() + ); + return true; + } + else if ( !Q_strncmp( eventName, "bomb_defused", Q_strlen("bomb_defused") ) ) + { + UTIL_LogPrintf("\"%s<%i><%s><CT>\" triggered \"Defused_The_Bomb\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString() + ); + return true; + } + else if ( !Q_strncmp( eventName, "bomb_dropped", Q_strlen("bomb_dropped") ) ) + { + UTIL_LogPrintf("\"%s<%i><%s><TERRORIST>\" triggered \"Dropped_The_Bomb\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString() + ); + return true; + } + else if ( !Q_strncmp( eventName, "bomb_begindefuse", Q_strlen("bomb_begindefuse") ) ) + { + const bool haskit = (event->GetInt( "haskit" ) == 1); + UTIL_LogPrintf("\"%s<%i><%s><CT>\" triggered \"%s\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString(), + haskit ? "Begin_Bomb_Defuse_With_Kit" : "Begin_Bomb_Defuse_Without_Kit" + ); + return true; + } + else if ( !Q_strncmp( eventName, "bomb_pickup", Q_strlen("bomb_pickup") ) ) + { + UTIL_LogPrintf("\"%s<%i><%s><TERRORIST>\" triggered \"Got_The_Bomb\"\n", + pPlayer->GetPlayerName(), + userid, + pPlayer->GetNetworkIDString() + ); + return true; + } + +// unused events: +//hostage_hurt +//bomb_exploded + + return false; + } + +}; + +CCSEventLog g_CSEventLog; + +//----------------------------------------------------------------------------- +// Singleton access +//----------------------------------------------------------------------------- +IGameSystem* GameLogSystem() +{ + return &g_CSEventLog; +} + diff --git a/game/server/cstrike/cs_gameinterface.cpp b/game/server/cstrike/cs_gameinterface.cpp new file mode 100644 index 0000000..869dc64 --- /dev/null +++ b/game/server/cstrike/cs_gameinterface.cpp @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "gameinterface.h" +#include "mapentities.h" +#include "cs_gameinterface.h" +#include "AI_ResponseSystem.h" + +// -------------------------------------------------------------------------------------------- // +// Mod-specific CServerGameClients implementation. +// -------------------------------------------------------------------------------------------- // + +void CServerGameClients::GetPlayerLimits( int& minplayers, int& maxplayers, int &defaultMaxPlayers ) const +{ + minplayers = 1; // allow single player for the test maps (but we default to multi) + maxplayers = MAX_PLAYERS; + + defaultMaxPlayers = 32; // Default to 32 players unless they change it. +} + + +// -------------------------------------------------------------------------------------------- // +// Mod-specific CServerGameDLL implementation. +// -------------------------------------------------------------------------------------------- // + +void CServerGameDLL::LevelInit_ParseAllEntities( const char *pMapEntities ) +{ + if ( Q_strcmp( STRING(gpGlobals->mapname), "cs_" ) ) + { + // don't precache AI responses (hostages) if it's not a hostage rescure map + extern IResponseSystem *g_pResponseSystem; + g_pResponseSystem->PrecacheResponses( false ); + } +} + + diff --git a/game/server/cstrike/cs_gameinterface.h b/game/server/cstrike/cs_gameinterface.h new file mode 100644 index 0000000..df3d028 --- /dev/null +++ b/game/server/cstrike/cs_gameinterface.h @@ -0,0 +1,14 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef CS_GAMEINTERFACE_H +#define CS_GAMEINTERFACE_H +#ifdef _WIN32 +#pragma once +#endif + + +#endif // CS_GAMEINTERFACE_H diff --git a/game/server/cstrike/cs_gamestats.cpp b/game/server/cstrike/cs_gamestats.cpp new file mode 100644 index 0000000..afb02a0 --- /dev/null +++ b/game/server/cstrike/cs_gamestats.cpp @@ -0,0 +1,1721 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: CS game stats +// +// $NoKeywords: $ +//=============================================================================// + +// Some tricky business here - we don't want to include the precompiled header for the statreader +// and trying to #ifdef it out does funky things like ignoring the #endif. Define our header file +// separately and include it based on the switch + +#include "cbase.h" + +#include <tier0/platform.h> +#include "cs_gamerules.h" +#include "cs_gamestats.h" +#include "weapon_csbase.h" +#include "props.h" +#include "cs_achievement_constants.h" +#include "../../shared/cstrike/weapon_c4.h" + +#include <time.h> +#include "filesystem.h" +#include "bot_util.h" +#include "cdll_int.h" +#include "steamworks_gamestats.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +float g_flGameStatsUpdateTime = 0.0f; +short g_iTerroristVictories[CS_NUM_LEVELS]; +short g_iCounterTVictories[CS_NUM_LEVELS]; +short g_iWeaponPurchases[WEAPON_MAX]; + +short g_iAutoBuyPurchases = 0; +short g_iReBuyPurchases = 0; +short g_iAutoBuyM4A1Purchases = 0; +short g_iAutoBuyAK47Purchases = 0; +short g_iAutoBuyFamasPurchases = 0; +short g_iAutoBuyGalilPurchases = 0; +short g_iAutoBuyVestHelmPurchases = 0; +short g_iAutoBuyVestPurchases = 0; + +struct PropModelStats_t +{ + const char* szPropModelName; + CSStatType_t statType; +} PropModelStatsTableInit[] = +{ + { "models/props/cs_office/computer_caseb.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/computer_monitor.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/phone.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/projector.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/TV_plasma.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/computer_keyboard.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/radio.mdl", CSSTAT_PROPSBROKEN_OFFICERADIO }, + { "models/props/cs_office/trash_can.mdl", CSSTAT_PROPSBROKEN_OFFICEJUNK }, + { "models/props/cs_office/file_box.mdl", CSSTAT_PROPSBROKEN_OFFICEJUNK }, + { "models/props_junk/watermelon01.mdl", CSSTAT_PROPSBROKEN_ITALY_MELON }, + // models/props/de_inferno/claypot01.mdl + // models/props/de_inferno/claypot02.mdl + // models/props/de_dust/grainbasket01c.mdl + // models/props_junk/wood_crate001a.mdl + // models/props/cs_office/file_box_p1.mdl +}; + + +struct ServerStats_t +{ + int achievementId; + int statId; + int roundRequirement; + int matchRequirement; + const char* mapFilter; + + bool IsMet(int roundStat, int matchStat) + { + return roundStat >= roundRequirement && matchStat >= matchRequirement; + } +} ServerStatBasedAchievements[] = +{ + { CSBreakWindows, CSSTAT_NUM_BROKEN_WINDOWS, AchievementConsts::BreakWindowsInOfficeRound_Windows, 0, "cs_office" }, + { CSBreakProps, CSSTAT_PROPSBROKEN_ALL, AchievementConsts::BreakPropsInRound_Props, 0, NULL }, + { CSUnstoppableForce, CSSTAT_KILLS, AchievementConsts::UnstoppableForce_Kills, 0, NULL }, + { CSHeadshotsInRound, CSSTAT_KILLS_HEADSHOT, AchievementConsts::HeadshotsInRound_Kills, 0, NULL }, + { CSDominationOverkillsMatch, CSSTAT_DOMINATION_OVERKILLS, 0, 10, NULL }, +}; + +//============================================================================= +// HPE_BEGIN: +// [Forrest] Allow nemesis/revenge to be turned off for a server +//============================================================================= +static void SvNoNemesisChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) +{ + ConVarRef var( pConVar ); + if ( var.IsValid() && var.GetBool() ) + { + // Clear all nemesis relationships. + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + CCSPlayer *pTemp = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + if ( pTemp ) + { + pTemp->RemoveNemesisRelationships(); + } + } + } +} + +ConVar sv_nonemesis( "sv_nonemesis", "0", 0, "Disable nemesis and revenge.", SvNoNemesisChangeCallback ); +//============================================================================= +// HPE_END +//============================================================================= + +int GetCSLevelIndex( const char *pLevelName ) +{ + for ( int i = 0; MapName_StatId_Table[i].statWinsId != CSSTAT_UNDEFINED; i ++ ) + { + if ( Q_strcmp( pLevelName, MapName_StatId_Table[i].szMapName ) == 0 ) + return i; + } + + return -1; +} + +ConVar sv_debugroundstats( "sv_debugroundstats", "0", 0, "A temporary variable that will print extra information about stats upload which may be useful in debugging any problems." ); + +//============================================================================= +// HPE_BEGIN: +// [menglish] Addition of CCS_GameStats class +//============================================================================= + +CCSGameStats CCS_GameStats; +CCSGameStats::StatContainerList_t* CCSGameStats::s_StatLists = new CCSGameStats::StatContainerList_t(); + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : - +//----------------------------------------------------------------------------- +CCSGameStats::CCSGameStats() +{ + gamestats = this; + Clear(); + m_fDisseminationTimerLow = m_fDisseminationTimerHigh = 0.0f; + + // create table for mapping prop models to stats + for ( int i = 0; i < ARRAYSIZE(PropModelStatsTableInit); ++i) + { + m_PropStatTable.Insert(PropModelStatsTableInit[i].szPropModelName, PropModelStatsTableInit[i].statType); + } + + m_numberOfRoundsForDirectAverages = 0; + m_numberOfTerroristEntriesForDirectAverages = 0; + m_numberOfCounterTerroristEntriesForDirectAverages = 0; + +} + +//----------------------------------------------------------------------------- +// Purpose: Destructor +// Input : - +//----------------------------------------------------------------------------- +CCSGameStats::~CCSGameStats() +{ + Clear(); +} + +//----------------------------------------------------------------------------- +// Purpose: Clear out game stats +// Input : - +//----------------------------------------------------------------------------- +void CCSGameStats::Clear( void ) +{ + m_PlayerSnipedPosition.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CCSGameStats::Init( void ) +{ + ListenForGameEvent( "round_start" ); + ListenForGameEvent( "round_end" ); + ListenForGameEvent( "break_prop" ); + ListenForGameEvent( "player_decal"); + ListenForGameEvent( "hegrenade_detonate"); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::PostInit( void ) +{ + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::LevelShutdownPreClearSteamAPIContext( void ) +{ + // If we have any unsent round stats we better send them now or we'll lose them + UploadRoundStats(); + + GetSteamWorksSGameStatsUploader().EndSession(); +} + +extern double g_rowCommitTime; +extern double g_rowWriteTime; + +void CCSGameStats::UploadRoundStats( void ) +{ + CFastTimer totalTimer, purchasesAndDeathsStatTimer, weaponsStatBuildTimer, weaponsStatSubmitTimer, submitTimer, cleanupTimer; + + // Prevent uploading empty data + if ( !m_bInRound ) + return; + + if ( sv_noroundstats.GetBool() ) + { + if ( sv_debugroundstats.GetBool() ) + { + Msg( "sv_noroundstats is enabled. Not uploading round stats.\n" ); + } + + m_MarketPurchases.PurgeAndDeleteElements(); + m_WeaponData.PurgeAndDeleteElements(); + m_DeathData.PurgeAndDeleteElements(); + return; + } + + m_bInRound = false; + + KeyValues *pKV = new KeyValues( "basedata" ); + if ( !pKV ) + return; + + totalTimer.Start(); + purchasesAndDeathsStatTimer.Start(); + + const char *pzMapName = gpGlobals->mapname.ToCStr(); + pKV->SetString( "MapID", pzMapName ); + + for ( int k=0 ; k < m_MarketPurchases.Count() ; ++k ) + SubmitStat( m_MarketPurchases[ k ] ); + + for ( int k=0 ; k < m_DeathData.Count() ; ++k ) + SubmitStat( m_DeathData[ k ] ); + + purchasesAndDeathsStatTimer.End(); + weaponsStatBuildTimer.Start(); + + // Now add the weapon stats that HPE collected + for (int iWeapon = 0; iWeapon < WEAPON_MAX; ++iWeapon) + { + CCSWeaponInfo* pInfo = GetWeaponInfo( (CSWeaponID)iWeapon ); + if ( !pInfo ) + continue; + + const char* pWeaponName = pInfo->szClassName; + if ( !pWeaponName || !pWeaponName[0] || ( m_weaponStats[iWeapon][0].shots == 0 && m_weaponStats[iWeapon][1].shots == 0 ) ) + continue; + + + m_WeaponData.AddToTail( new SCSSWeaponData( pWeaponName, m_weaponStats[iWeapon][0] ) ); + + // Now add the bot data if we're collecting detailled data + if ( GetSteamWorksSGameStatsUploader().IsCollectingDetails() ) + { + char pWeaponNameModified[64]; + V_snprintf( pWeaponNameModified, ARRAYSIZE(pWeaponNameModified), "%s_bot", pWeaponName ); + m_WeaponData.AddToTail( new SCSSWeaponData( pWeaponNameModified, m_weaponStats[iWeapon][1] ) ); + } + } + + weaponsStatBuildTimer.End(); + weaponsStatSubmitTimer.Start(); + + for ( int k=0 ; k < m_WeaponData.Count() ; ++k ) + SubmitStat( m_WeaponData[ k ] ); + + weaponsStatSubmitTimer.End(); + + // Perform the actual submission + submitTimer.Start(); + SubmitGameStats( pKV ); + submitTimer.End(); + + int iPurchases, iDeathData, iWeaponData; + int listCount = s_StatLists->Count(); + iPurchases = m_MarketPurchases.Count(); + iDeathData = m_DeathData.Count(); + iWeaponData = m_WeaponData.Count(); + + // Clear out the per round stats + cleanupTimer.Start(); + m_MarketPurchases.Purge(); + m_WeaponData.Purge(); + m_DeathData.Purge(); + pKV->deleteThis(); + cleanupTimer.End(); + + totalTimer.End(); + + if ( sv_debugroundstats.GetBool() ) + { + Msg( "**** ROUND STAT DEBUG ****\n" ); + Msg( "UploadRoundStats completed. %.3f msec. Breakdown:\n a: %.3f msec\n b: %.3f msec\n c: %.3f msec\n d: %.3f msec\n e: %.3f msec\n objects: %d %d %d %d\n commit: %.3fms\n write: %.3fms.\n\n", + totalTimer.GetDuration().GetMillisecondsF(), + purchasesAndDeathsStatTimer.GetDuration().GetMillisecondsF(), + weaponsStatBuildTimer.GetDuration().GetMillisecondsF(), + weaponsStatSubmitTimer.GetDuration().GetMillisecondsF(), + submitTimer.GetDuration().GetMillisecondsF(), + cleanupTimer.GetDuration().GetMillisecondsF(), + iPurchases, iDeathData, iWeaponData, listCount, + g_rowCommitTime, g_rowWriteTime); + } +} + +#ifdef _DEBUG +CON_COMMAND ( teststats, "Test command" ) +{ + CFastTimer totalTimer; + double uploadTime = 0.0f; + g_rowCommitTime = 0.0f; + g_rowWriteTime = 0.0f; + + + for( int i = 0; i < 1000; i++ ) + { + KeyValues *pKV = new KeyValues( "basedata" ); + if ( !pKV ) + return; + + pKV->SetName( "foobartest" ); + pKV->SetUint64( "test1", 1234 ); + pKV->SetUint64( "test2", 1234 ); + pKV->SetUint64( "test3", 1234 ); + pKV->SetUint64( "test4", 1234 ); + pKV->SetString( "test5", "TEST1234567890TEST1234567890TEST!"); + /*pKV->SetString( "test6", "TEST1234567890TEST1234567890TEST!"); + pKV->SetString( "test7", "TEST1234567890TEST1234567890TEST!"); + pKV->SetString( "test8", "TEST1234567890TEST1234567890TEST!"); + pKV->SetString( "test9", "TEST1234567890TEST1234567890TEST!");*/ + + totalTimer.Start(); + GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKV, args.ArgC() == 1 ); + totalTimer.End(); + + uploadTime += totalTimer.GetDuration().GetMillisecondsF(); + } + + Msg( "teststats took %.3f msec commit: %.3fms write: %.3fms.\n", uploadTime, g_rowCommitTime, g_rowWriteTime ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::SubmitGameStats( KeyValues *pKV ) +{ + int listCount = s_StatLists->Count(); + for( int i=0; i < listCount; ++i ) + { + // Create a master key value that has stats everybody should share (map name, session ID, etc) + (*s_StatLists)[i]->SendData(pKV); + (*s_StatLists)[i]->Clear(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_ShotFired( CBasePlayer *pPlayer, CBaseCombatWeapon* pWeapon ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + CWeaponCSBase* pCSWeapon = dynamic_cast< CWeaponCSBase * >(pWeapon); + + //============================================================================= + // HPE_BEGIN: + // [dwenger] adding tracking for weapon used fun fact + //============================================================================= + + if ( pCSPlayer ) + { + // [dwenger] Update the player's tracking of which weapon type they fired + pCSPlayer->PlayerUsedFirearm( pWeapon ); + + //============================================================================= + // HPE_END + //============================================================================= + + IncrementStat( pCSPlayer, CSSTAT_SHOTS_FIRED, 1 ); + // Increment the individual weapon + if( pCSWeapon ) + { + CSWeaponID weaponId = pCSWeapon->GetWeaponID(); + for (int i = 0; WeaponName_StatId_Table[i].shotStatId != CSSTAT_UNDEFINED; ++i) + { + if ( WeaponName_StatId_Table[i].weaponId == weaponId ) + { + IncrementStat( pCSPlayer, WeaponName_StatId_Table[i].shotStatId, 1 ); + break; + } + } + + int iType = pCSPlayer->IsBot(); + ++m_weaponStats[weaponId][iType].shots; + } + } +} + +void CCSGameStats::Event_ShotHit( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + IncrementStat( pCSPlayer, CSSTAT_SHOTS_HIT, 1 ); + + CBaseEntity *pInflictor = info.GetInflictor(); + + if ( pInflictor ) + { + if ( pInflictor == pPlayer ) + { + if ( pPlayer->GetActiveWeapon() ) + { + CWeaponCSBase* pCSWeapon = dynamic_cast< CWeaponCSBase * >(pPlayer->GetActiveWeapon()); + if (pCSWeapon) + { + CSWeaponID weaponId = pCSWeapon->GetWeaponID(); + for (int i = 0; WeaponName_StatId_Table[i].shotStatId != CSSTAT_UNDEFINED; ++i) + { + if ( WeaponName_StatId_Table[i].weaponId == weaponId ) + { + IncrementStat( pCSPlayer, WeaponName_StatId_Table[i].hitStatId, 1 ); + break; + } + } + } + } + } + } +} +void CCSGameStats::Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + IncrementStat( pCSPlayer, CSSTAT_DEATHS, 1 ); +} + +void CCSGameStats::Event_PlayerSprayedDecal( CCSPlayer* pPlayer ) +{ + IncrementStat( pPlayer, CSSTAT_DECAL_SPRAYS, 1 ); +} + +void CCSGameStats::Event_PlayerKilled_PreWeaponDrop( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + CCSPlayer *pAttacker = ToCSPlayer( info.GetAttacker() ); + bool victimZoomed = ( pCSPlayer->GetFOV() != pCSPlayer->GetDefaultFOV() ); + + if (victimZoomed) + { + IncrementStat(pAttacker, CSSTAT_KILLS_AGAINST_ZOOMED_SNIPER, 1); + } + + + //Check for knife fight + if (pAttacker && pCSPlayer && pAttacker == info.GetInflictor() && pAttacker->GetTeamNumber() != pCSPlayer->GetTeamNumber()) + { + CWeaponCSBase* attackerWeapon = pAttacker->GetActiveCSWeapon(); + CWeaponCSBase* victimWeapon = pCSPlayer->GetActiveCSWeapon(); + + if (attackerWeapon && victimWeapon) + { + CSWeaponID attackerWeaponID = attackerWeapon->GetWeaponID(); + CSWeaponID victimWeaponID = victimWeapon->GetWeaponID(); + + if (attackerWeaponID == WEAPON_KNIFE && victimWeaponID == WEAPON_KNIFE) + { + IncrementStat(pAttacker, CSSTAT_KILLS_KNIFE_FIGHT, 1); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_BombPlanted( CCSPlayer* pPlayer) +{ + IncrementStat( pPlayer, CSSTAT_NUM_BOMBS_PLANTED, 1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_BombDefused( CCSPlayer* pPlayer) +{ + + IncrementStat( pPlayer, CSSTAT_NUM_BOMBS_DEFUSED, 1 ); + IncrementStat( pPlayer, CSSTAT_OBJECTIVES_COMPLETED, 1 ); + if( pPlayer && pPlayer->HasDefuser() ) + { + IncrementStat( pPlayer, CSSTAT_BOMBS_DEFUSED_WITHKIT, 1 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Increment terrorist team stat +//----------------------------------------------------------------------------- +void CCSGameStats::Event_BombExploded( CCSPlayer* pPlayer ) +{ + IncrementStat( pPlayer, CSSTAT_OBJECTIVES_COMPLETED, 1 ); +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_HostageRescued( CCSPlayer* pPlayer) +{ + IncrementStat( pPlayer, CSSTAT_NUM_HOSTAGES_RESCUED, 1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Increment counter-terrorist team stat +//----------------------------------------------------------------------------- +void CCSGameStats::Event_AllHostagesRescued() +{ + IncrementTeamStat( TEAM_CT, CSSTAT_OBJECTIVES_COMPLETED, 1 ); +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_WindowShattered( CBasePlayer *pPlayer) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + IncrementStat( pCSPlayer, CSSTAT_NUM_BROKEN_WINDOWS, 1 ); +} + + +void CCSGameStats::Event_BreakProp( CCSPlayer* pPlayer, CBreakableProp *pProp ) +{ + if (!pPlayer) + return; + + DevMsg("Player %s broke a %s (%i)\n", pPlayer->GetPlayerName(), pProp->GetModelName().ToCStr(), pProp->entindex()); + + int iIndex = m_PropStatTable.Find(pProp->GetModelName().ToCStr()); + if (m_PropStatTable.IsValidIndex(iIndex)) + { + IncrementStat(pPlayer, m_PropStatTable[iIndex], 1); + } + IncrementStat(pPlayer, CSSTAT_PROPSBROKEN_ALL, 1); +} + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::UpdatePlayerRoundStats(int winner) +{ + int mapIndex = GetCSLevelIndex(gpGlobals->mapname.ToCStr()); + CSStatType_t mapStatWinIndex = CSSTAT_UNDEFINED, mapStatRoundIndex = CSSTAT_UNDEFINED; + + if ( mapIndex != -1 ) + { + mapStatWinIndex = MapName_StatId_Table[mapIndex].statWinsId; + mapStatRoundIndex = MapName_StatId_Table[mapIndex].statRoundsId; + } + + // increment the team specific stats + IncrementTeamStat( winner, CSSTAT_ROUNDS_WON, 1 ); + if ( mapStatWinIndex != CSSTAT_UNDEFINED ) + { + IncrementTeamStat( winner, mapStatWinIndex, 1 ); + } + if ( CSGameRules()->IsPistolRound() ) + { + IncrementTeamStat( winner, CSSTAT_PISTOLROUNDS_WON, 1 ); + } + IncrementTeamStat( TEAM_TERRORIST, CSSTAT_ROUNDS_PLAYED, 1 ); + IncrementTeamStat( TEAM_CT, CSSTAT_ROUNDS_PLAYED, 1 ); + IncrementTeamStat( TEAM_TERRORIST, mapStatRoundIndex, 1 ); + IncrementTeamStat( TEAM_CT, mapStatRoundIndex, 1 ); + + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected() ) + { + if ( winner == TEAM_CT ) + { + IncrementStat( pPlayer, CSSTAT_CT_ROUNDS_WON, 1, true ); + } + else if ( winner == TEAM_TERRORIST ) + { + IncrementStat( pPlayer, CSSTAT_T_ROUNDS_WON, 1, true ); + } + + if ( winner == TEAM_CT || winner == TEAM_TERRORIST ) + { + // Increment the win stats if this player is on the winning team + if ( pPlayer->GetTeamNumber() == winner ) + { + IncrementStat( pPlayer, CSSTAT_ROUNDS_WON, 1, true ); + + if ( CSGameRules()->IsPistolRound() ) + { + IncrementStat( pPlayer, CSSTAT_PISTOLROUNDS_WON, 1, true ); + } + + if ( mapStatWinIndex != CSSTAT_UNDEFINED ) + { + IncrementStat( pPlayer, mapStatWinIndex, 1, true ); + } + } + + IncrementStat( pPlayer, CSSTAT_ROUNDS_PLAYED, 1 ); + if ( mapStatWinIndex != CSSTAT_UNDEFINED ) + { + IncrementStat( pPlayer, mapStatRoundIndex, 1, true ); + } + + // set the play time for the round + IncrementStat( pPlayer, CSSTAT_PLAYTIME, (int)CSGameRules()->GetRoundElapsedTime(), true ); + } + } + } + + // send a stats update to all players + for ( int iPlayerIndex = 1; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected()) + { + SendStatsToPlayer(pPlayer, CSSTAT_PRIORITY_ENDROUND); + } + } +} + +void CCSGameStats::SendRollingStatsAveragesToAllPlayers() +{ + //The most stats we can send at once is the max message size minus the header, divided by the total size of each stat. + const int maxStatsPerMessage = (MAX_USER_MSG_DATA - sizeof(CSStatType_t)) / (3 * sizeof(float)); + + const int numMessagesNeeded = ((CSSTAT_MAX - CSSTAT_FIRST) / maxStatsPerMessage) + 1; + + for (int batchIndex = 0 ; batchIndex < numMessagesNeeded ; ++batchIndex) + { + int firstStatInThisBatch = (batchIndex * maxStatsPerMessage) + CSSTAT_FIRST; + int lastMessageInThisBatch = firstStatInThisBatch + maxStatsPerMessage - 1; + + CRecipientFilter filter; + filter.AddAllPlayers(); + UserMessageBegin( filter, "MatchStatsUpdate" ); + + WRITE_SHORT(firstStatInThisBatch); + + for ( int iStat = firstStatInThisBatch; iStat < CSSTAT_MAX && iStat <= lastMessageInThisBatch; ++iStat ) + { + WRITE_FLOAT(m_rollingTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_rollingCTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_rollingPlayerStatAverages.m_fStat[iStat]); + } + + MessageEnd(); + } +} + + +void CCSGameStats::SendDirectStatsAveragesToAllPlayers() +{ + //The most stats we can send at once is the max message size minus the header, divided by the total size of each stat. + const int maxStatsPerMessage = (MAX_USER_MSG_DATA - sizeof(CSStatType_t)) / (3 * sizeof(float)); + + const int numMessagesNeeded = ((CSSTAT_MAX - CSSTAT_FIRST) / maxStatsPerMessage) + 1; + + for (int batchIndex = 0 ; batchIndex < numMessagesNeeded ; ++batchIndex) + { + int firstStatInThisBatch = (batchIndex * maxStatsPerMessage) + CSSTAT_FIRST; + int lastMessageInThisBatch = firstStatInThisBatch + maxStatsPerMessage - 1; + + CRecipientFilter filter; + filter.AddAllPlayers(); + UserMessageBegin( filter, "MatchStatsUpdate" ); + + WRITE_SHORT(firstStatInThisBatch); + + for ( int iStat = firstStatInThisBatch; iStat < CSSTAT_MAX && iStat <= lastMessageInThisBatch; ++iStat ) + { + WRITE_FLOAT(m_directTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_directCTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_directPlayerStatAverages.m_fStat[iStat]); + } + + MessageEnd(); + } +} + +void CCSGameStats::ComputeRollingStatAverages() +{ + int numPlayers = 0; + int numCTs = 0; + int numTs = 0; + + RoundStatsRollingAverage_t currentRoundTStatsAverage; + RoundStatsRollingAverage_t currentRoundCTStatsAverage; + RoundStatsRollingAverage_t currentRoundPlayerStatsAverage; + + currentRoundTStatsAverage.Reset(); + currentRoundCTStatsAverage.Reset(); + currentRoundPlayerStatsAverage.Reset(); + + //for ( int iStat = CSSTAT_FIRST; iStat < CSSTAT_MAX; ++iStat ) + { + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected()) + { + StatsCollection_t &roundStats = m_aPlayerStats[pPlayer->entindex()].statsCurrentRound; + + int teamNumber = pPlayer->GetTeamNumber(); + if (teamNumber == TEAM_CT) + { + numCTs++; + numPlayers++; + currentRoundCTStatsAverage += roundStats; + currentRoundPlayerStatsAverage += roundStats; + } + else if (teamNumber == TEAM_TERRORIST) + { + numTs++; + numPlayers++; + currentRoundTStatsAverage += roundStats; + currentRoundPlayerStatsAverage += roundStats; + } + } + } + + if (numTs > 0) + { + currentRoundTStatsAverage /= numTs; + } + + if (numCTs > 0) + { + currentRoundCTStatsAverage /= numCTs; + } + + if (numPlayers > 0) + { + currentRoundPlayerStatsAverage /= numPlayers; + } + + m_rollingTStatAverages.RollDataSetIntoAverage(currentRoundTStatsAverage); + m_rollingCTStatAverages.RollDataSetIntoAverage(currentRoundCTStatsAverage); + m_rollingPlayerStatAverages.RollDataSetIntoAverage(currentRoundPlayerStatsAverage); + } +} + + +void CCSGameStats::ComputeDirectStatAverages() +{ + m_numberOfRoundsForDirectAverages++; + + m_directCTStatAverages.Reset(); + m_directTStatAverages.Reset(); + m_directPlayerStatAverages.Reset(); + + + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected()) + { + StatsCollection_t &matchStats = m_aPlayerStats[pPlayer->entindex()].statsCurrentMatch; + + int teamNumber = pPlayer->GetTeamNumber(); + if (teamNumber == TEAM_CT) + { + m_numberOfCounterTerroristEntriesForDirectAverages++; + m_directCTStatAverages += matchStats; + m_directPlayerStatAverages += matchStats; + } + else if (teamNumber == TEAM_TERRORIST) + { + m_numberOfTerroristEntriesForDirectAverages++; + m_directTStatAverages += matchStats; + m_directPlayerStatAverages += matchStats; + } + } + } + + if (m_numberOfTerroristEntriesForDirectAverages > 0) + { + m_directTStatAverages /= m_numberOfTerroristEntriesForDirectAverages; + m_directTStatAverages *= m_numberOfRoundsForDirectAverages; + } + + if (m_numberOfCounterTerroristEntriesForDirectAverages > 0) + { + m_directCTStatAverages /= m_numberOfCounterTerroristEntriesForDirectAverages; + m_directCTStatAverages *= m_numberOfRoundsForDirectAverages; + } + + int numPlayers = m_numberOfCounterTerroristEntriesForDirectAverages + m_numberOfTerroristEntriesForDirectAverages; + + if (numPlayers > 0) + { + m_directPlayerStatAverages /= numPlayers; + m_directPlayerStatAverages *= m_numberOfRoundsForDirectAverages; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Log accumulated weapon usage and performance data +//----------------------------------------------------------------------------- +void CCSGameStats::DumpMatchWeaponMetrics() +{ + //// generate a filename + //time_t t = time( NULL ); + //struct tm *now = localtime( &t ); + //if ( !now ) + // return; + + //int year = now->tm_year + 1900; + //int month = now->tm_mon + 1; + //int day = now->tm_mday; + //int hour = now->tm_hour; + //int minute = now->tm_min; + //int second = now->tm_sec; + + //char filename[ 128 ]; + //Q_snprintf( filename, sizeof(filename), "wm_%4d%02d%02d_%02d%02d%02d_%s.csv", + // year, month, day, hour, minute, second, gpGlobals->mapname.ToCStr()); + + //FileHandle_t hLogFile = filesystem->Open( filename, "wt" ); + + //if ( hLogFile == FILESYSTEM_INVALID_HANDLE ) + // return; + + //filesystem->FPrintf(hLogFile, "%s\n", "WeaponId, Mode, Cost, Bullets, CycleTime, TotalShots, TotalHits, TotalDamage, TotalKills"); + + //for (int iWeapon = 0; iWeapon < WEAPON_MAX; ++iWeapon) + //{ + // CCSWeaponInfo* pInfo = GetWeaponInfo( (CSWeaponID)iWeapon ); + // if ( !pInfo ) + // continue; + + // const char* pWeaponName = pInfo->szClassName; + // if ( !pWeaponName ) + // continue; + + // if ( V_strncmp(pWeaponName, "weapon_", 7) == 0 ) + // pWeaponName += 7; + + // for ( int iMode = 0; iMode < WeaponMode_MAX; ++iMode) + // { + // filesystem->FPrintf(hLogFile, "%s, %d, %d, %d, %f, %d, %d, %d, %d\n", + // pWeaponName, + // iMode, + // pInfo->GetWeaponPrice(), + // pInfo->m_iBullets, + // pInfo->m_flCycleTime, + // m_weaponStats[iWeapon][iMode].shots, + // m_weaponStats[iWeapon][iMode].hits, + // m_weaponStats[iWeapon][iMode].damage, + // m_weaponStats[iWeapon][iMode].kills); + // } + //} + + //filesystem->FPrintf(hLogFile, "\n"); + //filesystem->FPrintf(hLogFile, "weapon_accuracy_model, %d\n", weapon_accuracy_model.GetInt()); + //filesystem->FPrintf(hLogFile, "bot_difficulty, %d\n", cv_bot_difficulty.GetInt()); + + //g_pFullFileSystem->Close(hLogFile); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_PlayerConnected( CBasePlayer *pPlayer ) +{ + ResetPlayerStats( ToCSPlayer( pPlayer ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_PlayerDisconnected( CBasePlayer *pPlayer ) +{ + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + if ( !pCSPlayer ) + return; + + ResetPlayerStats( pCSPlayer ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_PlayerKilledOther( CBasePlayer *pAttacker, CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + // This also gets called when the victim is a building. That gets tracked separately as building destruction, don't count it here + if ( !pVictim->IsPlayer() ) + return; + + CBaseEntity *pInflictor = info.GetInflictor(); + + CCSPlayer *pPlayerAttacker = ToCSPlayer( pAttacker ); + CCSPlayer *pPlayerVictim = ToCSPlayer( pVictim ); + + // keep track of how many times every player kills every other player + TrackKillStats( pPlayerAttacker, pPlayerVictim ); + + // Skip rest of stat reporting for friendly fire + if ( pPlayerAttacker->GetTeam() == pVictim->GetTeam() ) + return; + + CSWeaponID weaponId = WEAPON_NONE; + + if ( pInflictor ) + { + if ( pInflictor == pAttacker ) + { + if ( pAttacker->GetActiveWeapon() ) + { + CBaseCombatWeapon* weapon = pAttacker->GetActiveWeapon(); + if (weapon) + { + CWeaponCSBase* pCSWeapon = static_cast<CWeaponCSBase*>(weapon); + + weaponId = pCSWeapon->GetWeaponID(); + + CCSWeaponInfo *info = GetWeaponInfo(weaponId); + + if (info && info->m_iTeam != TEAM_UNASSIGNED && pAttacker->GetTeamNumber() != info->m_iTeam ) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_ENEMY_WEAPON, 1); + } + } + } + } + else + { + //In the case of grenades, the inflictor is the spawned grenade entity. + if ( V_strcmp(pInflictor->m_iClassname.ToCStr(), "hegrenade_projectile") == 0 ) + weaponId = WEAPON_HEGRENADE; + } + } + + // update weapon stats + ++m_weaponStats[weaponId][pPlayerAttacker->IsBot()].kills; + + for (int i = 0; WeaponName_StatId_Table[i].killStatId != CSSTAT_UNDEFINED; ++i) + { + if ( WeaponName_StatId_Table[i].weaponId == weaponId ) + { + IncrementStat( pPlayerAttacker, WeaponName_StatId_Table[i].killStatId, 1 ); + break; + } + } + + if (pPlayerVictim && pPlayerVictim->IsBlind()) + { + IncrementStat( pPlayerAttacker, CSSTAT_KILLS_ENEMY_BLINDED, 1 ); + } + + if (pPlayerVictim && pPlayerAttacker && pPlayerAttacker->IsBlindForAchievement()) + { + IncrementStat( pPlayerAttacker, CSSTAT_KILLS_WHILE_BLINDED, 1 ); + } + + // [sbodenbender] check for deaths near planted bomb for funfact + if (pPlayerVictim && pPlayerAttacker && pPlayerAttacker->GetTeamNumber() == TEAM_TERRORIST && CSGameRules()->m_bBombPlanted) + { + float bombCheckDistSq = AchievementConsts::KillEnemyNearBomb_MaxDistance * AchievementConsts::KillEnemyNearBomb_MaxDistance; + for ( int i=0; i < g_PlantedC4s.Count(); i++ ) + { + CPlantedC4 *pC4 = g_PlantedC4s[i]; + + if ( pC4->IsBombActive() ) + { + Vector bombPos = pC4->GetAbsOrigin(); + Vector victimToBomb = pPlayerVictim->GetAbsOrigin() - bombPos; + Vector attackerToBomb = pPlayerAttacker->GetAbsOrigin() - bombPos; + if (victimToBomb.LengthSqr() < bombCheckDistSq || attackerToBomb.LengthSqr() < bombCheckDistSq) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_WHILE_DEFENDING_BOMB, 1); + break; // you only get credit for one kill even if you happen to be by more than one bomb + } + } + } + } + + //Increment stat if this is a headshot. + if (info.GetDamageType() & DMG_HEADSHOT) + { + IncrementStat( pPlayerAttacker, CSSTAT_KILLS_HEADSHOT, 1 ); + } + + IncrementStat( pPlayerAttacker, CSSTAT_KILLS, 1 ); + + // we don't have a simple way (yet) to check if the victim actually just achieved The Unstoppable Force, so we + // award this achievement simply if they've met the requirements and would have received it. + PlayerStats_t &victimStats = m_aPlayerStats[pVictim->entindex()]; + if (victimStats.statsCurrentRound[CSSTAT_KILLS] >= AchievementConsts::UnstoppableForce_Kills) + { + pPlayerAttacker->AwardAchievement(CSImmovableObject); + } + + CCSGameRules::TeamPlayerCounts playerCounts[TEAM_MAXCOUNT]; + + CSGameRules()->GetPlayerCounts(playerCounts); + int iAttackerTeamNumber = pPlayerAttacker->GetTeamNumber() ; + if (playerCounts[iAttackerTeamNumber].totalAlivePlayers == 1 && playerCounts[iAttackerTeamNumber].killedPlayers >= 2) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_WHILE_LAST_PLAYER_ALIVE, 1); + } + + //if they were damaged by more than one person that must mean that someone else did damage before the killer finished them off. + if (pPlayerVictim->GetNumEnemyDamagers() > 1) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_ENEMY_WOUNDED, 1); + } + + // Let's check for the "Happy Camper" achievement where we snipe two players while standing in the same spot. + if ( pPlayerAttacker && !pPlayerAttacker->IsBot() && weaponId != WEAPON_NONE ) + { + // Were we using a sniper rifle? + bool bUsingSniper = ( weaponId == WEAPON_AWP || + weaponId == WEAPON_SCOUT || + weaponId == WEAPON_SG550 || + weaponId == WEAPON_G3SG1 ); + + // If we're zoomed in + if ( bUsingSniper && pPlayerAttacker->GetFOV() != pPlayerAttacker->GetDefaultFOV() ) + { + // Get our position + Vector position = pPlayerAttacker->GetAbsOrigin(); + int userid = pPlayerAttacker->GetUserID(); + + bool bFoundPlayerEntry = false; + FOR_EACH_LL( m_PlayerSnipedPosition, iElement ) + { + sHappyCamperSnipePosition *pSnipePosition = &m_PlayerSnipedPosition[iElement]; + + // We've found this player's entry. Next, check to see if they meet the achievement criteria + if ( userid == pSnipePosition->m_iUserID ) + { + bFoundPlayerEntry = true; + Vector posDif = position - pSnipePosition->m_vPos; + + // Give a small epsilon to account for floating point anomalies + if ( posDif.IsLengthLessThan( .01f) ) + { + pPlayerAttacker->AwardAchievement( CSSnipeTwoFromSameSpot ); + } + + // Update their position + pSnipePosition->m_vPos = position; + break; + } + } + + // No entry so add one + if ( !bFoundPlayerEntry ) + { + m_PlayerSnipedPosition.AddToTail( sHappyCamperSnipePosition( userid, position ) ); + } + } + } +} + + +void CCSGameStats::CalculateOverkill(CCSPlayer* pAttacker, CCSPlayer* pVictim) +{ + //Count domination overkills - Do this before determining domination + if (pAttacker->GetTeam() != pVictim->GetTeam()) + { + if (pAttacker->IsPlayerDominated(pVictim->entindex())) + { + IncrementStat( pAttacker, CSSTAT_DOMINATION_OVERKILLS, 1 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Steamworks Gamestats death tracking +//----------------------------------------------------------------------------- +void CCSGameStats::PlayerKilled( CBasePlayer *pVictim, const CTakeDamageInfo &info ) +{ + if ( !pVictim ) + return; + + m_DeathData.AddToTail( new SCSSDeathData( pVictim, info ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Stats event for giving damage to player +//----------------------------------------------------------------------------- +void CCSGameStats::Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info ) +{ + CCSPlayer *pAttacker = ToCSPlayer( info.GetAttacker() ); + if ( pAttacker && pAttacker->GetTeam() != pBasePlayer->GetTeam() ) + { + CSWeaponMode weaponMode = Primary_Mode; + IncrementStat( pAttacker, CSSTAT_DAMAGE, info.GetDamage() ); + + if (pAttacker->m_bNightVisionOn) + { + IncrementStat( pAttacker, CSSTAT_NIGHTVISION_DAMAGE, info.GetDamage() ); + } + + const char *pWeaponName = NULL; + + if ( info.GetInflictor() == info.GetAttacker() ) + { + if ( pAttacker->GetActiveWeapon() ) + { + CWeaponCSBase* pCSWeapon = dynamic_cast< CWeaponCSBase * >(pAttacker->GetActiveWeapon()); + if (pCSWeapon) + { + pWeaponName = pCSWeapon->GetClassname(); + weaponMode = pCSWeapon->m_weaponMode; + } + } + } + // Need to determine the weapon name + else + { + pWeaponName = info.GetInflictor()->GetClassname(); + } + + // Unknown weapon?!? + if ( !pWeaponName ) + return; + + // Now update the damage this weapon has done + CSWeaponID weaponId = AliasToWeaponID( GetTranslatedWeaponAlias( pWeaponName ) ); + for (int i = 0; WeaponName_StatId_Table[i].shotStatId != CSSTAT_UNDEFINED; ++i) + { + if ( weaponId == WeaponName_StatId_Table[i].weaponId ) + { + int weaponId = WeaponName_StatId_Table[i].weaponId; + int iType = pAttacker->IsBot(); + ++m_weaponStats[weaponId][iType].hits; + m_weaponStats[weaponId][iType].damage += info.GetDamage(); + break; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Stats event for giving money to player +//----------------------------------------------------------------------------- +void CCSGameStats::Event_MoneyEarned( CCSPlayer* pPlayer, int moneyEarned) +{ + if ( pPlayer && moneyEarned > 0) + { + IncrementStat(pPlayer, CSSTAT_MONEY_EARNED, moneyEarned); + } +} + +void CCSGameStats::Event_MoneySpent( CCSPlayer* pPlayer, int moneySpent, const char *pItemName ) +{ + if ( pPlayer && moneySpent > 0) + { + IncrementStat(pPlayer, CSSTAT_MONEY_SPENT, moneySpent); + if ( pItemName && !pPlayer->IsBot() ) + { + CSteamID steamIDForBuyer; + pPlayer->GetSteamID( &steamIDForBuyer ); + m_MarketPurchases.AddToTail( new SMarketPurchases( steamIDForBuyer.ConvertToUint64(), moneySpent, pItemName ) ); + } + } +} + +void CCSGameStats::Event_PlayerDonatedWeapon (CCSPlayer* pPlayer) +{ + if (pPlayer) + { + IncrementStat(pPlayer, CSSTAT_WEAPONS_DONATED, 1); + } +} + +void CCSGameStats::Event_MVPEarned( CCSPlayer* pPlayer ) +{ + if (pPlayer) + { + IncrementStat(pPlayer, CSSTAT_MVPS, 1); + } +} + +void CCSGameStats::Event_KnifeUse( CCSPlayer* pPlayer, bool bStab, int iDamage ) +{ + if (pPlayer) + { + int weaponId = WEAPON_KNIFE; + int iType = pPlayer->IsBot(); + + IncrementStat( pPlayer, CSSTAT_SHOTS_KNIFE, 1 ); + ++m_weaponStats[weaponId][iType].shots; + if ( iDamage ) + { + IncrementStat( pPlayer, CSSTAT_HITS_KNIFE, 1 ); + ++m_weaponStats[weaponId][iType].hits; + + IncrementStat( pPlayer, CSSTAT_DAMAGE_KNIFE, iDamage ); + m_weaponStats[weaponId][iType].damage += iDamage; + } + } +} +//----------------------------------------------------------------------------- +// Purpose: Event handler +//----------------------------------------------------------------------------- +void CCSGameStats::FireGameEvent( IGameEvent *event ) +{ + const char *pEventName = event->GetName(); + + if ( V_strcmp(pEventName, "round_start") == 0 ) + { + m_PlayerSnipedPosition.Purge(); + m_bInRound = true; + } + else if ( V_strcmp(pEventName, "round_end") == 0 ) + { + const int reason = event->GetInt( "reason" ); + + if( reason == Game_Commencing ) + { + ResetPlayerClassMatchStats(); + } + else + { + UpdatePlayerRoundStats(event->GetInt("winner")); + ComputeDirectStatAverages(); + SendDirectStatsAveragesToAllPlayers(); + + UploadRoundStats(); + ResetWeaponStats(); + } + return; + } + else if ( V_strcmp(pEventName, "break_prop") == 0 ) + { + int userid = event->GetInt("userid", 0); + int entindex = event->GetInt("entindex", 0); + CBreakableProp* pProp = static_cast<CBreakableProp*>(CBaseEntity::Instance(entindex)); + Event_BreakProp(ToCSPlayer(UTIL_PlayerByUserId(userid)), pProp); + } + else if ( V_strcmp(pEventName, "player_decal") == 0 ) + { + int userid = event->GetInt("userid", 0); + Event_PlayerSprayedDecal(ToCSPlayer(UTIL_PlayerByUserId(userid))); + } + else if ( V_strcmp(pEventName, "hegrenade_detonate") == 0 ) + { + int userid = event->GetInt("userid", 0); + IncrementStat( ToCSPlayer(UTIL_PlayerByUserId(userid)), CSSTAT_SHOTS_HEGRENADE, 1 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Return stats for the given player +//----------------------------------------------------------------------------- +const PlayerStats_t& CCSGameStats::FindPlayerStats( CBasePlayer *pPlayer ) const +{ + return m_aPlayerStats[pPlayer->entindex()]; +} + +//----------------------------------------------------------------------------- +// Purpose: Return stats for the given team +//----------------------------------------------------------------------------- +const StatsCollection_t& CCSGameStats::GetTeamStats( int iTeamIndex ) const +{ + int arrayIndex = iTeamIndex - FIRST_GAME_TEAM; + Assert( arrayIndex >= 0 && arrayIndex < TEAM_MAXCOUNT - FIRST_GAME_TEAM ); + return m_aTeamStats[arrayIndex]; +} + +//----------------------------------------------------------------------------- +// Purpose: Resets the stats for each team +//----------------------------------------------------------------------------- +void CCSGameStats::ResetAllTeamStats() +{ + for ( int i = 0; i < ARRAYSIZE(m_aTeamStats); ++i ) + { + m_aTeamStats[i].Reset(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Resets all stats (including round, match, accumulated and rolling averages +//----------------------------------------------------------------------------- +void CCSGameStats::ResetAllStats() +{ + for ( int i = 0; i < ARRAYSIZE( m_aPlayerStats ); i++ ) + { + m_aPlayerStats[i].statsDelta.Reset(); + m_aPlayerStats[i].statsCurrentRound.Reset(); + m_aPlayerStats[i].statsCurrentMatch.Reset(); + m_rollingCTStatAverages.Reset(); + m_rollingTStatAverages.Reset(); + m_rollingPlayerStatAverages.Reset(); + m_numberOfRoundsForDirectAverages = 0; + m_numberOfTerroristEntriesForDirectAverages = 0; + m_numberOfCounterTerroristEntriesForDirectAverages = 0; + } +} + + +void CCSGameStats::ResetWeaponStats() +{ + V_memset(m_weaponStats, 0, sizeof(m_weaponStats)); +} + +void CCSGameStats::IncrementTeamStat( int iTeamIndex, int iStatIndex, int iAmount ) +{ + int arrayIndex = iTeamIndex - TEAM_TERRORIST; + Assert( iStatIndex >= 0 && iStatIndex < CSSTAT_MAX ); + if( arrayIndex >= 0 && arrayIndex < TEAM_MAXCOUNT - TEAM_TERRORIST ) + { + m_aTeamStats[arrayIndex][iStatIndex] += iAmount; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Resets all stats for this player +//----------------------------------------------------------------------------- +void CCSGameStats::ResetPlayerStats( CBasePlayer* pPlayer ) +{ + PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()]; + // reset the stats on this player + stats.Reset(); + // reset the matrix of who killed whom with respect to this player + ResetKillHistory( pPlayer ); +} + +//----------------------------------------------------------------------------- +// Purpose: Resets the kill history for this player +//----------------------------------------------------------------------------- +void CCSGameStats::ResetKillHistory( CBasePlayer* pPlayer ) +{ + int iPlayerIndex = pPlayer->entindex(); + + PlayerStats_t& statsPlayer = m_aPlayerStats[iPlayerIndex]; + + // for every other player, set all all the kills with respect to this player to 0 + for ( int i = 0; i < ARRAYSIZE( m_aPlayerStats ); i++ ) + { + //reset their record of us. + PlayerStats_t &statsOther = m_aPlayerStats[i]; + statsOther.statsKills.iNumKilled[iPlayerIndex] = 0; + statsOther.statsKills.iNumKilledBy[iPlayerIndex] = 0; + statsOther.statsKills.iNumKilledByUnanswered[iPlayerIndex] = 0; + + //reset our record of them + statsPlayer.statsKills.iNumKilled[i] = 0; + statsPlayer.statsKills.iNumKilledBy[i] = 0; + statsPlayer.statsKills.iNumKilledByUnanswered[i] = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Resets per-round stats for all players +//----------------------------------------------------------------------------- +void CCSGameStats::ResetRoundStats() +{ + for ( int i = 0; i < ARRAYSIZE( m_aPlayerStats ); i++ ) + { + m_aPlayerStats[i].statsCurrentRound.Reset(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Increments specified stat for specified player by specified amount +//----------------------------------------------------------------------------- +void CCSGameStats::IncrementStat( CCSPlayer* pPlayer, CSStatType_t statId, int iDelta, bool bPlayerOnly /* = false */ ) +{ + if ( pPlayer ) + { + PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()]; + stats.statsDelta[statId] += iDelta; + stats.statsCurrentRound[statId] += iDelta; + stats.statsCurrentMatch[statId] += iDelta; + + // increment team stat + int teamIndex = pPlayer->GetTeamNumber() - FIRST_GAME_TEAM; + if ( !bPlayerOnly && teamIndex >= 0 && teamIndex < ARRAYSIZE(m_aTeamStats) ) + { + m_aTeamStats[teamIndex][statId] += iDelta; + } + + for (int i = 0; i < ARRAYSIZE(ServerStatBasedAchievements); ++i) + { + if (ServerStatBasedAchievements[i].statId == statId) + { + // skip this if there is a map filter and it doesn't match + if (ServerStatBasedAchievements[i].mapFilter != NULL && V_strcmp(gpGlobals->mapname.ToCStr(), ServerStatBasedAchievements[i].mapFilter) != 0) + continue; + + bool bWasMet = ServerStatBasedAchievements[i].IsMet(stats.statsCurrentRound[statId] - iDelta, stats.statsCurrentMatch[statId] - iDelta); + bool bIsMet = ServerStatBasedAchievements[i].IsMet(stats.statsCurrentRound[statId], stats.statsCurrentMatch[statId]); + if (!bWasMet && bIsMet) + { + pPlayer->AwardAchievement(ServerStatBasedAchievements[i].achievementId); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the specified stat for specified player to the specified amount +//----------------------------------------------------------------------------- +void CCSGameStats::SetStat( CCSPlayer *pPlayer, CSStatType_t statId, int iValue ) +{ + if (pPlayer) + { + int oldRoundValue, oldMatchValue; + PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()]; + + oldRoundValue = stats.statsCurrentRound[statId]; + oldMatchValue = stats.statsCurrentMatch[statId]; + + stats.statsDelta[statId] = iValue; + stats.statsCurrentRound[statId] = iValue; + stats.statsCurrentMatch[statId] = iValue; + + for (int i = 0; i < ARRAYSIZE(ServerStatBasedAchievements); ++i) + { + if (ServerStatBasedAchievements[i].statId == statId) + { + // skip this if there is a map filter and it doesn't match + if (ServerStatBasedAchievements[i].mapFilter != NULL && V_strcmp(gpGlobals->mapname.ToCStr(), ServerStatBasedAchievements[i].mapFilter) != 0) + continue; + + bool bWasMet = ServerStatBasedAchievements[i].IsMet(oldRoundValue, oldMatchValue); + bool bIsMet = ServerStatBasedAchievements[i].IsMet(stats.statsCurrentRound[statId], stats.statsCurrentMatch[statId]); + if (!bWasMet && bIsMet) + { + pPlayer->AwardAchievement(ServerStatBasedAchievements[i].achievementId); + } + } + } + } +} + +void CCSGameStats::SendStatsToPlayer( CCSPlayer * pPlayer, int iMinStatPriority ) +{ + ASSERT(CSSTAT_MAX < 255); // if we add more than 255 stats, we'll need to update this protocol + if ( pPlayer && pPlayer->IsConnected()) + { + StatsCollection_t &deltaStats = m_aPlayerStats[pPlayer->entindex()].statsDelta; + + // check to see if we have any stats to actually send + byte iStatsToSend = 0; + for ( int iStat = CSSTAT_FIRST; iStat < CSSTAT_MAX; ++iStat ) + { + ASSERT(CSStatProperty_Table[iStat].statId == iStat); + if ( CSStatProperty_Table[iStat].statId != iStat ) + { + Warning( "CSStatProperty_Table[iStat].statId != iStat, (%d)", CSStatProperty_Table[iStat].statId ); + } + int iPriority = CSStatProperty_Table[iStat].flags & CSSTAT_PRIORITY_MASK; + if (deltaStats[iStat] != 0 && iPriority >= iMinStatPriority) + { + ++iStatsToSend; + } + } + + // nothing changed - bail out + if ( !iStatsToSend ) + return; + + CSingleUserRecipientFilter filter( pPlayer ); + filter.MakeReliable(); + UserMessageBegin( filter, "PlayerStatsUpdate" ); + + CRC32_t crc; + CRC32_Init( &crc ); + + // begin the CRC with a trivially hidden key value to discourage packet modification + const uint32 key = 0x82DA9F4C; // this key should match the key in cs_client_gamestats.cpp + CRC32_ProcessBuffer( &crc, &key, sizeof(key)); + + // if we make any change to the ordering of the stats or this message format, update this value + const byte version = 0x01; + CRC32_ProcessBuffer( &crc, &version, sizeof(version)); + WRITE_BYTE(version); + + CRC32_ProcessBuffer( &crc, &iStatsToSend, sizeof(iStatsToSend)); + WRITE_BYTE(iStatsToSend); + + for ( byte iStat = CSSTAT_FIRST; iStat < CSSTAT_MAX; ++iStat ) + { + int iPriority = CSStatProperty_Table[iStat].flags & CSSTAT_PRIORITY_MASK; + if (deltaStats[iStat] != 0 && iPriority >= iMinStatPriority) + { + CRC32_ProcessBuffer( &crc, &iStat, sizeof(iStat)); + WRITE_BYTE(iStat); + Assert(deltaStats[iStat] <= 0x7FFF && deltaStats[iStat] > 0); // make sure we aren't truncating bits + short delta = deltaStats[iStat]; + CRC32_ProcessBuffer( &crc, &delta, sizeof(delta)); + WRITE_SHORT( deltaStats[iStat]); + deltaStats[iStat] = 0; + --iStatsToSend; + } + } + + Assert(iStatsToSend == 0); + + CRC32_Final( &crc ); + WRITE_LONG(crc); + + MessageEnd(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Sends intermittent stats updates for stats that need to be updated during a round and/or life +//----------------------------------------------------------------------------- +void CCSGameStats::PreClientUpdate() +{ + int iMinStatPriority = -1; + m_fDisseminationTimerHigh += gpGlobals->frametime; + m_fDisseminationTimerLow += gpGlobals->frametime; + + if ( m_fDisseminationTimerHigh > cDisseminationTimeHigh) + { + iMinStatPriority = CSSTAT_PRIORITY_HIGH; + m_fDisseminationTimerHigh = 0.0f; + + if ( m_fDisseminationTimerLow > cDisseminationTimeLow) + { + iMinStatPriority = CSSTAT_PRIORITY_LOW; + m_fDisseminationTimerLow = 0.0f; + } + } + else + return; + + //The proper time has elapsed, now send the update to every player + for ( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex(iPlayerIndex) ); + SendStatsToPlayer(pPlayer, iMinStatPriority); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Updates the stats of who has killed whom +//----------------------------------------------------------------------------- +void CCSGameStats::TrackKillStats( CCSPlayer *pAttacker, CCSPlayer *pVictim ) +{ + int iPlayerIndexAttacker = pAttacker->entindex(); + int iPlayerIndexVictim = pVictim->entindex(); + + PlayerStats_t &statsAttacker = m_aPlayerStats[iPlayerIndexAttacker]; + PlayerStats_t &statsVictim = m_aPlayerStats[iPlayerIndexVictim]; + + statsVictim.statsKills.iNumKilledBy[iPlayerIndexAttacker]++; + statsVictim.statsKills.iNumKilledByUnanswered[iPlayerIndexAttacker]++; + statsAttacker.statsKills.iNumKilled[iPlayerIndexVictim]++; + statsAttacker.statsKills.iNumKilledByUnanswered[iPlayerIndexVictim] = 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Determines if attacker and victim have gotten domination or revenge +//----------------------------------------------------------------------------- +void CCSGameStats::CalcDominationAndRevenge( CCSPlayer *pAttacker, CCSPlayer *pVictim, int *piDeathFlags ) +{ + //============================================================================= + // HPE_BEGIN: + // [Forrest] Allow nemesis/revenge to be turned off for a server + //============================================================================= + if ( sv_nonemesis.GetBool() ) + { + return; + } + //============================================================================= + // HPE_END + //============================================================================= + + //If there is no attacker, there is no domination or revenge + if( !pAttacker || !pVictim ) + { + return; + } + if (pAttacker->GetTeam() == pVictim->GetTeam()) + { + return; + } + int iPlayerIndexVictim = pVictim->entindex(); + PlayerStats_t &statsVictim = m_aPlayerStats[iPlayerIndexVictim]; + // calculate # of unanswered kills between killer & victim + // This is plus 1 as this function gets called before the stat is updated. That is done so that the domination + // and revenge will be calculated prior to the death message being sent to the clients + int attackerEntityIndex = pAttacker->entindex(); + int iKillsUnanswered = statsVictim.statsKills.iNumKilledByUnanswered[attackerEntityIndex] + 1; + + if ( CS_KILLS_FOR_DOMINATION == iKillsUnanswered ) + { + // this is the Nth unanswered kill between killer and victim, killer is now dominating victim + *piDeathFlags |= ( CS_DEATH_DOMINATION ); + } + else if ( pVictim->IsPlayerDominated( pAttacker->entindex() ) ) + { + // the killer killed someone who was dominating him, gains revenge + *piDeathFlags |= ( CS_DEATH_REVENGE ); + } + + //Check the overkill on 1 player achievement + if (iKillsUnanswered == CS_KILLS_FOR_DOMINATION + AchievementConsts::ExtendedDomination_AdditionalKills) + { + pAttacker->AwardAchievement(CSExtendedDomination); + } + + if (iKillsUnanswered == CS_KILLS_FOR_DOMINATION) + { + //this is the Nth unanswered kill between killer and victim, killer is now dominating victim + //set victim to be dominated by killer + pAttacker->SetPlayerDominated( pVictim, true ); + + //Check concurrent dominations achievement + int numConcurrentDominations = 0; + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + CCSPlayer *pPlayer= ToCSPlayer( UTIL_PlayerByIndex( i ) ); + if (pPlayer && pAttacker->IsPlayerDominated(pPlayer->entindex())) + { + numConcurrentDominations++; + } + } + if (numConcurrentDominations >= AchievementConsts::ConcurrentDominations_MinDominations) + { + pAttacker->AwardAchievement(CSConcurrentDominations); + } + + + // record stats + Event_PlayerDominatedOther( pAttacker, pVictim ); + } + else if ( pVictim->IsPlayerDominated( pAttacker->entindex() ) ) + { + // the killer killed someone who was dominating him, gains revenge + // set victim to no longer be dominating the killer + + pVictim->SetPlayerDominated( pAttacker, false ); + // record stats + Event_PlayerRevenge( pAttacker ); + } +} + +void CCSGameStats::Event_PlayerDominatedOther( CCSPlayer *pAttacker, CCSPlayer* pVictim ) +{ + IncrementStat( pAttacker, CSSTAT_DOMINATIONS, 1 ); +} + +void CCSGameStats::Event_PlayerRevenge( CCSPlayer *pAttacker ) +{ + IncrementStat( pAttacker, CSSTAT_REVENGES, 1 ); +} + +void CCSGameStats::Event_PlayerAvengedTeammate( CCSPlayer* pAttacker, CCSPlayer* pAvengedPlayer ) +{ + if (pAttacker && pAvengedPlayer) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_avenged_teammate" ); + + if ( event ) + { + event->SetInt( "avenger_id", pAttacker->GetUserID() ); + event->SetInt( "avenged_player_id", pAvengedPlayer->GetUserID() ); + gameeventmanager->FireEvent( event ); + } + } +} + +void CCSGameStats::Event_LevelInit() +{ + ResetAllTeamStats(); + ResetWeaponStats(); + CBaseGameStats::Event_LevelInit(); + GetSteamWorksSGameStatsUploader().StartSession(); +} + +void CCSGameStats::Event_LevelShutdown( float fElapsed ) +{ + DumpMatchWeaponMetrics(); + CBaseGameStats::Event_LevelShutdown(fElapsed); +} + +// Reset any per match info that resides in the player class +void CCSGameStats::ResetPlayerClassMatchStats() +{ + for ( int i = 1; i <= MAX_PLAYERS; i++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + + if ( pPlayer ) + { + pPlayer->SetNumMVPs( 0 ); + } + } +} diff --git a/game/server/cstrike/cs_gamestats.h b/game/server/cstrike/cs_gamestats.h new file mode 100644 index 0000000..4a24efb --- /dev/null +++ b/game/server/cstrike/cs_gamestats.h @@ -0,0 +1,340 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: The CS game stats header +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef CS_GAMESTATS_H +#define CS_GAMESTATS_H +#ifdef _WIN32 +#pragma once +#endif + +#include "cs_blackmarket.h" +#include "gamestats.h" +#include "cs_gamestats_shared.h" +#include "GameEventListener.h" +#include "steamworks_gamestats.h" +#include "weapon_csbase.h" + +// forward declares +class CBreakableProp; + +#define CS_STATS_BLOB_VERSION 3 + +const float cDisseminationTimeHigh = 0.25f; // Time interval for high priority stats sent to the player +const float cDisseminationTimeLow = 2.5f; // Time interval for medium priority stats sent to the player + +int GetCSLevelIndex( const char *pLevelName ); + +typedef struct +{ + char szGameName[8]; + byte iVersion; + char szMapName[32]; + char ipAddr[4]; + short port; + int serverid; +} gamestats_header_t; + +typedef struct +{ + gamestats_header_t header; + short iMinutesPlayed; + + short iTerroristVictories[CS_NUM_LEVELS]; + short iCounterTVictories[CS_NUM_LEVELS]; + short iBlackMarketPurchases[WEAPON_MAX]; + + short iAutoBuyPurchases; + short iReBuyPurchases; + short iAutoBuyM4A1Purchases; + short iAutoBuyAK47Purchases; + short iAutoBuyFamasPurchases; + short iAutoBuyGalilPurchases; + short iAutoBuyVestHelmPurchases; + short iAutoBuyVestPurchases; + +} cs_gamestats_t; + +extern short g_iWeaponPurchases[WEAPON_MAX]; +extern float g_flGameStatsUpdateTime; +extern short g_iTerroristVictories[CS_NUM_LEVELS]; +extern short g_iCounterTVictories[CS_NUM_LEVELS]; +extern short g_iWeaponPurchases[WEAPON_MAX]; + +extern short g_iAutoBuyPurchases; +extern short g_iReBuyPurchases; +extern short g_iAutoBuyM4A1Purchases; +extern short g_iAutoBuyAK47Purchases; +extern short g_iAutoBuyFamasPurchases; +extern short g_iAutoBuyGalilPurchases; +extern short g_iAutoBuyVestHelmPurchases; +extern short g_iAutoBuyVestPurchases; + + +struct sHappyCamperSnipePosition +{ + sHappyCamperSnipePosition( int userid, Vector pos ) : m_iUserID(userid), m_vPos(pos) {} + + int m_iUserID; + Vector m_vPos; +}; + +struct SMarketPurchases : public BaseStatData +{ + SMarketPurchases( uint64 ulPlayerID, int iPrice, const char *pName ) : m_nPlayerID(ulPlayerID), ItemCost(iPrice) + { + if ( pName ) + { + Q_strncpy( ItemID, pName, ARRAYSIZE(ItemID) ); + } + else + { + Q_strncpy( ItemID, "unknown", ARRAYSIZE(ItemID) ); + } + } + uint64 m_nPlayerID; + int ItemCost; + char ItemID[64]; + + BEGIN_STAT_TABLE( "CSSMarketPurchase" ) + REGISTER_STAT( m_nPlayerID ) + REGISTER_STAT( ItemCost ) + REGISTER_STAT_STRING( ItemID ) + END_STAT_TABLE() +}; +typedef CUtlVector< SMarketPurchases* > VectorMarketPurchaseData; + +struct WeaponStats +{ + int shots; + int hits; + int kills; + int damage; +}; + +struct SCSSWeaponData : public BaseStatData +{ + SCSSWeaponData( const char *pWeaponName, const WeaponStats &wpnData ) + { + if ( pWeaponName ) + { + Q_strncpy( WeaponID, pWeaponName, ARRAYSIZE(WeaponID) ); + } + else + { + Q_strncpy( WeaponID, "unknown", ARRAYSIZE(WeaponID) ); + } + + Shots = wpnData.shots; + Hits = wpnData.hits; + Kills = wpnData.kills; + Damage = wpnData.damage; + } + + char WeaponID[64]; + int Shots; + int Hits; + int Kills; + int Damage; + + BEGIN_STAT_TABLE( "CSSWeaponData" ) + REGISTER_STAT_STRING( WeaponID ) + REGISTER_STAT( Shots ) + REGISTER_STAT( Hits ) + REGISTER_STAT( Kills ) + REGISTER_STAT( Damage ) + END_STAT_TABLE() +}; +typedef CUtlVector< SCSSWeaponData* > CSSWeaponData; + +struct SCSSDeathData : public BaseStatData +{ + SCSSDeathData( CBasePlayer *pVictim, const CTakeDamageInfo &info ) + { + m_bUseGlobalData = false; + + m_DeathPos = info.GetDamagePosition(); + m_iVictimTeam = pVictim->GetTeamNumber(); + + CCSPlayer *pCSPlayer = ToCSPlayer( info.GetAttacker() ); + if ( pCSPlayer ) + { + m_iKillerTeam = pCSPlayer->GetTeamNumber(); + } + else + { + m_iKillerTeam = -1; + } + + const char *pszWeaponName = info.GetInflictor() ? info.GetInflictor()->GetClassname() : "unknown"; + + if ( pszWeaponName ) + { + if ( V_strcmp(pszWeaponName, "player") == 0 ) + { + // get the player's weapon + if ( pCSPlayer && pCSPlayer->GetActiveCSWeapon() ) + { + pszWeaponName = pCSPlayer->GetActiveCSWeapon()->GetClassname(); + } + } + } + + + m_uiDeathParam = WEAPON_NONE; + if ( (m_uiDeathParam = AliasToWeaponID( pszWeaponName )) == WEAPON_NONE ) + { + m_uiDeathParam = AliasToWeaponID( pszWeaponName ); + } + + const char *pszMapName = gpGlobals->mapname.ToCStr() ? gpGlobals->mapname.ToCStr() : "unknown"; + Q_strncpy( m_szMapName, pszMapName, ARRAYSIZE(m_szMapName) ); + } + Vector m_DeathPos; + int m_iVictimTeam; + int m_iKillerTeam; + int m_iDamageType; + uint64 m_uiDeathParam; + char m_szMapName[64]; + + BEGIN_STAT_TABLE( "Deaths" ) + REGISTER_STAT_NAMED( m_DeathPos.x, "XCoord" ) + REGISTER_STAT_NAMED( m_DeathPos.y, "YCoord" ) + REGISTER_STAT_NAMED( m_DeathPos.z, "ZCoord" ) + REGISTER_STAT_NAMED( m_iVictimTeam, "Team" ) + REGISTER_STAT_NAMED( m_iKillerTeam, "DeathCause" ) + REGISTER_STAT_NAMED( m_uiDeathParam, "DeathParam" ) + REGISTER_STAT_NAMED( m_szMapName, "DeathMap" ) + END_STAT_TABLE() +}; +typedef CUtlVector< SCSSDeathData* > CSSDeathData; + +//============================================================================= +// +// CS Game Stats Class +// +class CCSGameStats : public CBaseGameStats, public CGameEventListener, public CAutoGameSystemPerFrame, public IGameStatTracker +{ +public: + + // Constructor/Destructor. + CCSGameStats( void ); + ~CCSGameStats( void ); + + virtual void Clear( void ); + virtual bool Init(); + virtual void PreClientUpdate(); + virtual void PostInit(); + virtual void LevelShutdownPreClearSteamAPIContext(); + + void UploadRoundStats( void ); + // Overridden events + virtual void Event_LevelInit( void ); + virtual void Event_LevelShutdown( float flElapsed ); + virtual void Event_ShotFired( CBasePlayer *pPlayer, CBaseCombatWeapon* pWeapon ); + virtual void Event_ShotHit( CBasePlayer *pPlayer, const CTakeDamageInfo &info ); + virtual void Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info ); + virtual void Event_PlayerKilled_PreWeaponDrop( CBasePlayer *pPlayer, const CTakeDamageInfo &info ); + void UpdatePlayerRoundStats(int winner); + virtual void Event_PlayerConnected( CBasePlayer *pPlayer ); + virtual void Event_PlayerDisconnected( CBasePlayer *pPlayer ); + virtual void Event_WindowShattered( CBasePlayer *pPlayer ); + virtual void Event_PlayerKilledOther( CBasePlayer *pAttacker, CBaseEntity *pVictim, const CTakeDamageInfo &info ); + + + // CSS specific events + void Event_BombPlanted( CCSPlayer *pPlayer ); + void Event_BombDefused( CCSPlayer *pPlayer ); + void Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info ); + void Event_BombExploded( CCSPlayer *pPlayer ); + void Event_MoneyEarned( CCSPlayer *pPlayer, int moneyEarned ); + void Event_MoneySpent( CCSPlayer *pPlayer, int moneySpent, const char *pItemName ); + void Event_HostageRescued( CCSPlayer *pPlayer ); + void Event_PlayerSprayedDecal( CCSPlayer*pPlayer ); + void Event_AllHostagesRescued(); + void Event_BreakProp( CCSPlayer *pPlayer, CBreakableProp *pProp ); + void Event_PlayerDonatedWeapon (CCSPlayer* pPlayer); + void Event_PlayerDominatedOther( CCSPlayer* pAttacker, CCSPlayer* pVictim); + void Event_PlayerRevenge( CCSPlayer* pAttacker ); + void Event_PlayerAvengedTeammate( CCSPlayer* pAttacker, CCSPlayer* pAvengedPlayer ); + void Event_MVPEarned( CCSPlayer* pPlayer ); + void Event_KnifeUse( CCSPlayer* pPlayer, bool bStab, int iDamage ); + + virtual void FireGameEvent( IGameEvent *event ); + + void DumpMatchWeaponMetrics(); + + const PlayerStats_t& FindPlayerStats( CBasePlayer *pPlayer ) const; + void ResetPlayerStats( CBasePlayer *pPlayer ); + void ResetKillHistory( CBasePlayer *pPlayer ); + void ResetRoundStats(); + void ResetPlayerClassMatchStats(); + + const StatsCollection_t& GetTeamStats( int iTeamIndex ) const; + void ResetAllTeamStats(); + void ResetAllStats(); + void ResetWeaponStats(); + void IncrementTeamStat( int iTeamIndex, int iStatIndex, int iAmount ); + void CalcDominationAndRevenge( CCSPlayer *pAttacker, CCSPlayer *pVictim, int *piDeathFlags ); + void CalculateOverkill( CCSPlayer* pAttacker, CCSPlayer* pVictim ); + void PlayerKilled( CBasePlayer *pVictim, const CTakeDamageInfo &info ); + + void IncrementStat( CCSPlayer* pPlayer, CSStatType_t statId, int iValue, bool bPlayerOnly = false ); + // Steamworks Gamestats + virtual void SubmitGameStats( KeyValues *pKV ); + + virtual StatContainerList_t* GetStatContainerList( void ) + { + return s_StatLists; + } + +protected: + void SetStat( CCSPlayer *pPlayer, CSStatType_t statId, int iValue ); + void TrackKillStats( CCSPlayer *pAttacker, CCSPlayer *pVictim ); + void ComputeRollingStatAverages(); + void ComputeDirectStatAverages(); + void SendRollingStatsAveragesToAllPlayers(); + void SendDirectStatsAveragesToAllPlayers(); + void SendStatsToPlayer( CCSPlayer * pPlayer, int iMinStatPriority ); + +private: + PlayerStats_t m_aPlayerStats[MAX_PLAYERS+1]; // List of stats for each player for current life - reset after each death + StatsCollection_t m_aTeamStats[TEAM_MAXCOUNT - FIRST_GAME_TEAM]; + + RoundStatsRollingAverage_t m_rollingCTStatAverages; + RoundStatsRollingAverage_t m_rollingTStatAverages; + RoundStatsRollingAverage_t m_rollingPlayerStatAverages; + + RoundStatsDirectAverage_t m_directCTStatAverages; + RoundStatsDirectAverage_t m_directTStatAverages; + RoundStatsDirectAverage_t m_directPlayerStatAverages; + + float m_fDisseminationTimerLow; // how long since last medium priority stat update + float m_fDisseminationTimerHigh; // how long since last high priority stat update + + int m_numberOfRoundsForDirectAverages; + int m_numberOfTerroristEntriesForDirectAverages; + int m_numberOfCounterTerroristEntriesForDirectAverages; + + CUtlDict< CSStatType_t, short > m_PropStatTable; + + CUtlLinkedList<sHappyCamperSnipePosition, int> m_PlayerSnipedPosition; + WeaponStats m_weaponStats[WEAPON_MAX][2]; + + // Steamworks Gamestats + VectorMarketPurchaseData m_MarketPurchases; + CSSWeaponData m_WeaponData; + CSSDeathData m_DeathData; + + // A static list of all the stat containers, one for each data structure being tracked + static StatContainerList_t * s_StatLists; + + bool m_bInRound; +}; + +extern CCSGameStats CCS_GameStats; + +#endif // CS_GAMESTATS_H diff --git a/game/server/cstrike/cs_hltvdirector.cpp b/game/server/cstrike/cs_hltvdirector.cpp new file mode 100644 index 0000000..30f451a --- /dev/null +++ b/game/server/cstrike/cs_hltvdirector.cpp @@ -0,0 +1,149 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "cbase.h" +#include "hltvdirector.h" +#include "igameevents.h" + +class CCSHLTVDirector : public CHLTVDirector +{ +public: + DECLARE_CLASS( CCSHLTVDirector, CHLTVDirector ); + + const char** GetModEvents(); + void SetHLTVServer( IHLTVServer *hltv ); + void CreateShotFromEvent( CHLTVGameEvent *event ); + +}; + +void CCSHLTVDirector::SetHLTVServer( IHLTVServer *hltv ) +{ + BaseClass::SetHLTVServer( hltv ); + + if ( m_pHLTVServer ) + { + // mod specific events the director uses to find interesting shots + ListenForGameEvent( "hostage_rescued" ); + ListenForGameEvent( "hostage_killed" ); + ListenForGameEvent( "hostage_hurt" ); + ListenForGameEvent( "hostage_follows" ); + ListenForGameEvent( "bomb_pickup" ); + ListenForGameEvent( "bomb_dropped" ); + ListenForGameEvent( "bomb_exploded" ); + ListenForGameEvent( "bomb_defused" ); + ListenForGameEvent( "bomb_planted" ); + ListenForGameEvent( "vip_escaped" ); + ListenForGameEvent( "vip_killed" ); + } +} + +void CCSHLTVDirector::CreateShotFromEvent( CHLTVGameEvent *event ) +{ + // show event at least for 2 more seconds after it occured + const char *name = event->m_Event->GetName(); + IGameEvent *shot = NULL; + + if ( !Q_strcmp( "hostage_rescued", name ) || + !Q_strcmp( "hostage_hurt", name ) || + !Q_strcmp( "hostage_follows", name ) || + !Q_strcmp( "hostage_killed", name ) ) + { + CBaseEntity *player = UTIL_PlayerByUserId( event->m_Event->GetInt("userid") ); + + if ( !player ) + return; + + // shot player as primary, hostage as secondary target + shot = gameeventmanager->CreateEvent( "hltv_chase", true ); + shot->SetInt( "target1", player->entindex() ); + shot->SetInt( "target2", event->m_Event->GetInt("hostage") ); + shot->SetFloat( "distance", 96.0f ); + shot->SetInt( "theta", 40 ); + shot->SetInt( "phi", 20 ); + + // shot 2 seconds after event + m_nNextShotTick = MIN( m_nNextShotTick, (event->m_Tick+TIME_TO_TICKS(2.0)) ); + m_iPVSEntity = player->entindex(); + } + + else if ( !Q_strcmp( "bomb_pickup", name ) || + !Q_strcmp( "bomb_dropped", name ) || + !Q_strcmp( "bomb_planted", name ) || + !Q_strcmp( "bomb_defused", name ) ) + { + CBaseEntity *player = UTIL_PlayerByUserId( event->m_Event->GetInt("userid") ); + + if ( !player ) + return; + + shot = gameeventmanager->CreateEvent( "hltv_chase", true ); + shot->SetInt( "target1", player->entindex() ); + shot->SetInt( "target2", 0 ); + shot->SetFloat( "distance", 64.0f ); + shot->SetInt( "theta", 200 ); + shot->SetInt( "phi", 10 ); + + // shot 2 seconds after pickup + m_nNextShotTick = MIN( m_nNextShotTick, (event->m_Tick+TIME_TO_TICKS(2.0)) ); + m_iPVSEntity = player->entindex(); + } + else + { + // let baseclass create a shot + BaseClass::CreateShotFromEvent( event ); + + return; + } + + if ( shot ) + { + m_pHLTVServer->BroadcastEvent( shot ); + gameeventmanager->FreeEvent( shot ); + DevMsg("DrcCmd: %s\n", name ); + } +} + +const char** CCSHLTVDirector::GetModEvents() +{ + // game events relayed to spectator clients + static const char *s_modevents[] = + { + "hltv_status", + "hltv_chat", + "player_connect", + "player_disconnect", + "player_team", + "player_info", + "server_cvar", + "player_death", + "player_chat", + "round_start", + "round_end", + // additional CS:S events: + "bomb_planted", + "bomb_defused", + "hostage_killed", + "hostage_hurt", + NULL + }; + + return s_modevents; +} + +static CCSHLTVDirector s_HLTVDirector; // singleton + +EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CHLTVDirector, IHLTVDirector, INTERFACEVERSION_HLTVDIRECTOR, s_HLTVDirector ); + +CHLTVDirector* HLTVDirector() +{ + return &s_HLTVDirector; +} + +IGameSystem* HLTVDirectorSystem() +{ + return &s_HLTVDirector; +}
\ No newline at end of file diff --git a/game/server/cstrike/cs_nav.h b/game/server/cstrike/cs_nav.h new file mode 100644 index 0000000..f37b90d --- /dev/null +++ b/game/server/cstrike/cs_nav.h @@ -0,0 +1,73 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav.h +// Data structures and constants for the Navigation Mesh system +// Author: Michael S. Booth ([email protected]), January 2003 + +#ifndef _CS_NAV_H_ +#define _CS_NAV_H_ + +#include "nav.h" + +/** + * Below are several constants used by the navigation system. + * @todo Move these into TheNavMesh singleton. + */ +const float BotRadius = 10.0f; ///< circular extent that contains bot + +class CNavArea; +class CSNavNode; + +#if 0 +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if given entity can be ignored when moving + */ +#define WALK_THRU_DOORS 0x01 +#define WALK_THRU_BREAKABLES 0x02 +#define WALK_THRU_TOGGLE_BRUSHES 0x04 +#define WALK_THRU_EVERYTHING (WALK_THRU_DOORS | WALK_THRU_BREAKABLES | WALK_THRU_TOGGLE_BRUSHES) +inline bool IsEntityWalkable( CBaseEntity *entity, unsigned int flags ) +{ + if (FClassnameIs( entity, "worldspawn" )) + return false; + + if (FClassnameIs( entity, "player" )) + return false; + + // if we hit a door, assume its walkable because it will open when we touch it + if (FClassnameIs( entity, "prop_door*" ) || FClassnameIs( entity, "func_door*" )) + return (flags & WALK_THRU_DOORS) ? true : false; + + // if we hit a clip brush, ignore it if it is not BRUSHSOLID_ALWAYS + if (FClassnameIs( entity, "func_brush" )) + { + CFuncBrush *brush = (CFuncBrush *)entity; + switch ( brush->m_iSolidity ) + { + case CFuncBrush::BRUSHSOLID_ALWAYS: + return false; + case CFuncBrush::BRUSHSOLID_NEVER: + return true; + case CFuncBrush::BRUSHSOLID_TOGGLE: + return (flags & WALK_THRU_TOGGLE_BRUSHES) ? true : false; + } + } + + // if we hit a breakable object, assume its walkable because we will shoot it when we touch it + if (FClassnameIs( entity, "func_breakable" ) && entity->GetHealth() && entity->m_takedamage == DAMAGE_YES) + return (flags & WALK_THRU_BREAKABLES) ? true : false; + + if (FClassnameIs( entity, "func_breakable_surf" ) && entity->m_takedamage == DAMAGE_YES) + return (flags & WALK_THRU_BREAKABLES) ? true : false; + + return false; +} +#endif + +#endif // _CS_NAV_H_ diff --git a/game/server/cstrike/cs_nav_area.cpp b/game/server/cstrike/cs_nav_area.cpp new file mode 100644 index 0000000..b1a2464 --- /dev/null +++ b/game/server/cstrike/cs_nav_area.cpp @@ -0,0 +1,473 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_area.cpp +// AI Navigation areas +// Author: Michael S. Booth ([email protected]), January 2003 + +#include "cbase.h" +#include "cs_nav_mesh.h" +#include "cs_nav_area.h" +#include "nav_pathfind.h" +#include "nav_colors.h" +#include "fmtstr.h" +#include "props_shared.h" +#include "func_breakablesurf.h" +#include "Color.h" +#include "collisionutils.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 + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor used during normal runtime. + */ +CCSNavArea::CCSNavArea( void ) +{ + m_approachCount = 0; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Destructor + */ +CCSNavArea::~CCSNavArea() +{ +} + + +void CCSNavArea::OnServerActivate( void ) +{ + CNavArea::OnServerActivate(); + +} + +void CCSNavArea::OnRoundRestart( void ) +{ + CNavArea::OnRoundRestart(); +} + + +void CCSNavArea::Save( CUtlBuffer &fileBuffer, unsigned int version ) const +{ + CNavArea::Save( fileBuffer, version ); + + // + // Save the approach areas for this area + // + + // save number of approach areas + fileBuffer.PutUnsignedChar(m_approachCount); + + // save approach area info + for( int a=0; a<m_approachCount; ++a ) + { + if (m_approach[a].here.area) + fileBuffer.PutUnsignedInt(m_approach[a].here.area->GetID()); + else + fileBuffer.PutUnsignedInt(0); + + if (m_approach[a].prev.area) + fileBuffer.PutUnsignedInt(m_approach[a].prev.area->GetID()); + else + fileBuffer.PutUnsignedInt(0); + fileBuffer.PutUnsignedChar(m_approach[a].prevToHereHow); + + if (m_approach[a].next.area) + fileBuffer.PutUnsignedInt(m_approach[a].next.area->GetID()); + else + fileBuffer.PutUnsignedInt(0); + fileBuffer.PutUnsignedChar(m_approach[a].hereToNextHow); + } +} + +NavErrorType CCSNavArea::Load( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ) +{ + if ( version < 15 ) + return LoadLegacy(fileBuffer, version, subVersion); + + // load base class data + NavErrorType error = CNavArea::Load( fileBuffer, version, subVersion ); + + switch ( subVersion ) + { + case 1: + // + // Load number of approach areas + // + m_approachCount = fileBuffer.GetUnsignedChar(); + + // load approach area info (IDs) + for( int a = 0; a < m_approachCount; ++a ) + { + m_approach[a].here.id = fileBuffer.GetUnsignedInt(); + + m_approach[a].prev.id = fileBuffer.GetUnsignedInt(); + m_approach[a].prevToHereHow = (NavTraverseType)fileBuffer.GetUnsignedChar(); + + m_approach[a].next.id = fileBuffer.GetUnsignedInt(); + m_approach[a].hereToNextHow = (NavTraverseType)fileBuffer.GetUnsignedChar(); + } + + if ( !fileBuffer.IsValid() ) + error = NAV_INVALID_FILE; + + // fall through + + case 0: + // legacy version + break; + + default: + Warning( "Unknown NavArea sub-version number\n" ); + error = NAV_INVALID_FILE; + } + + return error; +} + + +NavErrorType CCSNavArea::PostLoad( void ) +{ + NavErrorType error = CNavArea::PostLoad(); + + // resolve approach area IDs + for ( int a = 0; a < m_approachCount; ++a ) + { + m_approach[a].here.area = TheNavMesh->GetNavAreaByID( m_approach[a].here.id ); + if (m_approach[a].here.id && m_approach[a].here.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Approach Area (here).\n" ); + error = NAV_CORRUPT_DATA; + } + + m_approach[a].prev.area = TheNavMesh->GetNavAreaByID( m_approach[a].prev.id ); + if (m_approach[a].prev.id && m_approach[a].prev.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Approach Area (prev).\n" ); + error = NAV_CORRUPT_DATA; + } + + m_approach[a].next.area = TheNavMesh->GetNavAreaByID( m_approach[a].next.id ); + if (m_approach[a].next.id && m_approach[a].next.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Approach Area (next).\n" ); + error = NAV_CORRUPT_DATA; + } + } + return error; +} + + +void CCSNavArea::Draw( void ) const +{ + CNavArea::Draw(); +} + +void CCSNavArea::CustomAnalysis( bool isIncremental /*= false */ ) +{ + ComputeApproachAreas(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load legacy navigation area from the file + */ +NavErrorType CCSNavArea::LoadLegacy( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ) +{ + // load ID + m_id = fileBuffer.GetUnsignedInt(); + + // update nextID to avoid collisions + if (m_id >= m_nextID) + m_nextID = m_id+1; + + // load attribute flags + if ( version <= 8 ) + { + m_attributeFlags = fileBuffer.GetUnsignedChar(); + } + else if ( version < 13 ) + { + m_attributeFlags = fileBuffer.GetUnsignedShort(); + } + else + { + m_attributeFlags = fileBuffer.GetInt(); + } + + // load extent of area + fileBuffer.Get( &m_nwCorner, 3*sizeof(float) ); + fileBuffer.Get( &m_seCorner, 3*sizeof(float) ); + + m_center.x = (m_nwCorner.x + m_seCorner.x)/2.0f; + m_center.y = (m_nwCorner.y + m_seCorner.y)/2.0f; + m_center.z = (m_nwCorner.z + m_seCorner.z)/2.0f; + + if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) + { + m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); + m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); + } + else + { + m_invDxCorners = m_invDyCorners = 0; + + DevWarning( "Degenerate Navigation Area #%d at setpos %g %g %g\n", + m_id, m_center.x, m_center.y, m_center.z ); + } + + // load heights of implicit corners + m_neZ = fileBuffer.GetFloat(); + m_swZ = fileBuffer.GetFloat(); + + CheckWaterLevel(); + + // load connections (IDs) to adjacent areas + // in the enum order NORTH, EAST, SOUTH, WEST + for( int d=0; d<NUM_DIRECTIONS; d++ ) + { + // load number of connections for this direction + unsigned int count = fileBuffer.GetUnsignedInt(); + Assert( fileBuffer.IsValid() ); + + m_connect[d].EnsureCapacity( count ); + for( unsigned int i=0; i<count; ++i ) + { + NavConnect connect; + connect.id = fileBuffer.GetUnsignedInt(); + Assert( fileBuffer.IsValid() ); + + // don't allow self-referential connections + if ( connect.id != m_id ) + { + m_connect[d].AddToTail( connect ); + } + } + } + + // + // Load hiding spots + // + + // load number of hiding spots + unsigned char hidingSpotCount = fileBuffer.GetUnsignedChar(); + + if (version == 1) + { + // load simple vector array + Vector pos; + for( int h=0; h<hidingSpotCount; ++h ) + { + fileBuffer.Get( &pos, 3 * sizeof(float) ); + + // create new hiding spot and put on master list + HidingSpot *spot = TheNavMesh->CreateHidingSpot(); + spot->SetPosition( pos ); + spot->SetFlags( HidingSpot::IN_COVER ); + m_hidingSpots.AddToTail( spot ); + } + } + else + { + // load HidingSpot objects for this area + for( int h=0; h<hidingSpotCount; ++h ) + { + // create new hiding spot and put on master list + HidingSpot *spot = TheNavMesh->CreateHidingSpot(); + + spot->Load( fileBuffer, version ); + + m_hidingSpots.AddToTail( spot ); + } + } + + if ( version < 15 ) + { + // + // Load number of approach areas + // + m_approachCount = fileBuffer.GetUnsignedChar(); + + // load approach area info (IDs) + for( int a = 0; a < m_approachCount; ++a ) + { + m_approach[a].here.id = fileBuffer.GetUnsignedInt(); + + m_approach[a].prev.id = fileBuffer.GetUnsignedInt(); + m_approach[a].prevToHereHow = (NavTraverseType)fileBuffer.GetUnsignedChar(); + + m_approach[a].next.id = fileBuffer.GetUnsignedInt(); + m_approach[a].hereToNextHow = (NavTraverseType)fileBuffer.GetUnsignedChar(); + } + } + + + // + // Load encounter paths for this area + // + unsigned int count = fileBuffer.GetUnsignedInt(); + + if (version < 3) + { + // old data, read and discard + for( unsigned int e=0; e<count; ++e ) + { + SpotEncounter encounter; + + encounter.from.id = fileBuffer.GetUnsignedInt(); + encounter.to.id = fileBuffer.GetUnsignedInt(); + + fileBuffer.Get( &encounter.path.from.x, 3 * sizeof(float) ); + fileBuffer.Get( &encounter.path.to.x, 3 * sizeof(float) ); + + // read list of spots along this path + unsigned char spotCount = fileBuffer.GetUnsignedChar(); + + for( int s=0; s<spotCount; ++s ) + { + fileBuffer.GetFloat(); + fileBuffer.GetFloat(); + fileBuffer.GetFloat(); + fileBuffer.GetFloat(); + } + } + return NAV_OK; + } + + for( unsigned int e=0; e<count; ++e ) + { + SpotEncounter *encounter = new SpotEncounter; + + encounter->from.id = fileBuffer.GetUnsignedInt(); + + unsigned char dir = fileBuffer.GetUnsignedChar(); + encounter->fromDir = static_cast<NavDirType>( dir ); + + encounter->to.id = fileBuffer.GetUnsignedInt(); + + dir = fileBuffer.GetUnsignedChar(); + encounter->toDir = static_cast<NavDirType>( dir ); + + // read list of spots along this path + unsigned char spotCount = fileBuffer.GetUnsignedChar(); + + SpotOrder order; + for( int s=0; s<spotCount; ++s ) + { + order.id = fileBuffer.GetUnsignedInt(); + + unsigned char t = fileBuffer.GetUnsignedChar(); + + order.t = (float)t/255.0f; + + encounter->spots.AddToTail( order ); + } + + m_spotEncounters.AddToTail( encounter ); + } + + if (version < 5) + return NAV_OK; + + // + // Load Place data + // + PlaceDirectory::IndexType entry = fileBuffer.GetUnsignedShort(); + + // convert entry to actual Place + SetPlace( placeDirectory.IndexToPlace( entry ) ); + + if ( version < 7 ) + return NAV_OK; + + // load ladder data + for ( int dir=0; dir<CNavLadder::NUM_LADDER_DIRECTIONS; ++dir ) + { + count = fileBuffer.GetUnsignedInt(); + for( unsigned int i=0; i<count; ++i ) + { + NavLadderConnect connect; + connect.id = fileBuffer.GetUnsignedInt(); + + bool alreadyConnected = false; + FOR_EACH_VEC( m_ladder[dir], j ) + { + if ( m_ladder[dir][j].id == connect.id ) + { + alreadyConnected = true; + break; + } + } + + if ( !alreadyConnected ) + { + m_ladder[dir].AddToTail( connect ); + } + } + } + + if ( version < 8 ) + return NAV_OK; + + // load earliest occupy times + for( int i=0; i<MAX_NAV_TEAMS; ++i ) + { + // no spot in the map should take longer than this to reach + m_earliestOccupyTime[i] = fileBuffer.GetFloat(); + } + + if ( version < 11 ) + return NAV_OK; + + // load light intensity + for ( int i=0; i<NUM_CORNERS; ++i ) + { + m_lightIntensity[i] = fileBuffer.GetFloat(); + } + + if ( version < 16 ) + return NAV_OK; + + // load visibility information + unsigned int visibleAreaCount = fileBuffer.GetUnsignedInt(); + if ( !IsX360() ) + { + m_potentiallyVisibleAreas.EnsureCapacity( visibleAreaCount ); + } + else + { +/* TODO: Re-enable when latest 360 code gets integrated (MSB 5/5/09) + size_t nBytes = visibleAreaCount * sizeof( AreaBindInfo ); + m_potentiallyVisibleAreas.~CAreaBindInfoArray(); + new ( &m_potentiallyVisibleAreas ) CAreaBindInfoArray( (AreaBindInfo *)engine->AllocLevelStaticData( nBytes ), visibleAreaCount ); +*/ + } + + for( unsigned int j=0; j<visibleAreaCount; ++j ) + { + AreaBindInfo info; + info.id = fileBuffer.GetUnsignedInt(); + info.attributes = fileBuffer.GetUnsignedChar(); + + m_potentiallyVisibleAreas.AddToTail( info ); + } + + // read area from which we inherit visibility + m_inheritVisibilityFrom.id = fileBuffer.GetUnsignedInt(); + + return NAV_OK; +} + + diff --git a/game/server/cstrike/cs_nav_area.h b/game/server/cstrike/cs_nav_area.h new file mode 100644 index 0000000..e8d913c --- /dev/null +++ b/game/server/cstrike/cs_nav_area.h @@ -0,0 +1,81 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_area.h +// Navigation areas +// Author: Michael S. Booth ([email protected]), January 2003 + +#ifndef _CS_NAV_AREA_H_ +#define _CS_NAV_AREA_H_ + +#include "nav_area.h" + +//------------------------------------------------------------------------------------------------------------------- +/** + * A CNavArea is a rectangular region defining a walkable area in the environment + */ +class CCSNavArea : public CNavArea +{ +public: + DECLARE_CLASS( CCSNavArea, CNavArea ); + + CCSNavArea( void ); + ~CCSNavArea(); + + virtual void OnServerActivate( void ); // (EXTEND) invoked when map is initially loaded + virtual void OnRoundRestart( void ); // (EXTEND) invoked for each area when the round restarts + + virtual void Draw( void ) const; // draw area for debugging & editing + + virtual void Save( CUtlBuffer &fileBuffer, unsigned int version ) const; // (EXTEND) + virtual NavErrorType Load( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ); // (EXTEND) + virtual NavErrorType PostLoad( void ); // (EXTEND) invoked after all areas have been loaded - for pointer binding, etc + + virtual void CustomAnalysis( bool isIncremental = false ); // for game-specific analysis + + //- approach areas ---------------------------------------------------------------------------------- + struct ApproachInfo + { + NavConnect here; ///< the approach area + NavConnect prev; ///< the area just before the approach area on the path + NavTraverseType prevToHereHow; + NavConnect next; ///< the area just after the approach area on the path + NavTraverseType hereToNextHow; + }; + const ApproachInfo *GetApproachInfo( int i ) const { return &m_approach[i]; } + int GetApproachInfoCount( void ) const { return m_approachCount; } + void ComputeApproachAreas( void ); ///< determine the set of "approach areas" - for map learning + + //- player counting -------------------------------------------------------------------------------- + void ClearPlayerCount( void ); ///< set the player count to zero + +protected: + NavErrorType LoadLegacy( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ); + + +private: + //- approach areas ---------------------------------------------------------------------------------- + enum { MAX_APPROACH_AREAS = 16 }; + ApproachInfo m_approach[ MAX_APPROACH_AREAS ]; + unsigned char m_approachCount; +}; + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- +// +// Inlines +// + +inline void CCSNavArea::ClearPlayerCount( void ) +{ + for( int i=0; i<MAX_NAV_TEAMS; ++i ) + { + m_playerCount[ i ] = 0; + } +} + +#endif // _CS_NAV_AREA_H_ diff --git a/game/server/cstrike/cs_nav_edit.cpp b/game/server/cstrike/cs_nav_edit.cpp new file mode 100644 index 0000000..b49bd23 --- /dev/null +++ b/game/server/cstrike/cs_nav_edit.cpp @@ -0,0 +1,69 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_edit.cpp +// Implementation of Navigation Mesh edit mode +// Author: Michael Booth, 2003-2004 + +#include "cbase.h" +#include "nav_mesh.h" +#include "cs_nav_pathfind.h" +#include "cs_nav_node.h" +#include "nav_colors.h" +#include "Color.h" +#include "tier0/vprof.h" +#include "collisionutils.h" + +ConVar nav_show_area_info( "nav_show_area_info", "0.5", FCVAR_GAMEDLL, "Duration in seconds to show nav area ID and attributes while editing" ); +ConVar nav_snap_to_grid( "nav_snap_to_grid", "0", FCVAR_GAMEDLL, "Snap to the nav generation grid when creating new nav areas" ); +ConVar nav_create_place_on_ground( "nav_create_place_on_ground", "0", FCVAR_GAMEDLL, "If true, nav areas will be placed flush with the ground when created by hand." ); + +#if DEBUG_NAV_NODES +extern ConVar nav_show_nodes; +#endif // DEBUG_NAV_NODES + + +//-------------------------------------------------------------------------------------------------------------- +void EditNav_Precache(void *pUser) +{ + CBaseEntity::PrecacheScriptSound( "Bot.EditSwitchOn" ); + CBaseEntity::PrecacheScriptSound( "EDIT_TOGGLE_PLACE_MODE" ); + CBaseEntity::PrecacheScriptSound( "Bot.EditSwitchOff" ); + CBaseEntity::PrecacheScriptSound( "EDIT_PLACE_PICK" ); + CBaseEntity::PrecacheScriptSound( "EDIT_DELETE" ); + CBaseEntity::PrecacheScriptSound( "EDIT.ToggleAttribute" ); + CBaseEntity::PrecacheScriptSound( "EDIT_SPLIT.MarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_SPLIT.NoMarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MERGE.Enable" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MERGE.Disable" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MARK.Enable" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MARK.Disable" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MARK_UNNAMED.Enable" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MARK_UNNAMED.NoMarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MARK_UNNAMED.MarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_CONNECT.AllDirections" ); + CBaseEntity::PrecacheScriptSound( "EDIT_CONNECT.Added" ); + CBaseEntity::PrecacheScriptSound( "EDIT_DISCONNECT.MarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_DISCONNECT.NoMarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_SPLICE.MarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_SPLICE.NoMarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_SELECT_CORNER.MarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_SELECT_CORNER.NoMarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MOVE_CORNER.MarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_MOVE_CORNER.NoMarkedArea" ); + CBaseEntity::PrecacheScriptSound( "EDIT_BEGIN_AREA.Creating" ); + CBaseEntity::PrecacheScriptSound( "EDIT_BEGIN_AREA.NotCreating" ); + CBaseEntity::PrecacheScriptSound( "EDIT_END_AREA.Creating" ); + CBaseEntity::PrecacheScriptSound( "EDIT_END_AREA.NotCreating" ); + CBaseEntity::PrecacheScriptSound( "EDIT_WARP_TO_MARK" ); +} + +#ifdef CSTRIKE_DLL +PRECACHE_REGISTER_FN( EditNav_Precache ); +#endif + + diff --git a/game/server/cstrike/cs_nav_file.cpp b/game/server/cstrike/cs_nav_file.cpp new file mode 100644 index 0000000..3b7db5d --- /dev/null +++ b/game/server/cstrike/cs_nav_file.cpp @@ -0,0 +1,1365 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_file.cpp +// Reading and writing nav files +// Author: Michael S. Booth ([email protected]), January-September 2003 + +#include "cbase.h" +#include "nav_mesh.h" + +#ifdef CSTRIKE_DLL +#include "cs_shareddefs.h" +#include "cs_nav_pathfind.h" +#endif + +#if 0 +//-------------------------------------------------------------------------------------------------------------- +/// The current version of the nav file format +const int NavCurrentVersion = 9; + +//-------------------------------------------------------------------------------------------------------------- +// +// The 'place directory' is used to save and load places from +// nav files in a size-efficient manner that also allows for the +// order of the place ID's to change without invalidating the +// nav files. +// +// The place directory is stored in the nav file as a list of +// place name strings. Each nav area then contains an index +// into that directory, or zero if no place has been assigned to +// that area. +// +class PlaceDirectory +{ +public: + + typedef unsigned short IndexType; + + void Reset( void ) + { + m_directory.RemoveAll(); + } + + /// return true if this place is already in the directory + bool IsKnown( Place place ) const + { + return m_directory.HasElement( place ); + } + + /// return the directory index corresponding to this Place (0 = no entry) + IndexType GetIndex( Place place ) const + { + if (place == UNDEFINED_PLACE) + return 0; + + int i = m_directory.Find( place ); + + if (i < 0) + { + Assert( false && "PlaceDirectory::GetIndex failure" ); + return 0; + } + + return (IndexType)(i+1); + } + + /// add the place to the directory if not already known + void AddPlace( Place place ) + { + if (place == UNDEFINED_PLACE) + return; + + Assert( place < 1000 ); + + if (IsKnown( place )) + return; + + m_directory.AddToTail( place ); + } + + /// given an index, return the Place + Place IndexToPlace( IndexType entry ) const + { + if (entry == 0) + return UNDEFINED_PLACE; + + int i = entry-1; + + if (i >= m_directory.Count()) + { + Assert( false && "PlaceDirectory::IndexToPlace: Invalid entry" ); + return UNDEFINED_PLACE; + } + + return m_directory[ i ]; + } + + /// store the directory + void Save( FileHandle_t file ) + { + // store number of entries in directory + IndexType count = (IndexType)m_directory.Count(); + filesystem->Write( &count, sizeof(IndexType), file ); + + // store entries + for( int i=0; i<m_directory.Count(); ++i ) + { + const char *placeName = TheNavMesh->PlaceToName( m_directory[i] ); + + // store string length followed by string itself + unsigned short len = (unsigned short)(strlen( placeName ) + 1); + filesystem->Write( &len, sizeof(unsigned short), file ); + filesystem->Write( placeName, len, file ); + } + } + + /// load the directory + void Load( FileHandle_t file ) + { + // read number of entries + IndexType count; + filesystem->Read( &count, sizeof(IndexType), file ); + + m_directory.RemoveAll(); + + // read each entry + char placeName[256]; + unsigned short len; + for( int i=0; i<count; ++i ) + { + filesystem->Read( &len, sizeof(unsigned short), file ); + filesystem->Read( placeName, len, file ); + + AddPlace( TheNavMesh->NameToPlace( placeName ) ); + } + } + +private: + CUtlVector< Place > m_directory; +}; + +static PlaceDirectory placeDirectory; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Replace extension with "bsp" + */ +char *GetBspFilename( const char *navFilename ) +{ + static char bspFilename[256]; + + Q_snprintf( bspFilename, sizeof( bspFilename ), "maps\\%s.bsp", STRING( gpGlobals->mapname ) ); + + int len = strlen( bspFilename ); + if (len < 3) + return NULL; + + bspFilename[ len-3 ] = 'b'; + bspFilename[ len-2 ] = 's'; + bspFilename[ len-1 ] = 'p'; + + return bspFilename; +} + +//-------------------------------------------------------------------------------------------------------------- +/* +void CNavArea::Save( FILE *fp ) const +{ + fprintf( fp, "v %f %f %f\n", m_extent.lo.x, m_extent.lo.y, m_extent.lo.z ); + fprintf( fp, "v %f %f %f\n", m_extent.hi.x, m_extent.lo.y, m_neZ ); + fprintf( fp, "v %f %f %f\n", m_extent.hi.x, m_extent.hi.y, m_extent.hi.z ); + fprintf( fp, "v %f %f %f\n", m_extent.lo.x, m_extent.hi.y, m_swZ ); + + static int base = 1; + fprintf( fp, "\n\ng %04dArea%s%s%s%s\n", m_id, + (GetAttributes() & BOT_NAV_CROUCH) ? "CROUCH" : "", + (GetAttributes() & BOT_NAV_JUMP) ? "JUMP" : "", + (GetAttributes() & BOT_NAV_PRECISE) ? "PRECISE" : "", + (GetAttributes() & BOT_NAV_NO_JUMP) ? "NO_JUMP" : "" ); + fprintf( fp, "f %d %d %d %d\n\n", base, base+1, base+2, base+3 ); + base += 4; +} +*/ + +//-------------------------------------------------------------------------------------------------------------- +/** + * Save a navigation area to the opened binary stream + */ +void CNavArea::Save( FileHandle_t file, unsigned int version ) const +{ + // save ID + filesystem->Write( &m_id, sizeof(unsigned int), file ); + + // save attribute flags + filesystem->Write( &m_attributeFlags, sizeof(unsigned short), file ); + + // save extent of area + filesystem->Write( &m_extent, 6*sizeof(float), file ); + + // save heights of implicit corners + filesystem->Write( &m_neZ, sizeof(float), file ); + filesystem->Write( &m_swZ, sizeof(float), file ); + + // save connections to adjacent areas + // in the enum order NORTH, EAST, SOUTH, WEST + for( int d=0; d<NUM_DIRECTIONS; d++ ) + { + // save number of connections for this direction + unsigned int count = m_connect[d].Count(); + filesystem->Write( &count, sizeof(unsigned int), file ); + + FOR_EACH_LL( m_connect[d], it ) + { + NavConnect connect = m_connect[d][ it ]; + filesystem->Write( &connect.area->m_id, sizeof(unsigned int), file ); + } + } + + // + // Store hiding spots for this area + // + unsigned char count; + if (m_hidingSpotList.Count() > 255) + { + count = 255; + Warning( "Warning: NavArea #%d: Truncated hiding spot list to 255\n", m_id ); + } + else + { + count = (unsigned char)m_hidingSpotList.Count(); + } + filesystem->Write( &count, sizeof(unsigned char), file ); + + // store HidingSpot objects + unsigned int saveCount = 0; + FOR_EACH_LL( m_hidingSpotList, hit ) + { + HidingSpot *spot = m_hidingSpotList[ hit ]; + + spot->Save( file, version ); + + // overflow check + if (++saveCount == count) + break; + } + + // + // Save the approach areas for this area + // + + // save number of approach areas + filesystem->Write( &m_approachCount, sizeof(unsigned char), file ); + + // save approach area info + unsigned char type; + unsigned int zero = 0; + for( int a=0; a<m_approachCount; ++a ) + { + if (m_approach[a].here.area) + filesystem->Write( &m_approach[a].here.area->m_id, sizeof(unsigned int), file ); + else + filesystem->Write( &zero, sizeof(unsigned int), file ); + + if (m_approach[a].prev.area) + filesystem->Write( &m_approach[a].prev.area->m_id, sizeof(unsigned int), file ); + else + filesystem->Write( &zero, sizeof(unsigned int), file ); + type = (unsigned char)m_approach[a].prevToHereHow; + filesystem->Write( &type, sizeof(unsigned char), file ); + + if (m_approach[a].next.area) + filesystem->Write( &m_approach[a].next.area->m_id, sizeof(unsigned int), file ); + else + filesystem->Write( &zero, sizeof(unsigned int), file ); + type = (unsigned char)m_approach[a].hereToNextHow; + filesystem->Write( &type, sizeof(unsigned char), file ); + } + + // + // Save encounter spots for this area + // + { + // save number of encounter paths for this area + unsigned int count = m_spotEncounterList.Count(); + filesystem->Write( &count, sizeof(unsigned int), file ); + + SpotEncounter *e; + FOR_EACH_LL( m_spotEncounterList, it ) + { + e = m_spotEncounterList[ it ]; + + if (e->from.area) + filesystem->Write( &e->from.area->m_id, sizeof(unsigned int), file ); + else + filesystem->Write( &zero, sizeof(unsigned int), file ); + + unsigned char dir = (unsigned char)e->fromDir; + filesystem->Write( &dir, sizeof(unsigned char), file ); + + if (e->to.area) + filesystem->Write( &e->to.area->m_id, sizeof(unsigned int), file ); + else + filesystem->Write( &zero, sizeof(unsigned int), file ); + + dir = (unsigned char)e->toDir; + filesystem->Write( &dir, sizeof(unsigned char), file ); + + // write list of spots along this path + unsigned char spotCount; + if (e->spotList.Count() > 255) + { + spotCount = 255; + Warning( "Warning: NavArea #%d: Truncated encounter spot list to 255\n", m_id ); + } + else + { + spotCount = (unsigned char)e->spotList.Count(); + } + filesystem->Write( &spotCount, sizeof(unsigned char), file ); + + saveCount = 0; + FOR_EACH_LL( e->spotList, sit ) + { + SpotOrder *order = &e->spotList[ sit ]; + + // order->spot may be NULL if we've loaded a nav mesh that has been edited but not re-analyzed + unsigned int id = (order->spot) ? order->spot->GetID() : 0; + filesystem->Write( &id, sizeof(unsigned int), file ); + + unsigned char t = (unsigned char)(255 * order->t); + filesystem->Write( &t, sizeof(unsigned char), file ); + + // overflow check + if (++saveCount == spotCount) + break; + } + } + } + + // store place dictionary entry + PlaceDirectory::IndexType entry = placeDirectory.GetIndex( GetPlace() ); + filesystem->Write( &entry, sizeof(entry), file ); + + // write out ladder info + int i; + for ( i=0; i<CSNavLadder::NUM_LADDER_DIRECTIONS; ++i ) + { + // save number of encounter paths for this area + unsigned int count = m_ladder[i].Count(); + filesystem->Write( &count, sizeof(unsigned int), file ); + + NavLadderConnect ladder; + FOR_EACH_LL( m_ladder[i], it ) + { + ladder = m_ladder[i][it]; + + unsigned int id = ladder.ladder->GetID(); + filesystem->Write( &id, sizeof( id ), file ); + } + } + + // save earliest occupy times + for( i=0; i<MAX_NAV_TEAMS; ++i ) + { + // no spot in the map should take longer than this to reach + filesystem->Write( &m_earliestOccupyTime[i], sizeof(m_earliestOccupyTime[i]), file ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load a navigation area from the file + */ +void CNavArea::Load( FileHandle_t file, unsigned int version ) +{ + // load ID + filesystem->Read( &m_id, sizeof(unsigned int), file ); + + // update nextID to avoid collisions + if (m_id >= m_nextID) + m_nextID = m_id+1; + + // load attribute flags + if ( version <= 8 ) + { + unsigned char flags = 0; + filesystem->Read( &flags, sizeof(unsigned char), file ); + m_attributeFlags = flags; + } + else + { + filesystem->Read( &m_attributeFlags, sizeof(unsigned short), file ); + } + + // load extent of area + filesystem->Read( &m_extent, 6*sizeof(float), file ); + + m_center.x = (m_extent.lo.x + m_extent.hi.x)/2.0f; + m_center.y = (m_extent.lo.y + m_extent.hi.y)/2.0f; + m_center.z = (m_extent.lo.z + m_extent.hi.z)/2.0f; + + // load heights of implicit corners + filesystem->Read( &m_neZ, sizeof(float), file ); + filesystem->Read( &m_swZ, sizeof(float), file ); + + CheckWaterLevel(); + + // load connections (IDs) to adjacent areas + // in the enum order NORTH, EAST, SOUTH, WEST + for( int d=0; d<NUM_DIRECTIONS; d++ ) + { + // load number of connections for this direction + unsigned int count; + int result = filesystem->Read( &count, sizeof(unsigned int), file ); + Assert( result == sizeof(unsigned int) ); + + for( unsigned int i=0; i<count; ++i ) + { + NavConnect connect; + result = filesystem->Read( &connect.id, sizeof(unsigned int), file ); + Assert( result == sizeof(unsigned int) ); + + // don't allow self-referential connections + if ( connect.id != m_id ) + { + m_connect[d].AddToTail( connect ); + } + } + } + + // + // Load hiding spots + // + + // load number of hiding spots + unsigned char hidingSpotCount; + filesystem->Read( &hidingSpotCount, sizeof(unsigned char), file ); + + if (version == 1) + { + // load simple vector array + Vector pos; + for( int h=0; h<hidingSpotCount; ++h ) + { + filesystem->Read( &pos, 3 * sizeof(float), file ); + + // create new hiding spot and put on master list + HidingSpot *spot = TheNavMesh->CreateHidingSpot(); + spot->SetPosition( pos ); + spot->SetFlags( HidingSpot::IN_COVER ); + m_hidingSpotList.AddToTail( spot ); + } + } + else + { + // load HidingSpot objects for this area + for( int h=0; h<hidingSpotCount; ++h ) + { + // create new hiding spot and put on master list + HidingSpot *spot = TheNavMesh->CreateHidingSpot(); + + spot->Load( file, version ); + + m_hidingSpotList.AddToTail( spot ); + } + } + + // + // Load number of approach areas + // + filesystem->Read( &m_approachCount, sizeof(unsigned char), file ); + + // load approach area info (IDs) + unsigned char type; + for( int a=0; a<m_approachCount; ++a ) + { + filesystem->Read( &m_approach[a].here.id, sizeof(unsigned int), file ); + + filesystem->Read( &m_approach[a].prev.id, sizeof(unsigned int), file ); + filesystem->Read( &type, sizeof(unsigned char), file ); + m_approach[a].prevToHereHow = (NavTraverseType)type; + + filesystem->Read( &m_approach[a].next.id, sizeof(unsigned int), file ); + filesystem->Read( &type, sizeof(unsigned char), file ); + m_approach[a].hereToNextHow = (NavTraverseType)type; + } + + + // + // Load encounter paths for this area + // + unsigned int count; + filesystem->Read( &count, sizeof(unsigned int), file ); + + if (version < 3) + { + // old data, read and discard + for( unsigned int e=0; e<count; ++e ) + { + SpotEncounter encounter; + + filesystem->Read( &encounter.from.id, sizeof(unsigned int), file ); + filesystem->Read( &encounter.to.id, sizeof(unsigned int), file ); + + filesystem->Read( &encounter.path.from.x, 3 * sizeof(float), file ); + filesystem->Read( &encounter.path.to.x, 3 * sizeof(float), file ); + + // read list of spots along this path + unsigned char spotCount; + filesystem->Read( &spotCount, sizeof(unsigned char), file ); + + for( int s=0; s<spotCount; ++s ) + { + Vector pos; + filesystem->Read( &pos, 3*sizeof(float), file ); + filesystem->Read( &pos, sizeof(float), file ); + } + } + return; + } + + for( unsigned int e=0; e<count; ++e ) + { + SpotEncounter *encounter = new SpotEncounter; + + filesystem->Read( &encounter->from.id, sizeof(unsigned int), file ); + + unsigned char dir; + filesystem->Read( &dir, sizeof(unsigned char), file ); + encounter->fromDir = static_cast<NavDirType>( dir ); + + filesystem->Read( &encounter->to.id, sizeof(unsigned int), file ); + + filesystem->Read( &dir, sizeof(unsigned char), file ); + encounter->toDir = static_cast<NavDirType>( dir ); + + // read list of spots along this path + unsigned char spotCount; + filesystem->Read( &spotCount, sizeof(unsigned char), file ); + + SpotOrder order; + for( int s=0; s<spotCount; ++s ) + { + filesystem->Read( &order.id, sizeof(unsigned int), file ); + + unsigned char t; + filesystem->Read( &t, sizeof(unsigned char), file ); + + order.t = (float)t/255.0f; + + encounter->spotList.AddToTail( order ); + } + + m_spotEncounterList.AddToTail( encounter ); + } + + if (version < 5) + return; + + // + // Load Place data + // + PlaceDirectory::IndexType entry; + filesystem->Read( &entry, sizeof(entry), file ); + + // convert entry to actual Place + SetPlace( placeDirectory.IndexToPlace( entry ) ); + + if ( version < 7 ) + return; + + // load ladder data + for ( int dir=0; dir<CSNavLadder::NUM_LADDER_DIRECTIONS; ++dir ) + { + filesystem->Read( &count, sizeof(unsigned int), file ); + { + for( unsigned int i=0; i<count; ++i ) + { + NavLadderConnect connect; + filesystem->Read( &connect.id, sizeof(unsigned int), file ); + + bool alreadyConnected = false; + FOR_EACH_LL( m_ladder[dir], j ) + { + if ( m_ladder[dir][j].id == connect.id ) + { + alreadyConnected = true; + break; + } + } + + if ( !alreadyConnected ) + { + m_ladder[dir].AddToTail( connect ); + } + } + } + } + + if ( version < 8 ) + return; + + // load earliest occupy times + for( int i=0; i<MAX_NAV_TEAMS; ++i ) + { + // no spot in the map should take longer than this to reach + filesystem->Read( &m_earliestOccupyTime[i], sizeof(m_earliestOccupyTime[i]), file ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Convert loaded IDs to pointers + * Make sure all IDs are converted, even if corrupt data is encountered. + */ +NavErrorType CNavArea::PostLoad( void ) +{ + NavErrorType error = NAV_OK; + + for ( int dir=0; dir < CNavLadder::NUM_LADDER_DIRECTIONS; ++dir ) + { + FOR_EACH_VEC( m_ladder[dir], it ) + { + NavLadderConnect& connect = m_ladder[dir][it]; + + unsigned int id = connect.id; + + if ( TheNavMesh->GetLadders().Find( connect.ladder ) == TheNavMesh->GetLadders().InvalidIndex() ) + { + connect.ladder = TheNavMesh->GetLadderByID( id ); + } + + if (id && connect.ladder == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation ladder data. Cannot connect Navigation Areas.\n" ); + error = NAV_CORRUPT_DATA; + } + } + } + + // connect areas together + for( int d=0; d<NUM_DIRECTIONS; d++ ) + { + FOR_EACH_VEC( m_connect[d], it ) + { + NavConnect *connect = &m_connect[ d ][ it ]; + + unsigned int id = connect->id; + connect->area = TheNavMesh->GetNavAreaByID( id ); + if (id && connect->area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Cannot connect Navigation Areas.\n" ); + error = NAV_CORRUPT_DATA; + } + } + } + + // resolve approach area IDs + for( int a=0; a<m_approachCount; ++a ) + { + m_approach[a].here.area = TheNavMesh->GetNavAreaByID( m_approach[a].here.id ); + if (m_approach[a].here.id && m_approach[a].here.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Approach Area (here).\n" ); + error = NAV_CORRUPT_DATA; + } + + m_approach[a].prev.area = TheNavMesh->GetNavAreaByID( m_approach[a].prev.id ); + if (m_approach[a].prev.id && m_approach[a].prev.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Approach Area (prev).\n" ); + error = NAV_CORRUPT_DATA; + } + + m_approach[a].next.area = TheNavMesh->GetNavAreaByID( m_approach[a].next.id ); + if (m_approach[a].next.id && m_approach[a].next.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Approach Area (next).\n" ); + error = NAV_CORRUPT_DATA; + } + } + + // resolve spot encounter IDs + SpotEncounter *e; + FOR_EACH_VEC( m_spotEncounters, it ) + { + e = m_spotEncounters[ it ]; + + e->from.area = TheNavMesh->GetNavAreaByID( e->from.id ); + if (e->from.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing \"from\" Navigation Area for Encounter Spot.\n" ); + error = NAV_CORRUPT_DATA; + } + + e->to.area = TheNavMesh->GetNavAreaByID( e->to.id ); + if (e->to.area == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing \"to\" Navigation Area for Encounter Spot.\n" ); + error = NAV_CORRUPT_DATA; + } + + if (e->from.area && e->to.area) + { + // compute path + float halfWidth; + ComputePortal( e->to.area, e->toDir, &e->path.to, &halfWidth ); + ComputePortal( e->from.area, e->fromDir, &e->path.from, &halfWidth ); + + const float eyeHeight = HalfHumanHeight; + e->path.from.z = e->from.area->GetZ( e->path.from ) + eyeHeight; + e->path.to.z = e->to.area->GetZ( e->path.to ) + eyeHeight; + } + + // resolve HidingSpot IDs + FOR_EACH_VEC( e->spots, sit ) + { + SpotOrder *order = &e->spots[ sit ]; + + order->spot = GetHidingSpotByID( order->id ); + if (order->spot == NULL) + { + Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Hiding Spot\n" ); + error = NAV_CORRUPT_DATA; + } + } + } + + // build overlap list + /// @todo Optimize this + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + + if (area == this) + continue; + + if (IsOverlapping( area )) + m_overlappingAreas.AddToTail( area ); + } + + return error; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine the earliest time this hiding spot can be reached by either team + */ +void CNavArea::ComputeEarliestOccupyTimes( void ) +{ +#ifdef CSTRIKE_DLL + /// @todo Derive cstrike-specific navigation classes + + for( int i=0; i<MAX_NAV_TEAMS; ++i ) + { + // no spot in the map should take longer than this to reach + m_earliestOccupyTime[i] = 120.0f; + } + + if (nav_quicksave.GetBool()) + return; + + // maximum player speed in units/second + const float playerSpeed = 240.0f; + + ShortestPathCost cost; + CBaseEntity *spot; + + // determine the shortest time it will take a Terrorist to reach this area + int team = TEAM_TERRORIST % MAX_NAV_TEAMS; + for( spot = gEntList.FindEntityByClassname( NULL, "info_player_terrorist" ); + spot; + spot = gEntList.FindEntityByClassname( spot, "info_player_terrorist" ) ) + { + float travelDistance = NavAreaTravelDistance( spot->GetAbsOrigin(), m_center, cost ); + if (travelDistance < 0.0f) + continue; + + float travelTime = travelDistance / playerSpeed; + if (travelTime < m_earliestOccupyTime[ team ]) + { + m_earliestOccupyTime[ team ] = travelTime; + } + } + + + // determine the shortest time it will take a CT to reach this area + team = TEAM_CT % MAX_NAV_TEAMS; + for( spot = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); + spot; + spot = gEntList.FindEntityByClassname( spot, "info_player_counterterrorist" ) ) + { + float travelDistance = NavAreaTravelDistance( spot->GetAbsOrigin(), m_center, cost ); + if (travelDistance < 0.0f) + continue; + + float travelTime = travelDistance / playerSpeed; + if (travelTime < m_earliestOccupyTime[ team ]) + { + m_earliestOccupyTime[ team ] = travelTime; + } + } + +#else + for( int i=0; i<MAX_NAV_TEAMS; ++i ) + { + m_earliestOccupyTime[i] = 0.0f; + } +#endif +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine if this area is a "battlefront" area - where two rushing teams first meet. + */ +void CNavMesh::ComputeBattlefrontAreas( void ) +{ +#if 0 +#ifdef CSTRIKE_DLL + ShortestPathCost cost; + CBaseEntity *tSpawn, *ctSpawn; + + for( tSpawn = gEntList.FindEntityByClassname( NULL, "info_player_terrorist" ); + tSpawn; + tSpawn = gEntList.FindEntityByClassname( tSpawn, "info_player_terrorist" ) ) + { + CNavArea *tArea = TheNavMesh->GetNavArea( tSpawn->GetAbsOrigin() ); + if (tArea == NULL) + continue; + + for( ctSpawn = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); + ctSpawn; + ctSpawn = gEntList.FindEntityByClassname( ctSpawn, "info_player_counterterrorist" ) ) + { + CNavArea *ctArea = TheNavMesh->GetNavArea( ctSpawn->GetAbsOrigin() ); + + if (ctArea == NULL) + continue; + + if (tArea == ctArea) + { + m_isBattlefront = true; + return; + } + + // build path between these two spawn points - assume if path fails, it at least got close + // (ie: imagine spawn points that you jump down from - can't path to) + CNavArea *goalArea = NULL; + NavAreaBuildPath( tArea, ctArea, NULL, cost, &goalArea ); + + if (goalArea == NULL) + continue; + + +/** + * @todo Need to enumerate ALL paths between all pairs of spawn points to find all battlefront areas + */ + + // find the area with the earliest overlapping occupy times + CNavArea *battlefront = NULL; + float earliestTime = 999999.9f; + + const float epsilon = 1.0f; + CNavArea *area; + for( area = goalArea; area; area = area->GetParent() ) + { + if (fabs(area->GetEarliestOccupyTime( TEAM_TERRORIST ) - area->GetEarliestOccupyTime( TEAM_CT )) < epsilon) + { + } + + } + } + } +#endif +#endif +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the filename for this map's "nav map" file + */ +const char *CNavMesh::GetFilename( void ) const +{ + // filename is local to game dir for Steam, so we need to prepend game dir for regular file save + char gamePath[256]; + engine->GetGameDir( gamePath, 256 ); + + static char filename[256]; + Q_snprintf( filename, sizeof( filename ), "%s\\maps\\%s.nav", gamePath, STRING( gpGlobals->mapname ) ); + + return filename; +} + +//-------------------------------------------------------------------------------------------------------------- +/* +============ +COM_FixSlashes + +Changes all '/' characters into '\' characters, in place. +============ +*/ +inline void COM_FixSlashes( char *pname ) +{ +#ifdef _WIN32 + while ( *pname ) + { + if ( *pname == '/' ) + *pname = '\\'; + pname++; + } +#else + while ( *pname ) + { + if ( *pname == '\\' ) + *pname = '/'; + pname++; + } +#endif +} + +static void WarnIfMeshNeedsAnalysis( void ) +{ + // Quick check to warn about needing to analyze: nav_strip, nav_delete, etc set + // every CNavArea's m_approachCount to 0, and delete their m_spotEncounterList. + // So, if no area has either, odds are good we need an analyze. + { + bool hasApproachAreas = false; + bool hasSpotEncounters = false; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + if ( area->GetApproachInfoCount() ) + { + hasApproachAreas = true; + } + + if ( area->GetSpotEncounterCount() ) + { + hasSpotEncounters = true; + } + } + + if ( !hasApproachAreas || !hasSpotEncounters ) + { + Warning( "The nav mesh needs a full nav_analyze\n" ); + } + } +} + +/** + * Store Navigation Mesh to a file + */ +bool CNavMesh::Save( void ) const +{ + WarnIfMeshNeedsAnalysis(); + + const char *filename = GetFilename(); + if (filename == NULL) + return false; + + // + // Store the NAV file + // + COM_FixSlashes( const_cast<char *>(filename) ); + + // get size of source bsp file for later (before we open the nav file for writing, in + // case of failure) + char *bspFilename = GetBspFilename( filename ); + if (bspFilename == NULL) + { + return false; + } + + FileHandle_t file = filesystem->Open( filename, "wb" ); + + if (!file) + { + return false; + } + + // store "magic number" to help identify this kind of file + unsigned int magic = NAV_MAGIC_NUMBER; + filesystem->Write( &magic, sizeof(unsigned int), file ); + + // store version number of file + // 1 = hiding spots as plain vector array + // 2 = hiding spots as HidingSpot objects + // 3 = Encounter spots use HidingSpot ID's instead of storing vector again + // 4 = Includes size of source bsp file to verify nav data correlation + // ---- Beta Release at V4 ----- + // 5 = Added Place info + // ---- Conversion to Src ------ + // 6 = Added Ladder info + // 7 = Areas store ladder ID's so ladders can have one-way connections + // 8 = Added earliest occupy times (2 floats) to each area + // 9 = Promoted CNavArea's attribute flags to a short + unsigned int version = NavCurrentVersion; + filesystem->Write( &version, sizeof(unsigned int), file ); + + // store the size of source bsp file in the nav file + // so we can test if the bsp changed since the nav file was made + unsigned int bspSize = filesystem->Size( bspFilename ); + DevMsg( "Size of bsp file '%s' is %u bytes.\n", bspFilename, bspSize ); + + filesystem->Write( &bspSize, sizeof(unsigned int), file ); + + + // + // Build a directory of the Places in this map + // + placeDirectory.Reset(); + + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + + Place place = area->GetPlace(); + + if (place) + { + placeDirectory.AddPlace( place ); + } + } + + placeDirectory.Save( file ); + + + // + // Store navigation areas + // + { + // store number of areas + unsigned int count = TheNavAreas.Count(); + filesystem->Write( &count, sizeof(unsigned int), file ); + + // store each area + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + area->Save( file, version ); + } + } + + // + // Store ladders + // + { + // store number of ladders + unsigned int count = m_ladders.Count(); + filesystem->Write( &count, sizeof(unsigned int), file ); + + // store each ladder + FOR_EACH_VEC( m_ladders, it ) + { + CNavLadder *ladder = m_ladders[ it ]; + + ladder->Save( file, version ); + } + } + + filesystem->Flush( file ); + filesystem->Close( file ); + + unsigned int navSize = filesystem->Size( filename ); + DevMsg( "Size of nav file '%s' is %u bytes.\n", filename, navSize ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +static NavErrorType CheckNavFile( const char *bspFilename ) +{ + if ( !bspFilename ) + return NAV_CANT_ACCESS_FILE; + + char bspPathname[256]; + char filename[256]; + Q_strncpy( bspPathname, "maps/", sizeof( bspPathname ) ); + Q_strncat( bspPathname, bspFilename, sizeof( bspPathname ), COPY_ALL_CHARACTERS ); + Q_strncpy( filename, bspPathname, sizeof( filename ) ); + Q_SetExtension( filename, ".nav", sizeof( filename ) ); + + bool navIsInBsp = false; + FileHandle_t file = filesystem->Open( filename, "rb", "MOD" ); // this ignores .nav files embedded in the .bsp ... + if ( !file ) + { + navIsInBsp = true; + file = filesystem->Open( filename, "rb", "GAME" ); // ... and this looks for one if it's the only one around. + } + + if (!file) + { + return NAV_CANT_ACCESS_FILE; + } + + // check magic number + int result; + unsigned int magic; + result = filesystem->Read( &magic, sizeof(unsigned int), file ); + if (!result || magic != NAV_MAGIC_NUMBER) + { + filesystem->Close( file ); + return NAV_INVALID_FILE; + } + + // read file version number + unsigned int version; + result = filesystem->Read( &version, sizeof(unsigned int), file ); + if (!result || version > NavCurrentVersion || version < 4) + { + filesystem->Close( file ); + return NAV_BAD_FILE_VERSION; + } + + // get size of source bsp file and verify that the bsp hasn't changed + unsigned int saveBspSize; + filesystem->Read( &saveBspSize, sizeof(unsigned int), file ); + + // verify size + unsigned int bspSize = filesystem->Size( bspPathname ); + + if (bspSize != saveBspSize && !navIsInBsp) + { + return NAV_FILE_OUT_OF_DATE; + } + + return NAV_OK; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CommandNavCheckFileConsistency( void ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + FileFindHandle_t findHandle; + const char *bspFilename = filesystem->FindFirstEx( "maps/*.bsp", "MOD", &findHandle ); + while ( bspFilename ) + { + switch ( CheckNavFile( bspFilename ) ) + { + case NAV_CANT_ACCESS_FILE: + Warning( "Missing nav file for %s\n", bspFilename ); + break; + case NAV_INVALID_FILE: + Warning( "Invalid nav file for %s\n", bspFilename ); + break; + case NAV_BAD_FILE_VERSION: + Warning( "Old nav file for %s\n", bspFilename ); + break; + case NAV_FILE_OUT_OF_DATE: + Warning( "The nav file for %s is built from an old version of the map\n", bspFilename ); + break; + case NAV_OK: + Msg( "The nav file for %s is up-to-date\n", bspFilename ); + break; + } + + bspFilename = filesystem->FindNext( findHandle ); + } + filesystem->FindClose( findHandle ); +} +static ConCommand nav_check_file_consistency( "nav_check_file_consistency", CommandNavCheckFileConsistency, "Scans the maps directory and reports any missing/out-of-date navigation files.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Load AI navigation data from a file + */ +NavErrorType CNavMesh::Load( void ) +{ + // free previous navigation mesh data + Reset(); + placeDirectory.Reset(); + + CNavArea::m_nextID = 1; + + // nav filename is derived from map filename + char filename[256]; + Q_snprintf( filename, sizeof( filename ), "maps\\%s.nav", STRING( gpGlobals->mapname ) ); + + bool navIsInBsp = false; + FileHandle_t file = filesystem->Open( filename, "rb", "MOD" ); // this ignores .nav files embedded in the .bsp ... + if ( !file ) + { + navIsInBsp = true; + file = filesystem->Open( filename, "rb", "GAME" ); // ... and this looks for one if it's the only one around. + } + + if (!file) + { + return NAV_CANT_ACCESS_FILE; + } + + // check magic number + int result; + unsigned int magic; + result = filesystem->Read( &magic, sizeof(unsigned int), file ); + if (!result || magic != NAV_MAGIC_NUMBER) + { + Msg( "Invalid navigation file '%s'.\n", filename ); + filesystem->Close( file ); + return NAV_INVALID_FILE; + } + + // read file version number + unsigned int version; + result = filesystem->Read( &version, sizeof(unsigned int), file ); + if (!result || version > NavCurrentVersion) + { + Msg( "Unknown navigation file version.\n" ); + filesystem->Close( file ); + return NAV_BAD_FILE_VERSION; + } + + if (version >= 4) + { + // get size of source bsp file and verify that the bsp hasn't changed + unsigned int saveBspSize; + filesystem->Read( &saveBspSize, sizeof(unsigned int), file ); + + // verify size + char *bspFilename = GetBspFilename( filename ); + if ( bspFilename == NULL ) + { + filesystem->Close( file ); + return NAV_INVALID_FILE; + } + + unsigned int bspSize = filesystem->Size( bspFilename ); + + if (bspSize != saveBspSize && !navIsInBsp) + { + if ( engine->IsDedicatedServer() ) + { + // Warning doesn't print to the dedicated server console, so we'll use Msg instead + Msg( "The Navigation Mesh was built using a different version of this map.\n" ); + } + else + { + Warning( "The Navigation Mesh was built using a different version of this map.\n" ); + } + m_isFromCurrentMap = false; + } + } + + // load Place directory + if (version >= 5) + { + placeDirectory.Load( file ); + } + + // get number of areas + unsigned int count; + unsigned int i; + result = filesystem->Read( &count, sizeof(unsigned int), file ); + + Extent extent; + extent.lo.x = 9999999999.9f; + extent.lo.y = 9999999999.9f; + extent.hi.x = -9999999999.9f; + extent.hi.y = -9999999999.9f; + + // load the areas and compute total extent + for( i=0; i<count; ++i ) + { + CNavArea *area = new CNavArea; + area->Load( file, version ); + TheNavAreas.AddToTail( area ); + + Extent areaExtent; + area->GetExtent(&areaExtent); + + // check validity of nav area + if (areaExtent.lo.x >= areaExtent.hi.x || areaExtent.lo.y >= areaExtent.hi.y) + Warning( "WARNING: Degenerate Navigation Area #%d at ( %g, %g, %g )\n", + area->GetID(), area->m_center.x, area->m_center.y, area->m_center.z ); + + if (areaExtent.lo.x < extent.lo.x) + extent.lo.x = areaExtent.lo.x; + if (areaExtent.lo.y < extent.lo.y) + extent.lo.y = areaExtent.lo.y; + if (areaExtent.hi.x > extent.hi.x) + extent.hi.x = areaExtent.hi.x; + if (areaExtent.hi.y > extent.hi.y) + extent.hi.y = areaExtent.hi.y; + } + + // add the areas to the grid + AllocateGrid( extent.lo.x, extent.hi.x, extent.lo.y, extent.hi.y ); + + FOR_EACH_VEC( TheNavAreas, it ) + { + AddNavArea( TheNavAreas[ it ] ); + } + + + // + // Set up all the ladders + // + if (version >= 6) + { + result = filesystem->Read( &count, sizeof(unsigned int), file ); + + // load the ladders + for( i=0; i<count; ++i ) + { + CNavLadder *ladder = new CNavLadder; + ladder->Load( file, version ); + m_ladders.AddToTail( ladder ); + } + } + else + { + BuildLadders(); + } + + // allow areas to connect to each other, etc + FOR_EACH_VEC( TheNavAreas, pit ) + { + CNavArea *area = TheNavAreas[ pit ]; + area->PostLoad(); + } + + // allow hiding spots to compute information + FOR_EACH_VEC( TheHidingSpots, hit ) + { + HidingSpot *spot = TheHidingSpots[ hit ]; + spot->PostLoad(); + } + + if ( version < 8 ) + { + // Old nav meshes need to compute earliest occupy times + FOR_EACH_VEC( TheNavAreas, nit ) + { + CNavArea *area = TheNavAreas[ nit ]; + area->ComputeEarliestOccupyTimes(); + } + } + + ComputeBattlefrontAreas(); + + // the Navigation Mesh has been successfully loaded + m_isLoaded = true; + + filesystem->Close( file ); + + WarnIfMeshNeedsAnalysis(); + + return NAV_OK; +} +#endif
\ No newline at end of file diff --git a/game/server/cstrike/cs_nav_generate.cpp b/game/server/cstrike/cs_nav_generate.cpp new file mode 100644 index 0000000..18e9ffa --- /dev/null +++ b/game/server/cstrike/cs_nav_generate.cpp @@ -0,0 +1,269 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_generate.cpp +// Auto-generate a Navigation Mesh by sampling the current map +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "util_shared.h" +#include "nav_mesh.h" +#include "cs_nav_area.h" +#include "cs_nav_node.h" +#include "cs_nav_pathfind.h" +#include "viewport_panel_names.h" + +enum { MAX_BLOCKED_AREAS = 256 }; +static unsigned int blockedID[ MAX_BLOCKED_AREAS ]; +static int blockedIDCount = 0; +static float lastMsgTime = 0.0f; + + +//ConVar nav_slope_limit( "nav_slope_limit", "0.7", FCVAR_GAMEDLL, "The ground unit normal's Z component must be greater than this for nav areas to be generated." ); +ConVar nav_restart_after_analysis( "nav_restart_after_analysis", "1", FCVAR_GAMEDLL, "When nav nav_restart_after_analysis finishes, restart the server. Turning this off can cause crashes, but is useful for incremental generation." ); + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Shortest path cost, paying attention to "blocked" areas + */ +class ApproachAreaCost +{ +public: + // HPE_TODO[pmf]: check that these new parameters are okay to be ignored + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) + { + // check if this area is "blocked" + for( int i=0; i<blockedIDCount; ++i ) + { + if (area->GetID() == blockedID[i]) + { + return -1.0f; + } + } + + if (fromArea == NULL) + { + // first area in path, no cost + return 0.0f; + } + else + { + // compute distance traveled along path so far + float dist; + + if (ladder) + { + dist = ladder->m_length; + } + else + { + dist = (area->GetCenter() - fromArea->GetCenter()).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + return cost; + } + } +}; + +/* + * Determine the set of "approach areas". + * An approach area is an area representing a place where players + * move into/out of our local neighborhood of areas. + * @todo Optimize by search from eye outward and modifying pathfinder to treat all links as bi-directional + */ +void CCSNavArea::ComputeApproachAreas( void ) +{ + m_approachCount = 0; + + if (nav_quicksave.GetBool()) + return; + + // use the center of the nav area as the "view" point + Vector eye = m_center; + if (TheNavMesh->GetGroundHeight( eye, &eye.z ) == false) + return; + + // approximate eye position + if (GetAttributes() & NAV_MESH_CROUCH) + eye.z += 0.9f * HalfHumanHeight; + else + eye.z += 0.9f * HumanHeight; + + enum { MAX_PATH_LENGTH = 256 }; + CNavArea *path[ MAX_PATH_LENGTH ]; + ApproachAreaCost cost; + + enum SearchType + { + FROM_EYE, ///< start search from our eyepoint outward to farArea + TO_EYE, ///< start search from farArea beack towards our eye + SEARCH_FINISHED + }; + + // + // In order to *completely* enumerate all of the approach areas, we + // need to search from our eyepoint outward, as well as from outwards + // towards our eyepoint + // + for( int searchType = FROM_EYE; searchType != SEARCH_FINISHED; ++searchType ) + { + // + // In order to enumerate all of the approach areas, we need to + // run the algorithm many times, once for each "far away" area + // and keep the union of the approach area sets + // + int it; + for( it = 0; it < TheNavAreas.Count(); ++it ) + { + CNavArea *farArea = TheNavAreas[ it ]; + + blockedIDCount = 0; + + // skip the small areas + const float minSize = 200.0f; // 150 + Extent extent; + farArea->GetExtent(&extent); + if (extent.SizeX() < minSize || extent.SizeY() < minSize) + { + continue; + } + + // if we can see 'farArea', try again - the whole point is to go "around the bend", so to speak + if (farArea->IsVisible( eye )) + { + continue; + } + + // + // Keep building paths to farArea and blocking them off until we + // cant path there any more. + // As areas are blocked off, all exits will be enumerated. + // + while( m_approachCount < MAX_APPROACH_AREAS ) + { + CNavArea *from, *to; + + if (searchType == FROM_EYE) + { + // find another path *to* 'farArea' + // we must pathfind from us in order to pick up one-way paths OUT OF our area + from = this; + to = farArea; + } + else // TO_EYE + { + // find another path *from* 'farArea' + // we must pathfind to us in order to pick up one-way paths INTO our area + from = farArea; + to = this; + } + + // build the actual path + if (NavAreaBuildPath( from, to, NULL, cost ) == false) + { + break; + } + + // find number of areas on path + int count = 0; + CNavArea *area; + for( area = to; area; area = area->GetParent() ) + { + ++count; + } + + if (count > MAX_PATH_LENGTH) + { + count = MAX_PATH_LENGTH; + } + + // if the path is only two areas long, there can be no approach points + if (count <= 2) + { + break; + } + + // build path starting from eye + int i = 0; + + if (searchType == FROM_EYE) + { + for( area = to; i < count && area; area = area->GetParent() ) + { + path[ count-i-1 ] = area; + ++i; + } + } + else // TO_EYE + { + for( area = to; i < count && area; area = area->GetParent() ) + { + path[ i++ ] = area; + } + } + + // traverse path to find first area we cannot see (skip the first area) + for( i=1; i<count; ++i ) + { + // if we see this area, continue on + if (path[i]->IsVisible( eye )) + { + continue; + } + + // we can't see this area - mark this area as "blocked" and unusable by subsequent approach paths + if (blockedIDCount == MAX_BLOCKED_AREAS) + { + Msg( "Overflow computing approach areas for area #%d.\n", GetID()); + return; + } + + // if the area to be blocked is actually farArea, block the one just prior + // (blocking farArea will cause all subsequent pathfinds to fail) + int block = (path[i] == farArea) ? i-1 : i; + + // dont block the start area, or all subsequence pathfinds will fail + if (block == 0) + { + continue; + } + + blockedID[ blockedIDCount++ ] = path[ block ]->GetID(); + + // store new approach area if not already in set + int a; + for( a=0; a<m_approachCount; ++a ) + { + if (m_approach[a].here.area == path[block-1]) + { + break; + } + } + + if (a == m_approachCount) + { + m_approach[ m_approachCount ].prev.area = (block >= 2) ? path[block-2] : NULL; + + m_approach[ m_approachCount ].here.area = path[block-1]; + m_approach[ m_approachCount ].prevToHereHow = path[block-1]->GetParentHow(); + + m_approach[ m_approachCount ].next.area = path[block]; + m_approach[ m_approachCount ].hereToNextHow = path[block]->GetParentHow(); + + ++m_approachCount; + } + + // we are done with this path + break; + } + } + } + } +} diff --git a/game/server/cstrike/cs_nav_mesh.cpp b/game/server/cstrike/cs_nav_mesh.cpp new file mode 100644 index 0000000..0a46745 --- /dev/null +++ b/game/server/cstrike/cs_nav_mesh.cpp @@ -0,0 +1,127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// NavMesh.cpp +// Implementation of Navigation Mesh interface +// Author: Michael S. Booth, 2003-2004 + +#include "cbase.h" +#include "filesystem.h" +#include "cs_nav_mesh.h" +#include "cs_nav_node.h" +#include "cs_nav_area.h" +#include "fmtstr.h" +#include "utlbuffer.h" +#include "tier0/vprof.h" + +//-------------------------------------------------------------------------------------------------------------- +CSNavMesh::CSNavMesh( void ) +{ +} + +//-------------------------------------------------------------------------------------------------------------- +CSNavMesh::~CSNavMesh() +{ +} + +CNavArea * CSNavMesh::CreateArea( void ) const +{ + return new CCSNavArea; +} + +//------------------------------------------------------------------------- +void CSNavMesh::BeginCustomAnalysis( bool bIncremental ) +{ + +} + + +//------------------------------------------------------------------------- +// invoked when custom analysis step is complete +void CSNavMesh::PostCustomAnalysis( void ) +{ + +} + + +//------------------------------------------------------------------------- +void CSNavMesh::EndCustomAnalysis() +{ + +} + + +//------------------------------------------------------------------------- +/** + * Returns sub-version number of data format used by derived classes + */ +unsigned int CSNavMesh::GetSubVersionNumber( void ) const +{ + // 1: initial implementation - added ApproachArea data + return 1; +} + +//------------------------------------------------------------------------- +/** + * Store custom mesh data for derived classes + */ +void CSNavMesh::SaveCustomData( CUtlBuffer &fileBuffer ) const +{ + +} + +//------------------------------------------------------------------------- +/** + * Load custom mesh data for derived classes + */ +void CSNavMesh::LoadCustomData( CUtlBuffer &fileBuffer, unsigned int subVersion ) +{ + +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset the Navigation Mesh to initial values + */ +void CSNavMesh::Reset( void ) +{ + CNavMesh::Reset(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Zero player counts in all areas + */ +void CSNavMesh::ClearPlayerCounts( void ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CCSNavArea *area = (CCSNavArea*)TheNavAreas[ it ]; + area->ClearPlayerCount(); + } +} + +void CSNavMesh::Update( void ) +{ + CNavMesh::Update(); +} + +NavErrorType CSNavMesh::Load( void ) +{ + return CNavMesh::Load(); +} + +bool CSNavMesh::Save( void ) const +{ + return CNavMesh::Save(); +} + +NavErrorType CSNavMesh::PostLoad( unsigned int version ) +{ + return CNavMesh::PostLoad(version); +}
\ No newline at end of file diff --git a/game/server/cstrike/cs_nav_mesh.h b/game/server/cstrike/cs_nav_mesh.h new file mode 100644 index 0000000..a240a87 --- /dev/null +++ b/game/server/cstrike/cs_nav_mesh.h @@ -0,0 +1,68 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// cs_nav_mesh.h +// The Navigation Mesh interface +// Author: Michael S. Booth ([email protected]), January 2003 + +// +// Author: Michael S. Booth ([email protected]), 2003 +// +// NOTE: The Navigation 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 _CS_NAV_MESH_H_ +#define _CS_NAV_MESH_H_ + +#include "filesystem.h" + +#include "nav_mesh.h" + +#include "cs_nav.h" +#include "nav_area.h" +#include "nav_colors.h" + +class CNavArea; +class CCSNavArea; +class CBaseEntity; + +//-------------------------------------------------------------------------------------------------------- +/** + * The CSNavMesh is the global interface to the Navigation Mesh. + * @todo Make this an abstract base class interface, and derive mod-specific implementations. + */ +class CSNavMesh : public CNavMesh +{ +public: + CSNavMesh( void ); + virtual ~CSNavMesh(); + + virtual CNavArea *CreateArea( void ) const; // CNavArea factory + + virtual unsigned int GetSubVersionNumber( void ) const; // returns sub-version number of data format used by derived classes + virtual void SaveCustomData( CUtlBuffer &fileBuffer ) const; // store custom mesh data for derived classes + virtual void LoadCustomData( CUtlBuffer &fileBuffer, unsigned int subVersion ); // load custom mesh data for derived classes + + virtual void Reset( void ); ///< destroy Navigation Mesh data and revert to initial state + virtual void Update( void ); ///< invoked on each game frame + + virtual NavErrorType Load( void ); // load navigation data from a file + virtual NavErrorType PostLoad( unsigned int version ); // (EXTEND) invoked after all areas have been loaded - for pointer binding, etc + virtual bool Save( void ) const; ///< store Navigation Mesh to a file + + void ClearPlayerCounts( void ); ///< zero player counts in all areas + +protected: + virtual void BeginCustomAnalysis( bool bIncremental ); + virtual void PostCustomAnalysis( void ); // invoked when custom analysis step is complete + virtual void EndCustomAnalysis(); + +private: +}; + +#endif // _CS_NAV_MESH_H_ diff --git a/game/server/cstrike/cs_nav_node.cpp b/game/server/cstrike/cs_nav_node.cpp new file mode 100644 index 0000000..93f7b1f --- /dev/null +++ b/game/server/cstrike/cs_nav_node.cpp @@ -0,0 +1,383 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_node.cpp +// AI Navigation Nodes +// Author: Michael S. Booth ([email protected]), January 2003 + +#include "cbase.h" +#include "cs_nav_node.h" +#include "nav_colors.h" +#include "nav_mesh.h" + +NavDirType Opposite[ NUM_DIRECTIONS ] = { SOUTH, WEST, NORTH, EAST }; + +CSNavNode *CSNavNode::m_list = NULL; +unsigned int CSNavNode::m_listLength = 0; +unsigned int CSNavNode::m_nextID = 1; + +ConVar nav_show_nodes( "nav_show_nodes", "0" ); + + +//-------------------------------------------------------------------------------------------------------------- +class LookAtTarget +{ +public: + LookAtTarget( const Vector &target ) + { + m_target = target; + } + + bool operator()( CBasePlayer *player ) + { + QAngle angles; + Vector to = m_target - player->GetAbsOrigin(); + VectorAngles( to, angles ); + + player->SetLocalAngles( angles ); + player->SnapEyeAngles( angles ); + return true; + } + +private: + Vector m_target; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor + */ +CSNavNode::CSNavNode( const Vector &pos, const Vector &normal, CSNavNode *parent ) +{ + m_pos = pos; + m_normal = normal; + + m_id = m_nextID++; + + int i; + for( i=0; i<NUM_DIRECTIONS; ++i ) + { + m_to[ i ] = NULL; + } + + for ( i=0; i<NUM_CORNERS; ++i ) + { + m_crouch[ i ] = false; + } + + m_visited = 0; + m_parent = parent; + + m_next = m_list; + m_list = this; + m_listLength++; + + m_isCovered = false; + m_area = NULL; + + m_attributeFlags = 0; + + if ( nav_show_nodes.GetBool() ) + { + NDebugOverlay::Cross3D( m_pos, 10.0f, 128, 128, 128, true, 10.0f ); + NDebugOverlay::Cross3D( m_pos, 10.0f, 255, 255, 255, false, 10.0f ); + + LookAtTarget lookAt( m_pos ); + ForEachPlayer( lookAt ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +#if DEBUG_NAV_NODES +ConVar nav_show_node_id( "nav_show_node_id", "0" ); +ConVar nav_test_node( "nav_test_node", "0" ); +ConVar nav_test_node_crouch( "nav_test_node_crouch", "0" ); +ConVar nav_test_node_crouch_dir( "nav_test_node_crouch_dir", "4" ); +#endif // DEBUG_NAV_NODES + + +//-------------------------------------------------------------------------------------------------------------- +void CSNavNode::Draw( void ) +{ +#if DEBUG_NAV_NODES + if ( !nav_show_nodes.GetBool() ) + return; + + int r = 0, g = 0, b = 0; + + if ( m_isCovered ) + { + if ( GetAttributes() & NAV_MESH_CROUCH ) + { + b = 255; + } + else + { + r = 255; + } + } + else + { + if ( GetAttributes() & NAV_MESH_CROUCH ) + { + b = 255; + } + g = 255; + } + + NDebugOverlay::Cross3D( m_pos, 2, r, g, b, true, 0.1f ); + + if ( (!m_isCovered && nav_show_node_id.GetBool()) || (m_isCovered && nav_show_node_id.GetInt() < 0) ) + { + char text[16]; + Q_snprintf( text, sizeof( text ), "%d", m_id ); + NDebugOverlay::Text( m_pos, text, true, 0.1f ); + } + + if ( (unsigned int)(nav_test_node.GetInt()) == m_id ) + { + TheNavMesh->TestArea( this, 1, 1 ); + nav_test_node.SetValue( 0 ); + } + + if ( (unsigned int)(nav_test_node_crouch.GetInt()) == m_id ) + { + CheckCrouch(); + nav_test_node_crouch.SetValue( 0 ); + } + + if ( GetAttributes() & NAV_MESH_CROUCH ) + { + int i; + for( i=0; i<NUM_CORNERS; i++ ) + { + if ( m_crouch[i] ) + { + Vector2D dir; + CornerToVector2D( (NavCornerType)i, &dir ); + + const float scale = 3.0f; + Vector scaled( dir.x * scale, dir.y * scale, 0 ); + + NDebugOverlay::HorzArrow( m_pos, m_pos + scaled, 0.5, 0, 0, 255, 255, true, 0.1f ); + } + } + } + +#endif // DEBUG_NAV_NODES +} + + +//-------------------------------------------------------------------------------------------------------------- +void CSNavNode::CheckCrouch( void ) +{ + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_EVERYTHING ); + trace_t tr; + + // Trace downward from duck height to find the max floor height for the node's surroundings + Vector mins( -HalfHumanWidth, -HalfHumanWidth, 0 ); + Vector maxs( HalfHumanWidth, HalfHumanWidth, 0 ); + Vector start( m_pos.x, m_pos.y, m_pos.z + VEC_DUCK_HULL_MAX.z - 0.1f ); + UTIL_TraceHull( + start, + m_pos, + mins, + maxs, + MASK_PLAYERSOLID_BRUSHONLY, + &filter, + &tr ); + + Vector groundPos = tr.endpos; + + if ( tr.startsolid && !tr.allsolid ) + { + // Try going down out of the solid and re-check for the floor height + start.z -= tr.endpos.z - 0.1f; + + UTIL_TraceHull( + start, + m_pos, + mins, + maxs, + MASK_PLAYERSOLID_BRUSHONLY, + &filter, + &tr ); + + groundPos = tr.endpos; + } + + if ( tr.startsolid ) + { + // we don't even have duck height clear. try a simple check to find floor height. + float x, y; + + // Find the highest floor z - for a player to stand in this area, we need a full + // VEC_HULL_MAX.z of clearance above this height at all points. + float maxFloorZ = m_pos.z; + for( y = -HalfHumanWidth; y <= HalfHumanWidth + 0.1f; y += HalfHumanWidth ) + { + for( x = -HalfHumanWidth; x <= HalfHumanWidth + 0.1f; x += HalfHumanWidth ) + { + float floorZ; + if ( TheNavMesh->GetGroundHeight( m_pos, &floorZ ) ) + { + maxFloorZ = MAX( maxFloorZ, floorZ + 0.1f ); + } + } + } + + groundPos.Init( m_pos.x, m_pos.y, maxFloorZ ); + } + + // For each direction, trace upwards from our best ground height to VEC_HULL_MAX.z to see if we have standing room. + for ( int i=0; i<NUM_CORNERS; ++i ) + { +#if DEBUG_NAV_NODES + if ( nav_test_node_crouch_dir.GetInt() != NUM_CORNERS && i != nav_test_node_crouch_dir.GetInt() ) + continue; +#endif // DEBUG_NAV_NODES + + NavCornerType corner = (NavCornerType)i; + Vector2D cornerVec; + CornerToVector2D( corner, &cornerVec ); + + Vector actualGroundPos = groundPos; // we might need to adjust this if the tracehull failed above and we fell back to m_pos.z + + // Build a mins/maxs pair for the HumanWidth x HalfHumanWidth box facing the appropriate direction + mins.Init(); + maxs.Init( cornerVec.x * HalfHumanWidth, cornerVec.y * HalfHumanWidth, 0 ); + + // now make sure that mins is smaller than maxs + for ( int j=0; j<3; ++j ) + { + if ( mins[j] > maxs[j] ) + { + float tmp = mins[j]; + mins[j] = maxs[j]; + maxs[j] = tmp; + } + } + + UTIL_TraceHull( + actualGroundPos + Vector( 0, 0, 0.1f ), + actualGroundPos + Vector( 0, 0, VEC_HULL_MAX.z - 0.2f ), + mins, + maxs, + MASK_PLAYERSOLID_BRUSHONLY, + &filter, + &tr ); + actualGroundPos.z += tr.fractionleftsolid * VEC_HULL_MAX.z; + float maxHeight = actualGroundPos.z + VEC_DUCK_HULL_MAX.z; + for ( ; tr.startsolid && actualGroundPos.z <= maxHeight; actualGroundPos.z += 1.0f ) + { + // In case we didn't find a good ground pos above, we could start in the ground. Move us up some. + UTIL_TraceHull( + actualGroundPos + Vector( 0, 0, 0.1f ), + actualGroundPos + Vector( 0, 0, VEC_HULL_MAX.z - 0.2f ), + mins, + maxs, + MASK_PLAYERSOLID_BRUSHONLY, + &filter, + &tr ); + } + if (tr.startsolid || tr.fraction != 1.0f) + { + SetAttributes( NAV_MESH_CROUCH ); + m_crouch[corner] = true; + } + +#if DEBUG_NAV_NODES + if ( nav_show_nodes.GetBool() ) + { + if ( nav_test_node_crouch_dir.GetInt() == i || nav_test_node_crouch_dir.GetInt() == NUM_CORNERS ) + { + if ( tr.startsolid ) + { + NDebugOverlay::Box( actualGroundPos, mins, maxs+Vector( 0, 0, VEC_HULL_MAX.z), 255, 0, 0, 10, 20.0f ); + } + else if ( m_crouch[corner] ) + { + NDebugOverlay::Box( actualGroundPos, mins, maxs+Vector( 0, 0, VEC_HULL_MAX.z), 0, 0, 255, 10, 20.0f ); + } + else + { + NDebugOverlay::Box( actualGroundPos, mins, maxs+Vector( 0, 0, VEC_HULL_MAX.z), 0, 255, 0, 10, 10.0f ); + } + } + } +#endif // DEBUG_NAV_NODES + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Create a connection FROM this node TO the given node, in the given direction + */ +void CSNavNode::ConnectTo( CSNavNode *node, NavDirType dir ) +{ + m_to[ dir ] = node; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return node at given position. + * @todo Need a hash table to make this lookup fast + */ +CSNavNode *CSNavNode::GetNode( const Vector &pos ) +{ + const float tolerance = 0.45f * GenerationStepSize; // 1.0f + + for( CSNavNode *node = m_list; node; node = node->m_next ) + { + float dx = fabs( node->m_pos.x - pos.x ); + float dy = fabs( node->m_pos.y - pos.y ); + float dz = fabs( node->m_pos.z - pos.z ); + + if (dx < tolerance && dy < tolerance && dz < tolerance) + return node; + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this node is bidirectionally linked to + * another node in the given direction + */ +BOOL CSNavNode::IsBiLinked( NavDirType dir ) const +{ + if (m_to[ dir ] && m_to[ dir ]->m_to[ Opposite[dir] ] == this) + { + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this node is the NW corner of a quad of nodes + * that are all bidirectionally linked. + */ +BOOL CSNavNode::IsClosedCell( void ) const +{ + if (IsBiLinked( SOUTH ) && + IsBiLinked( EAST ) && + m_to[ EAST ]->IsBiLinked( SOUTH ) && + m_to[ SOUTH ]->IsBiLinked( EAST ) && + m_to[ EAST ]->m_to[ SOUTH ] == m_to[ SOUTH ]->m_to[ EAST ]) + { + return true; + } + + return false; +} + diff --git a/game/server/cstrike/cs_nav_node.h b/game/server/cstrike/cs_nav_node.h new file mode 100644 index 0000000..449f97e --- /dev/null +++ b/game/server/cstrike/cs_nav_node.h @@ -0,0 +1,135 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_node.h +// Navigation Nodes are used when generating a Navigation Mesh by point sampling the map +// Author: Michael S. Booth ([email protected]), January 2003 + +#ifndef _CS_NAV_NODE_H_ +#define _CS_NAV_NODE_H_ + +#include "cs_nav.h" + +// If DEBUG_NAV_NODES is true, nav_show_nodes controls drawing node positions, and +// nav_show_node_id allows you to show the IDs of nodes that didn't get used to create areas. +#ifdef _DEBUG +#define DEBUG_NAV_NODES 1 +#else +#define DEBUG_NAV_NODES 0 +#endif + +//-------------------------------------------------------------------------------------------------------------- +/** + * Navigation Nodes. + * These Nodes encapsulate world locations, and ways to get from one location to an adjacent one. + * Note that these links are not necessarily commutative (falling off of a ledge, for example). + */ +class CSNavNode +{ +public: + CSNavNode( const Vector &pos, const Vector &normal, CSNavNode *parent = NULL ); + + static CSNavNode *GetNode( const Vector &pos ); ///< return navigation node at the position, or NULL if none exists + + CSNavNode *GetConnectedNode( NavDirType dir ) const; ///< get navigation node connected in given direction, or NULL if cant go that way + const Vector *GetPosition( void ) const; + const Vector *GetNormal( void ) const { return &m_normal; } + unsigned int GetID( void ) const { return m_id; } + + static CSNavNode *GetFirst( void ) { return m_list; } + static unsigned int GetListLength( void ) { return m_listLength; } + CSNavNode *GetNext( void ) { return m_next; } + + void Draw( void ); + + void ConnectTo( CSNavNode *node, NavDirType dir ); ///< create a connection FROM this node TO the given node, in the given direction + CSNavNode *GetParent( void ) const; + + void MarkAsVisited( NavDirType dir ); ///< mark the given direction as having been visited + BOOL HasVisited( NavDirType dir ); ///< return TRUE if the given direction has already been searched + BOOL IsBiLinked( NavDirType dir ) const; ///< node is bidirectionally linked to another node in the given direction + BOOL IsClosedCell( void ) const; ///< node is the NW corner of a bi-linked quad of nodes + + void Cover( void ) { m_isCovered = true; } ///< @todo Should pass in area that is covering + BOOL IsCovered( void ) const { return m_isCovered; } ///< return true if this node has been covered by an area + + void AssignArea( CNavArea *area ); ///< assign the given area to this node + CNavArea *GetArea( void ) const; ///< return associated area + + void SetAttributes( unsigned char bits ) { m_attributeFlags = bits; } + unsigned char GetAttributes( void ) const { return m_attributeFlags; } + +private: + friend class CNavMesh; + + void CheckCrouch( void ); + + Vector m_pos; ///< position of this node in the world + Vector m_normal; ///< surface normal at this location + CSNavNode *m_to[ NUM_DIRECTIONS ]; ///< links to north, south, east, and west. NULL if no link + unsigned int m_id; ///< unique ID of this node + unsigned char m_attributeFlags; ///< set of attribute bit flags (see NavAttributeType) + + static CSNavNode *m_list; ///< the master list of all nodes for this map + static unsigned int m_listLength; + static unsigned int m_nextID; + CSNavNode *m_next; ///< next link in master list + + // below are only needed when generating + unsigned char m_visited; ///< flags for automatic node generation. If direction bit is clear, that direction hasn't been explored yet. + CSNavNode *m_parent; ///< the node prior to this in the search, which we pop back to when this node's search is done (a stack) + bool m_isCovered; ///< true when this node is "covered" by a CNavArea + CNavArea *m_area; ///< the area this node is contained within + + bool m_crouch[ NUM_CORNERS ]; +}; + +//-------------------------------------------------------------------------------------------------------------- +// +// Inlines +// + +inline CSNavNode *CSNavNode::GetConnectedNode( NavDirType dir ) const +{ + return m_to[ dir ]; +} + +inline const Vector *CSNavNode::GetPosition( void ) const +{ + return &m_pos; +} + +inline CSNavNode *CSNavNode::GetParent( void ) const +{ + return m_parent; +} + +inline void CSNavNode::MarkAsVisited( NavDirType dir ) +{ + m_visited |= (1 << dir); +} + +inline BOOL CSNavNode::HasVisited( NavDirType dir ) +{ + if (m_visited & (1 << dir)) + return true; + + return false; +} + +inline void CSNavNode::AssignArea( CNavArea *area ) +{ + m_area = area; +} + +inline CNavArea *CSNavNode::GetArea( void ) const +{ + return m_area; +} + + +#endif // _CS_NAV_NODE_H_ diff --git a/game/server/cstrike/cs_nav_path.cpp b/game/server/cstrike/cs_nav_path.cpp new file mode 100644 index 0000000..5a1a9ce --- /dev/null +++ b/game/server/cstrike/cs_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 "cs_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 CCSNavPath::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 NavLadderConnectVector *ladders = from->area->GetLadders( CNavLadder::LADDER_UP ); + int it; + for( it=0; it<ladders->Count(); ++it ) + { + CNavLadder *ladder = (*ladders)[ it ].ladder; + + // can't use "behind" area when ascending... + if (ladder->m_topForwardArea == to->area || + ladder->m_topLeftArea == to->area || + ladder->m_topRightArea == to->area) + { + to->ladder = ladder; + to->pos = ladder->m_bottom + ladder->GetNormal() * 2.0f * HalfHumanWidth; + break; + } + } + + if (it == ladders->Count()) + { + //PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + else if (to->how == GO_LADDER_DOWN) // to get to next area, must go down a ladder + { + // find our ladder + const NavLadderConnectVector *ladders = from->area->GetLadders( CNavLadder::LADDER_DOWN ); + int it; + for( it=0; it<ladders->Count(); ++it ) + { + CNavLadder *ladder = (*ladders)[ it ].ladder; + + if (ladder->m_bottomArea == to->area) + { + to->ladder = ladder; + to->pos = ladder->m_top; + to->pos = ladder->m_top - ladder->GetNormal() * 2.0f * HalfHumanWidth; + break; + } + } + + if (it == ladders->Count()) + { + //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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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 CCSNavPath::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/server/cstrike/cs_nav_path.h b/game/server/cstrike/cs_nav_path.h new file mode 100644 index 0000000..001bc04 --- /dev/null +++ b/game/server/cstrike/cs_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 _CS_NAV_PATH_H_ +#define _CS_NAV_PATH_H_ + +#include "nav_area.h" +#include "bot_util.h" + +class CImprovLocomotor; + +//-------------------------------------------------------------------------------------------------------- +/** + * The CCSNavPath class encapsulates a path through space + */ +class CCSNavPath +{ +public: + CCSNavPath( 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( CCSNavPath *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 + + CCSNavPath *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 // _CS_NAV_PATH_H_ + diff --git a/game/server/cstrike/cs_nav_pathfind.h b/game/server/cstrike/cs_nav_pathfind.h new file mode 100644 index 0000000..042179b --- /dev/null +++ b/game/server/cstrike/cs_nav_pathfind.h @@ -0,0 +1,65 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// nav_pathfind.h +// Path-finding mechanisms using the Navigation Mesh +// Author: Michael S. Booth ([email protected]), January 2003 + +#ifndef _CS_NAV_PATHFIND_H_ +#define _CS_NAV_PATHFIND_H_ + +#include "nav_pathfind.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute travel distance along shortest path from startPos to goalPos. + * Return -1 if can't reach endPos from goalPos. + */ +template< typename CostFunctor > +float NavAreaTravelDistance( const Vector &startPos, const Vector &goalPos, CostFunctor &costFunc ) +{ + CNavArea *startArea = TheNavMesh->GetNearestNavArea( startPos ); + if (startArea == NULL) + { + return -1.0f; + } + + // compute path between areas using given cost heuristic + CNavArea *goalArea = NULL; + if (NavAreaBuildPath( startArea, NULL, &goalPos, costFunc, &goalArea ) == false) + { + return -1.0f; + } + + // compute distance along path + if (goalArea->GetParent() == NULL) + { + // both points are in the same area - return euclidean distance + return (goalPos - startPos).Length(); + } + else + { + CNavArea *area; + float distance; + + // goalPos is assumed to be inside goalArea (or very close to it) - skip to next area + area = goalArea->GetParent(); + distance = (goalPos - area->GetCenter()).Length(); + + for( ; area->GetParent(); area = area->GetParent() ) + { + distance += (area->GetCenter() - area->GetParent()->GetCenter()).Length(); + } + + // add in distance to startPos + distance += (startPos - area->GetCenter()).Length(); + + return distance; + } +} + +#endif // _CS_NAV_PATHFIND_H_ diff --git a/game/server/cstrike/cs_player.cpp b/game/server/cstrike/cs_player.cpp new file mode 100644 index 0000000..79ed9c1 --- /dev/null +++ b/game/server/cstrike/cs_player.cpp @@ -0,0 +1,8267 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Player for HL1. +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "cs_player.h" +#include "cs_gamerules.h" +#include "trains.h" +#include "vcollide_parse.h" +#include "in_buttons.h" +#include "igamemovement.h" +#include "ai_hull.h" +#include "ndebugoverlay.h" +#include "weapon_csbase.h" +#include "decals.h" +#include "cs_ammodef.h" +#include "IEffects.h" +#include "cs_client.h" +#include "client.h" +#include "cs_shareddefs.h" +#include "shake.h" +#include "team.h" +#include "weapon_c4.h" +#include "weapon_parse.h" +#include "weapon_knife.h" +#include "movehelper_server.h" +#include "tier0/vprof.h" +#include "te_effect_dispatch.h" +#include "vphysics/player_controller.h" +#include "weapon_hegrenade.h" +#include "weapon_flashbang.h" +#include "weapon_csbasegun.h" +#include "weapon_smokegrenade.h" +#include <KeyValues.h> +#include "engine/IEngineSound.h" +#include "bot.h" +#include "studio.h" +#include <coordsize.h> +#include "predicted_viewmodel.h" +#include "props_shared.h" +#include "tier0/icommandline.h" +#include "info_camera_link.h" +#include "hintmessage.h" +#include "obstacle_pushaway.h" +#include "movevars_shared.h" +#include "death_pose.h" +#include "basecsgrenade_projectile.h" +#include "SoundEmitterSystem/isoundemittersystembase.h" +#include "CRagdollMagnet.h" +#include "datacache/imdlcache.h" +#include "npcevent.h" +#include "cs_gamestats.h" +#include "gamestats.h" +#include "holiday_gift.h" +#include "../../shared/cstrike/cs_achievement_constants.h" + +//============================================================================= +// HPE_BEGIN +//============================================================================= + +// [dwenger] Needed for global hostage list +#include "cs_simple_hostage.h" + +// [dwenger] Needed for weapon type used tracking +#include "../../shared/cstrike/cs_weapon_parse.h" + +#define REPORT_PLAYER_DAMAGE 0 + +//============================================================================= +// HPE_END +//============================================================================= + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#pragma optimize( "", off ) + +#pragma warning( disable : 4355 ) + +// Minimum interval between rate-limited commands that players can run. +#define CS_COMMAND_MAX_RATE 0.3 + +const float CycleLatchInterval = 0.2f; + +#define CS_PUSHAWAY_THINK_CONTEXT "CSPushawayThink" + +ConVar cs_ShowStateTransitions( "cs_ShowStateTransitions", "-2", FCVAR_CHEAT, "cs_ShowStateTransitions <ent index or -1 for all>. Show player state transitions." ); +ConVar sv_max_usercmd_future_ticks( "sv_max_usercmd_future_ticks", "8", 0, "Prevents clients from running usercmds too far in the future. Prevents speed hacks." ); +ConVar sv_motd_unload_on_dismissal( "sv_motd_unload_on_dismissal", "0", 0, "If enabled, the MOTD contents will be unloaded when the player closes the MOTD." ); +//============================================================================= +// HPE_BEGIN: +// [Forrest] Allow MVP to be turned off for a server +// [Forrest] Allow freezecam to be turned off for a server +// [Forrest] Allow win panel to be turned off for a server +//============================================================================= +static void SvNoMVPChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) +{ + ConVarRef var( pConVar ); + if ( var.IsValid() && var.GetBool() ) + { + // Clear the MVPs of all players when MVP is turned off. + for ( int i = 1; i <= MAX_PLAYERS; i++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + + if ( pPlayer ) + { + pPlayer->SetNumMVPs( 0 ); + } + } + } +} +ConVar sv_nomvp( "sv_nomvp", "0", 0, "Disable MVP awards.", SvNoMVPChangeCallback ); +ConVar sv_disablefreezecam( "sv_disablefreezecam", "0", FCVAR_REPLICATED, "Turn on/off freezecam on server" ); +ConVar sv_nowinpanel( "sv_nowinpanel", "0", FCVAR_REPLICATED, "Turn on/off win panel on server" ); +//============================================================================= +// HPE_END +//============================================================================= + + +// ConVar bot_mimic( "bot_mimic", "0", FCVAR_CHEAT ); +ConVar bot_freeze( "bot_freeze", "0", FCVAR_CHEAT ); +ConVar bot_crouch( "bot_crouch", "0", FCVAR_CHEAT ); +ConVar bot_mimic_yaw_offset( "bot_mimic_yaw_offset", "180", FCVAR_CHEAT ); + +ConVar sv_legacy_grenade_damage( "sv_legacy_grenade_damage", "0", FCVAR_REPLICATED, "Enable to replicate grenade damage behavior of the original Counter-Strike Source game." ); + +extern ConVar mp_autokick; +extern ConVar mp_holiday_nogifts; +extern ConVar sv_turbophysics; +//============================================================================= +// HPE_BEGIN: +// [menglish] Added in convars for freeze cam time length +//============================================================================= + +extern ConVar spec_freeze_time; +extern ConVar spec_freeze_traveltime; + +//============================================================================= +// HPE_END +//============================================================================= + +extern ConVar ammo_hegrenade_max; +extern ConVar ammo_flashbang_max; +extern ConVar ammo_smokegrenade_max; + + +#define THROWGRENADE_COUNTER_BITS 3 + + +EHANDLE g_pLastCTSpawn; +EHANDLE g_pLastTerroristSpawn; + +void TE_RadioIcon( IRecipientFilter& filter, float delay, CBaseEntity *pPlayer ); + + +// -------------------------------------------------------------------------------- // +// Classes +// -------------------------------------------------------------------------------- // + +class CPhysicsPlayerCallback : public IPhysicsPlayerControllerEvent +{ +public: + int ShouldMoveTo( IPhysicsObject *pObject, const Vector &position ) + { + CCSPlayer *pPlayer = (CCSPlayer *)pObject->GetGameData(); + if ( pPlayer ) + { + if ( pPlayer->TouchedPhysics() ) + { + return 0; + } + } + return 1; + } +}; + +static CPhysicsPlayerCallback playerCallback; + + +// -------------------------------------------------------------------------------- // +// Ragdoll entities. +// -------------------------------------------------------------------------------- // + +class CCSRagdoll : public CBaseAnimatingOverlay +{ +public: + DECLARE_CLASS( CCSRagdoll, CBaseAnimatingOverlay ); + DECLARE_SERVERCLASS(); + + // Transmit ragdolls to everyone. + virtual int UpdateTransmitState() + { + return SetTransmitState( FL_EDICT_ALWAYS ); + } + + void Init( void ) + { + SetSolid( SOLID_BBOX ); + SetMoveType( MOVETYPE_STEP ); + SetFriction( 1.0f ); + SetCollisionBounds( VEC_DUCK_HULL_MIN, VEC_DUCK_HULL_MAX ); + m_takedamage = DAMAGE_NO; + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + SetAbsOrigin( m_hPlayer->GetAbsOrigin() ); + SetAbsVelocity( m_hPlayer->GetAbsVelocity() ); + AddSolidFlags( FSOLID_NOT_SOLID ); + ChangeTeam( m_hPlayer->GetTeamNumber() ); + UseClientSideAnimation(); + } + +public: + // In case the client has the player entity, we transmit the player index. + // In case the client doesn't have it, we transmit the player's model index, origin, and angles + // so they can create a ragdoll in the right place. + CNetworkHandle( CBaseEntity, m_hPlayer ); // networked entity handle + CNetworkVector( m_vecRagdollVelocity ); + CNetworkVector( m_vecRagdollOrigin ); + CNetworkVar(int, m_iDeathPose ); + CNetworkVar(int, m_iDeathFrame ); +}; + +LINK_ENTITY_TO_CLASS( cs_ragdoll, CCSRagdoll ); + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CCSRagdoll, DT_CSRagdoll ) + SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_COORD|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ), + SendPropVector( SENDINFO(m_vecRagdollOrigin), -1, SPROP_COORD ), + SendPropEHandle( SENDINFO( m_hPlayer ) ), + SendPropModelIndex( SENDINFO( m_nModelIndex ) ), + SendPropInt ( SENDINFO(m_nForceBone), 8, 0 ), + SendPropVector ( SENDINFO(m_vecForce), -1, SPROP_NOSCALE ), + SendPropVector( SENDINFO( m_vecRagdollVelocity ) ), + SendPropInt( SENDINFO( m_iDeathPose ), ANIMATION_SEQUENCE_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iDeathFrame ), 5 ), + SendPropInt( SENDINFO(m_iTeamNum), TEAMNUM_NUM_BITS, 0), + SendPropInt( SENDINFO( m_bClientSideAnimation ), 1, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// -------------------------------------------------------------------------------- // +// Player animation event. Sent to the client when a player fires, jumps, reloads, etc.. +// -------------------------------------------------------------------------------- // + +class CTEPlayerAnimEvent : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEPlayerAnimEvent, CBaseTempEntity ); + DECLARE_SERVERCLASS(); + + CTEPlayerAnimEvent( const char *name ) : CBaseTempEntity( name ) + { + } + + CNetworkHandle( CBasePlayer, m_hPlayer ); + CNetworkVar( int, m_iEvent ); + CNetworkVar( int, m_nData ); +}; + +IMPLEMENT_SERVERCLASS_ST_NOBASE( CTEPlayerAnimEvent, DT_TEPlayerAnimEvent ) + SendPropEHandle( SENDINFO( m_hPlayer ) ), + SendPropInt( SENDINFO( m_iEvent ), Q_log2( PLAYERANIMEVENT_COUNT ) + 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_nData ), 32 ) +END_SEND_TABLE() + +static CTEPlayerAnimEvent g_TEPlayerAnimEvent( "PlayerAnimEvent" ); + +void TE_PlayerAnimEvent( CBasePlayer *pPlayer, PlayerAnimEvent_t event, int nData ) +{ + CPVSFilter filter( (const Vector&)pPlayer->EyePosition() ); + + g_TEPlayerAnimEvent.m_hPlayer = pPlayer; + g_TEPlayerAnimEvent.m_iEvent = event; + g_TEPlayerAnimEvent.m_nData = nData; + g_TEPlayerAnimEvent.Create( filter, 0 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Filters updates to a variable so that only non-local players see +// the changes. This is so we can send a low-res origin to non-local players +// while sending a hi-res one to the local player. +// Input : *pVarData - +// *pOut - +// objectID - +//----------------------------------------------------------------------------- + +void* SendProxy_SendNonLocalDataTable( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ) +{ + pRecipients->SetAllRecipients(); + pRecipients->ClearRecipient( objectID - 1 ); + return ( void * )pVarData; +} +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_SendNonLocalDataTable ); + + +// -------------------------------------------------------------------------------- // +// Tables. +// -------------------------------------------------------------------------------- // + +LINK_ENTITY_TO_CLASS( player, CCSPlayer ); +PRECACHE_REGISTER(player); + +BEGIN_SEND_TABLE_NOBASE( CCSPlayer, DT_CSLocalPlayerExclusive ) + SendPropFloat( SENDINFO( m_flStamina ), 14, 0, 0, 1400 ), + SendPropInt( SENDINFO( m_iDirection ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iShotsFired ), 8, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO( m_flVelocityModifier ), 8, 0, 0, 1 ), + + // send a hi-res origin to the local player for use in prediction + SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_NOSCALE|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ), + + //============================================================================= + // HPE_BEGIN: + // [tj]Set up the send table for per-client domination data + //============================================================================= + + SendPropArray3( SENDINFO_ARRAY3( m_bPlayerDominated ), SendPropBool( SENDINFO_ARRAY( m_bPlayerDominated ) ) ), + SendPropArray3( SENDINFO_ARRAY3( m_bPlayerDominatingMe ), SendPropBool( SENDINFO_ARRAY( m_bPlayerDominatingMe ) ) ), + + //============================================================================= + // HPE_END + //============================================================================= + +END_SEND_TABLE() + + +BEGIN_SEND_TABLE_NOBASE( CCSPlayer, DT_CSNonLocalPlayerExclusive ) + // send a lo-res origin to other players + SendPropVector (SENDINFO(m_vecOrigin), -1, SPROP_COORD|SPROP_CHANGES_OFTEN, 0.0f, HIGH_DEFAULT, SendProxy_Origin ), +END_SEND_TABLE() + + +IMPLEMENT_SERVERCLASS_ST( CCSPlayer, DT_CSPlayer ) + SendPropExclude( "DT_BaseAnimating", "m_flPoseParameter" ), + SendPropExclude( "DT_BaseAnimating", "m_flPlaybackRate" ), + SendPropExclude( "DT_BaseAnimating", "m_nSequence" ), + SendPropExclude( "DT_BaseAnimating", "m_nNewSequenceParity" ), + SendPropExclude( "DT_BaseAnimating", "m_nResetEventsParity" ), + SendPropExclude( "DT_BaseAnimating", "m_nMuzzleFlashParity" ), + SendPropExclude( "DT_BaseEntity", "m_angRotation" ), + SendPropExclude( "DT_BaseAnimatingOverlay", "overlay_vars" ), + + // cs_playeranimstate and clientside animation takes care of these on the client + SendPropExclude( "DT_ServerAnimationData" , "m_flCycle" ), + SendPropExclude( "DT_AnimTimeMustBeFirst" , "m_flAnimTime" ), + + // We need to send a hi-res origin to the local player to avoid prediction errors sliding along walls + SendPropExclude( "DT_BaseEntity", "m_vecOrigin" ), + + // Data that only gets sent to the local player. + SendPropDataTable( "cslocaldata", 0, &REFERENCE_SEND_TABLE(DT_CSLocalPlayerExclusive), SendProxy_SendLocalDataTable ), + SendPropDataTable( "csnonlocaldata", 0, &REFERENCE_SEND_TABLE(DT_CSNonLocalPlayerExclusive), SendProxy_SendNonLocalDataTable ), + + SendPropInt( SENDINFO( m_iThrowGrenadeCounter ), THROWGRENADE_COUNTER_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iAddonBits ), NUM_ADDON_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iPrimaryAddon ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iSecondaryAddon ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iPlayerState ), Q_log2( NUM_PLAYER_STATES )+1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iAccount ), 16, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_bInBombZone ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_bInBuyZone ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iClass ), Q_log2( CS_NUM_CLASSES )+1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_ArmorValue ), 8 ), + SendPropAngle( SENDINFO_VECTORELEM(m_angEyeAngles, 0), 11, SPROP_CHANGES_OFTEN ), + SendPropAngle( SENDINFO_VECTORELEM(m_angEyeAngles, 1), 11, SPROP_CHANGES_OFTEN ), + SendPropBool( SENDINFO( m_bHasDefuser ) ), + SendPropBool( SENDINFO( m_bNightVisionOn ) ), //send as int so we can use a RecvProxy on the client + SendPropBool( SENDINFO( m_bHasNightVision ) ), + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + //SendPropBool( SENDINFO( m_bPickedUpDefuser ) ), + //SendPropBool( SENDINFO( m_bDefusedWithPickedUpKit) ), + + //============================================================================= + // HPE_END + //============================================================================= + + SendPropBool( SENDINFO( m_bInHostageRescueZone ) ), + SendPropBool( SENDINFO( m_bIsDefusing ) ), + + SendPropBool( SENDINFO( m_bResumeZoom ) ), + SendPropInt( SENDINFO( m_iLastZoom ), 8, SPROP_UNSIGNED ), + +#ifdef CS_SHIELD_ENABLED + SendPropBool( SENDINFO( m_bHasShield ) ), + SendPropBool( SENDINFO( m_bShieldDrawn ) ), +#endif + SendPropBool( SENDINFO( m_bHasHelmet ) ), + SendPropFloat (SENDINFO(m_flFlashDuration), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flFlashMaxAlpha), 0, SPROP_NOSCALE ), + SendPropInt( SENDINFO( m_iProgressBarDuration ), 4, SPROP_UNSIGNED ), + SendPropFloat( SENDINFO( m_flProgressBarStartTime ), 0, SPROP_NOSCALE ), + SendPropEHandle( SENDINFO( m_hRagdoll ) ), + SendPropInt( SENDINFO( m_cycleLatch ), 4, SPROP_UNSIGNED ), + + +END_SEND_TABLE() + + +BEGIN_DATADESC( CCSPlayer ) + + DEFINE_INPUTFUNC( FIELD_VOID, "OnRescueZoneTouch", RescueZoneTouch ), + DEFINE_THINKFUNC( PushawayThink ) + +END_DATADESC() + + +// has to be included after above macros +#include "cs_bot.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + + +// -------------------------------------------------------------------------------- // + +void cc_CreatePredictionError_f( const CCommand &args ) +{ + float distance = 32; + + if ( args.ArgC() >= 2 ) + { + distance = atof(args[1]); + } + + CBaseEntity *pEnt = CBaseEntity::Instance( 1 ); + pEnt->SetAbsOrigin( pEnt->GetAbsOrigin() + Vector( distance, 0, 0 ) ); +} + +ConCommand cc_CreatePredictionError( "CreatePredictionError", cc_CreatePredictionError_f, "Create a prediction error", FCVAR_CHEAT ); + + +// -------------------------------------------------------------------------------- // +// CCSPlayer implementation. +// -------------------------------------------------------------------------------- // + +CCSPlayer::CCSPlayer() +{ + m_PlayerAnimState = CreatePlayerAnimState( this, this, LEGANIM_9WAY, true ); + + UseClientSideAnimation(); + + m_iLastWeaponFireUsercmd = 0; + m_iAddonBits = 0; + m_bEscaped = false; + m_iAccount = 0; + + m_bIsVIP = false; + m_iClass = (int)CS_CLASS_NONE; + m_angEyeAngles.Init(); + + SetViewOffset( VEC_VIEW_SCALED( this ) ); + + m_pCurStateInfo = NULL; // no state yet + m_iThrowGrenadeCounter = 0; + + m_lifeState = LIFE_DEAD; // Start "dead". + m_bInBombZone = false; + m_bInBuyZone = false; + m_bInHostageRescueZone = false; + m_flDeathTime = 0.0f; + m_iHostagesKilled = 0; + iRadioMenu = -1; + m_bTeamChanged = false; + m_iShotsFired = 0; + m_iDirection = 0; + m_receivesMoneyNextRound = true; + m_bIsBeingGivenItem = false; + m_isVIP = false; + + m_bJustKilledTeammate = false; + m_bPunishedForTK = false; + m_iTeamKills = 0; + m_flLastMovement = gpGlobals->curtime; + m_iNextTimeCheck = 0; + + m_szNewName[0] = 0; + m_szClanTag[0] = 0; + + for ( int i=0; i<NAME_CHANGE_HISTORY_SIZE; i++ ) + { + m_flNameChangeHistory[i] = -NAME_CHANGE_HISTORY_INTERVAL; + } + + m_iIgnoreGlobalChat = 0; + m_bIgnoreRadio = false; + + m_pHintMessageQueue = new CHintMessageQueue(this); + m_iDisplayHistoryBits = 0; + m_bShowHints = true; + m_flNextMouseoverUpdate = gpGlobals->curtime; + + m_lastDamageHealth = 0; + m_lastDamageArmor = 0; + + m_applyDeafnessTime = 0.0f; + + m_cycleLatch = 0; + m_cycleLatchTimer.Invalidate(); + + m_iShouldHaveCash = 0; + + m_lastNavArea = NULL; + + //============================================================================= + // HPE_BEGIN: + // [menglish] Init achievement variables + // [menglish] Init bullet collision variables + //============================================================================= + + m_NumEnemiesKilledThisRound = 0; + m_NumEnemiesAtRoundStart = 0; + m_KillingSpreeStartTime = -1; + m_firstKillBlindStartTime = -1; + m_killsWhileBlind = 0; + m_bSurvivedHeadshotDueToHelmet = false; + m_pGooseChaseDistractingPlayer = NULL; + m_gooseChaseStep = GC_NONE; + m_defuseDefenseStep = DD_NONE; + m_lastRoundResult = Invalid_Round_End_Reason; + m_bMadeFootstepNoise = false; + m_bombPickupTime = -1; + m_bMadePurchseThisRound = false; + m_roundsWonWithoutPurchase = 0; + m_iDeathFlags = 0; + m_lastFlashBangAttacker = NULL; + m_iMVPs = 0; + m_bKilledDefuser = false; + m_bKilledRescuer = false; + m_maxGrenadeKills = 0; + m_grenadeDamageTakenThisRound = 0; + + m_vLastHitLocationObjectSpace = Vector(0,0,0); + + m_wasNotKilledNaturally = false; + + //============================================================================= + // HPE_END + //============================================================================= +} + + +CCSPlayer::~CCSPlayer() +{ + delete m_pHintMessageQueue; + m_pHintMessageQueue = NULL; + + // delete the records of damage taken and given + ResetDamageCounters(); + m_PlayerAnimState->Release(); +} + + +CCSPlayer *CCSPlayer::CreatePlayer( const char *className, edict_t *ed ) +{ + CCSPlayer::s_PlayerEdict = ed; + return (CCSPlayer*)CreateEntityByName( className ); +} + + +void CCSPlayer::Precache() +{ + Vector mins( -13, -13, -10 ); + Vector maxs( 13, 13, 75 ); + + int i; + for ( i=0; i<CTPlayerModels.Count(); ++i ) + { + PrecacheModel( CTPlayerModels[i] ); + engine->ForceModelBounds( CTPlayerModels[i], mins, maxs ); + } + for ( i=0; i<TerroristPlayerModels.Count(); ++i ) + { + PrecacheModel( TerroristPlayerModels[i] ); + engine->ForceModelBounds( TerroristPlayerModels[i], mins, maxs ); + } + + // Sigh - have to force identical VMTs for the player models. I'm just going to hard-code these + // strings here, rather than have char***'s or the CUtlVector<CUtlVector<>> equivalent. + engine->ForceSimpleMaterial( "materials/models/player/ct_urban/ct_urban.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/ct_urban/ct_urban_glass.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/ct_sas/ct_sas.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/ct_sas/ct_sas_glass.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/ct_gsg9/ct_gsg9.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/ct_gign/ct_gign.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/ct_gign/ct_gign_glass.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/t_phoenix/t_phoenix.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/t_guerilla/t_guerilla.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/t_leet/t_leet.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/t_leet/t_leet_glass.vmt" ); + engine->ForceSimpleMaterial( "materials/models/player/t_arctic/t_arctic.vmt" ); + +#ifdef CS_SHIELD_ENABLED + PrecacheModel( SHIELD_VIEW_MODEL ); +#endif + + PrecacheScriptSound( "Player.DeathHeadShot" ); + PrecacheScriptSound( "Player.Death" ); + PrecacheScriptSound( "Player.DamageHelmet" ); + PrecacheScriptSound( "Player.DamageHeadShot" ); + PrecacheScriptSound( "Flesh.BulletImpact" ); + PrecacheScriptSound( "Player.DamageKevlar" ); + PrecacheScriptSound( "Player.PickupWeapon" ); + PrecacheScriptSound( "Player.NightVisionOff" ); + PrecacheScriptSound( "Player.NightVisionOn" ); + PrecacheScriptSound( "Player.FlashlightOn" ); + PrecacheScriptSound( "Player.FlashlightOff" ); + + // CS Bot sounds + PrecacheScriptSound( "Bot.StuckSound" ); + PrecacheScriptSound( "Bot.StuckStart" ); + PrecacheScriptSound( "Bot.FellOff" ); + + UTIL_PrecacheOther( "item_kevlar" ); + UTIL_PrecacheOther( "item_assaultsuit" ); + UTIL_PrecacheOther( "item_defuser" ); + + PrecacheModel ( "sprites/glow01.vmt" ); + PrecacheModel ( "models/items/cs_gift.mdl" ); + + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Allow pre-frame adjustments on the player +//----------------------------------------------------------------------------- +ConVar sv_runcmds( "sv_runcmds", "1" ); +void CCSPlayer::PlayerRunCommand( CUserCmd *ucmd, IMoveHelper *moveHelper ) +{ + VPROF( "CCSPlayer::PlayerRunCommand" ); + + if ( !sv_runcmds.GetInt() ) + return; + + // don't run commands in the future + if ( !IsEngineThreaded() && + ( ucmd->tick_count > (gpGlobals->tickcount + sv_max_usercmd_future_ticks.GetInt()) ) ) + { + DevMsg( "Client cmd out of sync (delta %i).\n", ucmd->tick_count - gpGlobals->tickcount ); + return; + } + + // If they use a negative bot_mimic value, then don't process their usercmds, but have + // bots process them instead (so they can stay still and have the bot move around). + CUserCmd tempCmd; + if ( -bot_mimic.GetInt() == entindex() ) + { + tempCmd = *ucmd; + ucmd = &tempCmd; + + ucmd->forwardmove = ucmd->sidemove = ucmd->upmove = 0; + ucmd->buttons = 0; + ucmd->impulse = 0; + } + + if ( IsBot() && bot_crouch.GetInt() ) + ucmd->buttons |= IN_DUCK; + + BaseClass::PlayerRunCommand( ucmd, moveHelper ); +} + + +bool CCSPlayer::RunMimicCommand( CUserCmd& cmd ) +{ + if ( !IsBot() ) + return false; + + int iMimic = abs( bot_mimic.GetInt() ); + if ( iMimic > gpGlobals->maxClients ) + return false; + + CBasePlayer *pPlayer = UTIL_PlayerByIndex( iMimic ); + if ( !pPlayer ) + return false; + + if ( !pPlayer->GetLastUserCommand() ) + return false; + + cmd = *pPlayer->GetLastUserCommand(); + cmd.viewangles[YAW] += bot_mimic_yaw_offset.GetFloat(); + + pl.fixangle = FIXANGLE_NONE; + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Simulates a single frame of movement for a player +//----------------------------------------------------------------------------- +void CCSPlayer::RunPlayerMove( const QAngle& viewangles, float forwardmove, float sidemove, float upmove, unsigned short buttons, byte impulse, float frametime ) +{ + CUserCmd cmd; + + // Store off the globals.. they're gonna get whacked + float flOldFrametime = gpGlobals->frametime; + float flOldCurtime = gpGlobals->curtime; + + float flTimeBase = gpGlobals->curtime + gpGlobals->frametime - frametime; + this->SetTimeBase( flTimeBase ); + + CUserCmd lastUserCmd = *GetLastUserCommand(); + Q_memset( &cmd, 0, sizeof( cmd ) ); + + if ( !RunMimicCommand( cmd ) ) + { + 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 ); + } + + MoveHelperServer()->SetHost( this ); + PlayerRunCommand( &cmd, MoveHelperServer() ); + + // save off the last good usercmd + if ( -bot_mimic.GetInt() == entindex() ) + { + CUserCmd lastCmd = *GetLastUserCommand(); + lastCmd.command_number = cmd.command_number; + lastCmd.tick_count = cmd.tick_count; + SetLastUserCommand( lastCmd ); + } + else + { + SetLastUserCommand( cmd ); + } + + // Clear out any fixangle that has been set + pl.fixangle = FIXANGLE_NONE; + + // Restore the globals.. + gpGlobals->frametime = flOldFrametime; + gpGlobals->curtime = flOldCurtime; + + MoveHelperServer()->SetHost( NULL ); +} + + +void CCSPlayer::InitialSpawn( void ) +{ + BaseClass::InitialSpawn(); + + // we're going to give the bots money here instead of FinishClientPutInServer() + // because of the bots' timing for purchasing weapons/items. + if ( IsBot() ) + { + m_iAccount = CSGameRules()->GetStartMoney(); + } + + if ( !engine->IsDedicatedServer() && TheNavMesh->IsOutOfDate() && this == UTIL_GetListenServerHost() ) + { + ClientPrint( this, HUD_PRINTCENTER, "The Navigation Mesh was built using a different version of this map." ); + } + + State_Enter( STATE_WELCOME ); + + //============================================================================= + // HPE_BEGIN: + // [tj] We reset the stats at the beginning of the map (including domination tracking) + //============================================================================= + + CCS_GameStats.ResetPlayerStats(this); + RemoveNemesisRelationships(); + + //============================================================================= + // HPE_END + //============================================================================= + +} + +void CCSPlayer::SetModelFromClass( void ) +{ + if ( GetTeamNumber() == TEAM_TERRORIST ) + { + int index = m_iClass - FIRST_T_CLASS; + if ( index < 0 || index >= TerroristPlayerModels.Count() ) + { + index = RandomInt( 0, TerroristPlayerModels.Count() - 1 ); + m_iClass = index + FIRST_T_CLASS; // clean up players who selected a higher class than we support yet + } + SetModel( TerroristPlayerModels[index] ); + } + else if ( GetTeamNumber() == TEAM_CT ) + { + int index = m_iClass - FIRST_CT_CLASS; + if ( index < 0 || index >= CTPlayerModels.Count() ) + { + index = RandomInt( 0, CTPlayerModels.Count() - 1 ); + m_iClass = index + FIRST_CT_CLASS; // clean up players who selected a higher class than we support yet + } + SetModel( CTPlayerModels[index] ); + } + else + { + SetModel( CTPlayerModels[0] ); + } +} + +void CCSPlayer::Spawn() +{ + m_RateLimitLastCommandTimes.Purge(); + + // Get rid of the progress bar... + SetProgressBarTime( 0 ); + + CreateViewModel( 1 ); + + // Set their player model. + SetModelFromClass(); + + BaseClass::Spawn(); + + //============================================================================= + // HPE_BEGIN: + // [pfreese] Clear the last known nav area (used to be done by CBasePlayer) + //============================================================================= + + m_lastNavArea = NULL; + + //============================================================================= + // HPE_END + //============================================================================= + + AddFlag(FL_ONGROUND); // set the player on the ground at the start of the round. + + // Override what CBasePlayer set for the view offset. + SetViewOffset( VEC_VIEW_SCALED( this ) ); + + // + // Our player movement speed is set once here. This will override the cl_xxxx + // cvars unless they are set to be lower than this. + // + SetMaxSpeed( CS_PLAYER_SPEED_RUN ); + + SetFOV( this, 0 ); + + m_bIsDefusing = false; + + //============================================================================= + // HPE_BEGIN + // [dwenger] Reset hostage-related variables + //============================================================================= + + m_bIsRescuing = false; + m_bInjuredAHostage = false; + m_iNumFollowers = 0; + + // [tj] Reset this flag if the player is not in observer mode (as happens when a player spawns late) + if (m_iPlayerState != STATE_OBSERVER_MODE) + { + m_wasNotKilledNaturally = false; + } + + //============================================================================= + // HPE_END + //============================================================================= + + m_iShotsFired = 0; + m_iDirection = 0; + + if ( m_pHintMessageQueue ) + { + m_pHintMessageQueue->Reset(); + } + m_iDisplayHistoryBits &= ~DHM_ROUND_CLEAR; + + // Special-case here. A bunch of things happen in CBasePlayer::Spawn(), and we really want the + // player states to control these things, so give whatever player state we're in a chance + // to reinitialize itself. + State_Transition( m_iPlayerState ); + + ClearFlashbangScreenFade(); + + m_flVelocityModifier = 1.0f; + + ResetStamina(); + + m_flLastRadarUpdateTime = 0.0f; + + m_iNumSpawns++; + + if ( !engine->IsDedicatedServer() && CSGameRules()->m_iTotalRoundsPlayed < 2 && TheNavMesh->IsOutOfDate() && this == UTIL_GetListenServerHost() ) + { + ClientPrint( this, HUD_PRINTCENTER, "The Navigation Mesh was built using a different version of this map." ); + } + + m_bTeamChanged = false; + m_iOldTeam = TEAM_UNASSIGNED; + + m_iRadioMessages = 60; + m_flRadioTime = gpGlobals->curtime; + + if ( m_hRagdoll ) + { + UTIL_Remove( m_hRagdoll ); + } + + m_hRagdoll = NULL; + + // did we change our name while we were dead? + if ( m_szNewName[0] != 0 ) + { + ChangeName( m_szNewName ); + m_szNewName[0] = 0; + } + + if ( m_bIsVIP ) + { + HintMessage( "#Hint_you_are_the_vip", true, true ); + } + + m_bIsInAutoBuy = false; + m_bIsInRebuy = false; + m_bAutoReload = false; + + SetContextThink( &CCSPlayer::PushawayThink, gpGlobals->curtime + PUSHAWAY_THINK_INTERVAL, CS_PUSHAWAY_THINK_CONTEXT ); + + if ( GetActiveWeapon() && !IsObserver() ) + { + GetActiveWeapon()->Deploy(); + m_flNextAttack = gpGlobals->curtime; // Allow reloads to finish, since we're playing the deploy anim instead. This mimics goldsrc behavior, anyway. + } + + m_applyDeafnessTime = 0.0f; + + m_cycleLatch = 0; + m_cycleLatchTimer.Start( RandomFloat( 0.0f, CycleLatchInterval ) ); + + StockPlayerAmmo(); + } + +void CCSPlayer::ShowViewPortPanel( const char * name, bool bShow, KeyValues *data ) +{ + if ( CSGameRules()->IsLogoMap() ) + return; + + if ( CommandLine()->FindParm("-makedevshots") ) + return; + + BaseClass::ShowViewPortPanel( name, bShow, data ); +} + +void CCSPlayer::ClearFlashbangScreenFade( void ) +{ + if( IsBlind() ) + { + color32 clr = { 0, 0, 0, 0 }; + UTIL_ScreenFade( this, clr, 0.01, 0.0, FFADE_OUT | FFADE_PURGE ); + + m_flFlashDuration = 0.0f; + m_flFlashMaxAlpha = 255.0f; + } + + // clear blind time (after screen fades are canceled) + m_blindUntilTime = 0.0f; + m_blindStartTime = 0.0f; +} + +void CCSPlayer::GiveDefaultItems() +{ + // Always give the player the knife. + CBaseCombatWeapon *pistol = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + if ( pistol ) + { + return; + } + + m_bUsingDefaultPistol = true; + + if ( GetTeamNumber() == TEAM_CT ) + { + GiveNamedItem( "weapon_knife" ); + GiveNamedItem( "weapon_usp" ); + GiveAmmo( 24, BULLET_PLAYER_45ACP ); + } + else if ( GetTeamNumber() == TEAM_TERRORIST ) + { + GiveNamedItem( "weapon_knife" ); + GiveNamedItem( "weapon_glock" ); + GiveAmmo( 40, BULLET_PLAYER_9MM ); + } +} + +void CCSPlayer::SetClanTag( const char *pTag ) +{ + if ( pTag ) + { + Q_strncpy( m_szClanTag, pTag, sizeof( m_szClanTag ) ); + } +} +void CCSPlayer::CreateRagdollEntity() +{ + // If we already have a ragdoll, don't make another one. + CCSRagdoll *pRagdoll = dynamic_cast< CCSRagdoll* >( m_hRagdoll.Get() ); + + if ( !pRagdoll ) + { + // create a new one + pRagdoll = dynamic_cast< CCSRagdoll* >( CreateEntityByName( "cs_ragdoll" ) ); + } + + if ( pRagdoll ) + { + pRagdoll->m_hPlayer = this; + pRagdoll->m_vecRagdollOrigin = GetAbsOrigin(); + pRagdoll->m_vecRagdollVelocity = GetAbsVelocity(); + pRagdoll->m_nModelIndex = m_nModelIndex; + pRagdoll->m_nForceBone = m_nForceBone; + pRagdoll->m_vecForce = m_vecTotalBulletForce; + pRagdoll->m_iDeathPose = m_iDeathPose; + pRagdoll->m_iDeathFrame = m_iDeathFrame; + pRagdoll->Init(); + } + + // ragdolls will be removed on round restart automatically + m_hRagdoll = pRagdoll; +} + +int CCSPlayer::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + // set damage type sustained + m_bitsDamageType |= info.GetDamageType(); + + if ( !CBaseCombatCharacter::OnTakeDamage_Alive( info ) ) + return 0; + + // don't apply damage forces in CS + + // fire global game event + + IGameEvent * event = gameeventmanager->CreateEvent( "player_hurt" ); + + if ( event ) + { + event->SetInt("userid", GetUserID() ); + event->SetInt("health", MAX(0, m_iHealth) ); + event->SetInt("armor", MAX(0, ArmorValue()) ); + + event->SetInt( "dmg_health", m_lastDamageHealth ); + event->SetInt( "dmg_armor", m_lastDamageArmor ); + + if ( info.GetDamageType() & DMG_BLAST ) + { + event->SetInt( "hitgroup", HITGROUP_GENERIC ); + } + else + { + event->SetInt( "hitgroup", m_LastHitGroup ); + } + + CBaseEntity * attacker = info.GetAttacker(); + const char *weaponName = ""; + + if ( attacker->IsPlayer() ) + { + CBasePlayer *player = ToBasePlayer( attacker ); + event->SetInt("attacker", player->GetUserID() ); // hurt by other player + + CBaseEntity *pInflictor = info.GetInflictor(); + if ( pInflictor ) + { + if ( pInflictor == player ) + { + // If the inflictor is the killer, then it must be their current weapon doing the damage + if ( player->GetActiveWeapon() ) + { + weaponName = player->GetActiveWeapon()->GetClassname(); + } + } + else + { + weaponName = STRING( pInflictor->m_iClassname ); // it's just that easy + } + } + } + else + { + event->SetInt("attacker", 0 ); // hurt by "world" + } + + if ( strncmp( weaponName, "weapon_", 7 ) == 0 ) + { + weaponName += 7; + } + else if( strncmp( weaponName, "hegrenade", 9 ) == 0 ) //"hegrenade_projectile" + { + //============================================================================= + // HPE_BEGIN: + // [tj] Handle grenade-surviving achievement + //============================================================================= + if (info.GetAttacker()->GetTeamNumber() != GetTeamNumber()) + { + m_grenadeDamageTakenThisRound += info.GetDamage(); + } + //============================================================================= + // HPE_END + //============================================================================= + + weaponName = "hegrenade"; + } + else if( strncmp( weaponName, "flashbang", 9 ) == 0 ) //"flashbang_projectile" + { + weaponName = "flashbang"; + } + else if( strncmp( weaponName, "smokegrenade", 12 ) == 0 ) //"smokegrenade_projectile" + { + weaponName = "smokegrenade"; + } + + event->SetString( "weapon", weaponName ); + event->SetInt( "priority", 5 ); + + gameeventmanager->FireEvent( event ); + } + + return 1; +} + +//============================================================================= +// HPE_BEGIN: +// [dwenger] Supports fun-fact +//============================================================================= + +// Returns the % of the enemies this player killed in the round +int CCSPlayer::GetPercentageOfEnemyTeamKilled() +{ + if ( m_NumEnemiesAtRoundStart > 0 ) + { + return (int)( ( (float)m_NumEnemiesKilledThisRound / (float)m_NumEnemiesAtRoundStart ) * 100.0f ); + } + + return 0; +} + +//============================================================================= +// HPE_END +//============================================================================= + +void CCSPlayer::Event_Killed( const CTakeDamageInfo &info ) +{ + //============================================================================= + // HPE_BEGIN: + // [pfreese] Process on-death achievements + //============================================================================= + + ProcessPlayerDeathAchievements(ToCSPlayer(info.GetAttacker()), this, info); + + //============================================================================= + // HPE_END + //============================================================================= + + SetArmorValue( 0 ); + + //============================================================================= + // HPE_BEGIN: + // [tj] Added a parameter so we know if it was death that caused the drop + // [menglish] Keep track of what the player has dropped for the freeze panel callouts + //============================================================================= + + CBaseEntity* pAttacker = info.GetAttacker(); + bool friendlyFire = pAttacker && pAttacker->GetTeamNumber() == GetTeamNumber(); + + //Only count the drop if it was not friendly fire + DropWeapons(true, !friendlyFire); + + //============================================================================= + // HPE_END + //============================================================================= + + + // Just in case the progress bar is on screen, kill it. + SetProgressBarTime( 0 ); + + m_bIsDefusing = false; + + m_bHasNightVision = false; + m_bNightVisionOn = false; + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + m_bPickedUpDefuser = false; + m_bDefusedWithPickedUpKit = false; + + //============================================================================= + // HPE_END + //============================================================================= + + m_bHasHelmet = false; + + m_flFlashDuration = 0.0f; + + FlashlightTurnOff(); + + // show killer in death cam mode + if( IsValidObserverTarget( info.GetAttacker() ) ) + { + SetObserverTarget( info.GetAttacker() ); + } + else + { + ResetObserverMode(); + } + + //update damage info with our accumulated physics force + CTakeDamageInfo subinfo = info; + + // HACK[pfreese]: scale impulse up for visual effect + const float kImpulseBonusScale = 2.0f; + subinfo.SetDamageForce( m_vecTotalBulletForce * kImpulseBonusScale); + + //Adrian: Select a death pose to extrapolate the ragdoll's velocity. + SelectDeathPose( info ); + + // See if there's a ragdoll magnet that should influence our force. + CRagdollMagnet *pMagnet = CRagdollMagnet::FindBestMagnet( this ); + if( pMagnet ) + { + m_vecTotalBulletForce += pMagnet->GetForceVector( this ); + } + + // Note: since we're dead, it won't draw us on the client, but we don't set EF_NODRAW + // because we still want to transmit to the clients in our PVS. + CreateRagdollEntity(); + + // Special code to drop holiday gifts for the holiday achievement + if ( ( mp_holiday_nogifts.GetBool() == false ) && UTIL_IsHolidayActive( 3 /*kHoliday_Christmas*/ ) ) + { + if ( RandomInt( 0, 100 ) < 20 ) + { + CHolidayGift::Create( WorldSpaceCenter(), GetAbsAngles(), EyeAngles(), GetAbsVelocity(), this ); + } + } + + State_Transition( STATE_DEATH_ANIM ); // Transition into the dying state. + BaseClass::Event_Killed( subinfo ); + + //============================================================================= + // HPE_BEGIN: + // [pfreese] If this kill ended the round, award the MVP to someone on the + // winning team. + // TODO - move this code somewhere else more MVP related + //============================================================================= + + bool roundWasAlreadyWon = (CSGameRules()->m_iRoundWinStatus != WINNER_NONE); + bool roundIsWonNow = CSGameRules()->CheckWinConditions(); + + if ( !roundWasAlreadyWon && roundIsWonNow ) + { + CCSPlayer* pMVP = NULL; + int maxKills = 0; + int maxDamage = 0; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer* pPlayer = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer ) + { + // only consider players on the winning team + if ( pPlayer->GetTeamNumber() != CSGameRules()->m_iRoundWinStatus ) + continue; + + int nKills = CCS_GameStats.FindPlayerStats( pPlayer ).statsCurrentRound[CSSTAT_KILLS]; + int nDamage = CCS_GameStats.FindPlayerStats( pPlayer ).statsCurrentRound[CSSTAT_DAMAGE]; + + if ( nKills > maxKills || ( nKills == maxKills && nDamage > maxDamage ) ) + { + pMVP = pPlayer; + maxKills = nKills; + maxDamage = nDamage; + } + } + } + + if ( pMVP ) + { + pMVP->IncrementNumMVPs( CSMVP_ELIMINATION ); + } + } + + //============================================================================= + // HPE_END + //============================================================================= + + OutputDamageGiven(); + OutputDamageTaken(); + ResetDamageCounters(); + + if ( m_bPunishedForTK ) + { + m_bPunishedForTK = false; + HintMessage( "#Hint_cannot_play_because_tk", true, true ); + } + + if ( !(m_iDisplayHistoryBits & DHF_SPEC_DUCK) ) + { + m_iDisplayHistoryBits |= DHF_SPEC_DUCK; + HintMessage( "#Spec_Duck", true, true ); + } +} + +//============================================================================= +// HPE_BEGIN: +// [menglish, tj] Update and check any one-off achievements based on the kill +//============================================================================= + +// Notify that I've killed some other entity. (called from Victim's Event_Killed). +void CCSPlayer::Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + BaseClass::Event_KilledOther(pVictim, info); +} + +//============================================================================= +// HPE_END +//============================================================================= + +void CCSPlayer::DeathSound( const CTakeDamageInfo &info ) +{ + if( m_LastHitGroup == HITGROUP_HEAD ) + { + EmitSound( "Player.DeathHeadShot" ); + } + else + { + EmitSound( "Player.Death" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSPlayer::InitVCollision( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity ) +{ + BaseClass::InitVCollision( vecAbsOrigin, vecAbsVelocity ); + + if ( sv_turbophysics.GetBool() ) + return; + + // Setup the HL2 specific callback. + GetPhysicsController()->SetEventHandler( &playerCallback ); +} + +void CCSPlayer::VPhysicsShadowUpdate( IPhysicsObject *pPhysics ) +{ + if ( !CanMove() ) + return; + + BaseClass::VPhysicsShadowUpdate( pPhysics ); +} + +bool CCSPlayer::HasShield() const +{ +#ifdef CS_SHIELD_ENABLED + return m_bHasShield; +#else + return false; +#endif +} + + +bool CCSPlayer::IsShieldDrawn() const +{ +#ifdef CS_SHIELD_ENABLED + return m_bShieldDrawn; +#else + return false; +#endif +} + + +void CCSPlayer::CheatImpulseCommands( int iImpulse ) +{ + switch( iImpulse ) + { + case 101: + { + if( sv_cheats->GetBool() ) + { + extern int gEvilImpulse101; + gEvilImpulse101 = true; + + AddAccount( 16000 ); + + GiveAmmo( 250, BULLET_PLAYER_50AE ); + GiveAmmo( 250, BULLET_PLAYER_762MM ); + GiveAmmo( 250, BULLET_PLAYER_338MAG ); + GiveAmmo( 250, BULLET_PLAYER_556MM ); + GiveAmmo( 250, BULLET_PLAYER_556MM_BOX ); + GiveAmmo( 250, BULLET_PLAYER_9MM ); + GiveAmmo( 250, BULLET_PLAYER_BUCKSHOT ); + GiveAmmo( 250, BULLET_PLAYER_45ACP ); + GiveAmmo( 250, BULLET_PLAYER_357SIG ); + GiveAmmo( 250, BULLET_PLAYER_57MM ); + + gEvilImpulse101 = false; + } + } + break; + + default: + { + BaseClass::CheatImpulseCommands( iImpulse ); + } + } +} + +void CCSPlayer::SetupVisibility( CBaseEntity *pViewEntity, unsigned char *pvs, int pvssize ) +{ + BaseClass::SetupVisibility( pViewEntity, pvs, pvssize ); + + int area = pViewEntity ? pViewEntity->NetworkProp()->AreaNum() : NetworkProp()->AreaNum(); + PointCameraSetupVisibility( this, area, pvs, pvssize ); +} + +void CCSPlayer::UpdateAddonBits() +{ + int iNewBits = 0; + + int nFlashbang = GetAmmoCount( GetAmmoDef()->Index( AMMO_TYPE_FLASHBANG ) ); + if ( dynamic_cast< CFlashbang* >( GetActiveWeapon() ) ) + { + --nFlashbang; + } + + if ( nFlashbang >= 1 ) + iNewBits |= ADDON_FLASHBANG_1; + + if ( nFlashbang >= 2 ) + iNewBits |= ADDON_FLASHBANG_2; + + if ( GetAmmoCount( GetAmmoDef()->Index( AMMO_TYPE_HEGRENADE ) ) && + !dynamic_cast< CHEGrenade* >( GetActiveWeapon() ) ) + { + iNewBits |= ADDON_HE_GRENADE; + } + + if ( GetAmmoCount( GetAmmoDef()->Index( AMMO_TYPE_SMOKEGRENADE ) ) && + !dynamic_cast< CSmokeGrenade* >( GetActiveWeapon() ) ) + { + iNewBits |= ADDON_SMOKE_GRENADE; + } + + if ( HasC4() && !dynamic_cast< CC4* >( GetActiveWeapon() ) ) + iNewBits |= ADDON_C4; + + if ( HasDefuser() ) + iNewBits |= ADDON_DEFUSEKIT; + + CWeaponCSBase *weapon = dynamic_cast< CWeaponCSBase * >(Weapon_GetSlot( WEAPON_SLOT_RIFLE )); + if ( weapon && weapon != GetActiveWeapon() ) + { + iNewBits |= ADDON_PRIMARY; + m_iPrimaryAddon = weapon->GetWeaponID(); + } + else + { + m_iPrimaryAddon = WEAPON_NONE; + } + + weapon = dynamic_cast< CWeaponCSBase * >(Weapon_GetSlot( WEAPON_SLOT_PISTOL )); + if ( weapon && weapon != GetActiveWeapon() ) + { + iNewBits |= ADDON_PISTOL; + if ( weapon->GetWeaponID() == WEAPON_ELITE ) + { + iNewBits |= ADDON_PISTOL2; + } + m_iSecondaryAddon = weapon->GetWeaponID(); + } + else if ( weapon && weapon->GetWeaponID() == WEAPON_ELITE ) + { + // The active weapon is weapon_elite. Set ADDON_PISTOL2 without ADDON_PISTOL, so we know + // to display the empty holster. + iNewBits |= ADDON_PISTOL2; + m_iSecondaryAddon = weapon->GetWeaponID(); + } + else + { + m_iSecondaryAddon = WEAPON_NONE; + } + + m_iAddonBits = iNewBits; +} + +void CCSPlayer::UpdateRadar() +{ + // update once a second + if ( (m_flLastRadarUpdateTime + 1.0) > gpGlobals->curtime ) + return; + + m_flLastRadarUpdateTime = gpGlobals->curtime; + + // update positions of all players outside of my PVS + CBitVec< ABSOLUTE_PLAYER_LIMIT > playerbits; + engine->Message_DetermineMulticastRecipients( false, EyePosition(), playerbits ); + + CSingleUserRecipientFilter user( this ); + UserMessageBegin( user, "UpdateRadar" ); + + for ( int i=0; i < MAX_PLAYERS; i++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( i+1 ) ); + + if ( !pPlayer ) + continue; // nothing there + + bool bSameTeam = pPlayer->GetTeamNumber() == GetTeamNumber(); + + if ( playerbits.Get(i) && bSameTeam == true ) + continue; // this player is in my PVS and not in my team, don't update radar pos + + if ( pPlayer == this ) + continue; + + if ( !pPlayer->IsAlive() || pPlayer->IsObserver() || !pPlayer->IsConnected() ) + continue; // don't update specattors or dead players + + WRITE_BYTE( i+1 ); // player index as entity + WRITE_SBITLONG( pPlayer->GetAbsOrigin().x/4, COORD_INTEGER_BITS-1 ); + WRITE_SBITLONG( pPlayer->GetAbsOrigin().y/4, COORD_INTEGER_BITS-1 ); + WRITE_SBITLONG( pPlayer->GetAbsOrigin().z/4, COORD_INTEGER_BITS-1 ); + WRITE_SBITLONG( AngleNormalize( pPlayer->GetAbsAngles().y ), 9 ); + } + + WRITE_BYTE( 0 ); // end marker + + MessageEnd(); +} + +void CCSPlayer::UpdateMouseoverHints() +{ + if ( IsBlind() || IsObserver() ) + return; + + Vector forward, up; + EyeVectors( &forward, NULL, &up ); + + trace_t tr; + // Search for objects in a sphere (tests for entities that are not solid, yet still useable) + Vector searchStart = EyePosition(); + Vector searchEnd = searchStart + forward * 2048; + + int useableContents = MASK_NPCSOLID_BRUSHONLY | MASK_VISIBLE_AND_NPCS; + + UTIL_TraceLine( searchStart, searchEnd, useableContents, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction != 1.0f ) + { + if (tr.DidHitNonWorldEntity() && tr.m_pEnt) + { + CBaseEntity *pObject = tr.m_pEnt; + switch ( pObject->Classify() ) + { + case CLASS_PLAYER: + { + const float grenadeBloat = 1.2f; // Be conservative in estimating what a player can distinguish + if ( !TheBots->IsLineBlockedBySmoke( EyePosition(), pObject->EyePosition(), grenadeBloat ) ) + { + if ( g_pGameRules->PlayerRelationship( this, pObject ) == GR_TEAMMATE ) + { + if ( !(m_iDisplayHistoryBits & DHF_FRIEND_SEEN) ) + { + m_iDisplayHistoryBits |= DHF_FRIEND_SEEN; + HintMessage( "#Hint_spotted_a_friend", true ); + } + } + else + { + if ( !(m_iDisplayHistoryBits & DHF_ENEMY_SEEN) ) + { + m_iDisplayHistoryBits |= DHF_ENEMY_SEEN; + HintMessage( "#Hint_spotted_an_enemy", true ); + } + } + } + } + break; + case CLASS_PLAYER_ALLY: + switch ( GetTeamNumber() ) + { + case TEAM_CT: + if ( !(m_iDisplayHistoryBits & DHF_HOSTAGE_SEEN_FAR) && tr.fraction > 0.1f ) + { + m_iDisplayHistoryBits |= DHF_HOSTAGE_SEEN_FAR; + HintMessage( "#Hint_rescue_the_hostages", true ); + } + else if ( !(m_iDisplayHistoryBits & DHF_HOSTAGE_SEEN_NEAR) && tr.fraction <= 0.1f ) + { + m_iDisplayHistoryBits |= DHF_HOSTAGE_SEEN_FAR; + m_iDisplayHistoryBits |= DHF_HOSTAGE_SEEN_NEAR; + HintMessage( "#Hint_press_use_so_hostage_will_follow", false ); + } + break; + case TEAM_TERRORIST: + if ( !(m_iDisplayHistoryBits & DHF_HOSTAGE_SEEN_FAR) ) + { + m_iDisplayHistoryBits |= DHF_HOSTAGE_SEEN_FAR; + HintMessage( "#Hint_prevent_hostage_rescue", true ); + } + break; + } + break; + } + } + } +} + +void CCSPlayer::PostThink() +{ + BaseClass::PostThink(); + + UpdateAddonBits(); + + UpdateRadar(); + + if ( !(m_iDisplayHistoryBits & DHF_ROUND_STARTED) && CanPlayerBuy(false) ) + { + HintMessage( "#Hint_press_buy_to_purchase", false ); + m_iDisplayHistoryBits |= DHF_ROUND_STARTED; + } + if ( m_flNextMouseoverUpdate < gpGlobals->curtime ) + { + m_flNextMouseoverUpdate = gpGlobals->curtime + 0.2f; + if ( m_bShowHints ) + { + UpdateMouseoverHints(); + } + } + if ( GetActiveWeapon() && !(m_iDisplayHistoryBits & DHF_AMMO_EXHAUSTED) ) + { + CBaseCombatWeapon *pWeapon = GetActiveWeapon(); + if ( !pWeapon->HasAnyAmmo() && !(pWeapon->GetWpnData().iFlags & ITEM_FLAG_EXHAUSTIBLE) ) + { + m_iDisplayHistoryBits |= DHF_AMMO_EXHAUSTED; + HintMessage( "#Hint_out_of_ammo", false ); + } + } + + QAngle angles = GetLocalAngles(); + angles[PITCH] = 0; + SetLocalAngles( angles ); + + // Store the eye angles pitch so the client can compute its animation state correctly. + m_angEyeAngles = EyeAngles(); + + m_PlayerAnimState->Update( m_angEyeAngles[YAW], m_angEyeAngles[PITCH] ); + + // check if we need to apply a deafness DSP effect. + if ((m_applyDeafnessTime != 0.0f) && (m_applyDeafnessTime <= gpGlobals->curtime)) + { + ApplyDeafnessEffect(); + } + + if ( IsPlayerUnderwater() && GetWaterLevel() < 3 ) + { + StopSound( "Player.AmbientUnderWater" ); + SetPlayerUnderwater( false ); + } + + if( IsAlive() && m_cycleLatchTimer.IsElapsed() ) + { + m_cycleLatchTimer.Start( CycleLatchInterval ); + + // Cycle is a float from 0 to 1. We don't need to transmit a whole float for that. Compress it in to a small fixed point + m_cycleLatch.GetForModify() = 16 * GetCycle();// 4 point fixed + } +} + + +void CCSPlayer::PushawayThink() +{ + // Push physics props out of our way. + PerformObstaclePushaway( this ); + SetNextThink( gpGlobals->curtime + PUSHAWAY_THINK_INTERVAL, CS_PUSHAWAY_THINK_CONTEXT ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns whether or not we can switch to the given weapon. +// Input : pWeapon - +//----------------------------------------------------------------------------- +bool CCSPlayer::Weapon_CanSwitchTo( CBaseCombatWeapon *pWeapon ) +{ + if ( !pWeapon->CanDeploy() ) + return false; + + if ( GetActiveWeapon() ) + { + if ( !GetActiveWeapon()->CanHolster() ) + return false; + } + + return true; +} + +bool CCSPlayer::ShouldDoLargeFlinch( int nHitGroup, CBaseEntity *pAttacker ) +{ + if ( FBitSet( GetFlags(), FL_DUCKING ) ) + return FALSE; + + if ( nHitGroup == HITGROUP_LEFTLEG ) + return FALSE; + + if ( nHitGroup == HITGROUP_RIGHTLEG ) + return FALSE; + + CCSPlayer *pPlayer = ToCSPlayer( pAttacker ); + + if ( pPlayer == NULL || !pPlayer->IsPlayer() ) + pPlayer = NULL; + + if ( pPlayer ) + { + CWeaponCSBase *pWeapon = pPlayer->GetActiveCSWeapon(); + + if ( pWeapon ) + { + if ( pWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_RIFLE || + pWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_SHOTGUN || + pWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_SNIPER_RIFLE ) + return true; + } + else + return false; + } + + return false; +} + +bool CCSPlayer::IsArmored( int nHitGroup ) +{ + bool bApplyArmor = false; + + if ( ArmorValue() > 0 ) + { + switch ( nHitGroup ) + { + case HITGROUP_GENERIC: + case HITGROUP_CHEST: + case HITGROUP_STOMACH: + case HITGROUP_LEFTARM: + case HITGROUP_RIGHTARM: + bApplyArmor = true; + break; + case HITGROUP_HEAD: + if ( m_bHasHelmet ) + { + bApplyArmor = true; + } + break; + default: + break; + } + } + + return bApplyArmor; +} + +void CCSPlayer::Pain( bool bHasArmour ) +{ + switch (m_LastHitGroup) + { + case HITGROUP_HEAD: + if (m_bHasHelmet) // He's wearing a helmet + { + EmitSound( "Player.DamageHelmet" ); + } + else // He's not wearing a helmet + { + EmitSound( "Player.DamageHeadShot" ); + } + break; + default: + if ( bHasArmour == false ) + { + EmitSound( "Flesh.BulletImpact" ); + } + else + { + EmitSound( "Player.DamageKevlar" ); + } + break; + } +} + +int CCSPlayer::OnTakeDamage( const CTakeDamageInfo &inputInfo ) +{ + CTakeDamageInfo info = inputInfo; + + CBaseEntity *pInflictor = info.GetInflictor(); + + if ( !pInflictor ) + return 0; + + if ( GetMoveType() == MOVETYPE_NOCLIP || GetMoveType() == MOVETYPE_OBSERVER ) + return 0; + + const float flArmorBonus = 0.5f; + float flArmorRatio = 0.5f; + float flDamage = info.GetDamage(); + + bool bFriendlyFire = CSGameRules()->IsFriendlyFireOn(); + + //============================================================================= + // HPE_BEGIN: + // [tj] Added properties for goose chase achievement + //============================================================================= + + CSGameRules()->PlayerTookDamage(this, inputInfo); + + //Check "Goose Chase" achievement + CCSPlayer *pAttacker = ToCSPlayer(info.GetAttacker()); + if (m_bIsDefusing && m_gooseChaseStep == GC_NONE && pAttacker && pAttacker->GetTeamNumber() != GetTeamNumber() ) + { + + //Count enemies + int livingEnemies = 0; + CTeam *pAttackerTeam = GetGlobalTeam( pAttacker->GetTeamNumber() ); + for ( int iPlayer=0; iPlayer < pAttackerTeam->GetNumPlayers(); iPlayer++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( pAttackerTeam->GetPlayer( iPlayer ) ); + Assert( pPlayer ); + if ( !pPlayer ) + continue; + + Assert( pPlayer->GetTeamNumber() == pAttackerTeam->GetTeamNumber() ); + + if ( pPlayer->m_lifeState == LIFE_ALIVE ) + { + livingEnemies++; + } + } + + //Must be last enemy alive; + if (livingEnemies == 1) + { + m_gooseChaseStep = GC_SHOT_DURING_DEFUSE; + m_pGooseChaseDistractingPlayer = pAttacker; + } + } + + //============================================================================= + // HPE_END + //============================================================================= + + + // warn about team attacks + if ( bFriendlyFire && pInflictor->GetTeamNumber() == GetTeamNumber() && pInflictor != this && info.GetAttacker() != this ) + { + CCSPlayer *pCSAttacker = ToCSPlayer( pInflictor ); + if ( !pCSAttacker ) + pCSAttacker = ToCSPlayer( info.GetAttacker() ); + + if ( pCSAttacker ) + { + if ( !(pCSAttacker->m_iDisplayHistoryBits & DHF_FRIEND_INJURED) ) + { + pCSAttacker->HintMessage( "#Hint_try_not_to_injure_teammates", false ); + pCSAttacker->m_iDisplayHistoryBits |= DHF_FRIEND_INJURED; + } + + if ( (pCSAttacker->m_flLastAttackedTeammate + 0.6f) < gpGlobals->curtime ) + { + pCSAttacker->m_flLastAttackedTeammate = gpGlobals->curtime; + + // tell the rest of this player's team + Msg( "%s attacked a teammate\n", pCSAttacker->GetPlayerName() ); + for ( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer && pPlayer->GetTeamNumber() == GetTeamNumber() ) + { + ClientPrint( pPlayer, HUD_PRINTTALK, "#Game_teammate_attack", pCSAttacker->GetPlayerName() ); + } + } + } + } + } + + if ( bFriendlyFire || + pInflictor->GetTeamNumber() != GetTeamNumber() || + pInflictor == this || + info.GetAttacker() == this ) + { + if ( bFriendlyFire && (info.GetDamageType() & DMG_BLAST) == 0 ) + { + if ( pInflictor->GetTeamNumber() == GetTeamNumber() ) + { + flDamage *= 0.35; // bullets hurt teammates less + } + } + + if ( ShouldDoLargeFlinch( m_LastHitGroup, info.GetAttacker() ) ) + { + if ( GetAbsVelocity().Length() < 300 ) + { + m_flVelocityModifier = 0.65; + } + } + else + { + m_flVelocityModifier = 0.5; + } + +//============================================================================= +// HPE_BEGIN: +//============================================================================= + // [menglish] Store whether or not the knife did this damage as knives do bullet damage, + // so we need to specifically check the weapon here + bool bKnifeDamage = false; + CCSPlayer *pPlayer = ToCSPlayer( info.GetAttacker() ); + + if ( pPlayer ) + { + + // [paquin. forest] if this is blast damage, and we haven't opted out with a cvar, + // we need to get the armor ratio out of the inflictor + + if ( (info.GetDamageType() & DMG_BLAST) && !sv_legacy_grenade_damage.GetBool() ) + { + + // [paquin] if we know this is a grenade, use it's armor ratio, otherwise + // use the he grenade armor ratio + + CBaseCSGrenadeProjectile *pGrenade = dynamic_cast< CBaseCSGrenadeProjectile * >( pInflictor ); + CCSWeaponInfo* pWeaponInfo; + + if ( pGrenade && pGrenade->m_pWeaponInfo ) + { + pWeaponInfo = pGrenade->m_pWeaponInfo; + } + else + { + pWeaponInfo = GetWeaponInfo( WEAPON_HEGRENADE ); + } + + if ( pWeaponInfo ) + { + flArmorRatio *= pWeaponInfo->m_flArmorRatio; + } + } + else + { + CWeaponCSBase *pWeapon = pPlayer->GetActiveCSWeapon(); + + if ( pWeapon ) + { + flArmorRatio *= pWeapon->GetCSWpnData().m_flArmorRatio; + //Knives do bullet damage, so we need to specifically check the weapon here + bKnifeDamage = pWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_KNIFE; + + if ( info.GetDamageType() & DMG_BULLET && !bKnifeDamage && pPlayer->GetTeam() != GetTeam() ) + { + CCS_GameStats.Event_ShotHit( pPlayer, info ); + } + } + } + } + +//============================================================================= +// HPE_END +//============================================================================= + + // keep track of amount of damage last sustained + m_lastDamageAmount = flDamage; + + // Deal with Armour + if ( ArmorValue() && !( info.GetDamageType() & (DMG_FALL | DMG_DROWN)) && IsArmored( m_LastHitGroup ) ) + { + float fDamageToHealth = flDamage * flArmorRatio; + float fDamageToArmor = (flDamage - fDamageToHealth) * flArmorBonus; + + int armorValue = ArmorValue(); + + // Does this use more armor than we have? + if (fDamageToArmor > armorValue ) + { + fDamageToHealth = flDamage - armorValue / flArmorBonus; + fDamageToArmor = armorValue; + armorValue = 0; + } + else + { + if ( fDamageToArmor < 0 ) + fDamageToArmor = 1; + + armorValue -= fDamageToArmor; + } + m_lastDamageArmor = (int)fDamageToArmor; + SetArmorValue(armorValue); + + //============================================================================= + // HPE_BEGIN: + // [tj] Handle headshot-surviving achievement + //============================================================================= + if (m_LastHitGroup == HITGROUP_HEAD && flDamage > m_iHealth && fDamageToHealth < m_iHealth) + { + m_bSurvivedHeadshotDueToHelmet = true; + } + //============================================================================= + // HPE_END + //============================================================================= + + flDamage = fDamageToHealth; + + info.SetDamage( flDamage ); + + if ( ArmorValue() <= 0.0) + m_bHasHelmet = false; + + if( !(info.GetDamageType() & DMG_FALL) ) + Pain( true /*has armor*/ ); + } + else + { + m_lastDamageArmor = 0; + if( !(info.GetDamageType() & DMG_FALL) ) + Pain( false /*no armor*/ ); + } + + // round damage to integer + m_lastDamageHealth = (int)flDamage; + info.SetDamage( m_lastDamageHealth ); + + if ( info.GetDamage() <= 0 ) + return 0; + + CSingleUserRecipientFilter user( this ); + user.MakeReliable(); + UserMessageBegin( user, "Damage" ); + WRITE_BYTE( (int)info.GetDamage() ); + WRITE_VEC3COORD( info.GetInflictor()->WorldSpaceCenter() ); +//============================================================================= +// HPE_BEGIN: +// [menglish] Send the info about where the player was hit +//============================================================================= + if ( !( info.GetDamageType() & DMG_BULLET ) || bKnifeDamage ) + { + WRITE_LONG( -1 ); + } + else + { + WRITE_LONG( m_LastHitBox ); + } + WRITE_VEC3COORD( m_vLastHitLocationObjectSpace ); + +//============================================================================= +// HPE_END +//============================================================================= + MessageEnd(); + + // Do special explosion damage effect + if ( info.GetDamageType() & DMG_BLAST ) + { + OnDamagedByExplosion( info ); + } + +//============================================================================= +// HPE_BEGIN: +// [menglish] Achievement award for kill stealing i.e. killing an enemy who was very damaged from other players +// [Forrest] Moved this check before RecordDamageTaken so that the damage currently being dealt by this player +// won't disqualify them from getting the achievement. +//============================================================================= + + if(m_iHealth - info.GetDamage() <= 0 && m_iHealth <= AchievementConsts::KillLowDamage_MaxHealthLeft) + { + bool onlyDamage = true; + CCSPlayer *pAttacker = ToCSPlayer(info.GetAttacker()); + if(pAttacker && pAttacker->GetTeamNumber() != GetTeamNumber()) + { + //Verify that the killer has not done damage to this player beforehand + FOR_EACH_LL( m_DamageTakenList, i ) + { + if( Q_strncmp( pAttacker->GetPlayerName(), m_DamageTakenList[i]->GetPlayerName(), MAX_PLAYER_NAME_LENGTH ) == 0 ) + { + onlyDamage = false; + break; + } + } + if(onlyDamage) + { + pAttacker->AwardAchievement(CSKillLowDamage); + } + } + } + +//============================================================================= +// HPE_END +//============================================================================= + +#if REPORT_PLAYER_DAMAGE + // damage output spew + char dmgtype[64]; + CTakeDamageInfo::DebugGetDamageTypeString( info.GetDamageType(), dmgtype, sizeof(dmgtype) ); + + if ( info.GetDamageType() & DMG_HEADSHOT ) + Q_strncat(dmgtype, "HEADSHOT", sizeof(dmgtype)); + + char outputString[256]; + Q_snprintf( outputString, sizeof(outputString), "%f: Player %s incoming %f damage from %s, type %s; applied %d health and %d armor\n", + gpGlobals->curtime, GetPlayerName(), + inputInfo.GetDamage(), info.GetInflictor()->GetDebugName(), dmgtype, + m_lastDamageHealth, m_lastDamageArmor); + + Msg(outputString); +#endif + + + if ( pPlayer ) + { + // Record for the shooter + pPlayer->RecordDamageGiven( GetPlayerName(), info.GetDamage() ); + + // And for the victim + RecordDamageTaken( pPlayer->GetPlayerName(), info.GetDamage() ); + } + else + { + RecordDamageTaken( "world", info.GetDamage() ); + } + + m_vecTotalBulletForce += info.GetDamageForce(); + + gamestats->Event_PlayerDamage( this, info ); + + return CBaseCombatCharacter::OnTakeDamage( info ); + } + else + { + return 0; + } +} + + +//MIKETODO: this probably should let the shield model catch the trace attacks. +bool CCSPlayer::IsHittingShield( const Vector &vecDirection, trace_t *ptr ) +{ + if ( HasShield() == false ) + return false; + + if ( IsShieldDrawn() == false ) + return false; + + float flDot; + Vector vForward; + Vector2D vec2LOS = vecDirection.AsVector2D(); + AngleVectors( GetLocalAngles(), &vForward ); + + Vector2DNormalize( vForward.AsVector2D() ); + Vector2DNormalize( vec2LOS ); + + flDot = DotProduct2D ( vec2LOS , vForward.AsVector2D() ); + + if ( flDot < -0.87f ) + return true; + + return false; +} + + +void CCSPlayer::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + bool bShouldBleed = true; + bool bShouldSpark = false; + bool bHitShield = IsHittingShield( vecDir, ptr ); + + CBasePlayer *pAttacker = (CBasePlayer*)ToBasePlayer( info.GetAttacker() ); + + // show blood for firendly fire only if FF is on + if ( pAttacker && ( GetTeamNumber() == pAttacker->GetTeamNumber() ) ) + bShouldBleed = CSGameRules()->IsFriendlyFireOn(); + + if ( m_takedamage != DAMAGE_YES ) + return; + + m_LastHitGroup = ptr->hitgroup; +//============================================================================= +// HPE_BEGIN: +// [menglish] Used when calculating the position this player was hit at in the bone space +//============================================================================= + m_LastHitBox = ptr->hitbox; +//============================================================================= +// HPE_END +//============================================================================= + + m_nForceBone = ptr->physicsbone; //Save this bone for ragdoll + + float flDamage = info.GetDamage(); + + bool bHeadShot = false; + + if ( bHitShield ) + { + flDamage = 0; + bShouldBleed = false; + bShouldSpark = true; + } + else if( info.GetDamageType() & DMG_BLAST ) + { + if ( ArmorValue() > 0 ) + bShouldBleed = false; + + if ( bShouldBleed == true ) + { + // punch view if we have no armor + QAngle punchAngle = GetPunchAngle(); + punchAngle.x = flDamage * -0.1; + + if ( punchAngle.x < -4 ) + punchAngle.x = -4; + + SetPunchAngle( punchAngle ); + } + } + else + { +//============================================================================= +// HPE_BEGIN: +// [menglish] Calculate the position this player was hit at in the bone space +//============================================================================= + + matrix3x4_t boneTransformToWorld, boneTransformToObject; + GetBoneTransform(GetHitboxBone(ptr->hitbox), boneTransformToWorld); + MatrixInvert(boneTransformToWorld, boneTransformToObject); + VectorTransform(ptr->endpos, boneTransformToObject, m_vLastHitLocationObjectSpace); + +//============================================================================= +// HPE_END +//============================================================================= + + switch ( ptr->hitgroup ) + { + case HITGROUP_GENERIC: + break; + + case HITGROUP_HEAD: + + if ( m_bHasHelmet ) + { +// bShouldBleed = false; + bShouldSpark = true; + } + + flDamage *= 4; + + if ( !m_bHasHelmet ) + { + QAngle punchAngle = GetPunchAngle(); + punchAngle.x = flDamage * -0.5; + + if ( punchAngle.x < -12 ) + punchAngle.x = -12; + + punchAngle.z = flDamage * random->RandomFloat(-1,1); + + if ( punchAngle.z < -9 ) + punchAngle.z = -9; + + else if ( punchAngle.z > 9 ) + punchAngle.z = 9; + + SetPunchAngle( punchAngle ); + } + + bHeadShot = true; + + break; + + case HITGROUP_CHEST: + + flDamage *= 1.0; + + if ( ArmorValue() <= 0 ) + { + QAngle punchAngle = GetPunchAngle(); + punchAngle.x = flDamage * -0.1; + + if ( punchAngle.x < -4 ) + punchAngle.x = -4; + + SetPunchAngle( punchAngle ); + } + break; + + case HITGROUP_STOMACH: + + flDamage *= 1.25; + + if ( ArmorValue() <= 0 ) + { + QAngle punchAngle = GetPunchAngle(); + punchAngle.x = flDamage * -0.1; + + if ( punchAngle.x < -4 ) + punchAngle.x = -4; + + SetPunchAngle( punchAngle ); + } + + break; + + case HITGROUP_LEFTARM: + case HITGROUP_RIGHTARM: + flDamage *= 1.0; + break; + + case HITGROUP_LEFTLEG: + case HITGROUP_RIGHTLEG: + flDamage *= 0.75; + break; + + default: + break; + } + } + + // Since this code only runs on the server, make sure it shows the tempents it creates. + CDisablePredictionFiltering disabler; + + if ( bShouldBleed ) + { + // This does smaller splotches on the guy and splats blood on the world. + TraceBleed( flDamage, vecDir, ptr, info.GetDamageType() ); + + CEffectData data; + data.m_vOrigin = ptr->endpos; + data.m_vNormal = vecDir * -1; + data.m_nEntIndex = ptr->m_pEnt ? ptr->m_pEnt->entindex() : 0; + data.m_flMagnitude = flDamage; + + // reduce blood effect if target has armor + if ( ArmorValue() > 0 ) + data.m_flMagnitude *= 0.5f; + + // reduce blood effect if target is hit in the helmet + if ( ptr->hitgroup == HITGROUP_HEAD && bShouldSpark ) + data.m_flMagnitude *= 0.5; + + DispatchEffect( "csblood", data ); + } + if ( ( ptr->hitgroup == HITGROUP_HEAD || bHitShield ) && bShouldSpark ) // they hit a helmet + { + // show metal spark effect + g_pEffects->Sparks( ptr->endpos, 1, 1, &ptr->plane.normal ); + } + + if ( !bHitShield ) + { + CTakeDamageInfo subInfo = info; + + subInfo.SetDamage( flDamage ); + + if( bHeadShot ) + subInfo.AddDamageType( DMG_HEADSHOT ); + + AddMultiDamage( subInfo, this ); + } +} + + +void CCSPlayer::Reset() +{ + ResetFragCount(); + ResetDeathCount(); + m_iAccount = 0; + AddAccount( -16000, false ); + + //remove any weapons they bought before the round started + RemoveAllItems( true ); + + //RemoveShield(); + + AddAccount( CSGameRules()->GetStartMoney(), true ); +} + +//----------------------------------------------------------------------------- +// Purpose: Displays a hint message to the player +// Input : *pMessage - +// bDisplayIfDead - +// bOverrideClientSettings - +//----------------------------------------------------------------------------- +void CCSPlayer::HintMessage( const char *pMessage, bool bDisplayIfDead, bool bOverrideClientSettings ) +{ + if ( ( !bDisplayIfDead && !IsAlive() ) || !IsNetClient() || !m_pHintMessageQueue ) + return; + + if ( bOverrideClientSettings || m_bShowHints ) + m_pHintMessageQueue->AddMessage( pMessage ); +} + +void CCSPlayer::AddAccount( int amount, bool bTrackChange, bool bItemBought, const char *pItemName ) +{ + m_iAccount += amount; + + //============================================================================= + // HPE_BEGIN: + // [menglish] Description of reason for change + //============================================================================= + + if(amount > 0) + { + CCS_GameStats.Event_MoneyEarned( this, amount ); + } + else if( amount < 0 && bItemBought) + { + CCS_GameStats.Event_MoneySpent( this, ABS(amount), pItemName ); + } + + //============================================================================= + // HPE_END + //============================================================================= + + if ( m_iAccount < 0 ) + m_iAccount = 0; + else if ( m_iAccount > 16000 ) + m_iAccount = 16000; +} + +void CCSPlayer::MarkAsNotReceivingMoneyNextRound() +{ + m_receivesMoneyNextRound = false; +} + +bool CCSPlayer::DoesPlayerGetRoundStartMoney() +{ + return m_receivesMoneyNextRound; +} + +CCSPlayer* CCSPlayer::Instance( int iEnt ) +{ + return dynamic_cast< CCSPlayer* >( CBaseEntity::Instance( INDEXENT( iEnt ) ) ); +} + + +void CCSPlayer::DropC4() +{ +} + + +bool CCSPlayer::HasDefuser() +{ + return m_bHasDefuser; +} + +void CCSPlayer::RemoveDefuser() +{ + m_bHasDefuser = false; +} + +void CCSPlayer::GiveDefuser(bool bPickedUp /* = false */) +{ + if ( !m_bHasDefuser ) + { + IGameEvent * event = gameeventmanager->CreateEvent( "item_pickup" ); + if( event ) + { + event->SetInt( "userid", GetUserID() ); + event->SetString( "item", "defuser" ); + gameeventmanager->FireEvent( event ); + } + } + + m_bHasDefuser = true; + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + m_bPickedUpDefuser = bPickedUp; + + //============================================================================= + // HPE_END + //============================================================================= +} + +// player blinded by a flashbang +void CCSPlayer::Blind( float holdTime, float fadeTime, float startingAlpha ) +{ + // Don't flash a spectator. + color32 clr = {255, 255, 255, 255}; + + clr.a = startingAlpha; + + // estimate when we can see again + float oldBlindUntilTime = m_blindUntilTime; + float oldBlindStartTime = m_blindStartTime; + m_blindUntilTime = MAX( m_blindUntilTime, gpGlobals->curtime + holdTime + 0.5f * fadeTime ); + m_blindStartTime = gpGlobals->curtime; + + // Spectators get a lessened flash. + if ( (GetObserverMode() != OBS_MODE_NONE) && (GetObserverMode() != OBS_MODE_IN_EYE) ) + { + if ( !mp_fadetoblack.GetBool() ) + { + clr.a = 150; + + fadeTime = MIN(fadeTime, 0.5f); // make sure the spectator flashbang time is 1/2 second or less. + holdTime = MIN(holdTime, fadeTime * 0.5f); // adjust the hold time to match the fade time. + UTIL_ScreenFade( this, clr, fadeTime, holdTime, FFADE_IN ); + } + } + else + { + fadeTime /= 1.4; + + if ( gpGlobals->curtime > oldBlindUntilTime ) + { + // The previous flashbang is wearing off, or completely gone + m_flFlashDuration = fadeTime; + m_flFlashMaxAlpha = startingAlpha; + } + else + { + // The previous flashbang is still going strong - only extend the duration + float remainingDuration = oldBlindStartTime + m_flFlashDuration - gpGlobals->curtime; + + m_flFlashDuration = MAX( remainingDuration, fadeTime ); + m_flFlashMaxAlpha = MAX( m_flFlashMaxAlpha, startingAlpha ); + } + + // allow bots to react + IGameEvent * event = gameeventmanager->CreateEvent( "player_blind" ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); + gameeventmanager->FireEvent( event ); + } + } +} + +void CCSPlayer::Deafen( float flDistance ) +{ + // Spectators don't get deafened + if ( (GetObserverMode() == OBS_MODE_NONE) || (GetObserverMode() == OBS_MODE_IN_EYE) ) + { + // dsp presets are defined in hl2/scripts/dsp_presets.txt + + int effect; + + if( flDistance < 600 ) + { + effect = 134; + } + else if( flDistance < 800 ) + { + effect = 135; + } + else if( flDistance < 1000 ) + { + effect = 136; + } + else + { + // too far for us to get an effect + return; + } + + CSingleUserRecipientFilter user( this ); + enginesound->SetPlayerDSP( user, effect, false ); + + //TODO: bots can't hear sound for a while? + } +} + +void CCSPlayer::GiveShield( void ) +{ +#ifdef CS_SHIELD_ENABLED + m_bHasShield = true; + m_bShieldDrawn = false; + + if ( HasSecondaryWeapon() ) + { + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + pWeapon->SetModel( pWeapon->GetViewModel() ); + pWeapon->Deploy(); + } + + CBaseViewModel *pVM = GetViewModel( 1 ); + + if ( pVM ) + { + ShowViewModel( true ); + pVM->RemoveEffects( EF_NODRAW ); + pVM->SetWeaponModel( SHIELD_VIEW_MODEL, GetActiveWeapon() ); + pVM->SendViewModelMatchingSequence( 1 ); + } +#endif +} + +void CCSPlayer::RemoveShield( void ) +{ +#ifdef CS_SHIELD_ENABLED + m_bHasShield = false; + + CBaseViewModel *pVM = GetViewModel( 1 ); + + if ( pVM ) + { + pVM->AddEffects( EF_NODRAW ); + } +#endif +} + +void CCSPlayer::RemoveAllItems( bool removeSuit ) +{ + if( HasDefuser() ) + { + RemoveDefuser(); + } + + if ( HasShield() ) + { + RemoveShield(); + } + + m_bHasNightVision = false; + m_bNightVisionOn = false; + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + m_bPickedUpDefuser = false; + m_bDefusedWithPickedUpKit = false; + + //============================================================================= + // HPE_END + //============================================================================= + + if ( removeSuit ) + { + m_bHasHelmet = false; + SetArmorValue( 0 ); + } + + BaseClass::RemoveAllItems( removeSuit ); +} + +void CCSPlayer::ObserverRoundRespawn() +{ + ClearFlashbangScreenFade(); + + // did we change our name last round? + if ( m_szNewName[0] != 0 ) + { + // ... and force the name change now. After this happens, the gamerules will get + // a ClientSettingsChanged callback from the above ClientCommand, but the name + // matches what we're setting here, so it will do nothing. + ChangeName( m_szNewName ); + m_szNewName[0] = 0; + } +} + +void CCSPlayer::RoundRespawn() +{ + //MIKETODO: menus + //if ( m_iMenu != Menu_ChooseAppearance ) + { + // Put them back into the game. + StopObserverMode(); + State_Transition( STATE_ACTIVE ); + respawn( this, false ); + m_nButtons = 0; + SetNextThink( TICK_NEVER_THINK ); + } + + m_receivesMoneyNextRound = true; // reset this variable so they can receive their cash next round. + + //If they didn't die, this will print out their damage info + OutputDamageGiven(); + OutputDamageTaken(); + ResetDamageCounters(); +} + +void CCSPlayer::CheckTKPunishment( void ) +{ + // teamkill punishment.. + if ( (m_bJustKilledTeammate == true) && mp_tkpunish.GetInt() ) + { + m_bJustKilledTeammate = false; + m_bPunishedForTK = true; + CommitSuicide(); + } +} + +CWeaponCSBase* CCSPlayer::GetActiveCSWeapon() const +{ + return dynamic_cast< CWeaponCSBase* >( GetActiveWeapon() ); +} + +void CCSPlayer::PreThink() +{ + BaseClass::PreThink(); + if ( m_bAutoReload ) + { + m_bAutoReload = false; + m_nButtons |= IN_RELOAD; + } + + if ( m_afButtonLast != m_nButtons ) + m_flLastMovement = gpGlobals->curtime; + + if ( g_fGameOver ) + return; + + State_PreThink(); + + if ( m_pHintMessageQueue ) + m_pHintMessageQueue->Update(); + + //Reset bullet force accumulator, only lasts one frame + m_vecTotalBulletForce = vec3_origin; + + if ( mp_autokick.GetBool() && !IsBot() && !IsHLTV() && !IsAutoKickDisabled() ) + { + if ( m_flLastMovement + CSGameRules()->GetRoundLength()*2 < gpGlobals->curtime ) + { + UTIL_ClientPrintAll( HUD_PRINTCONSOLE, "#Game_idle_kick", GetPlayerName() ); + engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", GetUserID() ) ); + m_flLastMovement = gpGlobals->curtime; + } + } +#ifndef _XBOX + // CS would like their players to continue to update their LastArea since it is displayed in the hud voice chat UI + // But we won't do the population tracking while dead. + CNavArea *area = TheNavMesh->GetNavArea( GetAbsOrigin(), 1000 ); + if (area && area != m_lastNavArea) + { + m_lastNavArea = area; + if ( area->GetPlace() != UNDEFINED_PLACE ) + { + const char *placeName = TheNavMesh->PlaceToName( area->GetPlace() ); + if ( placeName && *placeName ) + { + Q_strncpy( m_szLastPlaceName.GetForModify(), placeName, MAX_PLACE_NAME_LENGTH ); + } + } + } +#endif +} + +void CCSPlayer::MoveToNextIntroCamera() +{ + m_pIntroCamera = gEntList.FindEntityByClassname( m_pIntroCamera, "point_viewcontrol" ); + + // if m_pIntroCamera is NULL we just were at end of list, start searching from start again + if(!m_pIntroCamera) + m_pIntroCamera = gEntList.FindEntityByClassname(m_pIntroCamera, "point_viewcontrol"); + + // find the target + CBaseEntity *Target = NULL; + + if( m_pIntroCamera ) + { + Target = gEntList.FindEntityByName( NULL, STRING(m_pIntroCamera->m_target) ); + } + + // if we still couldn't find a camera, goto T spawn + if(!m_pIntroCamera) + m_pIntroCamera = gEntList.FindEntityByClassname(m_pIntroCamera, "info_player_terrorist"); + + SetViewOffset( vec3_origin ); // no view offset + UTIL_SetSize( this, vec3_origin, vec3_origin ); // no bbox + + if( !Target ) //if there are no cameras(or the camera has no target, find a spawn point and black out the screen + { + if ( m_pIntroCamera.IsValid() ) + SetAbsOrigin( m_pIntroCamera->GetAbsOrigin() + VEC_VIEW ); + + SetAbsAngles( QAngle( 0, 0, 0 ) ); + + m_pIntroCamera = NULL; // never update again + return; + } + + + Vector vCamera = Target->GetAbsOrigin() - m_pIntroCamera->GetAbsOrigin(); + Vector vIntroCamera = m_pIntroCamera->GetAbsOrigin(); + + VectorNormalize( vCamera ); + + QAngle CamAngles; + VectorAngles( vCamera, CamAngles ); + + SetAbsOrigin( vIntroCamera ); + SetAbsAngles( CamAngles ); + SnapEyeAngles( CamAngles ); + m_fIntroCamTime = gpGlobals->curtime + 6; +} + +class NotVIP +{ +public: + bool operator()( CBasePlayer *player ) + { + CCSPlayer *csPlayer = static_cast< CCSPlayer * >(player); + csPlayer->MakeVIP( false ); + + return true; + } +}; + +// Expose the VIP selection to plugins, since we don't have an official VIP mode. This +// allows plugins to access the (limited) VIP functionality already present (scoreboard +// identification and radar color). +CON_COMMAND( cs_make_vip, "Marks a player as the VIP" ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( args.ArgC() != 2 ) + { + return; + } + + CCSPlayer *player = static_cast< CCSPlayer * >(UTIL_PlayerByIndex( atoi( args[1] ) )); + if ( !player ) + { + // Invalid value clears out VIP + NotVIP notVIP; + ForEachPlayer( notVIP ); + return; + } + + player->MakeVIP( true ); +} + +void CCSPlayer::MakeVIP( bool isVIP ) +{ + if ( isVIP ) + { + NotVIP notVIP; + ForEachPlayer( notVIP ); + } + m_isVIP = isVIP; +} + +bool CCSPlayer::IsVIP() const +{ + return m_isVIP; +} + +void CCSPlayer::DropShield( void ) +{ +#ifdef CS_SHIELD_ENABLED + //Drop an item_defuser + Vector vForward, vRight; + AngleVectors( GetAbsAngles(), &vForward, &vRight, NULL ); + + RemoveShield(); + + CBaseAnimating *pShield = (CBaseAnimating *)CBaseEntity::Create( "item_shield", WorldSpaceCenter(), GetLocalAngles() ); + pShield->ApplyAbsVelocityImpulse( vForward * 200 + vRight * random->RandomFloat( -50, 50 ) ); + + CBaseCombatWeapon *pActive = GetActiveWeapon(); + + if ( pActive ) + { + pActive->Deploy(); + } +#endif +} + +void CCSPlayer::SetShieldDrawnState( bool bState ) +{ +#ifdef CS_SHIELD_ENABLED + m_bShieldDrawn = bState; +#endif +} + +bool CCSPlayer::CSWeaponDrop( CBaseCombatWeapon *pWeapon, bool bDropShield, bool bThrowForward ) +{ + bool bSuccess = false; + + if ( HasShield() && bDropShield == true ) + { + DropShield(); + return true; + } + + if ( pWeapon ) + { + Vector vForward; + + AngleVectors( EyeAngles(), &vForward, NULL, NULL ); + //GetVectors( &vForward, NULL, NULL ); + Vector vTossPos = WorldSpaceCenter(); + + if( bThrowForward ) + vTossPos = vTossPos + vForward * 64; + + Weapon_Drop( pWeapon, &vTossPos, NULL ); + + pWeapon->SetSolidFlags( FSOLID_NOT_STANDABLE | FSOLID_TRIGGER | FSOLID_USE_TRIGGER_BOUNDS ); + pWeapon->SetMoveCollide( MOVECOLLIDE_FLY_BOUNCE ); + + CWeaponCSBase *pCSWeapon = dynamic_cast< CWeaponCSBase* >( pWeapon ); + + if( pCSWeapon ) + { + pCSWeapon->SetWeaponModelIndex( pCSWeapon->GetCSWpnData().szWorldModel ); + + //Find out the index of the ammo type + int iAmmoIndex = pCSWeapon->GetPrimaryAmmoType(); + + //If it has an ammo type, find out how much the player has + if( iAmmoIndex != -1 ) + { + // Check to make sure we don't have other weapons using this ammo type + bool bAmmoTypeInUse = false; + if ( IsAlive() && GetHealth() > 0 ) + { + for ( int i=0; i<MAX_WEAPONS; ++i ) + { + CBaseCombatWeapon *pOtherWeapon = GetWeapon(i); + if ( pOtherWeapon && pOtherWeapon != pWeapon && pOtherWeapon->GetPrimaryAmmoType() == iAmmoIndex ) + { + bAmmoTypeInUse = true; + break; + } + } + } + + if ( !bAmmoTypeInUse ) + { + int iAmmoToDrop = GetAmmoCount( iAmmoIndex ); + + //Add this much to the dropped weapon + pCSWeapon->SetExtraAmmoCount( iAmmoToDrop ); + + //Remove all ammo of this type from the player + SetAmmoCount( 0, iAmmoIndex ); + } + } + } + + //========================================= + // Teleport the weapon to the player's hand + //========================================= + int iBIndex = -1; + int iWeaponBoneIndex = -1; + + MDLCACHE_CRITICAL_SECTION(); + CStudioHdr *hdr = pWeapon->GetModelPtr(); + // If I have a hand, set the weapon position to my hand bone position. + if ( hdr && hdr->numbones() > 0 ) + { + // Assume bone zero is the root + for ( iWeaponBoneIndex = 0; iWeaponBoneIndex < hdr->numbones(); ++iWeaponBoneIndex ) + { + iBIndex = LookupBone( hdr->pBone( iWeaponBoneIndex )->pszName() ); + // Found one! + if ( iBIndex != -1 ) + { + break; + } + } + + if ( iWeaponBoneIndex == hdr->numbones() ) + return true; + + if ( iBIndex == -1 ) + { + iBIndex = LookupBone( "ValveBiped.Bip01_R_Hand" ); + } + } + else + { + iBIndex = LookupBone( "ValveBiped.Bip01_R_Hand" ); + } + + if ( iBIndex != -1) + { + Vector origin; + QAngle angles; + matrix3x4_t transform; + + // Get the transform for the weapon bonetoworldspace in the NPC + GetBoneTransform( iBIndex, transform ); + + // find offset of root bone from origin in local space + // Make sure we're detached from hierarchy before doing this!!! + pWeapon->StopFollowingEntity(); + pWeapon->SetAbsOrigin( Vector( 0, 0, 0 ) ); + pWeapon->SetAbsAngles( QAngle( 0, 0, 0 ) ); + pWeapon->InvalidateBoneCache(); + matrix3x4_t rootLocal; + pWeapon->GetBoneTransform( iWeaponBoneIndex, rootLocal ); + + // invert it + matrix3x4_t rootInvLocal; + MatrixInvert( rootLocal, rootInvLocal ); + + matrix3x4_t weaponMatrix; + ConcatTransforms( transform, rootInvLocal, weaponMatrix ); + MatrixAngles( weaponMatrix, angles, origin ); + + pWeapon->Teleport( &origin, &angles, NULL ); + + //Have to teleport the physics object as well + + IPhysicsObject *pWeaponPhys = pWeapon->VPhysicsGetObject(); + + if( pWeaponPhys ) + { + Vector vPos; + QAngle vAngles; + pWeaponPhys->GetPosition( &vPos, &vAngles ); + pWeaponPhys->SetPosition( vPos, angles, true ); + + AngularImpulse angImp(0,0,0); + Vector vecAdd = GetAbsVelocity(); + pWeaponPhys->AddVelocity( &vecAdd, &angImp ); + } + } + + bSuccess = true; + } + + return bSuccess; +} + + +bool CCSPlayer::DropRifle( bool fromDeath ) +{ + bool bSuccess = false; + + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( WEAPON_SLOT_RIFLE ); + if ( pWeapon ) + { + bSuccess = CSWeaponDrop( pWeapon, false ); + } + + //============================================================================= + // HPE_BEGIN: + // [menglish] Add the dropped weapon to the dropped equipment list + //============================================================================= + if( fromDeath && bSuccess ) + { + m_hDroppedEquipment[DROPPED_WEAPON] = static_cast<CBaseEntity *>(pWeapon); + } + //============================================================================= + // HPE_END + //============================================================================= + + return bSuccess; +} + + +bool CCSPlayer::DropPistol( bool fromDeath ) +{ + bool bSuccess = false; + + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + if ( pWeapon ) + { + bSuccess = CSWeaponDrop( pWeapon, false ); + m_bUsingDefaultPistol = false; + } + //============================================================================= + // HPE_BEGIN: + // [menglish] Add the dropped weapon to the dropped equipment list + //============================================================================= + if( fromDeath && bSuccess ) + { + m_hDroppedEquipment[DROPPED_WEAPON] = static_cast<CBaseEntity *>(pWeapon); + } + //============================================================================= + // HPE_END + //============================================================================= + + return bSuccess; +} + +bool CCSPlayer::HasPrimaryWeapon( void ) +{ + bool bSuccess = false; + + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( WEAPON_SLOT_RIFLE ); + + if ( pWeapon ) + { + bSuccess = true; + } + + return bSuccess; +} + + +bool CCSPlayer::HasSecondaryWeapon( void ) +{ + bool bSuccess = false; + + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + if ( pWeapon ) + { + bSuccess = true; + } + + return bSuccess; +} + +bool CCSPlayer::IsInBuyZone() +{ + return m_bInBuyZone && !IsVIP(); +} + +bool CCSPlayer::CanPlayerBuy( bool display ) +{ + // is the player in a buy zone? + if ( !IsInBuyZone() ) + { + return false; + } + + CCSGameRules* mp = CSGameRules(); + + // is the player alive? + if ( m_lifeState != LIFE_ALIVE ) + { + return false; + } + + int buyTime = (int)(mp_buytime.GetFloat() * 60); + + if ( mp->IsBuyTimeElapsed() ) + { + if ( display == true ) + { + char strBuyTime[16]; + Q_snprintf( strBuyTime, sizeof( strBuyTime ), "%d", buyTime ); + ClientPrint( this, HUD_PRINTCENTER, "#Cant_buy", strBuyTime ); + } + + return false; + } + + if ( m_bIsVIP ) + { + if ( display == true ) + ClientPrint( this, HUD_PRINTCENTER, "#VIP_cant_buy" ); + + return false; + } + + if ( mp->m_bCTCantBuy && ( GetTeamNumber() == TEAM_CT ) ) + { + if ( display == true ) + ClientPrint( this, HUD_PRINTCENTER, "#CT_cant_buy" ); + + return false; + } + + if ( mp->m_bTCantBuy && ( GetTeamNumber() == TEAM_TERRORIST ) ) + { + if ( display == true ) + ClientPrint( this, HUD_PRINTCENTER, "#Terrorist_cant_buy" ); + + return false; + } + + return true; +} + + +BuyResult_e CCSPlayer::AttemptToBuyVest( void ) +{ + int iKevlarPrice = KEVLAR_PRICE; + + if ( CSGameRules()->IsBlackMarket() ) + { + iKevlarPrice = CSGameRules()->GetBlackMarketPriceForWeapon( WEAPON_KEVLAR ); + } + + if ( ArmorValue() >= 100 ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_Kevlar" ); + return BUY_ALREADY_HAVE; + } + else if ( m_iAccount < iKevlarPrice ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Not_Enough_Money" ); + return BUY_CANT_AFFORD; + } + else + { + if ( m_bHasHelmet ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_Helmet_Bought_Kevlar" ); + } + + IGameEvent * event = gameeventmanager->CreateEvent( "item_pickup" ); + if( event ) + { + event->SetInt( "userid", GetUserID() ); + event->SetString( "item", "vest" ); + gameeventmanager->FireEvent( event ); + } + + GiveNamedItem( "item_kevlar" ); + AddAccount( -iKevlarPrice, true, true, "item_kevlar" ); + BlackMarketAddWeapon( "item_kevlar", this ); + return BUY_BOUGHT; + } +} + + +BuyResult_e CCSPlayer::AttemptToBuyAssaultSuit( void ) +{ + // WARNING: This price logic also exists in C_CSPlayer::GetCurrentAssaultSuitPrice + // and must be kept in sync if changes are made. + + int fullArmor = ArmorValue() >= 100 ? 1 : 0; + + int price = 0, enoughMoney = 0; + + int iHelmetPrice = HELMET_PRICE; + int iKevlarPrice = KEVLAR_PRICE; + int iAssaultSuitPrice = ASSAULTSUIT_PRICE; + + if ( CSGameRules()->IsBlackMarket() ) + { + iKevlarPrice = CSGameRules()->GetBlackMarketPriceForWeapon( WEAPON_KEVLAR ); + iAssaultSuitPrice = CSGameRules()->GetBlackMarketPriceForWeapon( WEAPON_ASSAULTSUIT ); + + iHelmetPrice = iAssaultSuitPrice - iKevlarPrice; + } + + if ( fullArmor && m_bHasHelmet ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_Kevlar_Helmet" ); + return BUY_ALREADY_HAVE; + } + else if ( fullArmor && !m_bHasHelmet && m_iAccount >= iHelmetPrice ) + { + enoughMoney = 1; + price = iHelmetPrice; + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_Kevlar_Bought_Helmet" ); + } + else if ( !fullArmor && m_bHasHelmet && m_iAccount >= iKevlarPrice ) + { + enoughMoney = 1; + price = iKevlarPrice; + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_Helmet_Bought_Kevlar" ); + } + else if ( m_iAccount >= iAssaultSuitPrice ) + { + enoughMoney = 1; + price = iAssaultSuitPrice; + } + + // process the result + if ( !enoughMoney ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Not_Enough_Money" ); + return BUY_CANT_AFFORD; + } + else + { + IGameEvent * event = gameeventmanager->CreateEvent( "item_pickup" ); + if( event ) + { + event->SetInt( "userid", GetUserID() ); + event->SetString( "item", "vesthelm" ); + gameeventmanager->FireEvent( event ); + } + + GiveNamedItem( "item_assaultsuit" ); + AddAccount( -price, true, true, "item_assaultsuit" ); + BlackMarketAddWeapon( "item_assaultsuit", this ); + return BUY_BOUGHT; + } +} + +BuyResult_e CCSPlayer::AttemptToBuyShield( void ) +{ +#ifdef CS_SHIELD_ENABLED + if ( HasShield() ) // prevent this guy from buying more than 1 Defuse Kit + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_One" ); + return BUY_ALREADY_HAVE; + } + else if ( m_iAccount < SHIELD_PRICE ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Not_Enough_Money" ); + return BUY_CANT_AFFORD; + } + else + { + if ( HasSecondaryWeapon() ) + { + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + CWeaponCSBase *pCSWeapon = dynamic_cast< CWeaponCSBase* >( pWeapon ); + + if ( pCSWeapon && pCSWeapon->GetCSWpnData().m_bCanUseWithShield == false ) + return; + } + + if ( HasPrimaryWeapon() ) + DropRifle(); + + GiveShield(); + + CPASAttenuationFilter filter( this, "Player.PickupWeapon" ); + EmitSound( filter, entindex(), "Player.PickupWeapon" ); + + m_bAnythingBought = true; + AddAccount( -SHIELD_PRICE, true, true, "item_shield" ); + return BUY_BOUGHT; + } +#else + ClientPrint( this, HUD_PRINTCENTER, "Tactical shield disabled" ); + return BUY_NOT_ALLOWED; +#endif +} + +BuyResult_e CCSPlayer::AttemptToBuyDefuser( void ) +{ + CCSGameRules *MPRules = CSGameRules(); + + if( ( GetTeamNumber() == TEAM_CT ) && MPRules->IsBombDefuseMap() ) + { + if ( HasDefuser() ) // prevent this guy from buying more than 1 Defuse Kit + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_One" ); + return BUY_ALREADY_HAVE; + } + else if ( m_iAccount < DEFUSEKIT_PRICE ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Not_Enough_Money" ); + return BUY_CANT_AFFORD; + } + else + { + GiveDefuser(); + + CPASAttenuationFilter filter( this, "Player.PickupWeapon" ); + EmitSound( filter, entindex(), "Player.PickupWeapon" ); + + AddAccount( -DEFUSEKIT_PRICE, true, true, "item_defuser" ); + return BUY_BOUGHT; + } + } + + return BUY_NOT_ALLOWED; +} + +BuyResult_e CCSPlayer::AttemptToBuyNightVision( void ) +{ + int iNVGPrice = NVG_PRICE; + + if ( CSGameRules()->IsBlackMarket() ) + { + iNVGPrice = CSGameRules()->GetBlackMarketPriceForWeapon( WEAPON_NVG ); + } + + if ( m_bHasNightVision == TRUE ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Already_Have_One" ); + return BUY_ALREADY_HAVE; + } + else if ( m_iAccount < iNVGPrice ) + { + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Not_Enough_Money" ); + return BUY_CANT_AFFORD; + } + else + { + IGameEvent * event = gameeventmanager->CreateEvent( "item_pickup" ); + if( event ) + { + event->SetInt( "userid", GetUserID() ); + event->SetString( "item", "nvgs" ); + gameeventmanager->FireEvent( event ); + } + + GiveNamedItem( "item_nvgs" ); + AddAccount( -iNVGPrice, true, true ); + BlackMarketAddWeapon( "nightvision", this ); + + if ( !(m_iDisplayHistoryBits & DHF_NIGHTVISION) ) + { + HintMessage( "#Hint_use_nightvision", false ); + m_iDisplayHistoryBits |= DHF_NIGHTVISION; + } + return BUY_BOUGHT; + } +} + + +// Handles the special "buy" alias commands we're creating to accommodate the buy +// scripts players use (now that we've rearranged the buy menus and broken the scripts) +//============================================================================= +// HPE_BEGIN: +//[tj] This is essentially a shim so I can easily check the return +// value without adding new code to all the return points. +//============================================================================= + +BuyResult_e CCSPlayer::HandleCommand_Buy( const char *item ) +{ + BuyResult_e result = HandleCommand_Buy_Internal(item); + if (result == BUY_BOUGHT) + { + m_bMadePurchseThisRound = true; + CCS_GameStats.IncrementStat(this, CSSTAT_ITEMS_PURCHASED, 1); + } + return result; +} + +BuyResult_e CCSPlayer::HandleCommand_Buy_Internal( const char* wpnName ) +//============================================================================= +// HPE_END +//============================================================================= +{ + BuyResult_e result = CanPlayerBuy( false ) ? BUY_PLAYER_CANT_BUY : BUY_INVALID_ITEM; // set some defaults + + // translate the new weapon names to the old ones that are actually being used. + wpnName = GetTranslatedWeaponAlias(wpnName); + + CCSWeaponInfo *pWeaponInfo = GetWeaponInfo( AliasToWeaponID( wpnName ) ); + if ( pWeaponInfo == NULL ) + { + if ( Q_stricmp( wpnName, "primammo" ) == 0 ) + { + result = AttemptToBuyAmmo( 0 ); + } + else if ( Q_stricmp( wpnName, "secammo" ) == 0 ) + { + result = AttemptToBuyAmmo( 1 ); + } + else if ( Q_stristr( wpnName, "defuser" ) ) + { + if( CanPlayerBuy( true ) ) + { + result = AttemptToBuyDefuser(); + } + } + } + else + { + + if( !CanPlayerBuy( true ) ) + { + return BUY_PLAYER_CANT_BUY; + } + + BuyResult_e equipResult = BUY_INVALID_ITEM; + + if ( Q_stristr( wpnName, "kevlar" ) ) + { + equipResult = AttemptToBuyVest(); + } + else if ( Q_stristr( wpnName, "assaultsuit" ) ) + { + equipResult = AttemptToBuyAssaultSuit(); + } + else if ( Q_stristr( wpnName, "shield" ) ) + { + equipResult = AttemptToBuyShield(); + } + else if ( Q_stristr( wpnName, "nightvision" ) ) + { + equipResult = AttemptToBuyNightVision(); + } + + if ( equipResult != BUY_INVALID_ITEM ) + { + if ( equipResult == BUY_BOUGHT ) + { + BuildRebuyStruct(); + } + return equipResult; // intentional early return here + } + + bool bPurchase = false; + + // MIKETODO: assasination maps have a specific set of weapons that can be used in them. + if ( pWeaponInfo->m_iTeam != TEAM_UNASSIGNED && GetTeamNumber() != pWeaponInfo->m_iTeam ) + { + result = BUY_NOT_ALLOWED; + if ( pWeaponInfo->m_WrongTeamMsg[0] != 0 ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Alias_Not_Avail", pWeaponInfo->m_WrongTeamMsg ); + } + } + else if ( pWeaponInfo->GetWeaponPrice() <= 0 ) + { + // ClientPrint( this, HUD_PRINTCENTER, "#Cant_buy_this_item", pWeaponInfo->m_WrongTeamMsg ); + } + else if( pWeaponInfo->m_WeaponType == WEAPONTYPE_GRENADE ) + { + // make sure the player can afford this item. + if ( m_iAccount >= pWeaponInfo->GetWeaponPrice() ) + { + bPurchase = true; + + const char *szWeaponName = NULL; + int ammoMax = 0; + if ( Q_strstr( pWeaponInfo->szClassName, "flashbang" ) ) + { + szWeaponName = "weapon_flashbang"; + ammoMax = ammo_flashbang_max.GetInt(); + } + else if ( Q_strstr( pWeaponInfo->szClassName, "hegrenade" ) ) + { + szWeaponName = "weapon_hegrenade"; + ammoMax = ammo_hegrenade_max.GetInt(); + } + else if ( Q_strstr( pWeaponInfo->szClassName, "smokegrenade" ) ) + { + szWeaponName = "weapon_smokegrenade"; + ammoMax = ammo_smokegrenade_max.GetInt(); + } + + if ( szWeaponName != NULL ) + { + CBaseCombatWeapon* pGrenadeWeapon = Weapon_OwnsThisType( szWeaponName ); + { + if ( pGrenadeWeapon != NULL ) + { + int nAmmoType = pGrenadeWeapon->GetPrimaryAmmoType(); + + if( nAmmoType != -1 ) + { + if( GetAmmoCount(nAmmoType) >= ammoMax ) + { + result = BUY_ALREADY_HAVE; + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Carry_Anymore" ); + bPurchase = false; + } + } + } + } + } + } + } + else if ( !Weapon_OwnsThisType( pWeaponInfo->szClassName ) ) //don't buy duplicate weapons + { + // do they have enough money? + if ( m_iAccount >= pWeaponInfo->GetWeaponPrice() ) + { + if ( m_lifeState != LIFE_DEAD ) + { + if ( pWeaponInfo->iSlot == WEAPON_SLOT_PISTOL ) + { + DropPistol(); + } + else if ( pWeaponInfo->iSlot == WEAPON_SLOT_RIFLE ) + { + DropRifle(); + } + } + + bPurchase = true; + } + else + { + result = BUY_CANT_AFFORD; + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Not_Enough_Money" ); + } + } + else + { + result = BUY_ALREADY_HAVE; + } + + if ( HasShield() ) + { + if ( pWeaponInfo->m_bCanUseWithShield == false ) + { + result = BUY_NOT_ALLOWED; + bPurchase = false; + } + } + + if( bPurchase ) + { + result = BUY_BOUGHT; + + if ( bPurchase && pWeaponInfo->iSlot == WEAPON_SLOT_PISTOL ) + m_bUsingDefaultPistol = false; + + GiveNamedItem( pWeaponInfo->szClassName ); + AddAccount( -pWeaponInfo->GetWeaponPrice(), true, true, pWeaponInfo->szClassName ); + BlackMarketAddWeapon( wpnName, this ); + } + } + + if ( result == BUY_BOUGHT ) + { + BuildRebuyStruct(); + } + + return result; +} + + +BuyResult_e CCSPlayer::BuyGunAmmo( CBaseCombatWeapon *pWeapon, bool bBlinkMoney ) +{ + if ( !CanPlayerBuy( false ) ) + { + return BUY_PLAYER_CANT_BUY; + } + + // Ensure that the weapon uses ammo + int nAmmo = pWeapon->GetPrimaryAmmoType(); + if ( nAmmo == -1 ) + { + return BUY_ALREADY_HAVE; + } + + // Can only buy if the player does not already have full ammo + if ( GetAmmoCount( nAmmo ) >= GetAmmoDef()->MaxCarry( nAmmo ) ) + { + return BUY_ALREADY_HAVE; + } + + // Purchase the ammo if the player has enough money + if ( m_iAccount >= GetCSAmmoDef()->GetCost( nAmmo ) ) + { + GiveAmmo( GetCSAmmoDef()->GetBuySize( nAmmo ), nAmmo, true ); + AddAccount( -GetCSAmmoDef()->GetCost( nAmmo ), true, true, GetCSAmmoDef()->GetAmmoOfIndex( nAmmo )->pName ); + return BUY_BOUGHT; + } + + if ( bBlinkMoney ) + { + // Not enough money.. let the player know + if( !m_bIsInAutoBuy && !m_bIsInRebuy ) + ClientPrint( this, HUD_PRINTCENTER, "#Not_Enough_Money" ); + } + + return BUY_CANT_AFFORD; +} + + +BuyResult_e CCSPlayer::BuyAmmo( int nSlot, bool bBlinkMoney ) +{ + if ( !CanPlayerBuy( false ) ) + { + return BUY_PLAYER_CANT_BUY; + } + + if ( nSlot < 0 || nSlot > 1 ) + { + return BUY_INVALID_ITEM; + } + + // Buy one ammo clip for all weapons in the given slot + // + // nSlot == 1 : Primary weapons + // nSlot == 2 : Secondary weapons + + CBaseCombatWeapon *pSlot = Weapon_GetSlot( nSlot ); + if ( !pSlot ) + return BUY_INVALID_ITEM; + + //MIKETODO: shield. + //if ( player->HasShield() && player->m_rgpPlayerItems[2] ) + // pItem = player->m_rgpPlayerItems[2]; + + return BuyGunAmmo( pSlot, bBlinkMoney ); +} + + +BuyResult_e CCSPlayer::AttemptToBuyAmmo( int iAmmoType ) +{ + Assert( iAmmoType == 0 || iAmmoType == 1 ); + + BuyResult_e result = BuyAmmo( iAmmoType, true ); + + if ( result == BUY_BOUGHT ) + { + while ( BuyAmmo( iAmmoType, false ) == BUY_BOUGHT ) + { + // empty loop - keep buying + } + + return BUY_BOUGHT; + } + + return result; +} + +BuyResult_e CCSPlayer::AttemptToBuyAmmoSingle( int iAmmoType ) +{ + Assert( iAmmoType == 0 || iAmmoType == 1 ); + + BuyResult_e result = BuyAmmo( iAmmoType, true ); + + if ( result == BUY_BOUGHT ) + { + BuildRebuyStruct(); + } + + return result; +} + +const char *RadioEventName[ RADIO_NUM_EVENTS+1 ] = +{ + "RADIO_INVALID", + + "EVENT_START_RADIO_1", + + "EVENT_RADIO_COVER_ME", + "EVENT_RADIO_YOU_TAKE_THE_POINT", + "EVENT_RADIO_HOLD_THIS_POSITION", + "EVENT_RADIO_REGROUP_TEAM", + "EVENT_RADIO_FOLLOW_ME", + "EVENT_RADIO_TAKING_FIRE", + + "EVENT_START_RADIO_2", + + "EVENT_RADIO_GO_GO_GO", + "EVENT_RADIO_TEAM_FALL_BACK", + "EVENT_RADIO_STICK_TOGETHER_TEAM", + "EVENT_RADIO_GET_IN_POSITION_AND_WAIT", + "EVENT_RADIO_STORM_THE_FRONT", + "EVENT_RADIO_REPORT_IN_TEAM", + + "EVENT_START_RADIO_3", + + "EVENT_RADIO_AFFIRMATIVE", + "EVENT_RADIO_ENEMY_SPOTTED", + "EVENT_RADIO_NEED_BACKUP", + "EVENT_RADIO_SECTOR_CLEAR", + "EVENT_RADIO_IN_POSITION", + "EVENT_RADIO_REPORTING_IN", + "EVENT_RADIO_GET_OUT_OF_THERE", + "EVENT_RADIO_NEGATIVE", + "EVENT_RADIO_ENEMY_DOWN", + + "EVENT_RADIO_END", + + NULL // must be NULL-terminated +}; + + +/** + * Convert name to RadioType + */ +RadioType NameToRadioEvent( const char *name ) +{ + for( int i=0; RadioEventName[i]; ++i ) + if (!stricmp( RadioEventName[i], name )) + return static_cast<RadioType>( i ); + + return RADIO_INVALID; +} + + +void CCSPlayer::HandleMenu_Radio1( int slot ) +{ + if( m_iRadioMessages < 0 ) + return; + + if( m_flRadioTime > gpGlobals->curtime ) + return; + + m_iRadioMessages--; + m_flRadioTime = gpGlobals->curtime + 1.5; + + switch ( slot ) + { + case 1 : + Radio( "Radio.CoverMe", "#Cstrike_TitlesTXT_Cover_me" ); + break; + + case 2 : + Radio( "Radio.YouTakeThePoint", "#Cstrike_TitlesTXT_You_take_the_point" ); + break; + + case 3 : + Radio( "Radio.HoldPosition", "#Cstrike_TitlesTXT_Hold_this_position" ); + break; + + case 4 : + Radio( "Radio.Regroup", "#Cstrike_TitlesTXT_Regroup_team" ); + break; + + case 5 : + Radio( "Radio.FollowMe", "#Cstrike_TitlesTXT_Follow_me" ); + break; + + case 6 : + Radio( "Radio.TakingFire", "#Cstrike_TitlesTXT_Taking_fire" ); + break; + } + + // tell bots about radio message + IGameEvent * event = gameeventmanager->CreateEvent( "player_radio" ); + if ( event ) + { + event->SetInt("userid", GetUserID() ); + event->SetInt("slot", RADIO_START_1 + slot ); + gameeventmanager->FireEvent( event ); + } +} + +void CCSPlayer::HandleMenu_Radio2( int slot ) +{ + if( m_iRadioMessages < 0 ) + return; + + if( m_flRadioTime > gpGlobals->curtime ) + return; + + m_iRadioMessages--; + m_flRadioTime = gpGlobals->curtime + 1.5; + + switch ( slot ) + { + case 1 : + Radio( "Radio.GoGoGo", "#Cstrike_TitlesTXT_Go_go_go" ); + break; + + case 2 : + Radio( "Radio.TeamFallBack", "#Cstrike_TitlesTXT_Team_fall_back" ); + break; + + case 3 : + Radio( "Radio.StickTogether", "#Cstrike_TitlesTXT_Stick_together_team" ); + break; + + case 4 : + Radio( "Radio.GetInPosition", "#Cstrike_TitlesTXT_Get_in_position_and_wait" ); + break; + + case 5 : + Radio( "Radio.StormFront", "#Cstrike_TitlesTXT_Storm_the_front" ); + break; + + case 6 : + Radio( "Radio.ReportInTeam", "#Cstrike_TitlesTXT_Report_in_team" ); + break; + } + + // tell bots about radio message + IGameEvent * event = gameeventmanager->CreateEvent( "player_radio" ); + if ( event ) + { + event->SetInt("userid", GetUserID() ); + event->SetInt("slot", RADIO_START_2 + slot ); + gameeventmanager->FireEvent( event ); + } +} + +void CCSPlayer::HandleMenu_Radio3( int slot ) +{ + if( m_iRadioMessages < 0 ) + return; + + if( m_flRadioTime > gpGlobals->curtime ) + return; + + m_iRadioMessages--; + m_flRadioTime = gpGlobals->curtime + 1.5; + + switch ( slot ) + { + case 1 : + if ( random->RandomInt( 0,1 ) ) + Radio( "Radio.Affirmitive", "#Cstrike_TitlesTXT_Affirmative" ); + else + Radio( "Radio.Roger", "#Cstrike_TitlesTXT_Roger_that" ); + + break; + + case 2 : + Radio( "Radio.EnemySpotted", "#Cstrike_TitlesTXT_Enemy_spotted" ); + break; + + case 3 : + Radio( "Radio.NeedBackup", "#Cstrike_TitlesTXT_Need_backup" ); + break; + + case 4 : + Radio( "Radio.SectorClear", "#Cstrike_TitlesTXT_Sector_clear" ); + break; + + case 5 : + Radio( "Radio.InPosition", "#Cstrike_TitlesTXT_In_position" ); + break; + + case 6 : + Radio( "Radio.ReportingIn", "#Cstrike_TitlesTXT_Reporting_in" ); + break; + + case 7 : + Radio( "Radio.GetOutOfThere", "#Cstrike_TitlesTXT_Get_out_of_there" ); + break; + + case 8 : + Radio( "Radio.Negative", "#Cstrike_TitlesTXT_Negative" ); + break; + + case 9 : + Radio( "Radio.EnemyDown", "#Cstrike_TitlesTXT_Enemy_down" ); + break; + } + + // tell bots about radio message + IGameEvent * event = gameeventmanager->CreateEvent( "player_radio" ); + if ( event ) + { + event->SetInt("userid", GetUserID() ); + event->SetInt("slot", RADIO_START_3 + slot ); + gameeventmanager->FireEvent( event ); + } +} + +void UTIL_CSRadioMessage( IRecipientFilter& filter, int iClient, int msg_dest, const char *msg_name, const char *param1 = NULL, const char *param2 = NULL, const char *param3 = NULL, const char *param4 = NULL ) +{ + UserMessageBegin( filter, "RadioText" ); + WRITE_BYTE( msg_dest ); + WRITE_BYTE( iClient ); + WRITE_STRING( msg_name ); + + if ( param1 ) + WRITE_STRING( param1 ); + else + WRITE_STRING( "" ); + + if ( param2 ) + WRITE_STRING( param2 ); + else + WRITE_STRING( "" ); + + if ( param3 ) + WRITE_STRING( param3 ); + else + WRITE_STRING( "" ); + + if ( param4 ) + WRITE_STRING( param4 ); + else + WRITE_STRING( "" ); + + MessageEnd(); +} + +void CCSPlayer::ConstructRadioFilter( CRecipientFilter& filter ) +{ + filter.MakeReliable(); + + int localTeam = GetTeamNumber(); + + int i; + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSPlayer *player = static_cast<CCSPlayer *>( UTIL_PlayerByIndex( i ) ); + if ( !player ) + continue; + + // Skip players ignoring the radio + if ( player->m_bIgnoreRadio ) + continue; + + if( player->GetTeamNumber() == TEAM_SPECTATOR ) + { + // add spectators + if( player->m_iObserverMode == OBS_MODE_IN_EYE || player->m_iObserverMode == OBS_MODE_CHASE ) + { + filter.AddRecipient( player ); + } + } + else if( player->GetTeamNumber() == localTeam ) + { + // add teammates + filter.AddRecipient( player ); + } + } +} + +void CCSPlayer::Radio( const char *pszRadioSound, const char *pszRadioText ) +{ + if( !IsAlive() ) + return; + + if ( IsObserver() ) + return; + + CRecipientFilter filter; + ConstructRadioFilter( filter ); + + if( pszRadioText ) + { + const char *pszLocationText = CSGameRules()->GetChatLocation( true, this ); + if ( pszLocationText && *pszLocationText ) + { + UTIL_CSRadioMessage( filter, entindex(), HUD_PRINTTALK, "#Game_radio_location", GetPlayerName(), pszLocationText, pszRadioText ); + } + else + { + UTIL_CSRadioMessage( filter, entindex(), HUD_PRINTTALK, "#Game_radio", GetPlayerName(), pszRadioText ); + } + } + + UserMessageBegin ( filter, "SendAudio" ); + WRITE_STRING( pszRadioSound ); + MessageEnd(); + + //icon over the head for teammates + TE_RadioIcon( filter, 0.0, this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Outputs currently connected players to the console +//----------------------------------------------------------------------------- +void CCSPlayer::ListPlayers() +{ + char buf[64]; + for ( int i=1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer *pPlayer = dynamic_cast< CCSPlayer* >( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && !pPlayer->IsDormant() ) + { + if ( pPlayer->IsBot() ) + { + Q_snprintf( buf, sizeof(buf), "B %d : %s", pPlayer->GetUserID(), pPlayer->GetPlayerName() ); + } + else + { + Q_snprintf( buf, sizeof(buf), " %d : %s", pPlayer->GetUserID(), pPlayer->GetPlayerName() ); + } + ClientPrint( this, HUD_PRINTCONSOLE, buf ); + } + } + ClientPrint( this, HUD_PRINTCONSOLE, "\n" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &info - +//----------------------------------------------------------------------------- +void CCSPlayer::OnDamagedByExplosion( const CTakeDamageInfo &info ) +{ + float lastDamage = info.GetDamage(); + + //Adrian - This is hacky since we might have been damaged by something else + //but since the round is ending, who cares. + if ( CSGameRules()->m_bTargetBombed == true ) + return; + + float distanceFromPlayer = 9999.0f; + + CBaseEntity *inflictor = info.GetInflictor(); + if ( inflictor ) + { + Vector delta = GetAbsOrigin() - inflictor->GetAbsOrigin(); + distanceFromPlayer = delta.Length(); + } + + bool shock = lastDamage >= 30.0f; + + if ( !shock ) + return; + + m_applyDeafnessTime = gpGlobals->curtime + 0.3; + m_currentDeafnessFilter = 0; +} + +void CCSPlayer::ApplyDeafnessEffect() +{ + // what's happening here is that the low-pass filter and the oscillator frequency effects need + // to fade in and out slowly. So we have several filters that we switch between to achieve this + // effect. The first 3rd of the total effect will be the "fade in" of the effect. Which means going + // from filter to filter from the first to the last. Then we keep on the "last" filter for another + // third of the total effect time. Then the last third of the time we go back from the last filter + // to the first. Clear as mud? + + // glossary: + // filter: an individual filter as defined in dsp_presets.txt + // section: one of the sections for the total effect, fade in, full, fade out are the possible sections + // effect: the total effect of combining all the sections, the whole of what the player hears from start to finish. + + const int firstGrenadeFilterIndex = 137; + const int lastGrenadeFilterIndex = 139; + const float grenadeEffectLengthInSecs = 4.5f; // time of the total effect + const float fadeInSectionTime = 0.1f; + const float fadeOutSectionTime = 1.5f; + + const float timeForEachFilterInFadeIn = fadeInSectionTime / (lastGrenadeFilterIndex - firstGrenadeFilterIndex); + const float timeForEachFilterInFadeOut = fadeOutSectionTime / (lastGrenadeFilterIndex - firstGrenadeFilterIndex); + + float timeIntoEffect = gpGlobals->curtime - m_applyDeafnessTime; + + if (timeIntoEffect >= grenadeEffectLengthInSecs) + { + // the effect is done, so reset the deafness variables. + m_applyDeafnessTime = 0.0f; + m_currentDeafnessFilter = 0; + return; + } + + int section = 0; + + if (timeIntoEffect < fadeInSectionTime) + { + section = 0; + } + else if (timeIntoEffect < (grenadeEffectLengthInSecs - fadeOutSectionTime)) + { + section = 1; + } + else + { + section = 2; + } + + int filterToUse = 0; + + if (section == 0) + { + // fade into the effect. + int filterIndex = (int)(timeIntoEffect / timeForEachFilterInFadeIn); + filterToUse = filterIndex += firstGrenadeFilterIndex; + } + else if (section == 1) + { + // in full effect. + filterToUse = lastGrenadeFilterIndex; + } + else if (section == 2) + { + // fade out of the effect + float timeIntoSection = timeIntoEffect - (grenadeEffectLengthInSecs - fadeOutSectionTime); + int filterIndex = (int)(timeIntoSection / timeForEachFilterInFadeOut); + filterToUse = lastGrenadeFilterIndex - filterIndex - 1; + } + + if (filterToUse != m_currentDeafnessFilter) + { + m_currentDeafnessFilter = filterToUse; + + CSingleUserRecipientFilter user( this ); + enginesound->SetPlayerDSP( user, m_currentDeafnessFilter, false ); + } +} + + +void CCSPlayer::NoteWeaponFired() +{ + Assert( m_pCurrentCommand ); + if( m_pCurrentCommand ) + { + m_iLastWeaponFireUsercmd = m_pCurrentCommand->command_number; + } +} + + +bool CCSPlayer::WantsLagCompensationOnEntity( const CBasePlayer *pPlayer, const CUserCmd *pCmd, const CBitVec<MAX_EDICTS> *pEntityTransmitBits ) const +{ + // No need to lag compensate at all if we're not attacking in this command and + // we haven't attacked recently. + if ( !( pCmd->buttons & IN_ATTACK ) && (pCmd->command_number - m_iLastWeaponFireUsercmd > 5) ) + { + if ( ( pCmd->buttons & IN_ATTACK2 ) == 0 ) + return false; + + CWeaponCSBase *weapon = GetActiveCSWeapon(); + if ( !weapon ) + return false; + + if ( weapon->GetWeaponID() != WEAPON_KNIFE ) + return false; // IN_ATTACK2 with WEAPON_KNIFE should do lag compensation + } + + return BaseClass::WantsLagCompensationOnEntity( pPlayer, pCmd, pEntityTransmitBits ); +} + +// Handles the special "radio" alias commands we're creating to accommodate the scripts players use +// ** Returns true if we've handled the command ** +bool HandleRadioAliasCommands( CCSPlayer *pPlayer, const char *pszCommand ) +{ + bool bRetVal = false; + + // don't execute them if we are not alive or are an observer + if( !pPlayer->IsAlive() || pPlayer->IsObserver() ) + return false; + + // Radio1 commands + if ( FStrEq( pszCommand, "coverme" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio1( 1 ); + } + else if ( FStrEq( pszCommand, "takepoint" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio1( 2 ); + } + else if ( FStrEq( pszCommand, "holdpos" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio1( 3 ); + } + else if ( FStrEq( pszCommand, "regroup" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio1( 4 ); + } + else if ( FStrEq( pszCommand, "followme" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio1( 5 ); + } + else if ( FStrEq( pszCommand, "takingfire" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio1( 6 ); + } + // Radio2 commands + else if ( FStrEq( pszCommand, "go" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio2( 1 ); + } + else if ( FStrEq( pszCommand, "fallback" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio2( 2 ); + } + else if ( FStrEq( pszCommand, "sticktog" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio2( 3 ); + } + else if ( FStrEq( pszCommand, "getinpos" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio2( 4 ); + } + else if ( FStrEq( pszCommand, "stormfront" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio2( 5 ); + } + else if ( FStrEq( pszCommand, "report" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio2( 6 ); + } + // Radio3 commands + else if ( FStrEq( pszCommand, "roger" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 1 ); + } + else if ( FStrEq( pszCommand, "enemyspot" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 2 ); + } + else if ( FStrEq( pszCommand, "needbackup" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 3 ); + } + else if ( FStrEq( pszCommand, "sectorclear" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 4 ); + } + else if ( FStrEq( pszCommand, "inposition" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 5 ); + } + else if ( FStrEq( pszCommand, "reportingin" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 6 ); + } + else if ( FStrEq( pszCommand, "getout" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 7 ); + } + else if ( FStrEq( pszCommand, "negative" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 8 ); + } + else if ( FStrEq( pszCommand, "enemydown" ) ) + { + bRetVal = true; + pPlayer->HandleMenu_Radio3( 9 ); + } + + return bRetVal; +} + +bool CCSPlayer::ShouldRunRateLimitedCommand( const CCommand &args ) +{ + const char *pcmd = args[0]; + + int i = m_RateLimitLastCommandTimes.Find( pcmd ); + if ( i == m_RateLimitLastCommandTimes.InvalidIndex() ) + { + m_RateLimitLastCommandTimes.Insert( pcmd, gpGlobals->curtime ); + return true; + } + else if ( (gpGlobals->curtime - m_RateLimitLastCommandTimes[i]) < CS_COMMAND_MAX_RATE ) + { + // Too fast. + return false; + } + else + { + m_RateLimitLastCommandTimes[i] = gpGlobals->curtime; + return true; + } +} + +bool CCSPlayer::ClientCommand( const CCommand &args ) +{ + const char *pcmd = args[0]; + + // Bots mimic our client commands. +/* + if ( bot_mimic.GetInt() && !( GetFlags() & FL_FAKECLIENT ) ) + { + for ( int i=1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer *pPlayer = dynamic_cast< CCSPlayer* >( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer != this && ( pPlayer->GetFlags() & FL_FAKECLIENT ) ) + { + pPlayer->ClientCommand( pcmd ); + } + } + } +*/ + +#if defined ( DEBUG ) + + if ( FStrEq( pcmd, "bot_cmd" ) ) + { + CCSPlayer *pPlayer = dynamic_cast< CCSPlayer* >( UTIL_PlayerByIndex( atoi( args[1] ) ) ); + if ( pPlayer && pPlayer != this && ( pPlayer->GetFlags() & FL_FAKECLIENT ) ) + { + CCommand botArgs( args.ArgC() - 2, &args.ArgV()[2] ); + pPlayer->ClientCommand( botArgs ); + pPlayer->RemoveEffects( EF_NODRAW ); + } + return true; + } + + if ( FStrEq( pcmd, "blind" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + if ( args.ArgC() == 3 ) + { + Blind( atof( args[1] ), atof( args[2] ) ); + } + else + { + ClientPrint( this, HUD_PRINTCONSOLE, "usage: blind holdtime fadetime\n" ); + } + } + return true; + } + + if ( FStrEq( pcmd, "deafen" ) ) + { + Deafen( 0.0f ); + return true; + } + + if ( FStrEq( pcmd, "he_deafen" ) ) + { + m_applyDeafnessTime = gpGlobals->curtime + 0.3; + m_currentDeafnessFilter = 0; + return true; + } + + if ( FStrEq( pcmd, "hint_reset" ) ) + { + m_iDisplayHistoryBits = 0; + return true; + } + + if ( FStrEq( pcmd, "punch" ) ) + { + float flDamage = 100; + + QAngle punchAngle = GetPunchAngle(); + + punchAngle.x = flDamage * random->RandomFloat ( -0.15, 0.15 ); + punchAngle.y = flDamage * random->RandomFloat ( -0.15, 0.15 ); + punchAngle.z = flDamage * random->RandomFloat ( -0.15, 0.15 ); + + clamp( punchAngle.x, -4, punchAngle.x ); + clamp( punchAngle.y, -5, 5 ); + clamp( punchAngle.z, -5, 5 ); + + // +y == down + // +x == left + // +z == roll clockwise + if ( args.ArgC() == 4 ) + { + punchAngle.x = atof(args[1]); + punchAngle.y = atof(args[2]); + punchAngle.z = atof(args[3]); + } + + SetPunchAngle( punchAngle ); + + return true; + } + +#endif //DEBUG + + if ( FStrEq( pcmd, "jointeam" ) ) + { + if ( args.ArgC() < 2 ) + { + Warning( "Player sent bad jointeam syntax\n" ); + } + + if ( ShouldRunRateLimitedCommand( args ) ) + { + int iTeam = atoi( args[1] ); + HandleCommand_JoinTeam( iTeam ); + } + return true; + } + else if ( FStrEq( pcmd, "spectate" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + // instantly join spectators + HandleCommand_JoinTeam( TEAM_SPECTATOR ); + } + return true; + } + else if ( FStrEq( pcmd, "joingame" ) ) + { + // player just closed MOTD dialog + if ( m_iPlayerState == STATE_WELCOME ) + { + State_Transition( STATE_PICKINGTEAM ); + } + + return true; + } + else if ( FStrEq( pcmd, "joinclass" ) ) + { + if ( args.ArgC() < 2 ) + { + Warning( "Player sent bad joinclass syntax\n" ); + } + + if ( ShouldRunRateLimitedCommand( args ) ) + { + int iClass = atoi( args[1] ); + HandleCommand_JoinClass( iClass ); + } + return true; + } + else if ( FStrEq( pcmd, "drop" ) ) + { + CWeaponCSBase *pWeapon = dynamic_cast< CWeaponCSBase* >( GetActiveWeapon() ); + + if( pWeapon ) + { + //============================================================================= + // HPE_BEGIN: + // [dwenger] Determine value of dropped item. + //============================================================================= + + if ( !pWeapon->IsAPriorOwner( this ) ) + { + pWeapon->AddToPriorOwnerList( this ); + + CCS_GameStats.IncrementStat(this, CSTAT_ITEMS_DROPPED_VALUE, pWeapon->GetCSWpnData().GetWeaponPrice()); + } + + //============================================================================= + // HPE_END + //============================================================================= + + CSWeaponType type = pWeapon->GetCSWpnData().m_WeaponType; + + if( type != WEAPONTYPE_KNIFE && type != WEAPONTYPE_GRENADE ) + { + if (CSGameRules()->GetCanDonateWeapon() && !pWeapon->GetDonated()) + { + pWeapon->SetDonated(true); + pWeapon->SetDonor(this); + } + CSWeaponDrop( pWeapon, true, true ); + + } + } + + return true; + } + else if ( FStrEq( pcmd, "buy" ) ) + { + BuyResult_e result = BUY_INVALID_ITEM; + if ( args.ArgC() == 2 ) + { + result = HandleCommand_Buy( args[1] ); + } + if ( result == BUY_INVALID_ITEM ) + { + // Print out a message on the console + int msg_dest = HUD_PRINTCONSOLE; + + ClientPrint( this, msg_dest, "usage: buy <item>\n" ); + ClientPrint( this, msg_dest, " primammo\n" ); + ClientPrint( this, msg_dest, " secammo\n" ); + ClientPrint( this, msg_dest, " vest\n" ); + ClientPrint( this, msg_dest, " vesthelm\n" ); + ClientPrint( this, msg_dest, " defuser\n" ); + //ClientPrint( this, msg_dest, " shield\n" ); + ClientPrint( this, msg_dest, " nvgs\n" ); + ClientPrint( this, msg_dest, " flashbang\n" ); + ClientPrint( this, msg_dest, " hegrenade\n" ); + ClientPrint( this, msg_dest, " smokegrenade\n" ); + ClientPrint( this, msg_dest, " galil\n" ); + ClientPrint( this, msg_dest, " ak47\n" ); + ClientPrint( this, msg_dest, " scout\n" ); + ClientPrint( this, msg_dest, " sg552\n" ); + ClientPrint( this, msg_dest, " awp\n" ); + ClientPrint( this, msg_dest, " g3sg1\n" ); + ClientPrint( this, msg_dest, " famas\n" ); + ClientPrint( this, msg_dest, " m4a1\n" ); + ClientPrint( this, msg_dest, " aug\n" ); + ClientPrint( this, msg_dest, " sg550\n" ); + ClientPrint( this, msg_dest, " glock\n" ); + ClientPrint( this, msg_dest, " usp\n" ); + ClientPrint( this, msg_dest, " p228\n" ); + ClientPrint( this, msg_dest, " deagle\n" ); + ClientPrint( this, msg_dest, " elite\n" ); + ClientPrint( this, msg_dest, " fiveseven\n" ); + ClientPrint( this, msg_dest, " m3\n" ); + ClientPrint( this, msg_dest, " xm1014\n" ); + ClientPrint( this, msg_dest, " mac10\n" ); + ClientPrint( this, msg_dest, " tmp\n" ); + ClientPrint( this, msg_dest, " mp5navy\n" ); + ClientPrint( this, msg_dest, " ump45\n" ); + ClientPrint( this, msg_dest, " p90\n" ); + ClientPrint( this, msg_dest, " m249\n" ); + } + + return true; + } + else if ( FStrEq( pcmd, "buyammo1" ) ) + { + AttemptToBuyAmmoSingle(0); + return true; + } + else if ( FStrEq( pcmd, "buyammo2" ) ) + { + AttemptToBuyAmmoSingle(1); + return true; + } + else if ( FStrEq( pcmd, "nightvision" ) ) + { + if ( ShouldRunRateLimitedCommand( args ) ) + { + if( m_bHasNightVision ) + { + if( m_bNightVisionOn ) + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Player.NightVisionOff" ); + } + else + { + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "Player.NightVisionOn" ); + } + + m_bNightVisionOn = !m_bNightVisionOn; + } + } + return true; + } + else if ( FStrEq( pcmd, "menuselect" ) ) + { + return true; + } + else if ( HandleRadioAliasCommands( this, pcmd ) ) + { + return true; + } + else if ( FStrEq( pcmd, "listplayers" ) ) + { + ListPlayers(); + return true; + } + + else if ( FStrEq( pcmd, "ignorerad" ) ) + { + m_bIgnoreRadio = !m_bIgnoreRadio; + if ( m_bIgnoreRadio ) + { + ClientPrint( this, HUD_PRINTTALK, "#Ignore_Radio" ); + } + else + { + ClientPrint( this, HUD_PRINTTALK, "#Accept_Radio" ); + } + return true; + } + else if ( FStrEq( pcmd, "become_vip" ) ) + { + //MIKETODO: VIP mode + /* + if ( ( CSGameRules()->m_iMapHasVIPSafetyZone == 1 ) && ( m_iTeam == TEAM_CT ) ) + { + mp->AddToVIPQueue( this ); + } + */ + return true; + } + + return BaseClass::ClientCommand( args ); +} + + +// returns true if the selection has been handled and the player's menu +// can be closed...false if the menu should be displayed again +bool CCSPlayer::HandleCommand_JoinTeam( int team ) +{ + CCSGameRules *mp = CSGameRules(); + + if ( !GetGlobalTeam( team ) ) + { + DevWarning( "HandleCommand_JoinTeam( %d ) - invalid team index.\n", team ); + return false; + } + + // If this player is a VIP, don't allow him to switch teams/appearances unless the following conditions are met : + // a) There is another TEAM_CT player who is in the queue to be a VIP + // b) This player is dead + + //MIKETODO: handle this when doing VIP mode + /* + if ( m_bIsVIP == true ) + { + if ( !IsDead() ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Switch_From_VIP" ); + MenuReset(); + return true; + } + else if ( mp->IsVIPQueueEmpty() == true ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Switch_From_VIP" ); + MenuReset(); + return true; + } + } + + //MIKETODO: VIP mode + + case 3: + if ( ( mp->m_iMapHasVIPSafetyZone == 1 ) && ( m_iTeam == TEAM_CT ) ) + { + mp->AddToVIPQueue( player ); + MenuReset(); + return true; + } + else + { + return false; + } + break; + */ + + // If we already died and changed teams once, deny + if( m_bTeamChanged && team != m_iOldTeam && team != TEAM_SPECTATOR ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Only_1_Team_Change" ); + return true; + } + + // check if we're limited in our team selection + if ( team == TEAM_UNASSIGNED && !IsBot() ) + { + team = mp->GetHumanTeam(); // returns TEAM_UNASSIGNED if we're unrestricted + } + + if ( team == TEAM_UNASSIGNED ) + { + // Attempt to auto-select a team, may set team to T, CT or SPEC + team = mp->SelectDefaultTeam( !IsBot() ); + + if ( team == TEAM_UNASSIGNED ) + { + // still team unassigned, try to kick a bot if possible + + // kick a bot to allow human to join + if (cv_bot_auto_vacate.GetBool() && !IsBot()) + { + team = (random->RandomInt( 0, 1 ) == 0) ? TEAM_TERRORIST : TEAM_CT; + if (UTIL_KickBotFromTeam( team ) == false) + { + // no bots on that team, try the other + team = (team == TEAM_CT) ? TEAM_TERRORIST : TEAM_CT; + if (UTIL_KickBotFromTeam( team ) == false) + { + // couldn't kick any bots, fail + team = TEAM_UNASSIGNED; + } + } + } + + if (team == TEAM_UNASSIGNED) + { + ClientPrint( this, HUD_PRINTCENTER, "#All_Teams_Full" ); + ShowViewPortPanel( PANEL_TEAM ); + return false; + } + } + } + + if ( team == GetTeamNumber() ) + { + // Let people change class (skin) by re-joining the same team + if ( GetTeamNumber() == TEAM_TERRORIST && TerroristPlayerModels.Count() > 1 ) + { + ShowViewPortPanel( PANEL_CLASS_TER ); + } + else if ( GetTeamNumber() == TEAM_CT && CTPlayerModels.Count() > 1 ) + { + ShowViewPortPanel( PANEL_CLASS_CT ); + } + return true; // we wouldn't change the team + } + + if ( mp->TeamFull( team ) ) + { + // attempt to kick a bot to make room for this player + bool madeRoom = false; + if (cv_bot_auto_vacate.GetBool() && !IsBot()) + { + if (UTIL_KickBotFromTeam( team )) + madeRoom = true; + } + + if (!madeRoom) + { + if ( team == TEAM_TERRORIST ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Terrorists_Full" ); + } + else if ( team == TEAM_CT ) + { + ClientPrint( this, HUD_PRINTCENTER, "#CTs_Full" ); + } + + ShowViewPortPanel( PANEL_TEAM ); + return false; + } + } + + // check if humans are restricted to a single team (Tour of Duty, etc) + if ( !IsBot() && team != TEAM_SPECTATOR) + { + int humanTeam = mp->GetHumanTeam(); + if ( humanTeam != TEAM_UNASSIGNED && humanTeam != team ) + { + if ( humanTeam == TEAM_TERRORIST ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Humans_Join_Team_T" ); + } + else if ( humanTeam == TEAM_CT ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Humans_Join_Team_CT" ); + } + + ShowViewPortPanel( PANEL_TEAM ); + return false; + } + } + + if ( team == TEAM_SPECTATOR ) + { + // Prevent this is the cvar is set + if ( !mp_allowspectators.GetInt() && !IsHLTV() ) + { + ClientPrint( this, HUD_PRINTCENTER, "#Cannot_Be_Spectator" ); + return false; + } + + if ( GetTeamNumber() != TEAM_UNASSIGNED && State_Get() == STATE_ACTIVE ) + { + m_fNextSuicideTime = gpGlobals->curtime; // allow the suicide to work + + CommitSuicide(); + + // add 1 to frags to balance out the 1 subtracted for killing yourself + IncrementFragCount( 1 ); + } + + ChangeTeam( TEAM_SPECTATOR ); + m_iClass = (int)CS_CLASS_NONE; + + if ( !(m_iDisplayHistoryBits & DHF_SPEC_DUCK) ) + { + m_iDisplayHistoryBits |= DHF_SPEC_DUCK; + HintMessage( "#Spec_Duck", true, true ); + } + + // do we have fadetoblack on? (need to fade their screen back in) + if ( mp_fadetoblack.GetBool() ) + { + color32_s clr = { 0,0,0,255 }; + UTIL_ScreenFade( this, clr, 0, 0, FFADE_IN | FFADE_PURGE ); + } + + return true; + } + + // If the code gets this far, the team is not TEAM_UNASSIGNED + + + if (mp->TeamStacked( team, GetTeamNumber() ))//players are allowed to change to their own team so they can just change their model + { + // attempt to kick a bot to make room for this player + bool madeRoom = false; + if (cv_bot_auto_vacate.GetBool() && !IsBot()) + { + if (UTIL_KickBotFromTeam( team )) + madeRoom = true; + } + + if (!madeRoom) + { + // The specified team is full + ClientPrint( + this, + HUD_PRINTCENTER, + ( team == TEAM_TERRORIST ) ? "#Too_Many_Terrorists" : "#Too_Many_CTs" ); + + ShowViewPortPanel( PANEL_TEAM ); + return false; + } + } + + // Show the appropriate Choose Appearance menu + // This must come before ClientKill() for CheckWinConditions() to function properly + + // Switch their actual team... + ChangeTeam( team ); + + return true; +} + + +bool CCSPlayer::HandleCommand_JoinClass( int iClass ) +{ + if( iClass == CS_CLASS_NONE ) + { + // User choosed random class + switch ( GetTeamNumber() ) + { + case TEAM_TERRORIST : iClass = RandomInt(FIRST_T_CLASS, LAST_T_CLASS); + break; + + case TEAM_CT : iClass = RandomInt(FIRST_CT_CLASS, LAST_CT_CLASS); + break; + + default : iClass = CS_CLASS_NONE; + break; + } + } + + // clamp to valid classes + switch ( GetTeamNumber() ) + { + case TEAM_TERRORIST: + iClass = clamp( iClass, FIRST_T_CLASS, LAST_T_CLASS ); + break; + case TEAM_CT: + iClass = clamp( iClass, FIRST_CT_CLASS, LAST_CT_CLASS ); + break; + default: + iClass = CS_CLASS_NONE; + } + + // Reset the player's state + if ( State_Get() == STATE_ACTIVE ) + { + CSGameRules()->CheckWinConditions(); + } + + if ( !IsBot() && State_Get() == STATE_ACTIVE ) // Bots are responsible about only switching classes when they join. + { + // Kill player if switching classes while alive. + // This mimics goldsrc CS 1.6, and prevents a player from hiding, and switching classes to + // make the opposing team think there are more enemies than there really are. + CommitSuicide(); + } + + m_iClass = iClass; + + if (State_Get() == STATE_PICKINGCLASS) + { +// SetModelFromClass(); + GetIntoGame(); + } + + return true; +} + + +/* +void CheckStartMoney( void ) +{ + if ( mp_startmoney.GetInt() > 16000 ) + { + mp_startmoney.SetInt( 16000 ); + } + else if ( mp_startmoney.GetInt() < 800 ) + { + mp_startmoney.SetInt( 800 ); + } +} +*/ + +void CCSPlayer::GetIntoGame() +{ + // Set their model and if they're allowed to spawn right now, put them into the world. + //SetPlayerModel( iClass ); + + SetFOV( this, 0 ); + m_flLastMovement = gpGlobals->curtime; + + CCSGameRules *MPRules = CSGameRules(); + +/* //MIKETODO: Escape gameplay ? + if ( ( MPRules->m_bMapHasEscapeZone == true ) && ( m_iTeam == TEAM_CT ) ) + { + m_iAccount = 0; + + CheckStartMoney(); + AddAccount( (int)startmoney.value, true ); + } + */ + + + //****************New Code by SupraFiend************ + if ( !MPRules->FPlayerCanRespawn( this ) ) + { + // This player is joining in the middle of a round or is an observer. Put them directly into observer mode. + //pev->deadflag = DEAD_RESPAWNABLE; + //pev->classname = MAKE_STRING("player"); + //pev->flags &= ( FL_PROXY | FL_FAKECLIENT ); // clear flags, but keep proxy and bot flags that might already be set + //pev->flags |= FL_CLIENT | FL_SPECTATOR; + //SetThink(PlayerDeathThink); + if ( !(m_iDisplayHistoryBits & DHF_SPEC_DUCK) ) + { + m_iDisplayHistoryBits |= DHF_SPEC_DUCK; + HintMessage( "#Spec_Duck", true, true ); + } + + State_Transition( STATE_OBSERVER_MODE ); + + m_wasNotKilledNaturally = true; + + MPRules->CheckWinConditions(); + } + else// else spawn them right in + { + State_Transition( STATE_ACTIVE ); + + Spawn(); + + MPRules->CheckWinConditions(); + + //============================================================================= + // HPE_BEGIN: + // [menglish] Have the rules update anything related to a player spawning in late + //============================================================================= + + MPRules->SpawningLatePlayer(this); + + //============================================================================= + // HPE_END + //============================================================================= + + if( MPRules->m_flRestartRoundTime == 0.0f ) + { + //Bomb target, no bomber and no bomb lying around. + if( MPRules->IsBombDefuseMap() && !MPRules->IsThereABomber() && !MPRules->IsThereABomb() ) + MPRules->GiveC4(); //Checks for terrorists. + } + + // If a new terrorist is entering the fray, then up the # of potential escapers. + if ( GetTeamNumber() == TEAM_TERRORIST ) + MPRules->m_iNumEscapers++; + + //============================================================================= + // HPE_BEGIN: + // [menglish] Reset Round Based Achievement Variables + //============================================================================= + + ResetRoundBasedAchievementVariables(); + + //============================================================================= + // HPE_END + //============================================================================= + + } +} + + +int CCSPlayer::PlayerClass() const +{ + return m_iClass; +} + + + +bool CCSPlayer::SelectSpawnSpot( const char *pEntClassName, CBaseEntity* &pSpot ) +{ + // Find the next spawn spot. + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + + if ( pSpot == NULL ) // skip over the null point + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + + CBaseEntity *pFirstSpot = pSpot; + do + { + if ( pSpot ) + { + // check if pSpot is valid + if ( g_pGameRules->IsSpawnPointValid( pSpot, this ) ) + { + if ( pSpot->GetAbsOrigin() == Vector( 0, 0, 0 ) ) + { + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + continue; + } + + // if so, go to pSpot + return true; + } + } + // increment pSpot + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + } while ( pSpot != pFirstSpot ); // loop if we're not back to the start + + DevMsg("CCSPlayer::SelectSpawnSpot: couldn't find valid spawn point.\n"); + + return true; +} + + +CBaseEntity* CCSPlayer::EntSelectSpawnPoint() +{ + CBaseEntity *pSpot; + + /* MIKETODO: VIP + // VIP spawn point ************* + if ( ( g_pGameRules->IsDeathmatch() ) && ( ((CBasePlayer*)pPlayer)->m_bIsVIP == TRUE) ) + { + //ALERT (at_console,"Looking for a VIP spawn point\n"); + // Randomize the start spot + //for ( int i = RANDOM_LONG(1,5); i > 0; i-- ) + pSpot = UTIL_FindEntityByClassname( NULL, "info_vip_start" ); + if ( !FNullEnt( pSpot ) ) // skip over the null point + goto ReturnSpot; + else + goto CTSpawn; + } + + // + // the counter-terrorist spawns at "info_player_start" + else + */ + + pSpot = NULL; + if ( CSGameRules()->IsLogoMap() ) + { + // This is a logo map. Don't allow movement or logos or menus. + SelectSpawnSpot( "info_player_logo", pSpot ); + LockPlayerInPlace(); + goto ReturnSpot; + } + else + { + if ( GetTeamNumber() == TEAM_CT ) + { + pSpot = g_pLastCTSpawn; + if ( SelectSpawnSpot( "info_player_counterterrorist", pSpot )) + { + + g_pLastCTSpawn = pSpot; + goto ReturnSpot; + } + } + + /*********************************************************/ + // The terrorist spawn points + else if ( GetTeamNumber() == TEAM_TERRORIST ) + { + pSpot = g_pLastTerroristSpawn; + + if ( SelectSpawnSpot( "info_player_terrorist", pSpot ) ) + { + g_pLastTerroristSpawn = pSpot; + goto ReturnSpot; + } + } + } + + + // If startspot is set, (re)spawn there. + if ( !gpGlobals->startspot || !strlen(STRING(gpGlobals->startspot))) + { + pSpot = gEntList.FindEntityByClassname(NULL, "info_player_terrorist"); + if ( pSpot ) + goto ReturnSpot; + } + else + { + pSpot = gEntList.FindEntityByTarget( NULL, STRING(gpGlobals->startspot) ); + if ( pSpot ) + goto ReturnSpot; + } + +ReturnSpot: + if ( !pSpot ) + { + if( CSGameRules()->IsLogoMap() ) + Warning( "PutClientInServer: no info_player_logo on level\n" ); + else + Warning( "PutClientInServer: no info_player_start on level\n" ); + + return CBaseEntity::Instance( INDEXENT(0) ); + } + + return pSpot; +} + + +void CCSPlayer::SetProgressBarTime( int barTime ) +{ + m_iProgressBarDuration = barTime; + m_flProgressBarStartTime = this->m_flSimulationTime; +} + + +void CCSPlayer::PlayerDeathThink() +{ +} + + +void CCSPlayer::State_Transition( CSPlayerState newState ) +{ + State_Leave(); + State_Enter( newState ); +} + + +void CCSPlayer::State_Enter( CSPlayerState newState ) +{ + m_iPlayerState = newState; + m_pCurStateInfo = State_LookupInfo( newState ); + + if ( cs_ShowStateTransitions.GetInt() == -1 || cs_ShowStateTransitions.GetInt() == entindex() ) + { + if ( m_pCurStateInfo ) + Msg( "ShowStateTransitions: entering '%s'\n", m_pCurStateInfo->m_pStateName ); + else + Msg( "ShowStateTransitions: entering #%d\n", newState ); + } + + // Initialize the new state. + if ( m_pCurStateInfo && m_pCurStateInfo->pfnEnterState ) + (this->*m_pCurStateInfo->pfnEnterState)(); +} + + +void CCSPlayer::State_Leave() +{ + if ( m_pCurStateInfo && m_pCurStateInfo->pfnLeaveState ) + { + (this->*m_pCurStateInfo->pfnLeaveState)(); + } +} + + +void CCSPlayer::State_PreThink() +{ + if ( m_pCurStateInfo && m_pCurStateInfo->pfnPreThink ) + { + (this->*m_pCurStateInfo->pfnPreThink)(); + } +} + + +CCSPlayerStateInfo* CCSPlayer::State_LookupInfo( CSPlayerState state ) +{ + // This table MUST match the + static CCSPlayerStateInfo playerStateInfos[] = + { + { STATE_ACTIVE, "STATE_ACTIVE", &CCSPlayer::State_Enter_ACTIVE, NULL, &CCSPlayer::State_PreThink_ACTIVE }, + { STATE_WELCOME, "STATE_WELCOME", &CCSPlayer::State_Enter_WELCOME, NULL, &CCSPlayer::State_PreThink_WELCOME }, + { STATE_PICKINGTEAM, "STATE_PICKINGTEAM", &CCSPlayer::State_Enter_PICKINGTEAM, NULL, &CCSPlayer::State_PreThink_OBSERVER_MODE }, + { STATE_PICKINGCLASS, "STATE_PICKINGCLASS", &CCSPlayer::State_Enter_PICKINGCLASS, NULL, &CCSPlayer::State_PreThink_OBSERVER_MODE }, + { STATE_DEATH_ANIM, "STATE_DEATH_ANIM", &CCSPlayer::State_Enter_DEATH_ANIM, NULL, &CCSPlayer::State_PreThink_DEATH_ANIM }, + { STATE_DEATH_WAIT_FOR_KEY, "STATE_DEATH_WAIT_FOR_KEY", &CCSPlayer::State_Enter_DEATH_WAIT_FOR_KEY, NULL, &CCSPlayer::State_PreThink_DEATH_WAIT_FOR_KEY }, + { STATE_OBSERVER_MODE, "STATE_OBSERVER_MODE", &CCSPlayer::State_Enter_OBSERVER_MODE, NULL, &CCSPlayer::State_PreThink_OBSERVER_MODE } + }; + + for ( int i=0; i < ARRAYSIZE( playerStateInfos ); i++ ) + { + if ( playerStateInfos[i].m_iPlayerState == state ) + return &playerStateInfos[i]; + } + + return NULL; +} + + +void CCSPlayer::PhysObjectSleep() +{ + IPhysicsObject *pObj = VPhysicsGetObject(); + if ( pObj ) + pObj->Sleep(); +} + + +void CCSPlayer::PhysObjectWake() +{ + IPhysicsObject *pObj = VPhysicsGetObject(); + if ( pObj ) + pObj->Wake(); +} + + +void CCSPlayer::State_Enter_WELCOME() +{ + StartObserverMode( OBS_MODE_ROAMING ); + + // Important to set MOVETYPE_NONE or our physics object will fall while we're sitting at one of the intro cameras. + SetMoveType( MOVETYPE_NONE ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + PhysObjectSleep(); + + const ConVar *hostname = cvar->FindVar( "hostname" ); + const char *title = (hostname) ? hostname->GetString() : "MESSAGE OF THE DAY"; + + // Show info panel (if it's not a simple demo map). + if ( !CSGameRules()->IsLogoMap() ) + { + if ( CommandLine()->FindParm( "-makereslists" ) ) // don't show the MOTD when making reslists + { + engine->ClientCommand( edict(), "jointeam 3\n" ); + } + else + { + KeyValues *data = new KeyValues("data"); + data->SetString( "title", title ); // info panel title + data->SetString( "type", "1" ); // show userdata from stringtable entry + data->SetString( "msg", "motd" ); // use this stringtable entry + data->SetInt( "cmd", TEXTWINDOW_CMD_JOINGAME ); // exec this command if panel closed + data->SetBool( "unload", sv_motd_unload_on_dismissal.GetBool() ); + + ShowViewPortPanel( PANEL_INFO, true, data ); + + data->deleteThis(); + } + } +} + + +void CCSPlayer::State_PreThink_WELCOME() +{ + // Verify some state. + Assert( IsSolidFlagSet( FSOLID_NOT_SOLID ) ); + Assert( GetAbsVelocity().Length() == 0 ); + + // Update whatever intro camera it's at. + if( m_pIntroCamera && (gpGlobals->curtime >= m_fIntroCamTime) ) + { + MoveToNextIntroCamera(); + } +} + + +void CCSPlayer::State_Enter_PICKINGTEAM() +{ + ShowViewPortPanel( "team" ); // show the team menu +} + + +void CCSPlayer::State_Enter_DEATH_ANIM() +{ + if ( HasWeapons() ) + { + // we drop the guns here because weapons that have an area effect and can kill their user + // will sometimes crash coming back from CBasePlayer::Killed() if they kill their owner because the + // player class sometimes is freed. It's safer to manipulate the weapons once we know + // we aren't calling into any of their code anymore through the player pointer. + PackDeadPlayerItems(); + } + + // Used for a timer. + m_flDeathTime = gpGlobals->curtime; + + m_bAbortFreezeCam = false; + + StartObserverMode( OBS_MODE_DEATHCAM ); // go to observer mode + RemoveEffects( EF_NODRAW ); // still draw player body + + if ( mp_fadetoblack.GetBool() ) + { + color32_s clr = {0,0,0,255}; + UTIL_ScreenFade( this, clr, 3, 3, FFADE_OUT | FFADE_STAYOUT ); + //Don't perform any freezecam stuff if we are fading to black + State_Transition( STATE_DEATH_WAIT_FOR_KEY ); + } +} + + +//============================================================================= +// HPE_BEGIN: +// [menglish, pfreese] Added freeze cam logic +//============================================================================= + +void CCSPlayer::State_PreThink_DEATH_ANIM() +{ + // If the anim is done playing, go to the next state (waiting for a keypress to + // either respawn the guy or put him into observer mode). + if ( GetFlags() & FL_ONGROUND ) + { + float flForward = GetAbsVelocity().Length() - 20; + if (flForward <= 0) + { + SetAbsVelocity( vec3_origin ); + } + else + { + Vector vAbsVel = GetAbsVelocity(); + VectorNormalize( vAbsVel ); + vAbsVel *= flForward; + SetAbsVelocity( vAbsVel ); + } + } + + float fDeathEnd = m_flDeathTime + CS_DEATH_ANIMATION_TIME; + float fFreezeEnd = fDeathEnd + spec_freeze_traveltime.GetFloat() + spec_freeze_time.GetFloat(); + + // transition to Freezecam mode once the death animation is complete + if ( gpGlobals->curtime >= fDeathEnd ) + { + if ( GetObserverTarget() && GetObserverTarget() != this && + !m_bAbortFreezeCam && gpGlobals->curtime < fFreezeEnd && GetObserverMode() != OBS_MODE_FREEZECAM) + { + StartObserverMode( OBS_MODE_FREEZECAM ); + } + else if(GetObserverMode() == OBS_MODE_FREEZECAM) + { + if ( m_bAbortFreezeCam && !mp_fadetoblack.GetBool() ) + { + State_Transition( STATE_OBSERVER_MODE ); + } + } + } + + // Don't transfer to observer state until the freeze cam is done + if ( gpGlobals->curtime < fFreezeEnd ) + return; + + State_Transition( STATE_OBSERVER_MODE ); +} + +//============================================================================= +// HPE_END +//============================================================================= + + +void CCSPlayer::State_Enter_DEATH_WAIT_FOR_KEY() +{ + // Remember when we died, so we can automatically put them into observer mode + // if they don't hit a key soon enough. + + m_lifeState = LIFE_DEAD; + + StopAnimation(); + + // Don't do this. The ragdoll system expects to be able to read from this player on + // the next update and will read it at the new origin if this is set. + // Since it is more complicated to redesign the ragdoll system to not need that data + // it is easier to cause a less obvious bug than popping ragdolls + //AddEffects( EF_NOINTERP ); +} + + +void CCSPlayer::State_PreThink_DEATH_WAIT_FOR_KEY() +{ + // once we're done animating our death and we're on the ground, we want to set movetype to None so our dead body won't do collisions and stuff anymore + // this prevents a bug where the dead body would go to a player's head if he walked over it while the dead player was clicking their button to respawn + if ( GetMoveType() != MOVETYPE_NONE && (GetFlags() & FL_ONGROUND) ) + SetMoveType( MOVETYPE_NONE ); + + // if the player has been dead for one second longer than allowed by forcerespawn, + // forcerespawn isn't on. Send the player off to an intermission camera until they + // choose to respawn. + + bool fAnyButtonDown = (m_nButtons & ~IN_SCORE) != 0; + if ( mp_fadetoblack.GetBool() ) + fAnyButtonDown = false; + + // after a certain amount of time switch to observer mode even if they don't press a key. + if (gpGlobals->curtime >= (m_flDeathTime + DEATH_ANIMATION_TIME + 3.0)) + { + fAnyButtonDown = true; + } + + if ( fAnyButtonDown ) + { + if ( GetObserverTarget() ) + { + StartReplayMode( 8, 8, GetObserverTarget()->entindex() ); + } + + State_Transition( STATE_OBSERVER_MODE ); + } +} + +void CCSPlayer::State_Enter_OBSERVER_MODE() +{ + // do we have fadetoblack on? (need to fade their screen back in) + if ( mp_fadetoblack.GetBool() && mp_forcecamera.GetInt() != OBS_ALLOW_NONE) + { + color32_s clr = { 0,0,0,255 }; + UTIL_ScreenFade( this, clr, 0, 0, FFADE_IN | FFADE_PURGE ); + } + + int observerMode = m_iObserverLastMode; + if ( IsNetClient() ) + { + const char *pIdealMode = engine->GetClientConVarValue( engine->IndexOfEdict( edict() ), "cl_spec_mode" ); + if ( pIdealMode ) + { + int nIdealMode = atoi( pIdealMode ); + + if ( nIdealMode < OBS_MODE_IN_EYE ) + { + nIdealMode = OBS_MODE_IN_EYE; + } + else if ( nIdealMode > OBS_MODE_ROAMING ) + { + nIdealMode = OBS_MODE_ROAMING; + } + + observerMode = nIdealMode; + } + } + + StartObserverMode( observerMode ); + + PhysObjectSleep(); +} + +void CCSPlayer::State_PreThink_OBSERVER_MODE() +{ + // Make sure nobody has changed any of our state. +// Assert( GetMoveType() == MOVETYPE_FLY ); + Assert( m_takedamage == DAMAGE_NO ); + Assert( IsSolidFlagSet( FSOLID_NOT_SOLID ) ); +// Assert( IsEffectActive( EF_NODRAW ) ); + + // Must be dead. + Assert( m_lifeState == LIFE_DEAD ); + Assert( pl.deadflag ); +} + + +void CCSPlayer::State_Enter_PICKINGCLASS() +{ + if ( CommandLine()->FindParm( "-makereslists" ) ) // don't show the menu when making reslists + { + engine->ClientCommand( edict(), "joinclass 0\n" ); + return; + } + + // go to spec mode, if dying keep deathcam + if ( GetObserverMode() == OBS_MODE_DEATHCAM ) + { + StartObserverMode( OBS_MODE_DEATHCAM ); + } + else + { + StartObserverMode( OBS_MODE_FIXED ); + } + + m_iClass = (int)CS_CLASS_NONE; + + PhysObjectSleep(); + + // show the class menu: + if ( GetTeamNumber() == TEAM_TERRORIST && TerroristPlayerModels.Count() > 1 ) + { + ShowViewPortPanel( PANEL_CLASS_TER ); + } + else if ( GetTeamNumber() == TEAM_CT && CTPlayerModels.Count() > 1 ) + { + ShowViewPortPanel( PANEL_CLASS_CT ); + } + else + { + HandleCommand_JoinClass( 0 ); + } +} + +void CCSPlayer::State_Enter_ACTIVE() +{ + SetMoveType( MOVETYPE_WALK ); + RemoveSolidFlags( FSOLID_NOT_SOLID ); + m_Local.m_iHideHUD = 0; + PhysObjectWake(); +} + + +void CCSPlayer::State_PreThink_ACTIVE() +{ + // We only allow noclip here only because noclip is useful for debugging. + // It would be nice if the noclip command set some flag so we could tell that they + // did it intentionally. + if ( IsEFlagSet( EFL_NOCLIP_ACTIVE ) ) + { +// Assert( GetMoveType() == MOVETYPE_NOCLIP ); + } + else + { +// Assert( GetMoveType() == MOVETYPE_WALK ); + } + + Assert( !IsSolidFlagSet( FSOLID_NOT_SOLID ) ); +} + + +void CCSPlayer::Weapon_Equip( CBaseCombatWeapon *pWeapon ) +{ + CWeaponCSBase *pCSWeapon = dynamic_cast< CWeaponCSBase* >( pWeapon ); + if ( pCSWeapon ) + { + // For rifles, pistols, or the knife, drop our old weapon in this slot. + if ( pCSWeapon->GetSlot() == WEAPON_SLOT_RIFLE || + pCSWeapon->GetSlot() == WEAPON_SLOT_PISTOL || + pCSWeapon->GetSlot() == WEAPON_SLOT_KNIFE ) + { + CBaseCombatWeapon *pDropWeapon = Weapon_GetSlot( pCSWeapon->GetSlot() ); + if ( pDropWeapon ) + { + CSWeaponDrop( pDropWeapon, false, true ); + } + } + else if( pCSWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_GRENADE ) + { + //if we already have this weapon, just add the ammo and destroy it + if( Weapon_OwnsThisType( pCSWeapon->GetClassname() ) ) + { + Weapon_EquipAmmoOnly( pWeapon ); + UTIL_Remove( pCSWeapon ); + return; + } + } + + pCSWeapon->SetSolidFlags( FSOLID_NOT_SOLID ); + pCSWeapon->SetOwnerEntity( this ); + } + + BaseClass::Weapon_Equip( pWeapon ); +} + +bool CCSPlayer::Weapon_CanUse( CBaseCombatWeapon *pBaseWeapon ) +{ + CWeaponCSBase *pWeapon = dynamic_cast< CWeaponCSBase* >( pBaseWeapon ); + + if ( pWeapon ) + { + // Don't give weapon_c4 to non-terrorists + if( pWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_C4 && GetTeamNumber() != TEAM_TERRORIST ) + { + return false; + } + } + + return true; +} + +bool CCSPlayer::BumpWeapon( CBaseCombatWeapon *pBaseWeapon ) +{ + CWeaponCSBase *pWeapon = dynamic_cast< CWeaponCSBase* >( pBaseWeapon ); + if ( !pWeapon ) + { + Assert( !pWeapon ); + pBaseWeapon->AddSolidFlags( FSOLID_NOT_SOLID ); + pBaseWeapon->AddEffects( EF_NODRAW ); + Weapon_Equip( pBaseWeapon ); + return true; + } + + CBaseCombatCharacter *pOwner = pWeapon->GetOwner(); + + // Can I have this weapon type? + if ( pOwner || !Weapon_CanUse( pWeapon ) || !g_pGameRules->CanHavePlayerItem( this, pWeapon ) ) + { + extern int gEvilImpulse101; + if ( gEvilImpulse101 ) + { + UTIL_Remove( pWeapon ); + } + return false; + } + + // Even if we already have a grenade in this slot, we can pickup another one if we don't already + // own this type of grenade. + bool bPickupGrenade = ( pWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_GRENADE ); + + + /* + // ---------------------------------------- + // If I already have it just take the ammo + // ---------------------------------------- + if ( !bPickupGrenade && Weapon_SlotOccupied( pWeapon ) ) + { + Weapon_EquipAmmoOnly( pWeapon ); + // Only remove me if I have no ammo left + // Can't just check HasAnyAmmo because if I don't use clips, I want to be removed, + if ( pWeapon->UsesClipsForAmmo1() && pWeapon->HasPrimaryAmmo() ) + return false; + + UTIL_Remove( pWeapon ); + return false; + } + */ + + if ( HasShield() && pWeapon->GetCSWpnData().m_bCanUseWithShield == false ) + return false; + + // Check ammo counts for grenades, and don't try to pick up more grenades than we can carry + if ( bPickupGrenade ) + { + CBaseCombatWeapon *pOwnedGrenade = Weapon_OwnsThisType( pWeapon->GetClassname() ); + + if( pOwnedGrenade ) + { + int numGrenades = 0; + int maxGrenades = 0; + + int ammoIndex = pOwnedGrenade->GetPrimaryAmmoType(); + if( ammoIndex != -1 ) + { + numGrenades = GetAmmoCount( ammoIndex ); + } + maxGrenades = GetAmmoDef()->MaxCarry(ammoIndex); + + if( numGrenades >= maxGrenades ) + { + return false; + } + } + } + + if( bPickupGrenade || !Weapon_SlotOccupied( pWeapon ) ) + { + pWeapon->CheckRespawn(); + + pWeapon->AddSolidFlags( FSOLID_NOT_SOLID ); + pWeapon->AddEffects( EF_NODRAW ); + + CCSPlayer* pDonor = pWeapon->GetDonor(); + if ( pDonor && pDonor != this && pWeapon->GetCSWpnData().GetWeaponPrice() > m_iAccount ) + { + CCS_GameStats.Event_PlayerDonatedWeapon( pDonor ); + } + pWeapon->SetDonor(NULL); + + Weapon_Equip( pWeapon ); + + int iExtraAmmo = pWeapon->GetExtraAmmoCount(); + + if( iExtraAmmo && !bPickupGrenade ) + { + //Find out the index of the ammo + int iAmmoIndex = pWeapon->GetPrimaryAmmoType(); + + if( iAmmoIndex != -1 ) + { + //Remove the extra ammo from the weapon + pWeapon->SetExtraAmmoCount(0); + + //Give it to the player + SetAmmoCount( iExtraAmmo, iAmmoIndex ); + } + } + + IGameEvent * event = gameeventmanager->CreateEvent( "item_pickup" ); + if( event ) + { + const char *weaponName = pWeapon->GetClassname(); + if ( strncmp( weaponName, "weapon_", 7 ) == 0 ) + { + weaponName += 7; + } + event->SetInt( "userid", GetUserID() ); + event->SetString( "item", weaponName ); + gameeventmanager->FireEvent( event ); + } + + return true; + } + + return false; +} + + +void CCSPlayer::ResetStamina( void ) +{ + m_flStamina = 0.0f; +} + +void CCSPlayer::RescueZoneTouch( inputdata_t &inputdata ) +{ + m_bInHostageRescueZone = true; + if ( GetTeamNumber() == TEAM_CT && !(m_iDisplayHistoryBits & DHF_IN_RESCUE_ZONE) ) + { + HintMessage( "#Hint_hostage_rescue_zone", false ); + m_iDisplayHistoryBits |= DHF_IN_RESCUE_ZONE; + } +} + +//------------------------------------------------------------------------------------------ +CON_COMMAND( timeleft, "prints the time remaining in the match" ) +{ + CCSPlayer *pPlayer = ToCSPlayer( UTIL_GetCommandClient() ); + if ( pPlayer && pPlayer->m_iNextTimeCheck >= gpGlobals->curtime ) + { + return; // rate limiting + } + + int iTimeRemaining = (int)CSGameRules()->GetMapRemainingTime(); + + if ( iTimeRemaining < 0 ) + { + if ( pPlayer ) + { + ClientPrint( pPlayer, HUD_PRINTTALK, "#Game_no_timelimit" ); + } + else + { + Msg( "* No Time Limit *\n" ); + } + } + else if ( iTimeRemaining == 0 ) + { + if ( pPlayer ) + { + ClientPrint( pPlayer, HUD_PRINTTALK, "#Game_last_round" ); + } + else + { + Msg( "* Last Round *\n" ); + } + } + else + { + int iMinutes, iSeconds; + iMinutes = iTimeRemaining / 60; + iSeconds = iTimeRemaining % 60; + + char minutes[8]; + char seconds[8]; + + Q_snprintf( minutes, sizeof(minutes), "%d", iMinutes ); + Q_snprintf( seconds, sizeof(seconds), "%2.2d", iSeconds ); + + if ( pPlayer ) + { + ClientPrint( pPlayer, HUD_PRINTTALK, "#Game_timelimit", minutes, seconds ); + } + else + { + Msg( "Time Remaining: %s:%s\n", minutes, seconds ); + } + } + + if ( pPlayer ) + { + pPlayer->m_iNextTimeCheck = gpGlobals->curtime + 1; + } +} + +//------------------------------------------------------------------------------------------ +/** + * Emit given sound that only we can hear + */ +void CCSPlayer::EmitPrivateSound( const char *soundName ) +{ + CSoundParameters params; + if (!GetParametersForSound( soundName, params, NULL )) + return; + + CSingleUserRecipientFilter filter( this ); + EmitSound( filter, entindex(), soundName ); +} + + +//===================== +//Autobuy +//===================== +static void AutoBuy( void ) +{ + CCSPlayer *player = ToCSPlayer( UTIL_GetCommandClient() ); + + if ( player ) + player->AutoBuy(); +} +static ConCommand autobuy( "autobuy", AutoBuy, "Attempt to purchase items with the order listed in cl_autobuy" ); + +//============================================== +//AutoBuy - do the work of deciding what to buy +//============================================== +void CCSPlayer::AutoBuy() +{ + if ( !IsInBuyZone() ) + { + EmitPrivateSound( "BuyPreset.CantBuy" ); + return; + } + + const char *autobuyString = engine->GetClientConVarValue( engine->IndexOfEdict( edict() ), "cl_autobuy" ); + if ( !autobuyString || !*autobuyString ) + { + EmitPrivateSound( "BuyPreset.AlreadyBought" ); + return; + } + + bool boughtPrimary = false, boughtSecondary = false; + + m_bIsInAutoBuy = true; + ParseAutoBuyString(autobuyString, boughtPrimary, boughtSecondary); + m_bIsInAutoBuy = false; + + m_bAutoReload = true; + + //TODO ?: stripped out all the attempts to buy career weapons. + // as we're not porting cs:cz, these were skipped +} + +void CCSPlayer::ParseAutoBuyString(const char *string, bool &boughtPrimary, bool &boughtSecondary) +{ + char command[32]; + int nBuffSize = sizeof(command) - 1; // -1 to leave space for the NULL at the end of the string + const char *c = string; + + if (c == NULL) + { + EmitPrivateSound( "BuyPreset.AlreadyBought" ); + return; + } + + BuyResult_e overallResult = BUY_ALREADY_HAVE; + + // loop through the string of commands, trying each one in turn. + while (*c != 0) + { + int i = 0; + // copy the next word into the command buffer. + while ((*c != 0) && (*c != ' ') && (i < nBuffSize)) + { + command[i] = *(c); + ++c; + ++i; + } + if (*c == ' ') + { + ++c; // skip the space. + } + + command[i] = 0; // terminate the string. + + // clear out any spaces. + i = 0; + while (command[i] != 0) + { + if (command[i] == ' ') + { + command[i] = 0; + break; + } + ++i; + } + + // make sure we actually have a command. + if (strlen(command) == 0) + { + continue; + } + + AutoBuyInfoStruct * commandInfo = GetAutoBuyCommandInfo(command); + + if (ShouldExecuteAutoBuyCommand(commandInfo, boughtPrimary, boughtSecondary)) + { + BuyResult_e result = HandleCommand_Buy( command ); + + overallResult = CombineBuyResults( overallResult, result ); + + // check to see if we actually bought a primary or secondary weapon this time. + PostAutoBuyCommandProcessing(commandInfo, boughtPrimary, boughtSecondary); + } + } + + if ( overallResult == BUY_CANT_AFFORD ) + { + EmitPrivateSound( "BuyPreset.CantBuy" ); + } + else if ( overallResult == BUY_ALREADY_HAVE ) + { + EmitPrivateSound( "BuyPreset.AlreadyBought" ); + } + else if ( overallResult == BUY_BOUGHT ) + { + g_iAutoBuyPurchases++; + } +} + +BuyResult_e CCSPlayer::CombineBuyResults( BuyResult_e prevResult, BuyResult_e newResult ) +{ + if ( newResult == BUY_BOUGHT ) + { + prevResult = BUY_BOUGHT; + } + else if ( prevResult != BUY_BOUGHT && + (newResult == BUY_CANT_AFFORD || newResult == BUY_INVALID_ITEM || newResult == BUY_PLAYER_CANT_BUY ) ) + { + prevResult = BUY_CANT_AFFORD; + } + + return prevResult; +} + +//============================================== +//PostAutoBuyCommandProcessing +//============================================== +void CCSPlayer::PostAutoBuyCommandProcessing(const AutoBuyInfoStruct *commandInfo, bool &boughtPrimary, bool &boughtSecondary) +{ + if (commandInfo == NULL) + { + return; + } + + CBaseCombatWeapon *pPrimary = Weapon_GetSlot( WEAPON_SLOT_RIFLE ); + CBaseCombatWeapon *pSecondary = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + + if ((pPrimary != NULL) && (stricmp(pPrimary->GetClassname(), commandInfo->m_classname) == 0)) + { + // I just bought the gun I was trying to buy. + boughtPrimary = true; + } + else if ((pPrimary == NULL) && ((commandInfo->m_class & AUTOBUYCLASS_SHIELD) == AUTOBUYCLASS_SHIELD) && HasShield()) + { + // the shield is a primary weapon even though it isn't a "real" weapon. + boughtPrimary = true; + } + else if ((pSecondary != NULL) && (stricmp(pSecondary->GetClassname(), commandInfo->m_classname) == 0)) + { + // I just bought the pistol I was trying to buy. + boughtSecondary = true; + } +} + +bool CCSPlayer::ShouldExecuteAutoBuyCommand(const AutoBuyInfoStruct *commandInfo, bool boughtPrimary, bool boughtSecondary) +{ + if (commandInfo == NULL) + { + return false; + } + + if ((boughtPrimary) && ((commandInfo->m_class & AUTOBUYCLASS_PRIMARY) != 0) && ((commandInfo->m_class & AUTOBUYCLASS_AMMO) == 0)) + { + // this is a primary weapon and we already have one. + return false; + } + + if ((boughtSecondary) && ((commandInfo->m_class & AUTOBUYCLASS_SECONDARY) != 0) && ((commandInfo->m_class & AUTOBUYCLASS_AMMO) == 0)) + { + // this is a secondary weapon and we already have one. + return false; + } + + if( commandInfo->m_class & AUTOBUYCLASS_ARMOR && ArmorValue() >= 100 ) + { + return false; + } + + return true; +} + +AutoBuyInfoStruct *CCSPlayer::GetAutoBuyCommandInfo(const char *command) +{ + int i = 0; + AutoBuyInfoStruct *ret = NULL; + AutoBuyInfoStruct *temp = &(g_autoBuyInfo[i]); + + // loop through all the commands till we find the one that matches. + while ((ret == NULL) && (temp->m_class != (AutoBuyClassType)0)) + { + temp = &(g_autoBuyInfo[i]); + ++i; + + if (stricmp(temp->m_command, command) == 0) + { + ret = temp; + } + } + + return ret; +} + +//============================================== +//PostAutoBuyCommandProcessing +//- reorders the tokens in autobuyString based on the order of tokens in the priorityString. +//============================================== +void CCSPlayer::PrioritizeAutoBuyString(char *autobuyString, const char *priorityString) +{ + char newString[256]; + int newStringPos = 0; + char priorityToken[32]; + + if ((priorityString == NULL) || (autobuyString == NULL)) + { + return; + } + + const char *priorityChar = priorityString; + + while (*priorityChar != 0) + { + int i = 0; + + // get the next token from the priority string. + while ((*priorityChar != 0) && (*priorityChar != ' ')) + { + priorityToken[i] = *priorityChar; + ++i; + ++priorityChar; + } + priorityToken[i] = 0; + + // skip spaces + while (*priorityChar == ' ') + { + ++priorityChar; + } + + if (strlen(priorityToken) == 0) + { + continue; + } + + // see if the priority token is in the autobuy string. + // if it is, copy that token to the new string and blank out + // that token in the autobuy string. + char *autoBuyPosition = strstr(autobuyString, priorityToken); + if (autoBuyPosition != NULL) + { + while ((*autoBuyPosition != 0) && (*autoBuyPosition != ' ')) + { + newString[newStringPos] = *autoBuyPosition; + *autoBuyPosition = ' '; + ++newStringPos; + ++autoBuyPosition; + } + + newString[newStringPos++] = ' '; + } + } + + // now just copy anything left in the autobuyString to the new string in the order it's in already. + char *autobuyPosition = autobuyString; + while (*autobuyPosition != 0) + { + // skip spaces + while (*autobuyPosition == ' ') + { + ++autobuyPosition; + } + + // copy the token over to the new string. + while ((*autobuyPosition != 0) && (*autobuyPosition != ' ')) + { + newString[newStringPos] = *autobuyPosition; + ++newStringPos; + ++autobuyPosition; + } + + // add a space at the end. + newString[newStringPos++] = ' '; + } + + // terminate the string. Trailing spaces shouldn't matter. + newString[newStringPos] = 0; + + Q_snprintf(autobuyString, sizeof(autobuyString), "%s", newString); +} + + +//============================================================== +// ReBuy +// system for attempting to buy the weapons you had last round +//============================================================== +static void Rebuy( void ) +{ + CCSPlayer *player = ToCSPlayer( UTIL_GetCommandClient() ); + + if ( player ) + player->Rebuy(); +} +static ConCommand rebuy( "rebuy", Rebuy, "Attempt to repurchase items with the order listed in cl_rebuy" ); + +void CCSPlayer::BuildRebuyStruct() +{ + if (m_bIsInRebuy) + { + // if we are in the middle of a rebuy, we don't want to update the buy struct. + return; + } + + CBaseCombatWeapon *primary = Weapon_GetSlot( WEAPON_SLOT_RIFLE ); + CBaseCombatWeapon *secondary = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + + // do the primary weapon/ammo stuff. + if (primary == NULL) + { + // count a shieldgun as a primary. + if (HasShield()) + { + //m_rebuyStruct.m_primaryWeapon = WEAPON_SHIELDGUN; + Q_strncpy( m_rebuyStruct.m_szPrimaryWeapon, "shield", sizeof(m_rebuyStruct.m_szPrimaryWeapon) ); + m_rebuyStruct.m_primaryAmmo = 0; // shields don't have ammo. + } + else + { + + m_rebuyStruct.m_szPrimaryWeapon[0] = 0; // if we don't have a shield and we don't have a primary weapon, we got nuthin. + m_rebuyStruct.m_primaryAmmo = 0; // can't have ammo if we don't have a gun right? + } + } + else + { + //strip off the "weapon_" + + const char *wpnName = primary->GetClassname(); + + Q_strncpy( m_rebuyStruct.m_szPrimaryWeapon, wpnName + 7, sizeof(m_rebuyStruct.m_szPrimaryWeapon) ); + + if( primary->GetPrimaryAmmoType() != -1 ) + { + m_rebuyStruct.m_primaryAmmo = GetAmmoCount( primary->GetPrimaryAmmoType() ); + } + } + + // do the secondary weapon/ammo stuff. + if (secondary == NULL) + { + m_rebuyStruct.m_szSecondaryWeapon[0] = 0; + m_rebuyStruct.m_secondaryAmmo = 0; // can't have ammo if we don't have a gun right? + } + else + { + const char *wpnName = secondary->GetClassname(); + + Q_strncpy( m_rebuyStruct.m_szSecondaryWeapon, wpnName + 7, sizeof(m_rebuyStruct.m_szSecondaryWeapon) ); + + if( secondary->GetPrimaryAmmoType() != -1 ) + { + m_rebuyStruct.m_secondaryAmmo = GetAmmoCount( secondary->GetPrimaryAmmoType() ); + } + } + + CBaseCombatWeapon *pGrenade; + + //MATTTODO: right now you can't buy more than one grenade. make it so you can + //buy more and query the number you have. + // HE Grenade + pGrenade = Weapon_OwnsThisType( "weapon_hegrenade" ); + if ( pGrenade && pGrenade->GetPrimaryAmmoType() != -1 ) + { + m_rebuyStruct.m_heGrenade = GetAmmoCount(pGrenade->GetPrimaryAmmoType()); + } + else + m_rebuyStruct.m_heGrenade = 0; + + + // flashbang + pGrenade = Weapon_OwnsThisType( "weapon_flashbang" ); + if ( pGrenade && pGrenade->GetPrimaryAmmoType() != -1 ) + { + m_rebuyStruct.m_flashbang = GetAmmoCount(pGrenade->GetPrimaryAmmoType()); + } + else + m_rebuyStruct.m_flashbang = 0; + + // smokegrenade + pGrenade = Weapon_OwnsThisType( "weapon_smokegrenade" ); + if ( pGrenade /*&& pGrenade->GetPrimaryAmmoType() != -1*/ ) + { + m_rebuyStruct.m_smokeGrenade = 1; //GetAmmoCount(pGrenade->GetPrimaryAmmoType()); + } + else + m_rebuyStruct.m_smokeGrenade = 0; + + // defuser + m_rebuyStruct.m_defuser = HasDefuser(); + + // night vision + m_rebuyStruct.m_nightVision = m_bHasNightVision.Get(); //cast to avoid strange compiler warning + + // check for armor. + m_rebuyStruct.m_armor = ( m_bHasHelmet ? 2 : ( ArmorValue() > 0 ? 1 : 0 ) ); +} + +void CCSPlayer::Rebuy( void ) +{ + if ( !IsInBuyZone() ) + { + EmitPrivateSound( "BuyPreset.CantBuy" ); + return; + } + + const char *rebuyString = engine->GetClientConVarValue( engine->IndexOfEdict( edict() ), "cl_rebuy" ); + if ( !rebuyString || !*rebuyString ) + { + EmitPrivateSound( "BuyPreset.AlreadyBought" ); + return; + } + + m_bIsInRebuy = true; + BuyResult_e overallResult = BUY_ALREADY_HAVE; + + char token[256]; + rebuyString = engine->ParseFile( rebuyString, token, sizeof( token ) ); + + while (rebuyString != NULL) + { + BuyResult_e result = BUY_ALREADY_HAVE; + + if (!Q_strncmp(token, "PrimaryWeapon", 14)) + { + result = RebuyPrimaryWeapon(); + } + else if (!Q_strncmp(token, "PrimaryAmmo", 12)) + { + result = RebuyPrimaryAmmo(); + } + else if (!Q_strncmp(token, "SecondaryWeapon", 16)) + { + result = RebuySecondaryWeapon(); + } + else if (!Q_strncmp(token, "SecondaryAmmo", 14)) + { + result = RebuySecondaryAmmo(); + } + else if (!Q_strncmp(token, "HEGrenade", 10)) + { + result = RebuyHEGrenade(); + } + else if (!Q_strncmp(token, "Flashbang", 10)) + { + result = RebuyFlashbang(); + } + else if (!Q_strncmp(token, "SmokeGrenade", 13)) + { + result = RebuySmokeGrenade(); + } + else if (!Q_strncmp(token, "Defuser", 8)) + { + result = RebuyDefuser(); + } + else if (!Q_strncmp(token, "NightVision", 12)) + { + result = RebuyNightVision(); + } + else if (!Q_strncmp(token, "Armor", 6)) + { + result = RebuyArmor(); + } + + overallResult = CombineBuyResults( overallResult, result ); + + rebuyString = engine->ParseFile( rebuyString, token, sizeof( token ) ); + } + + m_bIsInRebuy = false; + + // after we're done buying, the user is done with their equipment purchasing experience. + // so we are effectively out of the buy zone. +// if (TheTutor != NULL) +// { +// TheTutor->OnEvent(EVENT_PLAYER_LEFT_BUY_ZONE); +// } + + m_bAutoReload = true; + + if ( overallResult == BUY_CANT_AFFORD ) + { + EmitPrivateSound( "BuyPreset.CantBuy" ); + } + else if ( overallResult == BUY_ALREADY_HAVE ) + { + EmitPrivateSound( "BuyPreset.AlreadyBought" ); + } + else if ( overallResult == BUY_BOUGHT ) + { + g_iReBuyPurchases++; + } +} + +BuyResult_e CCSPlayer::RebuyPrimaryWeapon() +{ + CBaseCombatWeapon *primary = Weapon_GetSlot( WEAPON_SLOT_RIFLE ); + if (primary != NULL) + { + return BUY_ALREADY_HAVE; // don't drop primary weapons via rebuy - if the player picked up a different weapon, he wants to keep it. + } + + if( strlen( m_rebuyStruct.m_szPrimaryWeapon ) > 0 ) + return HandleCommand_Buy(m_rebuyStruct.m_szPrimaryWeapon); + + return BUY_ALREADY_HAVE; +} + +BuyResult_e CCSPlayer::RebuySecondaryWeapon() +{ + CBaseCombatWeapon *pistol = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + if (pistol != NULL && !m_bUsingDefaultPistol) + { + return BUY_ALREADY_HAVE; // don't drop pistols via rebuy if we've bought one other than the default pistol + } + + if( strlen( m_rebuyStruct.m_szSecondaryWeapon ) > 0 ) + return HandleCommand_Buy(m_rebuyStruct.m_szSecondaryWeapon); + + return BUY_ALREADY_HAVE; +} + +BuyResult_e CCSPlayer::RebuyPrimaryAmmo() +{ + CBaseCombatWeapon *primary = Weapon_GetSlot( WEAPON_SLOT_RIFLE ); + + if (primary == NULL) + { + return BUY_ALREADY_HAVE; // can't buy ammo when we don't even have a gun. + } + + // Ensure that the weapon uses ammo + int nAmmo = primary->GetPrimaryAmmoType(); + if ( nAmmo == -1 ) + { + return BUY_ALREADY_HAVE; + } + + // if we had more ammo before than we have now, buy more. + if (m_rebuyStruct.m_primaryAmmo > GetAmmoCount( nAmmo )) + { + return HandleCommand_Buy("primammo"); + } + + return BUY_ALREADY_HAVE; +} + + +BuyResult_e CCSPlayer::RebuySecondaryAmmo() +{ + CBaseCombatWeapon *secondary = Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + + if (secondary == NULL) + { + return BUY_ALREADY_HAVE; // can't buy ammo when we don't even have a gun. + } + + // Ensure that the weapon uses ammo + int nAmmo = secondary->GetPrimaryAmmoType(); + if ( nAmmo == -1 ) + { + return BUY_ALREADY_HAVE; + } + + if (m_rebuyStruct.m_secondaryAmmo > GetAmmoCount( nAmmo )) + { + return HandleCommand_Buy("secammo"); + } + + return BUY_ALREADY_HAVE; +} + +BuyResult_e CCSPlayer::RebuyHEGrenade() +{ + CBaseCombatWeapon *pGrenade = Weapon_OwnsThisType( "weapon_hegrenade" ); + + int numGrenades = 0; + + if( pGrenade ) + { + int nAmmo = pGrenade->GetPrimaryAmmoType(); + if ( nAmmo == -1 ) + { + return BUY_ALREADY_HAVE; + } + + numGrenades = GetAmmoCount( nAmmo ); + } + + BuyResult_e overallResult = BUY_ALREADY_HAVE; + int numToBuy = MAX( 0, m_rebuyStruct.m_heGrenade - numGrenades ); + for (int i = 0; i < numToBuy; ++i) + { + BuyResult_e result = HandleCommand_Buy("hegrenade"); + overallResult = CombineBuyResults( overallResult, result ); + } + + return overallResult; +} + +BuyResult_e CCSPlayer::RebuyFlashbang() +{ + CBaseCombatWeapon *pGrenade = Weapon_OwnsThisType( "weapon_flashbang" ); + + int numGrenades = 0; + + if( pGrenade ) + { + int nAmmo = pGrenade->GetPrimaryAmmoType(); + if ( nAmmo == -1 ) + { + return BUY_ALREADY_HAVE; + } + numGrenades = GetAmmoCount( nAmmo ); + + } + + BuyResult_e overallResult = BUY_ALREADY_HAVE; + int numToBuy = MAX( 0, m_rebuyStruct.m_flashbang - numGrenades ); + for (int i = 0; i < numToBuy; ++i) + { + BuyResult_e result = HandleCommand_Buy("flashbang"); + overallResult = CombineBuyResults( overallResult, result ); + } + + return overallResult; +} + +BuyResult_e CCSPlayer::RebuySmokeGrenade() +{ + CBaseCombatWeapon *pGrenade = Weapon_OwnsThisType( "weapon_smokegrenade" ); + + int numGrenades = 0; + + if( pGrenade ) + { + int nAmmo = pGrenade->GetPrimaryAmmoType(); + if ( nAmmo == -1 ) + { + return BUY_ALREADY_HAVE; + } + + numGrenades = GetAmmoCount( nAmmo ); + } + + BuyResult_e overallResult = BUY_ALREADY_HAVE; + int numToBuy = MAX( 0, m_rebuyStruct.m_smokeGrenade - numGrenades ); + for (int i = 0; i < numToBuy; ++i) + { + BuyResult_e result = HandleCommand_Buy("smokegrenade"); + overallResult = CombineBuyResults( overallResult, result ); + } + + return overallResult; +} + +BuyResult_e CCSPlayer::RebuyDefuser() +{ + //If we don't have a defuser, and we want one, buy it! + if( !HasDefuser() && m_rebuyStruct.m_defuser ) + { + return HandleCommand_Buy("defuser"); + } + + return BUY_ALREADY_HAVE; +} + +BuyResult_e CCSPlayer::RebuyNightVision() +{ + //if we don't have night vision and we want one, buy it! + if( !m_bHasNightVision && m_rebuyStruct.m_nightVision ) + { + return HandleCommand_Buy("nvgs"); + } + + return BUY_ALREADY_HAVE; +} + +BuyResult_e CCSPlayer::RebuyArmor() +{ + if (m_rebuyStruct.m_armor > 0 ) + { + int armor = 0; + + if( m_bHasHelmet ) + armor = 2; + else if( ArmorValue() > 0 ) + armor = 1; + + if( armor < m_rebuyStruct.m_armor ) + { + if (m_rebuyStruct.m_armor == 1) + { + return HandleCommand_Buy("vest"); + } + else + { + return HandleCommand_Buy("vesthelm"); + } + } + + } + + return BUY_ALREADY_HAVE; +} + +bool CCSPlayer::IsUseableEntity( CBaseEntity *pEntity, unsigned int requiredCaps ) +{ + CWeaponCSBase *pCSWepaon = dynamic_cast<CWeaponCSBase*>(pEntity); + + if( pCSWepaon ) + { + // we can't USE dropped weapons + return true; + } + + CBaseCSGrenadeProjectile *pGrenade = dynamic_cast<CBaseCSGrenadeProjectile*>(pEntity); + if ( pGrenade ) + { + // we can't USE thrown grenades + } + + return BaseClass::IsUseableEntity( pEntity, requiredCaps ); +} + +CBaseEntity *CCSPlayer::FindUseEntity() +{ + CBaseEntity *entity = NULL; + + // Check to see if the bomb is close enough to use before attempting to use anything else. + + if ( CSGameRules()->IsBombDefuseMap() && GetTeamNumber() == TEAM_CT ) + { + // This is done separately since there might be something blocking our LOS to it + // but we might want to use it anyway if it's close enough. This should eliminate + // the vast majority of bomb placement exploits (places where the bomb can be planted + // but can't be "used". This also mimics goldsrc cstrike behavior. + CBaseEntity *bomb = gEntList.FindEntityByClassname( NULL, PLANTED_C4_CLASSNAME ); + if (bomb != NULL) + { + Vector bombPos = bomb->GetAbsOrigin(); + Vector vecLOS = EyePosition() - bombPos; + + if (vecLOS.LengthSqr() < (96*96)) // 64 is the distance in Goldsrc. However since Goldsrc did distance from the player's origin and we're doing distance from the player's eye, make the radius a bit bigger. + { + // bomb is close enough, now make sure the player is facing the bomb. + Vector forward; + AngleVectors(EyeAngles(), &forward, NULL, NULL); + + vecLOS.NormalizeInPlace(); + + float flDot = DotProduct(forward, vecLOS); + if (flDot < -0.7) // 0.7 taken from Goldsrc, +/- ~45 degrees + { + entity = bomb; + } + } + } + } + + if ( entity == NULL ) + { + entity = BaseClass::FindUseEntity(); + } + + return entity; +} + +void CCSPlayer::StockPlayerAmmo( CBaseCombatWeapon *pNewWeapon ) +{ + CWeaponCSBase *pWeapon = dynamic_cast< CWeaponCSBase * >( pNewWeapon ); + + if ( pWeapon ) + { + if ( pWeapon->GetWpnData().iFlags & ITEM_FLAG_EXHAUSTIBLE ) + return; + + int nAmmo = pWeapon->GetPrimaryAmmoType(); + + if ( nAmmo != -1 ) + { + GiveAmmo( 9999, GetAmmoDef()->GetAmmoOfIndex(nAmmo)->pName ); + pWeapon->m_iClip1 = pWeapon->GetMaxClip1(); + } + + return; + } + + pWeapon = dynamic_cast< CWeaponCSBase * >(Weapon_GetSlot( WEAPON_SLOT_RIFLE )); + + if ( pWeapon ) + { + int nAmmo = pWeapon->GetPrimaryAmmoType(); + + if ( nAmmo != -1 ) + { + GiveAmmo( 9999, GetAmmoDef()->GetAmmoOfIndex(nAmmo)->pName ); + pWeapon->m_iClip1 = pWeapon->GetMaxClip1(); + } + } + + pWeapon = dynamic_cast< CWeaponCSBase * >(Weapon_GetSlot( WEAPON_SLOT_PISTOL )); + + if ( pWeapon ) + { + int nAmmo = pWeapon->GetPrimaryAmmoType(); + + if ( nAmmo != -1 ) + { + GiveAmmo( 9999, GetAmmoDef()->GetAmmoOfIndex(nAmmo)->pName ); + pWeapon->m_iClip1 = pWeapon->GetMaxClip1(); + } + } +} + +CBaseEntity *CCSPlayer::GiveNamedItem( const char *pszName, int iSubType ) +{ + EHANDLE pent; + + if ( !pszName || !pszName[0] ) + return NULL; + +#ifndef CS_SHIELD_ENABLED + if ( !Q_stricmp( pszName, "weapon_shield" ) ) + return NULL; +#endif + + pent = CreateEntityByName(pszName); + if ( pent == NULL ) + { + Msg( "NULL Ent in GiveNamedItem!\n" ); + return NULL; + } + + pent->SetLocalOrigin( GetLocalOrigin() ); + pent->AddSpawnFlags( SF_NORESPAWN ); + + CBaseCombatWeapon *pWeapon = dynamic_cast<CBaseCombatWeapon*>( (CBaseEntity*)pent ); + if ( pWeapon ) + { + if ( iSubType ) + { + pWeapon->SetSubType( iSubType ); + } + } + + DispatchSpawn( pent ); + + m_bIsBeingGivenItem = true; + if ( pent != NULL && !(pent->IsMarkedForDeletion()) ) + { + pent->Touch( this ); + } + m_bIsBeingGivenItem = false; + + StockPlayerAmmo( pWeapon ); + return pent; +} + + +void CCSPlayer::DoAnimationEvent( PlayerAnimEvent_t event, int nData ) +{ + if ( event == PLAYERANIMEVENT_THROW_GRENADE ) + { + // Grenade throwing has to synchronize exactly with the player's grenade weapon going away, + // and events get delayed a bit, so we let CCSPlayerAnimState pickup the change to this + // variable. + m_iThrowGrenadeCounter = (m_iThrowGrenadeCounter+1) % (1<<THROWGRENADE_COUNTER_BITS); + } + else + { + m_PlayerAnimState->DoAnimationEvent( event, nData ); + TE_PlayerAnimEvent( this, event, nData ); // Send to any clients who can see this guy. + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CCSPlayer::FlashlightIsOn( void ) +{ + return IsEffectActive( EF_DIMLIGHT ); +} + +extern ConVar flashlight; + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CCSPlayer::FlashlightTurnOn( void ) +{ + if( flashlight.GetInt() > 0 && IsAlive() ) + { + AddEffects( EF_DIMLIGHT ); + EmitSound( "Player.FlashlightOn" ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CCSPlayer::FlashlightTurnOff( void ) +{ + RemoveEffects( EF_DIMLIGHT ); + + if( IsAlive() ) + { + EmitSound( "Player.FlashlightOff" ); + } +} + + +//Drop the appropriate weapons: +// Defuser if we have one +// C4 if we have one +// The best weapon we have, first check primary, +// then secondary and drop the best one + +//============================================================================= +// HPE_BEGIN: +// [tj] Added a parameter so we know if it was death that caused the drop +// [menglish] Clear all previously dropped equipment and add the c4 to the dropped equipment +//============================================================================= + +void CCSPlayer::DropWeapons( bool fromDeath, bool friendlyFire ) +{ + for ( int i = 0; i < DROPPED_COUNT; ++i ) + { + m_hDroppedEquipment[i] = NULL; + } + + CBaseCombatWeapon *pC4 = Weapon_OwnsThisType( "weapon_c4" ); + if ( pC4 ) + { + CSWeaponDrop( pC4, false, true ); + if( fromDeath ) + { + if( friendlyFire ) + { + (static_cast<CC4*> (pC4))->SetDroppedFromDeath(true); + } + m_hDroppedEquipment[DROPPED_C4] = static_cast<CBaseEntity *>(pC4); + } + } + + //NOTE: Function continues beyond comment block. This is just the part I touched. + +//============================================================================= +// HPE_END +//============================================================================= + + + if( HasDefuser() ) + { + //Drop an item_defuser + Vector vForward, vRight; + AngleVectors( GetAbsAngles(), &vForward, &vRight, NULL ); + + CBaseAnimating *pDefuser = (CBaseAnimating *)CBaseEntity::Create( "item_defuser", WorldSpaceCenter(), GetLocalAngles(), this ); + pDefuser->ApplyAbsVelocityImpulse( vForward * 200 + vRight * random->RandomFloat( -50, 50 ) ); + + RemoveDefuser(); + + //============================================================================= + // HPE_BEGIN: + // [menglish] Add the newly created defuser to the dropped equipment list + //============================================================================= + + if(fromDeath) + { + m_hDroppedEquipment[DROPPED_DEFUSE] = static_cast<CBaseEntity *>(pDefuser); + } + + //============================================================================= + // HPE_END + //============================================================================= + } + + if( HasShield() ) + { + DropShield(); + } + else + { + //drop the best weapon we have + if( !DropRifle( true ) ) + DropPistol( true ); + } + + // drop any live grenades so they explode + CBaseCSGrenade *pGrenade = dynamic_cast< CBaseCSGrenade * >(Weapon_OwnsThisType("weapon_hegrenade")); + if ( pGrenade && ( pGrenade->IsPinPulled() || pGrenade->IsBeingThrown() ) ) + { + pGrenade->DropGrenade(); + pGrenade->DecrementAmmo( this ); + } + else + { + pGrenade = dynamic_cast< CBaseCSGrenade * >(Weapon_OwnsThisType("weapon_flashbang")); + if ( pGrenade && ( pGrenade->IsPinPulled() || pGrenade->IsBeingThrown() ) ) + { + pGrenade->DropGrenade(); + pGrenade->DecrementAmmo( this ); + } + else + { + pGrenade = dynamic_cast< CBaseCSGrenade * >(Weapon_OwnsThisType("weapon_smokegrenade")); + if ( pGrenade && ( pGrenade->IsPinPulled() || pGrenade->IsBeingThrown() ) ) + { + pGrenade->DropGrenade(); + pGrenade->DecrementAmmo( this ); + } + } + } + + // drop the "best" grenade remaining + CBaseCombatWeapon *pWeapon = Weapon_OwnsThisType("weapon_hegrenade"); + bool grenadeDrop = false; + if ( pWeapon && pWeapon->HasAmmo() ) + { + grenadeDrop = CSWeaponDrop(pWeapon, false); + } + else + { + pWeapon = Weapon_OwnsThisType("weapon_flashbang"); + if ( pWeapon && pWeapon->HasAmmo() ) + { + grenadeDrop = CSWeaponDrop(pWeapon, false); + } + else + { + pWeapon = Weapon_OwnsThisType("weapon_smokegrenade"); + if ( pWeapon && pWeapon->HasAmmo() ) + { + grenadeDrop = CSWeaponDrop(pWeapon, false); + } + } + } + + //============================================================================= + // HPE_BEGIN: + // [menglish] Add whichever, if any, grenade was dropped + //============================================================================= + + if( pWeapon && grenadeDrop ) + { + m_hDroppedEquipment[DROPPED_GRENADE] = static_cast<CBaseEntity *>(pWeapon); + } + + //============================================================================= + // HPE_END + //============================================================================= +} + +//----------------------------------------------------------------------------- +// Purpose: Put the player in the specified team +//----------------------------------------------------------------------------- +void CCSPlayer::ChangeTeam( int iTeamNum ) +{ + if ( !GetGlobalTeam( iTeamNum ) ) + { + Warning( "CCSPlayer::ChangeTeam( %d ) - invalid team index.\n", iTeamNum ); + return; + } + + int iOldTeam = GetTeamNumber(); + + // if this is our current team, just abort + if ( iTeamNum == iOldTeam ) + return; + + //============================================================================= + // HPE_BEGIN: + //============================================================================= + + // [tj] Added a parameter so we know if it was death that caused the drop + // Drop Our best weapon + DropWeapons(false, false); + + // [tj] Clear out dominations + RemoveNemesisRelationships(); + + //============================================================================= + // HPE_END + //============================================================================= + + + // Always allow a change to spectator, and don't count it as one of our team changes. + // We now store the old team, so if a player changes once to one team, then to spectator, + // they won't be able to change back to their old old team, but will still be able to join + // the team they initially changed to. + if( iTeamNum != TEAM_SPECTATOR ) + { + m_bTeamChanged = true; + } + else + { + m_iOldTeam = iOldTeam; + } + + // do the team change: + BaseClass::ChangeTeam( iTeamNum ); + + //reset class + m_iClass = (int)CS_CLASS_NONE; + + // update client state + + if ( iTeamNum == TEAM_UNASSIGNED ) + { + State_Transition( STATE_OBSERVER_MODE ); + } + else if ( iTeamNum == TEAM_SPECTATOR ) + { + //============================================================================= + // HPE_BEGIN: + // [tj] Removed these lines so players keep their money when switching to spectator. + //============================================================================= + //Reset money + //m_iAccount = 0; + //============================================================================= + // HPE_END + //============================================================================= + RemoveAllItems( true ); + + State_Transition( STATE_OBSERVER_MODE ); + } + else // active player + { + if ( iOldTeam == TEAM_SPECTATOR ) + { + // If they're switching from being a spectator to ingame player + //============================================================================= + // HPE_BEGIN: + // [tj] Changed this so players either retain their existing money or, + // if they have less than the default, give them the default. + //============================================================================= + int startMoney = CSGameRules()->GetStartMoney(); + if (startMoney > m_iAccount) + { + m_iAccount = startMoney; + } + //============================================================================= + // HPE_END + //============================================================================= + } + + // bots get to this state on TEAM_UNASSIGNED, yet they are marked alive. Don't kill them. + else if ( iOldTeam != TEAM_UNASSIGNED && !IsDead() ) + { + // Kill player if switching teams while alive + CommitSuicide(); + } + + // Put up the class selection menu. + State_Transition( STATE_PICKINGCLASS ); + } + + // Initialize the player counts now that a player has switched teams + int NumDeadCT, NumDeadTerrorist, NumAliveTerrorist, NumAliveCT; + CSGameRules()->InitializePlayerCounts( NumAliveTerrorist, NumAliveCT, NumDeadTerrorist, NumDeadCT ); +} + +//----------------------------------------------------------------------------- +// Purpose: Put the player in the specified team without penalty +//----------------------------------------------------------------------------- +void CCSPlayer::SwitchTeam( int iTeamNum ) +{ + if ( !GetGlobalTeam( iTeamNum ) || (iTeamNum != TEAM_CT && iTeamNum != TEAM_TERRORIST) ) + { + Warning( "CCSPlayer::SwitchTeam( %d ) - invalid team index.\n", iTeamNum ); + return; + } + + int iOldTeam = GetTeamNumber(); + + // if this is our current team, just abort + if ( iTeamNum == iOldTeam ) + return; + + // Always allow a change to spectator, and don't count it as one of our team changes. + // We now store the old team, so if a player changes once to one team, then to spectator, + // they won't be able to change back to their old old team, but will still be able to join + // the team they initially changed to. + m_bTeamChanged = true; + + // do the team change: + BaseClass::ChangeTeam( iTeamNum ); + + if( HasDefuser() ) + { + RemoveDefuser(); + } + + //reset class + switch ( m_iClass ) + { + // Terrorist -> CT + case CS_CLASS_PHOENIX_CONNNECTION: + m_iClass = (int)CS_CLASS_SEAL_TEAM_6; + break; + case CS_CLASS_L337_KREW: + m_iClass = (int)CS_CLASS_GSG_9; + break; + case CS_CLASS_ARCTIC_AVENGERS: + m_iClass = (int)CS_CLASS_SAS; + break; + case CS_CLASS_GUERILLA_WARFARE: + m_iClass = (int)CS_CLASS_GIGN; + break; + + // CT -> Terrorist + case CS_CLASS_SEAL_TEAM_6: + m_iClass = (int)CS_CLASS_PHOENIX_CONNNECTION; + break; + case CS_CLASS_GSG_9: + m_iClass = (int)CS_CLASS_L337_KREW; + break; + case CS_CLASS_SAS: + m_iClass = (int)CS_CLASS_ARCTIC_AVENGERS; + break; + case CS_CLASS_GIGN: + m_iClass = (int)CS_CLASS_GUERILLA_WARFARE; + break; + + case CS_CLASS_NONE: + default: + break; + } + + // Initialize the player counts now that a player has switched teams + int NumDeadCT, NumDeadTerrorist, NumAliveTerrorist, NumAliveCT; + CSGameRules()->InitializePlayerCounts( NumAliveTerrorist, NumAliveCT, NumDeadTerrorist, NumDeadCT ); +} + +void CCSPlayer::ModifyOrAppendPlayerCriteria( AI_CriteriaSet& set ) +{ + // this is for giving player info to the hostage response system + // and is as yet unused. + // Eventually we could give the hostage a few tidbits about this player, + // eg their health, what weapons they have, and the hostage could + // comment accordingly. + + //do not append any player data to the Criteria! + + //we don't know which player we should be caring about +} + +static unsigned int s_BulletGroupCounter = 0; + +void CCSPlayer::StartNewBulletGroup() +{ + s_BulletGroupCounter++; +} + +//======================================================= +// Remember this amount of damage that we dealt for stats +//======================================================= +void CCSPlayer::RecordDamageGiven( const char *szDamageTaker, int iDamageGiven ) +{ + FOR_EACH_LL( m_DamageGivenList, i ) + { + if( Q_strncmp( szDamageTaker, m_DamageGivenList[i]->GetPlayerName(), MAX_PLAYER_NAME_LENGTH ) == 0 ) + { + m_DamageGivenList[i]->AddDamage( iDamageGiven, s_BulletGroupCounter ); + return; + } + } + + CDamageRecord *record = new CDamageRecord( szDamageTaker, iDamageGiven, s_BulletGroupCounter ); + int k = m_DamageGivenList.AddToTail(); + m_DamageGivenList[k] = record; +} + +//======================================================= +// Remember this amount of damage that we took for stats +//======================================================= +void CCSPlayer::RecordDamageTaken( const char *szDamageDealer, int iDamageTaken ) +{ + FOR_EACH_LL( m_DamageTakenList, i ) + { + if( Q_strncmp( szDamageDealer, m_DamageTakenList[i]->GetPlayerName(), MAX_PLAYER_NAME_LENGTH ) == 0 ) + { + m_DamageTakenList[i]->AddDamage( iDamageTaken, s_BulletGroupCounter ); + return; + } + } + + CDamageRecord *record = new CDamageRecord( szDamageDealer, iDamageTaken, s_BulletGroupCounter ); + int k = m_DamageTakenList.AddToTail(); + m_DamageTakenList[k] = record; +} + +//======================================================= +// Reset our damage given and taken counters +//======================================================= +void CCSPlayer::ResetDamageCounters() +{ + m_DamageGivenList.PurgeAndDeleteElements(); + m_DamageTakenList.PurgeAndDeleteElements(); +} + +//======================================================= +// Output the damage that we dealt to other players +//======================================================= +void CCSPlayer::OutputDamageTaken( void ) +{ + bool bPrintHeader = true; + CDamageRecord *pRecord; + char buf[64]; + int msg_dest = HUD_PRINTCONSOLE; + + FOR_EACH_LL( m_DamageTakenList, i ) + { + if( bPrintHeader ) + { + ClientPrint( this, msg_dest, "Player: %s1 - Damage Taken\n", GetPlayerName() ); + ClientPrint( this, msg_dest, "-------------------------\n" ); + bPrintHeader = false; + } + pRecord = m_DamageTakenList[i]; + + if( pRecord ) + { + if (pRecord->GetNumHits() == 1) + { + Q_snprintf( buf, sizeof(buf), "%d in %d hit", pRecord->GetDamage(), pRecord->GetNumHits() ); + } + else + { + Q_snprintf( buf, sizeof(buf), "%d in %d hits", pRecord->GetDamage(), pRecord->GetNumHits() ); + } + ClientPrint( this, msg_dest, "Damage Taken from \"%s1\" - %s2\n", pRecord->GetPlayerName(), buf ); + } + } +} + +//======================================================= +// Output the damage that we took from other players +//======================================================= +void CCSPlayer::OutputDamageGiven( void ) +{ + bool bPrintHeader = true; + CDamageRecord *pRecord; + char buf[64]; + int msg_dest = HUD_PRINTCONSOLE; + + FOR_EACH_LL( m_DamageGivenList, i ) + { + if( bPrintHeader ) + { + ClientPrint( this, msg_dest, "Player: %s1 - Damage Given\n", GetPlayerName() ); + ClientPrint( this, msg_dest, "-------------------------\n" ); + bPrintHeader = false; + } + + pRecord = m_DamageGivenList[i]; + + if( pRecord ) + { + if (pRecord->GetNumHits() == 1) + { + Q_snprintf( buf, sizeof(buf), "%d in %d hit", pRecord->GetDamage(), pRecord->GetNumHits() ); + } + else + { + Q_snprintf( buf, sizeof(buf), "%d in %d hits", pRecord->GetDamage(), pRecord->GetNumHits() ); + } + ClientPrint( this, msg_dest, "Damage Given to \"%s1\" - %s2\n", pRecord->GetPlayerName(), buf ); + } + } +} + +void CCSPlayer::CreateViewModel( int index /*=0*/ ) +{ + Assert( index >= 0 && index < MAX_VIEWMODELS ); + + if ( GetViewModel( index ) ) + return; + + CPredictedViewModel *vm = ( CPredictedViewModel * )CreateEntityByName( "predicted_viewmodel" ); + if ( vm ) + { + vm->SetAbsOrigin( GetAbsOrigin() ); + vm->SetOwner( this ); + vm->SetIndex( index ); + DispatchSpawn( vm ); + vm->FollowEntity( this, false ); + m_hViewModel.Set( index, vm ); + } +} + +bool CCSPlayer::HasC4() const +{ + return ( Weapon_OwnsThisType( "weapon_c4" ) != NULL ); +} + +int CCSPlayer::GetNextObserverSearchStartPoint( bool bReverse ) +{ + // If we are currently watching someone who is dead, they must have died while we were watching (since + // a dead guy is not a valid pick to start watching). He was given his killer as an observer target + // when he died, so let's start by trying to observe his killer. If we fail, we'll use the normal way. + // And this is just the start point anyway, but we want to start the search here in case it is okay. + if( m_hObserverTarget && !m_hObserverTarget->IsAlive() ) + { + CCSPlayer *targetPlayer = ToCSPlayer(m_hObserverTarget); + if( targetPlayer && targetPlayer->GetObserverTarget() ) + return targetPlayer->GetObserverTarget()->entindex(); + } + + return BaseClass::GetNextObserverSearchStartPoint( bReverse ); +} + +void CCSPlayer::PlayStepSound( Vector &vecOrigin, surfacedata_t *psurface, float fvol, bool force ) +{ + BaseClass::PlayStepSound( vecOrigin, psurface, fvol, force ); + + if ( !sv_footsteps.GetFloat() ) + return; + + if ( !psurface ) + return; + + IGameEvent * event = gameeventmanager->CreateEvent( "player_footstep" ); + if ( event ) + { + event->SetInt("userid", GetUserID() ); + gameeventmanager->FireEvent( event ); + } + + m_bMadeFootstepNoise = true; +} + + +void CCSPlayer::SelectDeathPose( const CTakeDamageInfo &info ) +{ + MDLCACHE_CRITICAL_SECTION(); + if ( !GetModelPtr() ) + return; + + Activity aActivity = ACT_INVALID; + int iDeathFrame = 0; + + SelectDeathPoseActivityAndFrame( this, info, m_LastHitGroup, aActivity, iDeathFrame ); + if ( aActivity == ACT_INVALID ) + { + SetDeathPose( ACT_INVALID ); + SetDeathPoseFrame( 0 ); + return; + } + + SetDeathPose( SelectWeightedSequence( aActivity ) ); + SetDeathPoseFrame( iDeathFrame ); +} + + +void CCSPlayer::HandleAnimEvent( animevent_t *pEvent ) +{ + if ( pEvent->event == 4001 || pEvent->event == 4002 ) + { + // Ignore these for now - soon we will be playing footstep sounds based on these events + // that mark footfalls in the anims. + } + else + { + BaseClass::HandleAnimEvent( pEvent ); + } +} + + +bool CCSPlayer::CanChangeName( void ) +{ + if ( IsBot() ) + return true; + + // enforce the minimum interval + if ( (m_flNameChangeHistory[0] + MIN_NAME_CHANGE_INTERVAL) >= gpGlobals->curtime ) + { + return false; + } + + // enforce that we dont do more than NAME_CHANGE_HISTORY_SIZE + // changes within NAME_CHANGE_HISTORY_INTERVAL + if ( (m_flNameChangeHistory[NAME_CHANGE_HISTORY_SIZE-1] + NAME_CHANGE_HISTORY_INTERVAL) >= gpGlobals->curtime ) + { + return false; + } + + return true; +} + +void CCSPlayer::ChangeName( const char *pszNewName ) +{ + // make sure name is not too long + char trimmedName[MAX_PLAYER_NAME_LENGTH]; + Q_strncpy( trimmedName, pszNewName, sizeof( trimmedName ) ); + + const char *pszOldName = GetPlayerName(); + + // send colored message to everyone + CReliableBroadcastRecipientFilter filter; + UTIL_SayText2Filter( filter, this, false, "#Cstrike_Name_Change", pszOldName, trimmedName ); + + // broadcast event + IGameEvent * event = gameeventmanager->CreateEvent( "player_changename" ); + if ( event ) + { + event->SetInt( "userid", GetUserID() ); + event->SetString( "oldname", pszOldName ); + event->SetString( "newname", trimmedName ); + gameeventmanager->FireEvent( event ); + } + + // change shared player name + SetPlayerName( trimmedName ); + + // tell engine to use new name + engine->ClientCommand( edict(), "name \"%s\"", trimmedName ); + + // remember time of name change + for ( int i=NAME_CHANGE_HISTORY_SIZE-1; i>0; i-- ) + { + m_flNameChangeHistory[i] = m_flNameChangeHistory[i-1]; + } + + m_flNameChangeHistory[0] = gpGlobals->curtime; // last change +} + +bool CCSPlayer::StartReplayMode( float fDelay, float fDuration, int iEntity ) +{ + if ( !BaseClass::StartReplayMode( fDelay, fDuration, iEntity ) ) + return false; + + CSingleUserRecipientFilter filter( this ); + filter.MakeReliable(); + + UserMessageBegin( filter, "KillCam" ); + WRITE_BYTE( OBS_MODE_IN_EYE ); + + if ( m_hObserverTarget.Get() ) + { + WRITE_BYTE( m_hObserverTarget.Get()->entindex() ); // first target + WRITE_BYTE( entindex() ); //second target + } + else + { + WRITE_BYTE( entindex() ); // first target + WRITE_BYTE( 0 ); //second target + } + MessageEnd(); + + ClientPrint( this, HUD_PRINTCENTER, "Kill Cam Replay" ); + + return true; +} + +void CCSPlayer::StopReplayMode() +{ + BaseClass::StopReplayMode(); + + CSingleUserRecipientFilter filter( this ); + filter.MakeReliable(); + + UserMessageBegin( filter, "KillCam" ); + WRITE_BYTE( OBS_MODE_NONE ); + WRITE_BYTE( 0 ); + WRITE_BYTE( 0 ); + MessageEnd(); +} + +void CCSPlayer::PlayUseDenySound() +{ + // Don't do a sound here because it can mute your footsteps giving you an advantage. + // The CS:S content for this sound is silent anyways. + //EmitSound( "Player.UseDeny" ); +} + +//============================================================================= +// HPE_BEGIN: +//============================================================================= + +// [menglish, tj] This is where we reset all the per-round information for achievements for this player +void CCSPlayer::ResetRoundBasedAchievementVariables() +{ + m_KillingSpreeStartTime = -1; + + int numCTPlayers = 0, numTPlayers = 0; + for (int i = 0; i < g_Teams.Count(); i++ ) + { + if(g_Teams[i]) + { + if ( g_Teams[i]->GetTeamNumber() == TEAM_CT ) + numCTPlayers = g_Teams[i]->GetNumPlayers(); + else if(g_Teams[i]->GetTeamNumber() == TEAM_TERRORIST) + numTPlayers = g_Teams[i]->GetNumPlayers(); + } + } + m_NumEnemiesKilledThisRound = 0; + if(GetTeamNumber() == TEAM_CT) + m_NumEnemiesAtRoundStart = numTPlayers; + else if(GetTeamNumber() == TEAM_TERRORIST) + m_NumEnemiesAtRoundStart = numCTPlayers; + + + //Clear the previous owner field for currently held weapons + CWeaponCSBase* pWeapon = dynamic_cast< CWeaponCSBase * >(Weapon_GetSlot( WEAPON_SLOT_RIFLE )); + if ( pWeapon ) + { + pWeapon->SetPreviousOwner(NULL); + } + pWeapon = dynamic_cast< CWeaponCSBase * >(Weapon_GetSlot( WEAPON_SLOT_PISTOL)); + if ( pWeapon ) + { + pWeapon->SetPreviousOwner(NULL); + } + + //Clear list of weapons used to get kills + m_killWeapons.RemoveAll(); + + //Clear sliding window of kill times + m_killTimes.RemoveAll(); + + //clear round kills + m_enemyPlayersKilledThisRound.RemoveAll(); + + m_killsWhileBlind = 0; + + m_bSurvivedHeadshotDueToHelmet = false; + + m_gooseChaseStep = GC_NONE; + m_defuseDefenseStep = DD_NONE; + m_pGooseChaseDistractingPlayer = NULL; + + m_bMadeFootstepNoise = false; + + m_bombPickupTime = -1; + + m_bMadePurchseThisRound = false; + + m_bKilledDefuser = false; + m_bKilledRescuer = false; + m_maxGrenadeKills = 0; + m_grenadeDamageTakenThisRound = 0; + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Needed for fun-fact implementation + //============================================================================= + + WieldingKnifeAndKilledByGun(false); + + m_WeaponTypesUsed.RemoveAll(); + + m_bPickedUpDefuser = false; + m_bDefusedWithPickedUpKit = false; + + //============================================================================= + // HPE_END + //============================================================================= +} + + +/** + * static public CCSPlayer::GetCSWeaponIDCausingDamage() + * + * Helper function to get the ID of the weapon used to kill a player. + * This is slightly non-trivial because the grenade because a separate + * entity when thrown. + * + * Parameters: + * info - + * + * Returns: + * int - + */ +CSWeaponID CCSPlayer::GetWeaponIdCausingDamange( const CTakeDamageInfo &info ) +{ + CBaseEntity *pInflictor = info.GetInflictor(); + CCSPlayer *pAttacker = ToCSPlayer(info.GetAttacker()); + if (pAttacker == pInflictor) + { + CWeaponCSBase* pAttackerWeapon = dynamic_cast< CWeaponCSBase * >(pAttacker->GetActiveWeapon()); + if (!pAttackerWeapon) + return WEAPON_NONE; + + return pAttackerWeapon->GetWeaponID(); + } + else if (pInflictor && V_strcmp(pInflictor->GetClassname(), "hegrenade_projectile") == 0) + { + return WEAPON_HEGRENADE; + } + return WEAPON_NONE; +} + + +//============================================================================= +// HPE_BEGIN: +// [dwenger] adding tracking for weapon used fun fact +//============================================================================= +void CCSPlayer::PlayerUsedFirearm( CBaseCombatWeapon* pBaseWeapon ) +{ + if ( pBaseWeapon ) + { + CWeaponCSBase* pWeapon = dynamic_cast< CWeaponCSBase* >( pBaseWeapon ); + + if ( pWeapon ) + { + CSWeaponType weaponType = pWeapon->GetCSWpnData().m_WeaponType; + CSWeaponID weaponID = pWeapon->GetWeaponID(); + + if ( weaponType != WEAPONTYPE_KNIFE && weaponType != WEAPONTYPE_C4 && weaponType != WEAPONTYPE_GRENADE ) + { + if ( m_WeaponTypesUsed.Find( weaponID ) == -1 ) + { + // Add this weapon to the list of weapons used by the player + m_WeaponTypesUsed.AddToTail( weaponID ); + } + } + } + } +} + + +/** + * public CCSPlayer::ProcessPlayerDeathAchievements() + * + * Do Achievement processing whenever a player is killed + * + * Parameters: + * pAttacker - + * pVictim - + * info - + */ +void CCSPlayer::ProcessPlayerDeathAchievements( CCSPlayer *pAttacker, CCSPlayer *pVictim, const CTakeDamageInfo &info ) +{ + Assert(pVictim != NULL); + CBaseEntity *pInflictor = info.GetInflictor(); + + // all these achievements require a valid attacker on a different team + if ( pAttacker != NULL && pVictim != NULL && pVictim->GetTeamNumber() != pAttacker->GetTeamNumber() ) + { + // get the weapon used - some of the achievements will need this data + CWeaponCSBase* pAttackerWeapon = dynamic_cast< CWeaponCSBase * >(pAttacker->GetActiveWeapon()); + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Fun-fact processing + //============================================================================= + + CWeaponCSBase* pVictimWeapon = dynamic_cast< CWeaponCSBase* >(pVictim->GetActiveWeapon()); + + //============================================================================= + // HPE_END + //============================================================================= + + CSWeaponID attackerWeaponId = GetWeaponIdCausingDamange(info); + + if (pVictim->m_bIsDefusing) + { + pAttacker->AwardAchievement(CSKilledDefuser); + pAttacker->m_bKilledDefuser = true; + + if (attackerWeaponId == WEAPON_HEGRENADE) + { + pAttacker->AwardAchievement(CSKilledDefuserWithGrenade); + } + } + + // [pfreese] Achievement check for attacker killing player while reloading + if (pVictim->IsReloading()) + { + pAttacker->AwardAchievement(CSKillEnemyReloading); + } + + if (pVictim->IsRescuing()) + { + // Ensure the killer did not injure any hostages + if ( !pAttacker->InjuredAHostage() && pVictim->GetNumFollowers() == g_Hostages.Count() ) + { + pAttacker->AwardAchievement(CSKilledRescuer); + pAttacker->m_bKilledRescuer = true; + } + } + + // [menglish] Achievement check for doing 95% or more damage to a player and having another player kill them + FOR_EACH_LL( pVictim->m_DamageTakenList, i ) + { + if( pVictim->m_DamageTakenList[i]->GetDamage() >= pVictim->GetMaxHealth() - AchievementConsts::DamageNoKill_MaxHealthLeftOnKill && + Q_strncmp( pAttacker->GetPlayerName(), pVictim->m_DamageTakenList[i]->GetPlayerName(), MAX_PLAYER_NAME_LENGTH ) != 0 ) + { + //Now find the player who did that amount of damage + for ( int j = 1; j <= MAX_PLAYERS; j++ ) + { + CBasePlayer *pPlayerIter = UTIL_PlayerByIndex( j ); + + if ( pPlayerIter && Q_strncmp( pPlayerIter->GetPlayerName(), pVictim->m_DamageTakenList[i]->GetPlayerName(), MAX_PLAYER_NAME_LENGTH ) == 0 && + pPlayerIter->GetTeamNumber() != pVictim->GetTeamNumber() ) + { + ToCSPlayer(pPlayerIter)->AwardAchievement(CSDamageNoKill); + break; + } + } + } + } + + pAttacker->m_NumEnemiesKilledThisRound++; + + //store a list of kill times for spree tracking + pAttacker->m_killTimes.AddToTail(gpGlobals->curtime); + + //Add the victim to the list of players killed this round + pAttacker->m_enemyPlayersKilledThisRound.AddToTail(pVictim); + + //Calculate Avenging for all players the victim has killed + for ( int avengedIndex = 0; avengedIndex < pVictim->m_enemyPlayersKilledThisRound.Count(); avengedIndex++ ) + { + CCSPlayer* avengedPlayer = pVictim->m_enemyPlayersKilledThisRound[avengedIndex]; + + if (avengedPlayer) + { + //Make sure you are avenging someone on your own team (This is the expected flow. Just here to avoid edge cases like team-switching). + if (pAttacker->GetTeamNumber() == avengedPlayer->GetTeamNumber()) + { + CCS_GameStats.Event_PlayerAvengedTeammate(pAttacker, pVictim->m_enemyPlayersKilledThisRound[avengedIndex]); + } + } + } + + + + //remove elements older than a certain time + while (pAttacker->m_killTimes.Count() > 0 && pAttacker->m_killTimes[0] + AchievementConsts::KillingSpree_WindowTime < gpGlobals->curtime) + { + pAttacker->m_killTimes.Remove(0); + } + + //If we killed enough players in the time window, award the achievement + if (pAttacker->m_killTimes.Count() >= AchievementConsts::KillingSpree_Kills) + { + pAttacker->m_KillingSpreeStartTime = gpGlobals->curtime; + pAttacker->AwardAchievement(CSKillingSpree); + } + + // Did the attacker just kill someone on a killing spree? + if (pVictim->m_KillingSpreeStartTime >= 0 && pVictim->m_KillingSpreeStartTime - gpGlobals->curtime <= AchievementConsts::KillingSpreeEnder_TimeWindow) + { + pAttacker->AwardAchievement(CSKillingSpreeEnder); + } + + //Check the "killed someone with their own weapon" achievement + if (pAttackerWeapon && pAttackerWeapon->GetPreviousOwner() == pVictim) + { + pAttacker->AwardAchievement(CSKillEnemyWithFormerGun); + } + + //If this player has killed the entire team award him the achievement + if (pAttacker->m_NumEnemiesKilledThisRound == pAttacker->m_NumEnemiesAtRoundStart && pAttacker->m_NumEnemiesKilledThisRound >= AchievementConsts::KillEnemyTeam_MinKills) + { + pAttacker->AwardAchievement(CSKillEnemyTeam); + } + + //If this is a posthumous kill award the achievement + if (!pAttacker->IsAlive() && attackerWeaponId == WEAPON_HEGRENADE) + { + CCS_GameStats.IncrementStat(pAttacker, CSSTAT_GRENADE_POSTHUMOUSKILLS, 1); + ToCSPlayer(pAttacker)->AwardAchievement(CSPosthumousGrenadeKill); + } + + if (pAttacker->GetActiveWeapon() && pAttacker->GetActiveWeapon()->Clip1() == 0 && pAttackerWeapon && pAttackerWeapon->GetCSWpnData().m_WeaponType != WEAPONTYPE_SNIPER_RIFLE) + { + if (pInflictor == pAttacker) + { + pAttacker->AwardAchievement(CSKillEnemyLastBullet); + CCS_GameStats.IncrementStat(pAttacker, CSSTAT_KILLS_WITH_LAST_ROUND, 1); + } + } + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Fun-fact processing + //============================================================================= + + if (pVictimWeapon && pVictimWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_KNIFE && pAttackerWeapon && + pAttackerWeapon->GetCSWpnData().m_WeaponType != WEAPONTYPE_KNIFE && pAttackerWeapon->GetCSWpnData().m_WeaponType != WEAPONTYPE_C4 && pAttackerWeapon->GetCSWpnData().m_WeaponType != WEAPONTYPE_GRENADE) + { + // Victim was wielding knife when killed by a gun + pVictim->WieldingKnifeAndKilledByGun(true); + } + + //============================================================================= + // HPE_END + //============================================================================= + + //see if this is a unique weapon + if (attackerWeaponId != WEAPON_NONE) + { + if (pAttacker->m_killWeapons.Find(attackerWeaponId) == -1) + { + pAttacker->m_killWeapons.AddToTail(attackerWeaponId); + if (pAttacker->m_killWeapons.Count() >= AchievementConsts::KillsWithMultipleGuns_MinWeapons) + { + pAttacker->AwardAchievement(CSKillsWithMultipleGuns); + } + } + } + + //Check for kills while blind + if (pAttacker->IsBlindForAchievement()) + { + //if this is from a different blinding, restart the kill counter and set the time + if (pAttacker->m_blindStartTime != pAttacker->m_firstKillBlindStartTime) + { + pAttacker->m_killsWhileBlind = 0; + pAttacker->m_firstKillBlindStartTime = pAttacker->m_blindStartTime; + } + + ++pAttacker->m_killsWhileBlind; + if (pAttacker->m_killsWhileBlind >= AchievementConsts::KillEnemiesWhileBlind_Kills) + { + pAttacker->AwardAchievement(CSKillEnemiesWhileBlind); + } + + if (pAttacker->m_killsWhileBlind >= AchievementConsts::KillEnemiesWhileBlindHard_Kills) + { + pAttacker->AwardAchievement(CSKillEnemiesWhileBlindHard); + } + } + + //Check sniper killing achievements + bool victimZoomed = ( pVictim->GetFOV() != pVictim->GetDefaultFOV() ); + bool attackerZoomed = ( pAttacker->GetFOV() != pAttacker->GetDefaultFOV() ); + bool attackerUsedSniperRifle = pAttackerWeapon && pAttackerWeapon->GetCSWpnData().m_WeaponType == WEAPONTYPE_SNIPER_RIFLE && pInflictor == pAttacker; + if (victimZoomed && attackerUsedSniperRifle) + { + pAttacker->AwardAchievement(CSKillSniperWithSniper); + } + + if (attackerWeaponId == WEAPON_KNIFE && victimZoomed) + { + pAttacker->AwardAchievement(CSKillSniperWithKnife); + } + if (attackerUsedSniperRifle && !attackerZoomed) + { + pAttacker->AwardAchievement(CSHipShot); + } + + //Kill a player at low health + if (pAttacker->IsAlive() && pAttacker->GetHealth() <= AchievementConsts::KillWhenAtLowHealth_MaxHealth) + { + pAttacker->AwardAchievement(CSKillWhenAtLowHealth); + } + + //Kill a player with a knife during the pistol round + if (CSGameRules()->IsPistolRound()) + { + if (attackerWeaponId == WEAPON_KNIFE) + { + pAttacker->AwardAchievement(CSPistolRoundKnifeKill); + } + } + + //[tj] Check for dual elites fight + CWeaponCSBase* victimWeapon = pVictim->GetActiveCSWeapon(); + + if (victimWeapon) + { + CSWeaponID victimWeaponID = victimWeapon->GetWeaponID(); + + if (attackerWeaponId == WEAPON_ELITE && victimWeaponID == WEAPON_ELITE) + { + pAttacker->AwardAchievement(CSWinDualDuel); + } + } + + //[tj] See if the attacker or defender are in the air [sbodenbender] dont include ladders + bool attackerInAir = pAttacker->GetMoveType() != MOVETYPE_LADDER && pAttacker->GetNearestSurfaceBelow(AchievementConsts::KillInAir_MinimumHeight) == NULL; + bool victimInAir = pVictim->GetMoveType() != MOVETYPE_LADDER && pVictim->GetNearestSurfaceBelow(AchievementConsts::KillInAir_MinimumHeight) == NULL; + + if (attackerInAir) + { + pAttacker->AwardAchievement(CSKillWhileInAir); + } + if (victimInAir) + { + pAttacker->AwardAchievement(CSKillEnemyInAir); + } + if (attackerInAir && victimInAir) + { + pAttacker->AwardAchievement(CSKillerAndEnemyInAir); + } + + //[tj] advance to the next stage of the defuse defense achievement + if (pAttacker->m_defuseDefenseStep == DD_STARTED_DEFUSE) + { + pAttacker->m_defuseDefenseStep = DD_KILLED_TERRORIST; + } + + if (pVictim->HasC4() && pVictim->GetBombPickuptime() + AchievementConsts::KillBombPickup_MaxTime > gpGlobals->curtime) + { + pAttacker->AwardAchievement(CSKillBombPickup); + } + + } + + + //If you kill a friendly player while blind (from an enemy player), give the guy that blinded you an achievement + if ( pAttacker != NULL && pVictim != NULL && pVictim->GetTeamNumber() == pAttacker->GetTeamNumber() && pAttacker->IsBlind()) + { + CCSPlayer* flashbangAttacker = pAttacker->GetLastFlashbangAttacker(); + if (flashbangAttacker && pAttacker->GetTeamNumber() != flashbangAttacker->GetTeamNumber()) + { + flashbangAttacker->AwardAchievement(CSCauseFriendlyFireWithFlashbang); + } + } + + // do a scan to determine count of players still alive + int livePlayerCount = 0; + int teamCount[TEAM_MAXCOUNT]; + int teamIgnoreCount[TEAM_MAXCOUNT]; + memset(teamCount, 0, sizeof(teamCount)); + memset(teamIgnoreCount, 0, sizeof(teamIgnoreCount)); + CCSPlayer *pAlivePlayer = NULL; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer* pPlayer = (CCSPlayer*)UTIL_PlayerByIndex( i ); + if (pPlayer) + { + int teamNum = pPlayer->GetTeamNumber(); + if ( teamNum >= 0 ) + { + ++teamCount[teamNum]; + if (pPlayer->WasNotKilledNaturally()) + { + teamIgnoreCount[teamNum]++; + } + } + if (pPlayer->IsAlive() && pPlayer != pVictim) + { + ++livePlayerCount; + pAlivePlayer = pPlayer; + } + } + } + + // Achievement check for being the last player alive in a match + if (pAlivePlayer) + { + int alivePlayerTeam = pAlivePlayer->GetTeamNumber(); + int alivePlayerOpposingTeam = alivePlayerTeam == TEAM_CT ? TEAM_TERRORIST : TEAM_CT; + if (livePlayerCount == 1 + && CSGameRules()->m_iRoundWinStatus == WINNER_NONE + && teamCount[alivePlayerTeam] - teamIgnoreCount[alivePlayerTeam] >= AchievementConsts::LastPlayerAlive_MinPlayersOnTeam + && teamCount[alivePlayerOpposingTeam] - teamIgnoreCount[alivePlayerOpposingTeam] >= AchievementConsts::DefaultMinOpponentsForAchievement + && ( !(pAlivePlayer->m_iDisplayHistoryBits & DHF_FRIEND_KILLED) )) + { + pAlivePlayer->AwardAchievement(CSLastPlayerAlive); + } + } + + // [tj] Added hook into player killed stat that happens before weapon drop + CCS_GameStats.Event_PlayerKilled_PreWeaponDrop(pVictim, info); +} + +//[tj] traces up to maxTrace units down and returns any standable object it hits +// (doesn't check slope for standability) +CBaseEntity* CCSPlayer::GetNearestSurfaceBelow(float maxTrace) +{ + trace_t trace; + Ray_t ray; + + Vector traceStart = this->GetAbsOrigin(); + Vector traceEnd = traceStart; + traceEnd.z -= maxTrace; + + Vector minExtent = this->m_Local.m_bDucked ? VEC_DUCK_HULL_MIN_SCALED( this ) : VEC_HULL_MIN_SCALED( this ); + Vector maxExtent = this->m_Local.m_bDucked ? VEC_DUCK_HULL_MAX_SCALED( this ) : VEC_HULL_MAX_SCALED( this ); + + ray.Init( traceStart, traceEnd, minExtent, maxExtent ); + UTIL_TraceRay( ray, MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER_MOVEMENT, &trace ); + + return trace.m_pEnt; +} + +// [tj] Added a way to react to the round ending before we reset. +// It is important to note that this happens before the bomb explodes, so a player may die +// after this from a bomb explosion or a late kill after a defuse/detonation/rescue. +void CCSPlayer::OnRoundEnd(int winningTeam, int reason) +{ + if (winningTeam == WINNER_CT || winningTeam == WINNER_TER) + { + int losingTeamId = (winningTeam == TEAM_CT) ? TEAM_TERRORIST : TEAM_CT; + + CTeam* losingTeam = GetGlobalTeam(losingTeamId); + + int losingTeamPlayers = 0; + + if (losingTeam) + { + losingTeamPlayers = losingTeam->GetNumPlayers(); + + int ignoreCount = 0; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer* pPlayer = (CCSPlayer*)UTIL_PlayerByIndex( i ); + if (pPlayer) + { + int teamNum = pPlayer->GetTeamNumber(); + if ( teamNum == losingTeamId ) + { + if (pPlayer->WasNotKilledNaturally()) + { + ignoreCount++; + } + } + + } + } + + losingTeamPlayers -= ignoreCount; + } + + //Check fast round win achievement + if ( IsAlive() && + gpGlobals->curtime - CSGameRules()->GetRoundStartTime() < AchievementConsts::FastRoundWin_Time && + GetTeamNumber() == winningTeam && + losingTeamPlayers >= AchievementConsts::DefaultMinOpponentsForAchievement) + { + AwardAchievement(CSFastRoundWin); + } + + //Check goosechase achievement + if (IsAlive() && reason == Target_Bombed && m_gooseChaseStep == GC_STOPPED_AFTER_GETTING_SHOT && m_pGooseChaseDistractingPlayer) + { + m_pGooseChaseDistractingPlayer->AwardAchievement(CSGooseChase); + } + + //Check Defuse Defense achievement + if (IsAlive() && reason == Bomb_Defused && m_defuseDefenseStep == DD_KILLED_TERRORIST) + { + AwardAchievement(CSDefuseDefense); + } + + //Check silent win + if (m_NumEnemiesKilledThisRound > 0 && GetTeamNumber() == winningTeam && !m_bMadeFootstepNoise) + { + AwardAchievement(CSSilentWin); + } + + //Process && Check "win rounds without buying" achievement + if (GetTeamNumber() == winningTeam && !m_bMadePurchseThisRound) + { + m_roundsWonWithoutPurchase++; + if (m_roundsWonWithoutPurchase > AchievementConsts::WinRoundsWithoutBuying_Rounds) + { + AwardAchievement(CSWinRoundsWithoutBuying); + } + } + else + { + m_roundsWonWithoutPurchase = 0; + } + } + + m_lastRoundResult = reason; +} + +void CCSPlayer::OnPreResetRound() +{ + //Check headshot survival achievement + if (IsAlive() && m_bSurvivedHeadshotDueToHelmet) + { + AwardAchievement(CSSurvivedHeadshotDueToHelmet); + } + + if (IsAlive() && m_grenadeDamageTakenThisRound > AchievementConsts::SurviveGrenade_MinDamage) + { + AwardAchievement(CSSurviveGrenade); + } + + + //Check achievement for surviving attacks from multiple players. + if (IsAlive()) + { + int numberOfEnemyDamagers = GetNumEnemyDamagers(); + + if (numberOfEnemyDamagers >= AchievementConsts::SurviveManyAttacks_NumberDamagingPlayers) + { + AwardAchievement(CSSurviveManyAttacks); + } + } +} + +void CCSPlayer::OnCanceledDefuse() +{ + if (m_gooseChaseStep == GC_SHOT_DURING_DEFUSE) + { + m_gooseChaseStep = GC_STOPPED_AFTER_GETTING_SHOT; + } +} + + +void CCSPlayer::OnStartedDefuse() +{ + if (m_defuseDefenseStep == DD_NONE) + { + m_defuseDefenseStep = DD_STARTED_DEFUSE; + } +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSPlayer::AttemptToExitFreezeCam( void ) +{ + float fEndFreezeTravel = m_flDeathTime + CS_DEATH_ANIMATION_TIME + spec_freeze_traveltime.GetFloat(); + if ( gpGlobals->curtime < fEndFreezeTravel ) + return; + + m_bAbortFreezeCam = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets whether this player is dominating the specified other player +//----------------------------------------------------------------------------- +void CCSPlayer::SetPlayerDominated( CCSPlayer *pPlayer, bool bDominated ) +{ + int iPlayerIndex = pPlayer->entindex(); + m_bPlayerDominated.Set( iPlayerIndex, bDominated ); + pPlayer->SetPlayerDominatingMe( this, bDominated ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets whether this player is being dominated by the other player +//----------------------------------------------------------------------------- +void CCSPlayer::SetPlayerDominatingMe( CCSPlayer *pPlayer, bool bDominated ) +{ + int iPlayerIndex = pPlayer->entindex(); + m_bPlayerDominatingMe.Set( iPlayerIndex, bDominated ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns whether this player is dominating the specified other player +//----------------------------------------------------------------------------- +bool CCSPlayer::IsPlayerDominated( int iPlayerIndex ) +{ + return m_bPlayerDominated.Get( iPlayerIndex ); +} + +bool CCSPlayer::IsPlayerDominatingMe( int iPlayerIndex ) +{ + return m_bPlayerDominatingMe.Get( iPlayerIndex ); +} + +//============================================================================= +// HPE_BEGIN: +// [menglish] MVP functions +//============================================================================= + +void CCSPlayer::IncrementNumMVPs( CSMvpReason_t mvpReason ) +{ + //============================================================================= + // HPE_BEGIN: + // [Forrest] Allow MVP to be turned off for a server + //============================================================================= + if ( sv_nomvp.GetBool() ) + { + Msg( "Round MVP disabled: sv_nomvp is set.\n" ); + return; + } + //============================================================================= + // HPE_END + //============================================================================= + + m_iMVPs++; + CCS_GameStats.Event_MVPEarned( this ); + IGameEvent *mvpEvent = gameeventmanager->CreateEvent( "round_mvp" ); + + if ( mvpEvent ) + { + mvpEvent->SetInt( "userid", GetUserID() ); + mvpEvent->SetInt( "reason", mvpReason ); + gameeventmanager->FireEvent( mvpEvent ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the number of rounds this player has caused to be won for their team +//----------------------------------------------------------------------------- +void CCSPlayer::SetNumMVPs( int iNumMVP ) +{ + m_iMVPs = iNumMVP; +} +//----------------------------------------------------------------------------- +// Purpose: Returns the number of rounds this player has caused to be won for their team +//----------------------------------------------------------------------------- +int CCSPlayer::GetNumMVPs() +{ + return m_iMVPs; +} + +//============================================================================= +// HPE_END +//============================================================================= + +//----------------------------------------------------------------------------- +// Purpose: Removes all nemesis relationships between this player and others +//----------------------------------------------------------------------------- +void CCSPlayer::RemoveNemesisRelationships() +{ + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + CCSPlayer *pTemp = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + if ( pTemp && pTemp != this ) + { + // set this player to be not dominating anyone else + SetPlayerDominated( pTemp, false ); + + // set no one else to be dominating this player + pTemp->SetPlayerDominated( this, false ); + } + } +} + +void CCSPlayer::CheckMaxGrenadeKills(int grenadeKills) +{ + if (grenadeKills > m_maxGrenadeKills) + { + m_maxGrenadeKills = grenadeKills; + } +} + +void CCSPlayer::CommitSuicide( bool bExplode /*= false*/, bool bForce /*= false*/ ) +{ + m_wasNotKilledNaturally = true; + BaseClass::CommitSuicide(bExplode, bForce); +} + +void CCSPlayer::CommitSuicide( const Vector &vecForce, bool bExplode /*= false*/, bool bForce /*= false*/ ) +{ + m_wasNotKilledNaturally = true; + BaseClass::CommitSuicide(vecForce, bExplode, bForce); +} + +int CCSPlayer::GetNumEnemyDamagers() +{ + int numberOfEnemyDamagers = 0; + FOR_EACH_LL( m_DamageTakenList, i ) + { + for ( int j = 1; j <= MAX_PLAYERS; j++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( j ); + + if ( pPlayer && V_strncmp( pPlayer->GetPlayerName(), m_DamageTakenList[i]->GetPlayerName(), MAX_PLAYER_NAME_LENGTH ) == 0 && + pPlayer->GetTeamNumber() != GetTeamNumber() ) + { + numberOfEnemyDamagers++; + } + } + } + return numberOfEnemyDamagers; +} + + +int CCSPlayer::GetNumEnemiesDamaged() +{ + int numberOfEnemiesDamaged = 0; + FOR_EACH_LL( m_DamageGivenList, i ) + { + for ( int j = 1; j <= MAX_PLAYERS; j++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( j ); + + if ( pPlayer && V_strncmp( pPlayer->GetPlayerName(), m_DamageGivenList[i]->GetPlayerName(), MAX_PLAYER_NAME_LENGTH ) == 0 && + pPlayer->GetTeamNumber() != GetTeamNumber() ) + { + numberOfEnemiesDamaged++; + } + } + } + return numberOfEnemiesDamaged; +} + +//============================================================================= +// HPE_END +//============================================================================= + +void UTIL_AwardMoneyToTeam( int iAmount, int iTeam, CBaseEntity *pIgnore ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer *pPlayer = (CCSPlayer*) UTIL_PlayerByIndex( i ); + + if ( !pPlayer ) + continue; + + if ( pPlayer->GetTeamNumber() != iTeam ) + continue; + + if ( pPlayer == pIgnore ) + continue; + + pPlayer->AddAccount( iAmount ); + } +} + diff --git a/game/server/cstrike/cs_player.h b/game/server/cstrike/cs_player.h new file mode 100644 index 0000000..9abf96a --- /dev/null +++ b/game/server/cstrike/cs_player.h @@ -0,0 +1,1116 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Player for HL1. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef CS_PLAYER_H +#define CS_PLAYER_H +#pragma once + + +#include "basemultiplayerplayer.h" +#include "server_class.h" +#include "cs_playeranimstate.h" +#include "cs_shareddefs.h" +#include "cs_autobuy.h" +#include "utldict.h" + + + +class CWeaponCSBase; +class CMenu; +class CHintMessageQueue; +class CNavArea; + +#include "cs_weapon_parse.h" + + +void UTIL_AwardMoneyToTeam( int iAmount, int iTeam, CBaseEntity *pIgnore ); + +#define MENU_STRING_BUFFER_SIZE 1024 +#define MENU_MSG_TEXTCHUNK_SIZE 50 + +enum +{ + MIN_NAME_CHANGE_INTERVAL = 10, // minimum number of seconds between name changes + NAME_CHANGE_HISTORY_SIZE = 5, // number of times a player can change names in NAME_CHANGE_HISTORY_INTERVAL + NAME_CHANGE_HISTORY_INTERVAL = 600, // no more than NAME_CHANGE_HISTORY_SIZE name changes can be made in this many seconds +}; + +extern ConVar bot_mimic; + + +// Function table for each player state. +class CCSPlayerStateInfo +{ +public: + CSPlayerState m_iPlayerState; + const char *m_pStateName; + + void (CCSPlayer::*pfnEnterState)(); // Init and deinit the state. + void (CCSPlayer::*pfnLeaveState)(); + + void (CCSPlayer::*pfnPreThink)(); // Do a PreThink() in this state. +}; + + +//======================================= +//Record of either damage taken or given. +//Contains the player name that we hurt or that hurt us, +//and the total damage +//======================================= +class CDamageRecord +{ +public: + CDamageRecord( const char *name, int iDamage, int iCounter ) + { + Q_strncpy( m_szPlayerName, name, sizeof(m_szPlayerName) ); + m_iDamage = iDamage; + m_iNumHits = 1; + m_iLastBulletUpdate = iCounter; + } + + void AddDamage( int iDamage, int iCounter ) + { + m_iDamage += iDamage; + + if ( m_iLastBulletUpdate != iCounter ) + m_iNumHits++; + + m_iLastBulletUpdate = iCounter; + } + + char *GetPlayerName( void ) { return m_szPlayerName; } + int GetDamage( void ) { return m_iDamage; } + int GetNumHits( void ) { return m_iNumHits; } + +private: + char m_szPlayerName[MAX_PLAYER_NAME_LENGTH]; + int m_iDamage; //how much damage was done + int m_iNumHits; //how many hits + int m_iLastBulletUpdate; // update counter +}; + +// Message display history (CCSPlayer::m_iDisplayHistoryBits) +// These bits are set when hint messages are displayed, and cleared at +// different times, according to the DHM_xxx bitmasks that follow + +#define DHF_ROUND_STARTED ( 1 << 1 ) +#define DHF_HOSTAGE_SEEN_FAR ( 1 << 2 ) +#define DHF_HOSTAGE_SEEN_NEAR ( 1 << 3 ) +#define DHF_HOSTAGE_USED ( 1 << 4 ) +#define DHF_HOSTAGE_INJURED ( 1 << 5 ) +#define DHF_HOSTAGE_KILLED ( 1 << 6 ) +#define DHF_FRIEND_SEEN ( 1 << 7 ) +#define DHF_ENEMY_SEEN ( 1 << 8 ) +#define DHF_FRIEND_INJURED ( 1 << 9 ) +#define DHF_FRIEND_KILLED ( 1 << 10 ) +#define DHF_ENEMY_KILLED ( 1 << 11 ) +#define DHF_BOMB_RETRIEVED ( 1 << 12 ) +#define DHF_AMMO_EXHAUSTED ( 1 << 15 ) +#define DHF_IN_TARGET_ZONE ( 1 << 16 ) +#define DHF_IN_RESCUE_ZONE ( 1 << 17 ) +#define DHF_IN_ESCAPE_ZONE ( 1 << 18 ) // unimplemented +#define DHF_IN_VIPSAFETY_ZONE ( 1 << 19 ) // unimplemented +#define DHF_NIGHTVISION ( 1 << 20 ) +#define DHF_HOSTAGE_CTMOVE ( 1 << 21 ) +#define DHF_SPEC_DUCK ( 1 << 22 ) + +// DHF_xxx bits to clear when the round restarts + +#define DHM_ROUND_CLEAR ( \ + DHF_ROUND_STARTED | \ + DHF_HOSTAGE_KILLED | \ + DHF_FRIEND_KILLED | \ + DHF_BOMB_RETRIEVED ) + + +// DHF_xxx bits to clear when the player is restored + +#define DHM_CONNECT_CLEAR ( \ + DHF_HOSTAGE_SEEN_FAR | \ + DHF_HOSTAGE_SEEN_NEAR | \ + DHF_HOSTAGE_USED | \ + DHF_HOSTAGE_INJURED | \ + DHF_FRIEND_SEEN | \ + DHF_ENEMY_SEEN | \ + DHF_FRIEND_INJURED | \ + DHF_ENEMY_KILLED | \ + DHF_AMMO_EXHAUSTED | \ + DHF_IN_TARGET_ZONE | \ + DHF_IN_RESCUE_ZONE | \ + DHF_IN_ESCAPE_ZONE | \ + DHF_IN_VIPSAFETY_ZONE | \ + DHF_HOSTAGE_CTMOVE | \ + DHF_SPEC_DUCK ) + +// radio messages (these must be kept in sync with actual radio) ------------------------------------- +enum RadioType +{ + RADIO_INVALID = 0, + + RADIO_START_1, ///< radio messages between this and RADIO_START_2 and part of Radio1() + + RADIO_COVER_ME, + RADIO_YOU_TAKE_THE_POINT, + RADIO_HOLD_THIS_POSITION, + RADIO_REGROUP_TEAM, + RADIO_FOLLOW_ME, + RADIO_TAKING_FIRE, + + RADIO_START_2, ///< radio messages between this and RADIO_START_3 are part of Radio2() + + RADIO_GO_GO_GO, + RADIO_TEAM_FALL_BACK, + RADIO_STICK_TOGETHER_TEAM, + RADIO_GET_IN_POSITION_AND_WAIT, + RADIO_STORM_THE_FRONT, + RADIO_REPORT_IN_TEAM, + + RADIO_START_3, ///< radio messages above this are part of Radio3() + + RADIO_AFFIRMATIVE, + RADIO_ENEMY_SPOTTED, + RADIO_NEED_BACKUP, + RADIO_SECTOR_CLEAR, + RADIO_IN_POSITION, + RADIO_REPORTING_IN, + RADIO_GET_OUT_OF_THERE, + RADIO_NEGATIVE, + RADIO_ENEMY_DOWN, + + RADIO_END, + + RADIO_NUM_EVENTS +}; + +extern const char *RadioEventName[ RADIO_NUM_EVENTS+1 ]; + +/** + * Convert name to RadioType + */ +extern RadioType NameToRadioEvent( const char *name ); + +enum BuyResult_e +{ + BUY_BOUGHT, + BUY_ALREADY_HAVE, + BUY_CANT_AFFORD, + BUY_PLAYER_CANT_BUY, // not in the buy zone, is the VIP, is past the timelimit, etc + BUY_NOT_ALLOWED, // weapon is restricted by VIP mode, team, etc + BUY_INVALID_ITEM, +}; + +//============================================================================= +// HPE_BEGIN: +//============================================================================= + +// [tj] The phases for the "Goose Chase" achievement +enum GooseChaseAchievementStep +{ + GC_NONE, + GC_SHOT_DURING_DEFUSE, + GC_STOPPED_AFTER_GETTING_SHOT +}; + +// [tj] The phases for the "Defuse Defense" achievement +enum DefuseDefenseAchivementStep +{ + DD_NONE, + DD_STARTED_DEFUSE, + DD_KILLED_TERRORIST +}; + +//============================================================================= +// HPE_END +//============================================================================= + + +//============================================================================= +// >> CounterStrike player +//============================================================================= +class CCSPlayer : public CBaseMultiplayerPlayer, public ICSPlayerAnimStateHelpers +{ +public: + DECLARE_CLASS( CCSPlayer, CBaseMultiplayerPlayer ); + DECLARE_SERVERCLASS(); + DECLARE_PREDICTABLE(); + DECLARE_DATADESC(); + + CCSPlayer(); + ~CCSPlayer(); + + static CCSPlayer *CreatePlayer( const char *className, edict_t *ed ); + static CCSPlayer* Instance( int iEnt ); + + virtual void Precache(); + virtual void Spawn(); + virtual void InitialSpawn( void ); + + virtual void CheatImpulseCommands( int iImpulse ); + virtual void PlayerRunCommand( CUserCmd *ucmd, IMoveHelper *moveHelper ); + virtual void PostThink(); + + virtual int OnTakeDamage( const CTakeDamageInfo &inputInfo ); + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + virtual void Event_Killed( const CTakeDamageInfo &info ); + + //============================================================================= + // HPE_BEGIN: + // [tj] We have a custom implementation so we can check for achievements. + //============================================================================= + + virtual void Event_KilledOther( CBaseEntity *pVictim, const CTakeDamageInfo &info ); + + //============================================================================= + // HPE_END + //============================================================================= + + virtual void TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + virtual CBaseEntity *GiveNamedItem( const char *pszName, int iSubType = 0 ); + virtual bool IsBeingGivenItem() const { return m_bIsBeingGivenItem; } + + virtual CBaseEntity *FindUseEntity( void ); + virtual bool IsUseableEntity( CBaseEntity *pEntity, unsigned int requiredCaps ); + + virtual void CreateViewModel( int viewmodelindex = 0 ); + virtual void ShowViewPortPanel( const char * name, bool bShow = true, KeyValues *data = NULL ); + + // This passes the event to the client's and server's CPlayerAnimState. + void DoAnimationEvent( PlayerAnimEvent_t event, int nData = 0 ); + + // from CBasePlayer + virtual void SetupVisibility( CBaseEntity *pViewEntity, unsigned char *pvs, int pvssize ); + + virtual int GetNextObserverSearchStartPoint( bool bReverse ); +// In shared code. +public: + + // ICSPlayerAnimState overrides. + virtual CWeaponCSBase* CSAnim_GetActiveWeapon(); + virtual bool CSAnim_CanMove(); + + virtual float GetPlayerMaxSpeed(); + + void FireBullet( + Vector vecSrc, + const QAngle &shootAngles, + float flDistance, + int iPenetration, + int iBulletType, + int iDamage, + float flRangeModifier, + CBaseEntity *pevAttacker, + bool bDoEffects, + float xSpread, float ySpread ); + + void KickBack( + float up_base, + float lateral_base, + float up_modifier, + float lateral_modifier, + float up_max, + float lateral_max, + int direction_change ); + + void GetBulletTypeParameters( + int iBulletType, + float &fPenetrationPower, + float &flPenetrationDistance ); + + // Returns true if the player is allowed to move. + bool CanMove() const; + + void OnJump( float fImpulse ); + void OnLand( float fVelocity ); + + bool HasC4() const; // Is this player carrying a C4 bomb? + bool IsVIP() const; + + int GetClass( void ) const; + + void MakeVIP( bool isVIP ); + + virtual void SetAnimation( PLAYER_ANIM playerAnim ); + ICSPlayerAnimState *GetPlayerAnimState() { return m_PlayerAnimState; } + + virtual bool StartReplayMode( float fDelay, float fDuration, int iEntity ); + virtual void StopReplayMode(); + virtual void PlayUseDenySound(); + + +public: + + // Simulates a single frame of movement for a player + void RunPlayerMove( const QAngle& viewangles, float forwardmove, float sidemove, float upmove, unsigned short buttons, byte impulse, float frametime ); + virtual void HandleAnimEvent( animevent_t *pEvent ); + + virtual void UpdateStepSound( surfacedata_t *psurface, const Vector &vecOrigin, const Vector &vecVelocity ); + virtual void PlayStepSound( Vector &vecOrigin, surfacedata_t *psurface, float fvol, bool force ); + + // from cbasecombatcharacter + void InitVCollision( const Vector &vecAbsOrigin, const Vector &vecAbsVelocity ); + void VPhysicsShadowUpdate( IPhysicsObject *pPhysics ); + + bool HasShield() const; + bool IsShieldDrawn() const; + void GiveShield( void ); + void RemoveShield( void ); + bool IsProtectedByShield( void ) const; // returns true if player has a shield and is currently hidden behind it + + bool HasPrimaryWeapon( void ); + bool HasSecondaryWeapon( void ); + + bool IsReloading( void ) const; // returns true if current weapon is reloading + + void GiveDefaultItems(); + void RemoveAllItems( bool removeSuit ); //overridden to remove the defuser + + // Reset account, get rid of shield, etc.. + void Reset(); + + void RoundRespawn( void ); + void ObserverRoundRespawn( void ); + void CheckTKPunishment( void ); + + // Add money to this player's account. + void AddAccount( int amount, bool bTrackChange=true, bool bItemBought=false, const char *pItemName = NULL ); + + void HintMessage( const char *pMessage, bool bDisplayIfDead, bool bOverrideClientSettings = false ); // Displays a hint message to the player + CHintMessageQueue *m_pHintMessageQueue; + unsigned int m_iDisplayHistoryBits; + bool m_bShowHints; + float m_flLastAttackedTeammate; + float m_flNextMouseoverUpdate; + void UpdateMouseoverHints(); + + // mark this player as not receiving money at the start of the next round. + void MarkAsNotReceivingMoneyNextRound(); + bool DoesPlayerGetRoundStartMoney(); // self-explanitory :) + + void DropC4(); // Get rid of the C4 bomb. + + bool HasDefuser(); // Is this player carrying a bomb defuser? + void GiveDefuser(bool bPickedUp = false); // give the player a defuser + void RemoveDefuser(); // remove defuser from the player and remove the model attachment + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + bool PickedUpDefuser() { return m_bPickedUpDefuser; } + void SetDefusedWithPickedUpKit(bool bDefusedWithPickedUpKit) { m_bDefusedWithPickedUpKit = bDefusedWithPickedUpKit; } + bool GetDefusedWithPickedUpKit() { return m_bDefusedWithPickedUpKit; } + + //============================================================================= + // HPE_END + //============================================================================= + + + //============================================================================= + // HPE_BEGIN + // [sbodenbender] Need a different test for player blindness for the achievements + //============================================================================= + + bool IsBlindForAchievement(); // more stringent than IsBlind; more accurately represents when the player can see again + + //============================================================================= + // HPE_END + //============================================================================= + + bool IsBlind( void ) const; // return true if this player is blind (from a flashbang) + virtual void Blind( float holdTime, float fadeTime, float startingAlpha = 255 ); // player blinded by a flashbang + float m_blindUntilTime; + float m_blindStartTime; + + void Deafen( float flDistance ); //make the player deaf / apply dsp preset to muffle sound + + void ApplyDeafnessEffect(); // apply the deafness effect for a nearby explosion. + + bool IsAutoFollowAllowed( void ) const; // return true if this player will allow bots to auto follow + void InhibitAutoFollow( float duration ); // prevent bots from auto-following for given duration + void AllowAutoFollow( void ); // allow bots to auto-follow immediately + float m_allowAutoFollowTime; // bots can auto-follow after this time + + // Have this guy speak a message into his radio. + void Radio( const char *szRadioSound, const char *szRadioText = NULL ); + void ConstructRadioFilter( CRecipientFilter& filter ); + + void EmitPrivateSound( const char *soundName ); ///< emit given sound that only we can hear + + CWeaponCSBase* GetActiveCSWeapon() const; + + void PreThink(); + + // This is the think function for the player when they first join the server and have to select a team + void JoiningThink(); + + virtual bool ClientCommand( const CCommand &args ); + + bool HandleCommand_JoinClass( int iClass ); + bool HandleCommand_JoinTeam( int iTeam ); + + BuyResult_e HandleCommand_Buy( const char *item ); + + BuyResult_e HandleCommand_Buy_Internal( const char * item ); + + void HandleMenu_Radio1( int slot ); + void HandleMenu_Radio2( int slot ); + void HandleMenu_Radio3( int slot ); + + float m_flRadioTime; + int m_iRadioMessages; + int iRadioMenu; + + void ListPlayers(); + + bool m_bIgnoreRadio; + + // Returns one of the CS_CLASS_ enums. + int PlayerClass() const; + + void MoveToNextIntroCamera(); + + // Used to be GETINTOGAME state. + void GetIntoGame(); + + CBaseEntity* EntSelectSpawnPoint(); + + void SetProgressBarTime( int barTime ); + virtual void PlayerDeathThink(); + + void Weapon_Equip( CBaseCombatWeapon *pWeapon ); + virtual bool BumpWeapon( CBaseCombatWeapon *pWeapon ); + virtual bool Weapon_CanUse( CBaseCombatWeapon *pWeapon ); + + void ClearFlashbangScreenFade ( void ); + bool ShouldDoLargeFlinch( int nHitGroup, CBaseEntity *pAttacker ); + + void ResetStamina( void ); + bool IsArmored( int nHitGroup ); + void Pain( bool HasArmour ); + + void DeathSound( const CTakeDamageInfo &info ); + + bool Weapon_CanSwitchTo( CBaseCombatWeapon *pWeapon ); + + void ChangeTeam( int iTeamNum ); + void SwitchTeam( int iTeamNum ); // Changes teams without penalty - used for auto team balancing + + void ModifyOrAppendPlayerCriteria( AI_CriteriaSet& set ); + + virtual void OnDamagedByExplosion( const CTakeDamageInfo &info ); + + // Called whenever this player fires a shot. + void NoteWeaponFired(); + virtual bool WantsLagCompensationOnEntity( const CBasePlayer *pPlayer, const CUserCmd *pCmd, const CBitVec<MAX_EDICTS> *pEntityTransmitBits ) const; + +// ------------------------------------------------------------------------------------------------ // +// Player state management. +// ------------------------------------------------------------------------------------------------ // +public: + + void State_Transition( CSPlayerState newState ); // Cleanup the previous state and enter a new state. + CSPlayerState State_Get() const; // Get the current state. + + +private: + void State_Enter( CSPlayerState newState ); // Initialize the new state. + void State_Leave(); // Cleanup the previous state. + void State_PreThink(); // Update the current state. + + // Find the state info for the specified state. + static CCSPlayerStateInfo* State_LookupInfo( CSPlayerState state ); + + // This tells us which state the player is currently in (joining, observer, dying, etc). + // Each state has a well-defined set of parameters that go with it (ie: observer is movetype_noclip, nonsolid, + // invisible, etc). + CNetworkVar( CSPlayerState, m_iPlayerState ); + + CCSPlayerStateInfo *m_pCurStateInfo; // This can be NULL if no state info is defined for m_iPlayerState. + + // tells us whether or not this player gets money at the start of the next round. + bool m_receivesMoneyNextRound; + + + // Specific state handler functions. + void State_Enter_WELCOME(); + void State_PreThink_WELCOME(); + + void State_Enter_PICKINGTEAM(); + void State_Enter_PICKINGCLASS(); + + void State_Enter_ACTIVE(); + void State_PreThink_ACTIVE(); + + void State_Enter_OBSERVER_MODE(); + void State_PreThink_OBSERVER_MODE(); + + void State_Enter_DEATH_WAIT_FOR_KEY(); + void State_PreThink_DEATH_WAIT_FOR_KEY(); + + void State_Enter_DEATH_ANIM(); + void State_PreThink_DEATH_ANIM(); + + int FlashlightIsOn( void ); + void FlashlightTurnOn( void ); + void FlashlightTurnOff( void ); + + void UpdateAddonBits(); + void UpdateRadar(); + +public: + + void SetDeathPose( const int &iDeathPose ) { m_iDeathPose = iDeathPose; } + void SetDeathPoseFrame( const int &iDeathPoseFrame ) { m_iDeathFrame = iDeathPoseFrame; } + + void SelectDeathPose( const CTakeDamageInfo &info ); + +private: + int m_iDeathPose; + int m_iDeathFrame; + +//============================================================================= +// HPE_BEGIN: +// [menglish] Freeze cam function and variable declarations +//============================================================================= + + bool m_bAbortFreezeCam; + +protected: + void AttemptToExitFreezeCam( void ); + +//============================================================================= +// HPE_END +//============================================================================= + +public: + + // Predicted variables. + CNetworkVar( bool, m_bResumeZoom ); + CNetworkVar( int , m_iLastZoom ); // after firing a shot, set the FOV to 90, and after showing the animation, bring the FOV back to last zoom level. + CNetworkVar( bool, m_bIsDefusing ); // tracks whether this player is currently defusing a bomb + int m_LastHitGroup; // the last body region that took damage + //============================================================================= + // HPE_BEGIN: + // [menglish] Adding two variables, keeping track of damage to the player + //============================================================================= + + int m_LastHitBox; // the last body hitbox that took damage + Vector m_vLastHitLocationObjectSpace; //position where last hit occured in space of the bone associated with the hitbox + EHANDLE m_hDroppedEquipment[DROPPED_COUNT]; + + // [tj] overriding the base suicides to trash CS specific stuff + virtual void CommitSuicide( bool bExplode = false, bool bForce = false ); + virtual void CommitSuicide( const Vector &vecForce, bool bExplode = false, bool bForce = false ); + + void WieldingKnifeAndKilledByGun( bool bState ) { m_bWieldingKnifeAndKilledByGun = bState; } + bool WasWieldingKnifeAndKilledByGun() { return m_bWieldingKnifeAndKilledByGun; } + + // [dwenger] adding tracking for weapon used fun fact + void PlayerUsedFirearm( CBaseCombatWeapon* pBaseWeapon ); + int GetNumFirearmsUsed() { return m_WeaponTypesUsed.Count(); } + + //============================================================================= + // HPE_END + //============================================================================= + + CNetworkVar( bool, m_bHasHelmet ); // Does the player have helmet armor + bool m_bEscaped; // Has this terrorist escaped yet? + + // Other variables. + bool m_bIsVIP; // Are we the VIP? + int m_iNumSpawns; // Number of times player has spawned this round + int m_iOldTeam; // Keep what team they were last on so we can allow joining spec and switching back to their real team + bool m_bTeamChanged; // Just allow one team change per round + CNetworkVar( int, m_iAccount ); // How much cash this player has. + int m_iShouldHaveCash; + + bool m_bJustKilledTeammate; + bool m_bPunishedForTK; + int m_iTeamKills; + float m_flLastMovement; + int m_iNextTimeCheck; // Next time the player can execute a "timeleft" command + + float m_flNameChangeHistory[NAME_CHANGE_HISTORY_SIZE]; // index 0 = most recent change + + bool CanChangeName( void ); // Checks if the player can change his name + void ChangeName( const char *pszNewName ); + + void SetClanTag( const char *pTag ); + const char *GetClanTag( void ) const; + + + CNetworkVar( bool, m_bHasDefuser ); // Does this player have a defuser kit? + CNetworkVar( bool, m_bHasNightVision ); // Does this player have night vision? + CNetworkVar( bool, m_bNightVisionOn ); // Is the NightVision turned on ? + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + //CNetworkVar( bool, m_bPickedUpDefuser ); // Did player pick up the defuser kit as opposed to buying it? + //CNetworkVar( bool, m_bDefusedWithPickedUpKit); // Did player defuse the bomb with a picked-up defuse kit? + + //============================================================================= + // HPE_END + //============================================================================= + + float m_flLastRadarUpdateTime; + + // last known navigation area of player - NULL if unknown + CNavArea *m_lastNavArea; + + // Backup copy of the menu text so the player can change this and the menu knows when to update. + char m_MenuStringBuffer[MENU_STRING_BUFFER_SIZE]; + + // When the player joins, it cycles their view between trigger_camera entities. + // This is the current camera, and the time that we'll switch to the next one. + EHANDLE m_pIntroCamera; + float m_fIntroCamTime; + + // Set to true each frame while in a bomb zone. + // Reset after prediction (in PostThink). + CNetworkVar( bool, m_bInBombZone ); + CNetworkVar( bool, m_bInBuyZone ); + int m_iBombSiteIndex; + + bool IsInBuyZone(); + bool CanPlayerBuy( bool display ); + + CNetworkVar( bool, m_bInHostageRescueZone ); + void RescueZoneTouch( inputdata_t &inputdata ); + + CNetworkVar( float, m_flStamina ); + CNetworkVar( int, m_iDirection ); // The current lateral kicking direction; 1 = right, 0 = left + CNetworkVar( int, m_iShotsFired ); // number of shots fired recently + + // Make sure to register changes for armor. + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_ArmorValue ); + + CNetworkVar( float, m_flVelocityModifier ); + + int m_iHostagesKilled; + + void SetShieldDrawnState( bool bState ); + void DropShield( void ); + + char m_szNewName [MAX_PLAYER_NAME_LENGTH]; // not empty if player requested a namechange + char m_szClanTag[MAX_CLAN_TAG_LENGTH]; + + Vector m_vecTotalBulletForce; //Accumulator for bullet force in a single frame + + CNetworkVar( float, m_flFlashDuration ); + CNetworkVar( float, m_flFlashMaxAlpha ); + + CNetworkVar( float, m_flProgressBarStartTime ); + CNetworkVar( int, m_iProgressBarDuration ); + CNetworkVar( int, m_iThrowGrenadeCounter ); // used to trigger grenade throw animations. + + // Tracks our ragdoll entity. + CNetworkHandle( CBaseEntity, m_hRagdoll ); // networked entity handle + + // Bots and hostages auto-duck during jumps + bool m_duckUntilOnGround; + + Vector m_lastStandingPos; // used by the gamemovement code for finding ladders + + void SurpressLadderChecks( const Vector& pos, const Vector& normal ); + bool CanGrabLadder( const Vector& pos, const Vector& normal ); + + CNetworkVar( bool, m_bDetected ); + +private: + CountdownTimer m_ladderSurpressionTimer; + Vector m_lastLadderNormal; + Vector m_lastLadderPos; + +protected: + + void CreateRagdollEntity(); + + bool IsHittingShield( const Vector &vecDirection, trace_t *ptr ); + + void PhysObjectSleep(); + void PhysObjectWake(); + + bool RunMimicCommand( CUserCmd& cmd ); + + bool SelectSpawnSpot( const char *pEntClassName, CBaseEntity* &pSpot ); + + void SetModelFromClass( void ); + CNetworkVar( int, m_iClass ); // One of the CS_CLASS_ enums. + + bool CSWeaponDrop( CBaseCombatWeapon *pWeapon, bool bDropShield = true, bool bThrow = false ); + bool DropRifle( bool fromDeath = false ); + bool DropPistol( bool fromDeath = false ); + + //============================================================================= + // HPE_BEGIN: + // [tj] Added a parameter so we know if it was death that caused the drop + // [menglish] New parameter to always know if this is from death and not just an enemy death + //============================================================================= + + void DropWeapons( bool fromDeath, bool friendlyFire ); + + //============================================================================= + // HPE_END + //============================================================================= + + + virtual int SpawnArmorValue( void ) const { return ArmorValue(); } + + BuyResult_e AttemptToBuyAmmo( int iAmmoType ); + BuyResult_e AttemptToBuyAmmoSingle( int iAmmoType ); + BuyResult_e AttemptToBuyVest( void ); + BuyResult_e AttemptToBuyAssaultSuit( void ); + BuyResult_e AttemptToBuyDefuser( void ); + BuyResult_e AttemptToBuyNightVision( void ); + BuyResult_e AttemptToBuyShield( void ); + + BuyResult_e BuyAmmo( int nSlot, bool bBlinkMoney ); + BuyResult_e BuyGunAmmo( CBaseCombatWeapon *pWeapon, bool bBlinkMoney ); + + void PushawayThink(); + +private: + + ICSPlayerAnimState *m_PlayerAnimState; + + // Aiming heuristics code + float m_flIdleTime; //Amount of time we've been motionless + float m_flMoveTime; //Amount of time we've been in motion + float m_flLastDamageTime; //Last time we took damage + float m_flTargetFindTime; + + int m_lastDamageHealth; // Last damage given to our health + int m_lastDamageArmor; // Last damage given to our armor + + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + bool m_bPickedUpDefuser; // Did player pick up the defuser kit as opposed to buying it? + bool m_bDefusedWithPickedUpKit; // Did player defuse the bomb with a picked-up defuse kit? + + //============================================================================= + // HPE_END + //============================================================================= + + + // Last usercmd we shot a bullet on. + int m_iLastWeaponFireUsercmd; + + // Copyed from EyeAngles() so we can send it to the client. + CNetworkQAngle( m_angEyeAngles ); + + bool m_bVCollisionInitted; + +// AutoBuy functions. +public: + void AutoBuy(); // this should take into account what the player can afford and should buy the best equipment for them. + + bool IsInAutoBuy( void ) { return m_bIsInAutoBuy; } + bool IsInReBuy( void ) { return m_bIsInRebuy; } + +private: + bool ShouldExecuteAutoBuyCommand(const AutoBuyInfoStruct *commandInfo, bool boughtPrimary, bool boughtSecondary); + void PostAutoBuyCommandProcessing(const AutoBuyInfoStruct *commandInfo, bool &boughtPrimary, bool &boughtSecondary); + void ParseAutoBuyString(const char *string, bool &boughtPrimary, bool &boughtSecondary); + AutoBuyInfoStruct *GetAutoBuyCommandInfo(const char *command); + void PrioritizeAutoBuyString(char *autobuyString, const char *priorityString); // reorders the tokens in autobuyString based on the order of tokens in the priorityString. + BuyResult_e CombineBuyResults( BuyResult_e prevResult, BuyResult_e newResult ); + + bool m_bIsInAutoBuy; + bool m_bAutoReload; + +//ReBuy functions + +public: + void Rebuy(); +private: + void BuildRebuyStruct(); + + BuyResult_e RebuyPrimaryWeapon(); + BuyResult_e RebuyPrimaryAmmo(); + BuyResult_e RebuySecondaryWeapon(); + BuyResult_e RebuySecondaryAmmo(); + BuyResult_e RebuyHEGrenade(); + BuyResult_e RebuyFlashbang(); + BuyResult_e RebuySmokeGrenade(); + BuyResult_e RebuyDefuser(); + BuyResult_e RebuyNightVision(); + BuyResult_e RebuyArmor(); + + bool m_bIsInRebuy; + RebuyStruct m_rebuyStruct; + bool m_bUsingDefaultPistol; + + bool m_bIsBeingGivenItem; + +#ifdef CS_SHIELD_ENABLED + CNetworkVar( bool, m_bHasShield ); + CNetworkVar( bool, m_bShieldDrawn ); +#endif + + // This is a combination of the ADDON_ flags in cs_shareddefs.h. + CNetworkVar( int, m_iAddonBits ); + + // Clients don't know about holstered weapons, so we need to tell them the weapon type here + CNetworkVar( int, m_iPrimaryAddon ); + CNetworkVar( int, m_iSecondaryAddon ); + +//Damage record functions +public: + + static void StartNewBulletGroup(); // global function + + void RecordDamageTaken( const char *szDamageDealer, int iDamageTaken ); + void RecordDamageGiven( const char *szDamageTaker, int iDamageGiven ); + + void ResetDamageCounters(); //Reset all lists + + void OutputDamageTaken( void ); + void OutputDamageGiven( void ); + + void StockPlayerAmmo( CBaseCombatWeapon *pNewWeapon = NULL ); + + CUtlLinkedList< CDamageRecord *, int >& GetDamageGivenList() {return m_DamageGivenList;} + CUtlLinkedList< CDamageRecord *, int >& GetDamageTakenList() {return m_DamageTakenList;} + +private: + //A list of damage given + CUtlLinkedList< CDamageRecord *, int > m_DamageGivenList; + + //A list of damage taken + CUtlLinkedList< CDamageRecord *, int > m_DamageTakenList; + +protected: + float m_applyDeafnessTime; + int m_currentDeafnessFilter; + + bool m_isVIP; + +// Command rate limiting. +private: + + bool ShouldRunRateLimitedCommand( const CCommand &args ); + + // This lets us rate limit the commands the players can execute so they don't overflow things like reliable buffers. + CUtlDict<float,int> m_RateLimitLastCommandTimes; + + CNetworkVar(int, m_cycleLatch); // Every so often, we are going to transmit our cycle to the client to correct divergence caused by PVS changes + CountdownTimer m_cycleLatchTimer; + +//============================================================================= +// HPE_BEGIN: +// [menglish, tj] Achievement-based addition to CS player class. +//============================================================================= + +public: + void ResetRoundBasedAchievementVariables(); + void OnRoundEnd(int winningTeam, int reason); + void OnPreResetRound(); + + int GetNumEnemyDamagers(); + int GetNumEnemiesDamaged(); + CBaseEntity* GetNearestSurfaceBelow(float maxTrace); + + // Returns the % of the enemies this player killed in the round + int GetPercentageOfEnemyTeamKilled(); + + //List of times of recent kills to check for sprees + CUtlVector<float> m_killTimes; + + //List of all players killed this round + CUtlVector<CHandle<CCSPlayer> > m_enemyPlayersKilledThisRound; + + //List of weapons we have used to kill players with this round + CUtlVector<int> m_killWeapons; + + int m_NumEnemiesKilledThisRound; + int m_NumEnemiesAtRoundStart; + int m_KillingSpreeStartTime; + + float m_firstKillBlindStartTime; //This is the start time of the blind effect during which we got our most recent kill. + int m_killsWhileBlind; + + bool m_bIsRescuing; // tracks whether this player is currently rescuing a hostage + bool m_bInjuredAHostage; // tracks whether this player injured a hostage + int m_iNumFollowers; // Number of hostages following this player + bool m_bSurvivedHeadshotDueToHelmet; + + void IncrementNumFollowers() { m_iNumFollowers++; } + void DecrementNumFollowers() { m_iNumFollowers--; if (m_iNumFollowers < 0) m_iNumFollowers = 0; } + int GetNumFollowers() { return m_iNumFollowers; } + void SetIsRescuing(bool in_bRescuing) { m_bIsRescuing = in_bRescuing; } + bool IsRescuing() { return m_bIsRescuing; } + void SetInjuredAHostage(bool in_bInjured) { m_bInjuredAHostage = in_bInjured; } + bool InjuredAHostage() { return m_bInjuredAHostage; } + float GetBombPickuptime() { return m_bombPickupTime; } + void SetBombPickupTime(float time) { m_bombPickupTime = time; } + CCSPlayer* GetLastFlashbangAttacker() { return m_lastFlashBangAttacker; } + void SetLastFlashbangAttacker(CCSPlayer* attacker) { m_lastFlashBangAttacker = attacker; } + + static CSWeaponID GetWeaponIdCausingDamange( const CTakeDamageInfo &info ); + static void ProcessPlayerDeathAchievements( CCSPlayer *pAttacker, CCSPlayer *pVictim, const CTakeDamageInfo &info ); + + void OnCanceledDefuse(); + void OnStartedDefuse(); + GooseChaseAchievementStep m_gooseChaseStep; + DefuseDefenseAchivementStep m_defuseDefenseStep; + CHandle<CCSPlayer> m_pGooseChaseDistractingPlayer; + + int m_lastRoundResult; //save the reason for the last round ending. + + bool m_bMadeFootstepNoise; + + float m_bombPickupTime; + + bool m_bMadePurchseThisRound; + + int m_roundsWonWithoutPurchase; + + bool m_bKilledDefuser; + bool m_bKilledRescuer; + int m_maxGrenadeKills; + + int m_grenadeDamageTakenThisRound; + + bool GetKilledDefuser() { return m_bKilledDefuser; } + bool GetKilledRescuer() { return m_bKilledRescuer; } + int GetMaxGrenadeKills() { return m_maxGrenadeKills; } + + void CheckMaxGrenadeKills(int grenadeKills); + + CHandle<CCSPlayer> m_lastFlashBangAttacker; + + void SetPlayerDominated( CCSPlayer *pPlayer, bool bDominated ); + void SetPlayerDominatingMe( CCSPlayer *pPlayer, bool bDominated ); + bool IsPlayerDominated( int iPlayerIndex ); + bool IsPlayerDominatingMe( int iPlayerIndex ); + + bool m_wasNotKilledNaturally; //Set if the player is dead from a kill command or late login + + bool WasNotKilledNaturally() { return m_wasNotKilledNaturally; } + + //============================================================================= + // [menglish] MVP functions + //============================================================================= + + void SetNumMVPs( int iNumMVP ); + void IncrementNumMVPs( CSMvpReason_t mvpReason ); + int GetNumMVPs(); + + //============================================================================= + // HPE_END + //============================================================================= + void RemoveNemesisRelationships(); + void SetDeathFlags( int iDeathFlags ) { m_iDeathFlags = iDeathFlags; } + int GetDeathFlags() { return m_iDeathFlags; } + +private: + CNetworkArray( bool, m_bPlayerDominated, MAX_PLAYERS+1 ); // array of state per other player whether player is dominating other players + CNetworkArray( bool, m_bPlayerDominatingMe, MAX_PLAYERS+1 ); // array of state per other player whether other players are dominating this player + + //============================================================================= + // HPE_BEGIN: + //============================================================================= + + // [menglish] number of rounds this player has caused to be won for their team + int m_iMVPs; + + // [dwenger] adding tracking for fun fact + bool m_bWieldingKnifeAndKilledByGun; + + // [dwenger] adding tracking for which weapons this player has used in a round + CUtlVector<CSWeaponID> m_WeaponTypesUsed; + + //============================================================================= + // HPE_END + //============================================================================= + int m_iDeathFlags; // Flags holding revenge and domination info about a death + +//============================================================================= +// HPE_END +//============================================================================= +}; + + +inline CSPlayerState CCSPlayer::State_Get() const +{ + return m_iPlayerState; +} + +inline CCSPlayer *ToCSPlayer( CBaseEntity *pEntity ) +{ + if ( !pEntity || !pEntity->IsPlayer() ) + return NULL; + + return dynamic_cast<CCSPlayer*>( pEntity ); +} + +inline bool CCSPlayer::IsReloading( void ) const +{ + CBaseCombatWeapon *gun = GetActiveWeapon(); + if (gun == NULL) + return false; + + return gun->m_bInReload; +} + +inline bool CCSPlayer::IsProtectedByShield( void ) const +{ + return HasShield() && IsShieldDrawn(); +} + +inline bool CCSPlayer::IsBlind( void ) const +{ + return gpGlobals->curtime < m_blindUntilTime; +} + +//============================================================================= +// HPE_BEGIN +// [sbodenbender] Need a different test for player blindness for the achievements +//============================================================================= +inline bool CCSPlayer::IsBlindForAchievement() +{ + return (m_blindStartTime + m_flFlashDuration) > gpGlobals->curtime; +} +//============================================================================= +// HPE_END +//============================================================================= + +inline bool CCSPlayer::IsAutoFollowAllowed( void ) const +{ + return (gpGlobals->curtime > m_allowAutoFollowTime); +} + +inline void CCSPlayer::InhibitAutoFollow( float duration ) +{ + m_allowAutoFollowTime = gpGlobals->curtime + duration; +} + +inline void CCSPlayer::AllowAutoFollow( void ) +{ + m_allowAutoFollowTime = 0.0f; +} + +inline int CCSPlayer::GetClass( void ) const +{ + return m_iClass; +} + +inline const char *CCSPlayer::GetClanTag( void ) const +{ + return m_szClanTag; +} + +#endif //CS_PLAYER_H diff --git a/game/server/cstrike/cs_player_resource.cpp b/game/server/cstrike/cs_player_resource.cpp new file mode 100644 index 0000000..ec13230 --- /dev/null +++ b/game/server/cstrike/cs_player_resource.cpp @@ -0,0 +1,343 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: CS's custom CPlayerResource +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "cs_player.h" +#include "player_resource.h" +#include "cs_simple_hostage.h" +#include "cs_player_resource.h" +#include "weapon_c4.h" +#include <coordsize.h> +#include "cs_bot_manager.h" +#include "cs_gamerules.h" + +// Datatable +IMPLEMENT_SERVERCLASS_ST(CCSPlayerResource, DT_CSPlayerResource) + SendPropInt( SENDINFO( m_iPlayerC4 ), 8, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iPlayerVIP ), 8, SPROP_UNSIGNED ), + SendPropVector( SENDINFO(m_vecC4), -1, SPROP_COORD), + SendPropArray3( SENDINFO_ARRAY3(m_bHostageAlive), SendPropInt( SENDINFO_ARRAY(m_bHostageAlive), 1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_isHostageFollowingSomeone), SendPropInt( SENDINFO_ARRAY(m_isHostageFollowingSomeone), 1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iHostageEntityIDs), SendPropInt( SENDINFO_ARRAY(m_iHostageEntityIDs), -1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iHostageY), SendPropInt( SENDINFO_ARRAY(m_iHostageY), COORD_INTEGER_BITS+1, 0 ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iHostageX), SendPropInt( SENDINFO_ARRAY(m_iHostageX), COORD_INTEGER_BITS+1, 0 ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iHostageZ), SendPropInt( SENDINFO_ARRAY(m_iHostageZ), COORD_INTEGER_BITS+1, 0 ) ), + SendPropVector( SENDINFO(m_bombsiteCenterA), -1, SPROP_COORD), + SendPropVector( SENDINFO(m_bombsiteCenterB), -1, SPROP_COORD), + SendPropArray3( SENDINFO_ARRAY3(m_hostageRescueX), SendPropInt( SENDINFO_ARRAY(m_hostageRescueX), COORD_INTEGER_BITS+1, 0 ) ), + SendPropArray3( SENDINFO_ARRAY3(m_hostageRescueY), SendPropInt( SENDINFO_ARRAY(m_hostageRescueY), COORD_INTEGER_BITS+1, 0 ) ), + SendPropArray3( SENDINFO_ARRAY3(m_hostageRescueZ), SendPropInt( SENDINFO_ARRAY(m_hostageRescueZ), COORD_INTEGER_BITS+1, 0 ) ), + SendPropBool( SENDINFO( m_bBombSpotted ) ), + SendPropArray3( SENDINFO_ARRAY3(m_bPlayerSpotted), SendPropInt( SENDINFO_ARRAY(m_bPlayerSpotted), 1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_iMVPs), SendPropInt( SENDINFO_ARRAY(m_iMVPs), COORD_INTEGER_BITS+1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_bHasDefuser), SendPropInt( SENDINFO_ARRAY(m_bHasDefuser), 1, SPROP_UNSIGNED ) ), + SendPropArray3( SENDINFO_ARRAY3(m_szClan), SendPropStringT( SENDINFO_ARRAY(m_szClan) ) ), +END_SEND_TABLE() +//============================================================================= +// HPE_END +//============================================================================= + +BEGIN_DATADESC( CCSPlayerResource ) + // DEFINE_ARRAY( m_iPing, FIELD_INTEGER, MAX_PLAYERS+1 ), + // DEFINE_ARRAY( m_iPacketloss, FIELD_INTEGER, MAX_PLAYERS+1 ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( cs_player_manager, CCSPlayerResource ); + +CCSPlayerResource::CCSPlayerResource( void ) +{ + +} + + +//-------------------------------------------------------------------------------------------------------- +class Spotter +{ +public: + Spotter( CBaseEntity *entity, const Vector &target, int spottingTeam ) + { + m_targetEntity = entity; + m_target = target; + m_team = spottingTeam; + m_spotted = false; + } + + bool operator()( CBasePlayer *player ) + { + if ( !player->IsAlive() || player->GetTeamNumber() != m_team ) + return true; + + CCSPlayer *csPlayer = ToCSPlayer( player ); + if ( !csPlayer ) + return true; + + if ( csPlayer->IsBlind() ) + return true; + + Vector eye, forward; + player->EyePositionAndVectors( &eye, &forward, NULL, NULL ); + Vector path( m_target - eye ); + float distance = path.Length(); + path.NormalizeInPlace(); + float dot = DotProduct( forward, path ); + if( (dot > 0.995f ) + || (dot > 0.98f && distance < 900) + || (dot > 0.8f && distance < 250) + ) + { + trace_t tr; + CTraceFilterSkipTwoEntities filter( player, m_targetEntity, COLLISION_GROUP_DEBRIS ); + UTIL_TraceLine( eye, m_target, + (CONTENTS_OPAQUE|CONTENTS_SOLID|CONTENTS_MOVEABLE|CONTENTS_DEBRIS), &filter, &tr ); + + if( tr.fraction == 1.0f ) + { + if ( TheCSBots()->IsLineBlockedBySmoke( eye, m_target ) ) + { + return true; + } + + m_spotted = true; + return false; // spotted already, so no reason to check for other players spotting the same thing. + } + } + + return true; + } + + bool Spotted( void ) const + { + return m_spotted; + } + +private: + CBaseEntity *m_targetEntity; + Vector m_target; + int m_team; + bool m_spotted; +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSPlayerResource::UpdatePlayerData( void ) +{ + int i; + + m_iPlayerC4 = 0; + m_iPlayerVIP = 0; + + for ( i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer *pPlayer = (CCSPlayer*)UTIL_PlayerByIndex( i ); + + if ( pPlayer && pPlayer->IsConnected() ) + { + if ( pPlayer->IsVIP() ) + { + // we should only have one VIP + Assert( m_iPlayerVIP == 0 ); + m_iPlayerVIP = i; + } + + if ( pPlayer->HasC4() ) + { + // we should only have one bomb + m_iPlayerC4 = i; + } + + m_szClan.Set(i, AllocPooledString( pPlayer->GetClanTag() ) ); + + m_iMVPs.Set(i, pPlayer->GetNumMVPs()); + + m_bHasDefuser.Set(i, pPlayer->HasDefuser()); + + } + else + { + m_szClan.Set( i, MAKE_STRING( "" ) ); + m_iMVPs.Set( i, 0 ); + } + } + + CBaseEntity *c4 = NULL; + if ( m_iPlayerC4 == 0 ) + { + // no player has C4, update C4 position + if ( g_C4s.Count() > 0 ) + { + c4 = g_C4s[0]; + m_vecC4 = c4->GetAbsOrigin(); + } + else + { + m_vecC4.Init(); + } + } + + int numHostages = g_Hostages.Count(); + + for ( i = 0; i < MAX_HOSTAGES; i++ ) + { + if ( i >= numHostages ) + { +// engine->Con_NPrintf( i, "Dead" ); + m_bHostageAlive.Set( i, false ); + m_isHostageFollowingSomeone.Set( i, false ); + continue; + } + + CHostage* pHostage = g_Hostages[i]; + + m_bHostageAlive.Set( i, pHostage->IsRescuable() ); + + if ( pHostage->IsValid() ) + { + m_iHostageX.Set( i, (int) pHostage->GetAbsOrigin().x ); + m_iHostageY.Set( i, (int) pHostage->GetAbsOrigin().y ); + m_iHostageZ.Set( i, (int) pHostage->GetAbsOrigin().z ); + m_iHostageEntityIDs.Set( i, pHostage->entindex() ); + m_isHostageFollowingSomeone.Set( i, pHostage->IsFollowingSomeone() ); +// engine->Con_NPrintf( i, "ID:%d Pos:(%.0f,%.0f,%.0f)", pHostage->entindex(), pHostage->GetAbsOrigin().x, pHostage->GetAbsOrigin().y, pHostage->GetAbsOrigin().z ); + } + else + { +// engine->Con_NPrintf( i, "Invalid" ); + } + } + + if( !m_foundGoalPositions ) + { + // We only need to update these once a map, but we need the client to know about them. + CBaseEntity* ent = NULL; + while ( ( ent = gEntList.FindEntityByClassname( ent, "func_bomb_target" ) ) != NULL ) + { + const Vector &pos = ent->WorldSpaceCenter(); + CNavArea *area = TheNavMesh->GetNearestNavArea( pos, true, 10000.0f, false, false ); + const char *placeName = (area) ? TheNavMesh->PlaceToName( area->GetPlace() ) : NULL; + if ( placeName == NULL ) + { + // The bomb site has no area or place name, so just choose A then B + if ( m_bombsiteCenterA.Get().IsZero() ) + { + m_bombsiteCenterA = pos; + } + else + { + m_bombsiteCenterB = pos; + } + } + else + { + // The bomb site has a place name, so choose accordingly + if( FStrEq( placeName, "BombsiteA" ) ) + { + m_bombsiteCenterA = pos; + } + else + { + m_bombsiteCenterB = pos; + } + } + m_foundGoalPositions = true; + } + + int hostageRescue = 0; + while ( (( ent = gEntList.FindEntityByClassname( ent, "func_hostage_rescue" ) ) != NULL) && (hostageRescue < MAX_HOSTAGE_RESCUES) ) + { + const Vector &pos = ent->WorldSpaceCenter(); + m_hostageRescueX.Set( hostageRescue, (int) pos.x ); + m_hostageRescueY.Set( hostageRescue, (int) pos.y ); + m_hostageRescueZ.Set( hostageRescue, (int) pos.z ); + + hostageRescue++; + m_foundGoalPositions = true; + } + } + + bool bombSpotted = false; + if ( c4 ) + { + Spotter spotter( c4, m_vecC4, TEAM_CT ); + ForEachPlayer( spotter ); + if ( spotter.Spotted() ) + { + bombSpotted = true; + } + } + + for ( int i=0; i < MAX_PLAYERS+1; i++ ) + { + CCSPlayer *target = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + + if ( !target || !target->IsAlive() ) + { + m_bPlayerSpotted.Set( i, 0 ); + continue; + } + + Spotter spotter( target, target->EyePosition(), (target->GetTeamNumber()==TEAM_CT) ? TEAM_TERRORIST : TEAM_CT ); + ForEachPlayer( spotter ); + if ( spotter.Spotted() ) + { + if ( target->HasC4() ) + { + bombSpotted = true; + } + m_bPlayerSpotted.Set( i, 1 ); + } + else + { + m_bPlayerSpotted.Set( i, 0 ); + } + } + + if ( bombSpotted ) + { + m_bBombSpotted = true; + } + else + { + m_bBombSpotted = false; + } + + BaseClass::UpdatePlayerData(); +} + +void CCSPlayerResource::Spawn( void ) +{ + m_vecC4.Init(); + m_iPlayerC4 = 0; + m_iPlayerVIP = 0; + m_bombsiteCenterA.Init(); + m_bombsiteCenterB.Init(); + m_foundGoalPositions = false; + + for ( int i=0; i < MAX_HOSTAGES; i++ ) + { + m_bHostageAlive.Set( i, 0 ); + m_isHostageFollowingSomeone.Set( i, 0 ); + m_iHostageEntityIDs.Set(i, 0); + } + + for ( int i=0; i < MAX_HOSTAGE_RESCUES; i++ ) + { + m_hostageRescueX.Set( i, 0 ); + m_hostageRescueY.Set( i, 0 ); + m_hostageRescueZ.Set( i, 0 ); + } + + m_bBombSpotted = false; + for ( int i=0; i < MAX_PLAYERS+1; i++ ) + { + m_bPlayerSpotted.Set( i, 0 ); + m_szClan.Set( i, MAKE_STRING( "" ) ); + m_iMVPs.Set( i, 0 ); + m_bHasDefuser.Set(i, false); + } + + BaseClass::Spawn(); +} diff --git a/game/server/cstrike/cs_player_resource.h b/game/server/cstrike/cs_player_resource.h new file mode 100644 index 0000000..d59cabc --- /dev/null +++ b/game/server/cstrike/cs_player_resource.h @@ -0,0 +1,55 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: CS's custom CPlayerResource +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef CS_PLAYER_RESOURCE_H +#define CS_PLAYER_RESOURCE_H +#ifdef _WIN32 +#pragma once +#endif + +class CCSPlayerResource : public CPlayerResource +{ + DECLARE_CLASS( CCSPlayerResource, CPlayerResource ); + +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CCSPlayerResource(); + + virtual void UpdatePlayerData( void ); + virtual void Spawn( void ); +protected: + + CNetworkVar( int, m_iPlayerC4 ); // entity index of C4 carrier or 0 + CNetworkVar( int, m_iPlayerVIP ); // entity index of VIP player or 0 + CNetworkVector( m_vecC4 ); // position of C4 + CNetworkArray( bool, m_bHostageAlive, MAX_HOSTAGES ); + CNetworkArray( bool, m_isHostageFollowingSomeone, MAX_HOSTAGES ); + CNetworkArray( int, m_iHostageEntityIDs, MAX_HOSTAGES ); + CNetworkArray( int, m_iHostageX, MAX_HOSTAGES ); + CNetworkArray( int, m_iHostageY, MAX_HOSTAGES ); + CNetworkArray( int, m_iHostageZ, MAX_HOSTAGES ); + CNetworkVector( m_bombsiteCenterA );// Location of bombsite A + CNetworkVector( m_bombsiteCenterB );// Location of bombsite B + CNetworkArray( int, m_hostageRescueX, MAX_HOSTAGE_RESCUES );// Locations of all hostage rescue spots + CNetworkArray( int, m_hostageRescueY, MAX_HOSTAGE_RESCUES ); + CNetworkArray( int, m_hostageRescueZ, MAX_HOSTAGE_RESCUES ); + + CNetworkVar( bool, m_bBombSpotted ); + CNetworkArray( bool, m_bPlayerSpotted, MAX_PLAYERS+1 ); + + CNetworkArray( string_t, m_szClan, MAX_PLAYERS+1 ); + + CNetworkArray( int, m_iMVPs, MAX_PLAYERS + 1 ); + CNetworkArray( bool, m_bHasDefuser, MAX_PLAYERS + 1); + +private: + bool m_foundGoalPositions; +}; + +#endif // CS_PLAYER_RESOURCE_H diff --git a/game/server/cstrike/cs_playermove.cpp b/game/server/cstrike/cs_playermove.cpp new file mode 100644 index 0000000..a8f4eee --- /dev/null +++ b/game/server/cstrike/cs_playermove.cpp @@ -0,0 +1,98 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "player_command.h" +#include "igamemovement.h" +#include "in_buttons.h" +#include "ipredictionsystem.h" +#include "iservervehicle.h" +#include "cs_player.h" + + +static CMoveData g_MoveData; +CMoveData *g_pMoveData = &g_MoveData; + +IPredictionSystem *IPredictionSystem::g_pPredictionSystems = NULL; + + +//----------------------------------------------------------------------------- +// Sets up the move data for TF2 +//----------------------------------------------------------------------------- +class CCSPlayerMove : public CPlayerMove +{ +DECLARE_CLASS( CCSPlayerMove, CPlayerMove ); + +public: + virtual void StartCommand( CBasePlayer *player, CUserCmd *cmd ); + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ); + virtual void FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ); +}; + +// PlayerMove Interface +static CCSPlayerMove g_PlayerMove; + +//----------------------------------------------------------------------------- +// Singleton accessor +//----------------------------------------------------------------------------- +CPlayerMove *PlayerMove() +{ + return &g_PlayerMove; +} + +//----------------------------------------------------------------------------- +// Main setup, finish +//----------------------------------------------------------------------------- + +void CCSPlayerMove::StartCommand( CBasePlayer *player, CUserCmd *cmd ) +{ + CCSPlayer *pPlayer = ToCSPlayer( player ); + + // Reset this.. it gets reset each frame that we're in a bomb zone. + pPlayer->m_bInBombZone = false; + pPlayer->m_bInBuyZone = false; + pPlayer->m_bInHostageRescueZone = false; + + BaseClass::StartCommand( player, cmd ); +} + +//----------------------------------------------------------------------------- +// Purpose: This is called pre player movement and copies all the data necessary +// from the player for movement. (Server-side, the client-side version +// of this code can be found in prediction.cpp.) +//----------------------------------------------------------------------------- +void CCSPlayerMove::SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) +{ + player->AvoidPhysicsProps( ucmd ); + + BaseClass::SetupMove( player, ucmd, pHelper, move ); + + IServerVehicle *pVehicle = player->GetVehicle(); + if (pVehicle && gpGlobals->frametime != 0) + { + pVehicle->SetupMove( player, ucmd, pHelper, move ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: This is called post player movement to copy back all data that +// movement could have modified and that is necessary for future +// movement. (Server-side, the client-side version of this code can +// be found in prediction.cpp.) +//----------------------------------------------------------------------------- +void CCSPlayerMove::FinishMove( CBasePlayer *player, CUserCmd *ucmd, CMoveData *move ) +{ + // Call the default FinishMove code. + BaseClass::FinishMove( player, ucmd, move ); + + IServerVehicle *pVehicle = player->GetVehicle(); + if (pVehicle && gpGlobals->frametime != 0) + { + pVehicle->FinishMove( player, ucmd, move ); + } +} diff --git a/game/server/cstrike/cs_team.cpp b/game/server/cstrike/cs_team.cpp new file mode 100644 index 0000000..61360ea --- /dev/null +++ b/game/server/cstrike/cs_team.cpp @@ -0,0 +1,101 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Team management class. Contains all the details for a specific team +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "cs_team.h" +#include "entitylist.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +// Datatable +IMPLEMENT_SERVERCLASS_ST(CCSTeam, DT_CSTeam) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( cs_team_manager, CCSTeam ); + +//----------------------------------------------------------------------------- +// Purpose: Get a pointer to the specified TF team manager +//----------------------------------------------------------------------------- +CCSTeam *GetGlobalTFTeam( int iIndex ) +{ + return (CCSTeam*)GetGlobalTeam( iIndex ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Needed because this is an entity, but should never be used +//----------------------------------------------------------------------------- +void CCSTeam::Init( const char *pName, int iNumber ) +{ + BaseClass::Init( pName, iNumber ); + + // Only detect changes every half-second. + NetworkProp()->SetUpdateInterval( 0.75f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CCSTeam::~CCSTeam( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSTeam::Precache( void ) +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: Called every frame +//----------------------------------------------------------------------------- +void CCSTeam::Think( void ) +{ +} + + +//------------------------------------------------------------------------------------------------------------------ +// PLAYERS +//----------------------------------------------------------------------------- +// Purpose: Add the specified player to this team. Remove them from their current team, if any. +//----------------------------------------------------------------------------- +void CCSTeam::AddPlayer( CBasePlayer *pPlayer ) +{ + BaseClass::AddPlayer( pPlayer ); +} + +//----------------------------------------------------------------------------- +// Purpose: Clean up the player's objects when they leave +//----------------------------------------------------------------------------- +void CCSTeam::RemovePlayer( CBasePlayer *pPlayer ) +{ + BaseClass::RemovePlayer( pPlayer ); +} + +//------------------------------------------------------------------------------------------------------------------ +// UTILITY FUNCS +//----------------------------------------------------------------------------- + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CCSTeam* CCSTeam::GetEnemyTeam() +{ + // Look for nearby enemy objects we can capture. + int iMyTeam = GetTeamNumber(); + if( iMyTeam == 0 ) + return NULL; + + int iEnemyTeam = !(iMyTeam - 1) + 1; + return (CCSTeam*)GetGlobalTeam( iEnemyTeam ); +} + + diff --git a/game/server/cstrike/cs_team.h b/game/server/cstrike/cs_team.h new file mode 100644 index 0000000..683ee9b --- /dev/null +++ b/game/server/cstrike/cs_team.h @@ -0,0 +1,60 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Team management class. Contains all the details for a specific team +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef CS_TEAM_H +#define CS_TEAM_H + +#ifdef _WIN32 +#pragma once +#endif + + +#include "utlvector.h" +#include "team.h" + + +//----------------------------------------------------------------------------- +// Purpose: Team Manager +//----------------------------------------------------------------------------- +class CCSTeam : public CTeam +{ + DECLARE_CLASS( CCSTeam, CTeam ); +public: + virtual ~CCSTeam( void ); + + DECLARE_SERVERCLASS(); + + // Initialization + virtual void Init( const char *pName, int iNumber ); + + virtual void Precache( void ); + virtual void Think( void ); + + //----------------------------------------------------------------------------- + // Players + //----------------------------------------------------------------------------- + virtual void AddPlayer( CBasePlayer *pPlayer ); + virtual void RemovePlayer( CBasePlayer *pPlayer ); + + //----------------------------------------------------------------------------- + // Utility funcs + //----------------------------------------------------------------------------- + CCSTeam* GetEnemyTeam(); + +private: + + // Used to distribute resources to a team + float m_flNextResourceTime; + + int m_iLastUpdateSentAt; +}; + + +extern CCSTeam *GetGlobalTFTeam( int iIndex ); + + +#endif // TF_TEAM_H diff --git a/game/server/cstrike/cs_vehicle_jeep.cpp b/game/server/cstrike/cs_vehicle_jeep.cpp new file mode 100644 index 0000000..11a378f --- /dev/null +++ b/game/server/cstrike/cs_vehicle_jeep.cpp @@ -0,0 +1,1566 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "vehicle_base.h" +#include "engine/IEngineSound.h" +#include "in_buttons.h" +#include "ammodef.h" +#include "IEffects.h" +#include "beam_shared.h" +#include "soundenvelope.h" +#include "decals.h" +#include "soundent.h" +#include "te_effect_dispatch.h" +#include "ndebugoverlay.h" +#include "movevars_shared.h" +#include "bone_setup.h" +#include "ai_basenpc.h" +#include "ai_hint.h" +#include "globalstate.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define VEHICLE_HITBOX_DRIVER 1 +#define LOCK_SPEED 10 +#define JEEP_GUN_YAW "vehicle_weapon_yaw" +#define JEEP_GUN_PITCH "vehicle_weapon_pitch" +#define JEEP_GUN_SPIN "gun_spin" +#define JEEP_GUN_SPIN_RATE 20 + +#define CANNON_MAX_UP_PITCH 20 +#define CANNON_MAX_DOWN_PITCH 20 +#define CANNON_MAX_LEFT_YAW 90 +#define CANNON_MAX_RIGHT_YAW 90 + +#define OVERTURNED_EXIT_WAITTIME 2.0f + +#define JEEP_AMMOCRATE_HITGROUP 5 +#define JEEP_WHEEL_COUNT 4 + +#define JEEP_AMMO_CRATE_CLOSE_DELAY 2.0f + +#define JEEP_DELTA_LENGTH_MAX 12.0f // 1 foot +#define JEEP_FRAMETIME_MIN 1e-6 + +ConVar hud_jeephint_numentries( "hud_jeephint_numentries", "10", FCVAR_NONE ); +ConVar g_jeepexitspeed( "g_jeepexitspeed", "100", FCVAR_CHEAT ); + +//============================================================================= +// +// Jeep water data. +// +struct JeepWaterData_t +{ + bool m_bWheelInWater[JEEP_WHEEL_COUNT]; + bool m_bWheelWasInWater[JEEP_WHEEL_COUNT]; + Vector m_vecWheelContactPoints[JEEP_WHEEL_COUNT]; + float m_flNextRippleTime[JEEP_WHEEL_COUNT]; + bool m_bBodyInWater; + bool m_bBodyWasInWater; + + DECLARE_SIMPLE_DATADESC(); +}; + +BEGIN_SIMPLE_DATADESC( JeepWaterData_t ) + DEFINE_ARRAY( m_bWheelInWater, FIELD_BOOLEAN, JEEP_WHEEL_COUNT ), + DEFINE_ARRAY( m_bWheelWasInWater, FIELD_BOOLEAN, JEEP_WHEEL_COUNT ), + DEFINE_ARRAY( m_vecWheelContactPoints, FIELD_VECTOR, JEEP_WHEEL_COUNT ), + DEFINE_ARRAY( m_flNextRippleTime, FIELD_TIME, JEEP_WHEEL_COUNT ), + DEFINE_FIELD( m_bBodyInWater, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bBodyWasInWater, FIELD_BOOLEAN ), +END_DATADESC() + +//----------------------------------------------------------------------------- +// Purpose: Four wheel physics vehicle server vehicle with weaponry +//----------------------------------------------------------------------------- +class CJeepFourWheelServerVehicle : public CFourWheelServerVehicle +{ + typedef CFourWheelServerVehicle BaseClass; +// IServerVehicle +public: + bool NPC_HasPrimaryWeapon( void ) { return true; } + void NPC_AimPrimaryWeapon( Vector vecTarget ); + int GetExitAnimToUse( Vector &vecEyeExitEndpoint, bool &bAllPointsBlocked ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CPropJeep : public CPropVehicleDriveable +{ + DECLARE_CLASS( CPropJeep, CPropVehicleDriveable ); + +public: + + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CPropJeep( void ); + + // CPropVehicle + virtual void OnRestore( void ); + virtual void ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ); + virtual void DriveVehicle( float flFrameTime, CUserCmd *ucmd, int iButtonsDown, int iButtonsReleased ); + virtual void SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ); + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual void DampenEyePosition( Vector &vecVehicleEyePos, QAngle &vecVehicleEyeAngles ); + virtual bool AllowBlockedExit( CBasePlayer *pPlayer, int nRole ) { return false; } + virtual bool CanExitVehicle( CBaseEntity *pEntity ); + virtual bool IsVehicleBodyInWater() { return m_WaterData.m_bBodyInWater; } + + // CBaseEntity + void Think(void); + void Precache( void ); + void Spawn( void ); + + virtual void CreateServerVehicle( void ); + virtual Vector BodyTarget( const Vector &posSrc, bool bNoisy = true ); + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual void EnterVehicle( CBasePlayer *pPlayer ); + virtual void ExitVehicle( int nRole ); + + void AimGunAt( Vector *endPos, float flInterval ); + bool TauCannonHasBeenCutOff( void ) { return m_bGunHasBeenCutOff; } + + // NPC Driving + bool NPC_HasPrimaryWeapon( void ) { return true; } + void NPC_AimPrimaryWeapon( Vector vecTarget ); + + const char *GetTracerType( void ) { return "AR2Tracer"; } + void DoImpactEffect( trace_t &tr, int nDamageType ); + + bool HeadlightIsOn( void ) { return m_bHeadlightIsOn; } + void HeadlightTurnOn( void ) { m_bHeadlightIsOn = true; } + void HeadlightTurnOff( void ) { m_bHeadlightIsOn = false; } + +private: + + void FireCannon( void ); + void ChargeCannon( void ); + void FireChargedCannon( void ); + + void DrawBeam( const Vector &startPos, const Vector &endPos, float width ); + void StopChargeSound( void ); + void GetCannonAim( Vector *resultDir ); + + void InitWaterData( void ); + void HandleWater( void ); + bool CheckWater( void ); + void CheckWaterLevel( void ); + void CreateSplash( const Vector &vecPosition ); + void CreateRipple( const Vector &vecPosition ); + + void UpdateSteeringAngle( void ); + void CreateDangerSounds( void ); + + void ComputePDControllerCoefficients( float *pCoefficientsOut, float flFrequency, float flDampening, float flDeltaTime ); + void DampenForwardMotion( Vector &vecVehicleEyePos, QAngle &vecVehicleEyeAngles, float flFrameTime ); + void DampenUpMotion( Vector &vecVehicleEyePos, QAngle &vecVehicleEyeAngles, float flFrameTime ); + + void InputStartRemoveTauCannon( inputdata_t &inputdata ); + void InputFinishRemoveTauCannon( inputdata_t &inputdata ); + +private: + + bool m_bGunHasBeenCutOff; + float m_flDangerSoundTime; + int m_nBulletType; + bool m_bCannonCharging; + float m_flCannonTime; + float m_flCannonChargeStartTime; + Vector m_vecGunOrigin; + CSoundPatch *m_sndCannonCharge; + int m_nSpinPos; + float m_aimYaw; + float m_aimPitch; + float m_throttleDisableTime; + float m_flAmmoCrateCloseTime; + + // handbrake after the fact to keep vehicles from rolling + float m_flHandbrakeTime; + bool m_bInitialHandbrake; + + float m_flOverturnedTime; + + Vector m_vecLastEyePos; + Vector m_vecLastEyeTarget; + Vector m_vecEyeSpeed; + Vector m_vecTargetSpeed; + + JeepWaterData_t m_WaterData; + + int m_iNumberOfEntries; + int m_nAmmoType; + + // Seagull perching + float m_flPlayerExitedTime; // Time at which the player last left this vehicle + float m_flLastSawPlayerAt; // Time at which we last saw the player + EHANDLE m_hLastPlayerInVehicle; + bool m_bHasPoop; + + CNetworkVar( bool, m_bHeadlightIsOn ); +}; + +BEGIN_DATADESC( CPropJeep ) + DEFINE_FIELD( m_bGunHasBeenCutOff, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flDangerSoundTime, FIELD_TIME ), + DEFINE_FIELD( m_nBulletType, FIELD_INTEGER ), + DEFINE_FIELD( m_bCannonCharging, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flCannonTime, FIELD_TIME ), + DEFINE_FIELD( m_flCannonChargeStartTime, FIELD_TIME ), + DEFINE_FIELD( m_vecGunOrigin, FIELD_POSITION_VECTOR ), + DEFINE_SOUNDPATCH( m_sndCannonCharge ), + DEFINE_FIELD( m_nSpinPos, FIELD_INTEGER ), + DEFINE_FIELD( m_aimYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_aimPitch, FIELD_FLOAT ), + DEFINE_FIELD( m_throttleDisableTime, FIELD_TIME ), + DEFINE_FIELD( m_flHandbrakeTime, FIELD_TIME ), + DEFINE_FIELD( m_bInitialHandbrake, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flOverturnedTime, FIELD_TIME ), + DEFINE_FIELD( m_flAmmoCrateCloseTime, FIELD_FLOAT ), + DEFINE_FIELD( m_vecLastEyePos, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecLastEyeTarget, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecEyeSpeed, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecTargetSpeed, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_bHeadlightIsOn, FIELD_BOOLEAN ), + DEFINE_EMBEDDED( m_WaterData ), + + DEFINE_FIELD( m_iNumberOfEntries, FIELD_INTEGER ), + DEFINE_FIELD( m_nAmmoType, FIELD_INTEGER ), + + DEFINE_FIELD( m_flPlayerExitedTime, FIELD_TIME ), + DEFINE_FIELD( m_flLastSawPlayerAt, FIELD_TIME ), + DEFINE_FIELD( m_hLastPlayerInVehicle, FIELD_EHANDLE ), + DEFINE_FIELD( m_bHasPoop, FIELD_BOOLEAN ), + + DEFINE_INPUTFUNC( FIELD_VOID, "StartRemoveTauCannon", InputStartRemoveTauCannon ), + DEFINE_INPUTFUNC( FIELD_VOID, "FinishRemoveTauCannon", InputFinishRemoveTauCannon ), + +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPropJeep, DT_PropJeep ) + SendPropBool( SENDINFO( m_bHeadlightIsOn ) ), +END_SEND_TABLE(); + +//LINK_ENTITY_TO_CLASS( prop_vehicle_jeep, CPropJeep ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CPropJeep::CPropJeep( void ) +{ + m_bHasGun = true; + m_bGunHasBeenCutOff = false; + m_bCannonCharging = false; + m_flCannonChargeStartTime = 0; + m_flCannonTime = 0; + m_nBulletType = -1; + m_flOverturnedTime = 0.0f; + m_iNumberOfEntries = 0; + + m_vecEyeSpeed.Init(); + + InitWaterData(); + + m_bUnableToFire = true; + m_flAmmoCrateCloseTime = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::CreateServerVehicle( void ) +{ + // Create our armed server vehicle + m_pServerVehicle = new CJeepFourWheelServerVehicle(); + m_pServerVehicle->SetVehicle( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::Precache( void ) +{ + UTIL_PrecacheOther( "npc_seagull" ); + + PrecacheScriptSound( "PropJeep.AmmoClose" ); + PrecacheScriptSound( "PropJeep.FireCannon" ); + PrecacheScriptSound( "PropJeep.FireChargedCannon" ); + PrecacheScriptSound( "PropJeep.AmmoOpen" ); + + PrecacheScriptSound( "Jeep.GaussCharge" ); + +// PrecacheModel( GAUSS_BEAM_SPRITE ); + + BaseClass::Precache(); +} + +//------------------------------------------------ +// Spawn +//------------------------------------------------ +void CPropJeep::Spawn( void ) +{ + // Setup vehicle as a real-wheels car. + SetVehicleType( VEHICLE_TYPE_CAR_WHEELS ); + + BaseClass::Spawn(); + m_flHandbrakeTime = gpGlobals->curtime + 0.1; + m_bInitialHandbrake = false; + + m_VehiclePhysics.SetHasBrakePedal( false ); + + m_flMinimumSpeedToEnterExit = LOCK_SPEED; + + m_nBulletType = GetAmmoDef()->Index("GaussEnergy"); + + if ( m_bHasGun ) + { + SetBodygroup( 1, true ); + } + else + { + SetBodygroup( 1, false ); + } + + // Initialize pose parameters + SetPoseParameter( JEEP_GUN_YAW, 0 ); + SetPoseParameter( JEEP_GUN_PITCH, 0 ); + m_nSpinPos = 0; + SetPoseParameter( JEEP_GUN_SPIN, m_nSpinPos ); + m_aimYaw = 0; + m_aimPitch = 0; + + AddSolidFlags( FSOLID_NOT_STANDABLE ); + + CAmmoDef *pAmmoDef = GetAmmoDef(); + m_nAmmoType = pAmmoDef->Index("GaussEnergy"); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &tr - +// nDamageType - +//----------------------------------------------------------------------------- +void CPropJeep::DoImpactEffect( trace_t &tr, int nDamageType ) +{ + //Draw our beam + DrawBeam( tr.startpos, tr.endpos, 2.4 ); + + if ( (tr.surface.flags & SURF_SKY) == false ) + { + CPVSFilter filter( tr.endpos ); + te->GaussExplosion( filter, 0.0f, tr.endpos, tr.plane.normal, 0 ); + + UTIL_ImpactTrace( &tr, m_nBulletType ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo info = inputInfo; + if ( ptr->hitbox != VEHICLE_HITBOX_DRIVER ) + { + if ( inputInfo.GetDamageType() & DMG_BULLET ) + { + info.ScaleDamage( 0.0001 ); + } + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CPropJeep::OnTakeDamage( const CTakeDamageInfo &inputInfo ) +{ + //Do scaled up physics damage to the car + CTakeDamageInfo info = inputInfo; + info.ScaleDamage( 25 ); + + // HACKHACK: Scale up grenades until we get a better explosion/pressure damage system + if ( inputInfo.GetDamageType() & DMG_BLAST ) + { + info.SetDamageForce( inputInfo.GetDamageForce() * 10 ); + } + VPhysicsTakeDamage( info ); + + // reset the damage + info.SetDamage( inputInfo.GetDamage() ); + + // small amounts of shock damage disrupt the car, but aren't transferred to the player + if ( info.GetDamageType() == DMG_SHOCK ) + { + if ( info.GetDamage() <= 10 ) + { + // take 10% damage and make the engine stall + info.ScaleDamage( 0.1 ); + m_throttleDisableTime = gpGlobals->curtime + 2; + } + } + + //Check to do damage to driver + if ( GetDriver() ) + { + //Take no damage from physics damages + if ( info.GetDamageType() & DMG_CRUSH ) + return 0; + + // Take the damage (strip out the DMG_BLAST) + info.SetDamageType( info.GetDamageType() & (~DMG_BLAST) ); + GetDriver()->TakeDamage( info ); + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Vector CPropJeep::BodyTarget( const Vector &posSrc, bool bNoisy ) +{ + Vector shotPos; + matrix3x4_t matrix; + + int eyeAttachmentIndex = LookupAttachment("vehicle_driver_eyes"); + GetAttachment( eyeAttachmentIndex, matrix ); + MatrixGetColumn( matrix, 3, shotPos ); + + if ( bNoisy ) + { + shotPos[0] += random->RandomFloat( -8.0f, 8.0f ); + shotPos[1] += random->RandomFloat( -8.0f, 8.0f ); + shotPos[2] += random->RandomFloat( -8.0f, 8.0f ); + } + + return shotPos; +} + +//----------------------------------------------------------------------------- +// Purpose: Aim Gun at a target +//----------------------------------------------------------------------------- +void CPropJeep::AimGunAt( Vector *endPos, float flInterval ) +{ + Vector aimPos = *endPos; + + // See if the gun should be allowed to aim + if ( IsOverturned() || m_bEngineLocked ) + { + SetPoseParameter( JEEP_GUN_YAW, 0 ); + SetPoseParameter( JEEP_GUN_PITCH, 0 ); + SetPoseParameter( JEEP_GUN_SPIN, 0 ); + return; + + // Make the gun go limp and look "down" + Vector v_forward, v_up; + AngleVectors( GetLocalAngles(), NULL, &v_forward, &v_up ); + aimPos = WorldSpaceCenter() + ( v_forward * -32.0f ) - Vector( 0, 0, 128.0f ); + } + + matrix3x4_t gunMatrix; + GetAttachment( LookupAttachment("gun_ref"), gunMatrix ); + + // transform the enemy into gun space + Vector localEnemyPosition; + VectorITransform( aimPos, gunMatrix, localEnemyPosition ); + + // do a look at in gun space (essentially a delta-lookat) + QAngle localEnemyAngles; + VectorAngles( localEnemyPosition, localEnemyAngles ); + + // convert to +/- 180 degrees + localEnemyAngles.x = UTIL_AngleDiff( localEnemyAngles.x, 0 ); + localEnemyAngles.y = UTIL_AngleDiff( localEnemyAngles.y, 0 ); + + float targetYaw = m_aimYaw + localEnemyAngles.y; + float targetPitch = m_aimPitch + localEnemyAngles.x; + + // Constrain our angles + float newTargetYaw = clamp( targetYaw, -CANNON_MAX_LEFT_YAW, CANNON_MAX_RIGHT_YAW ); + float newTargetPitch = clamp( targetPitch, -CANNON_MAX_DOWN_PITCH, CANNON_MAX_UP_PITCH ); + + // If the angles have been clamped, we're looking outside of our valid range + if ( fabs(newTargetYaw-targetYaw) > 1e-4 || fabs(newTargetPitch-targetPitch) > 1e-4 ) + { + m_bUnableToFire = true; + } + + targetYaw = newTargetYaw; + targetPitch = newTargetPitch; + + // Exponentially approach the target + float yawSpeed = 8; + float pitchSpeed = 8; + + m_aimYaw = UTIL_Approach( targetYaw, m_aimYaw, yawSpeed ); + m_aimPitch = UTIL_Approach( targetPitch, m_aimPitch, pitchSpeed ); + + SetPoseParameter( JEEP_GUN_YAW, -m_aimYaw); + SetPoseParameter( JEEP_GUN_PITCH, -m_aimPitch ); + + InvalidateBoneCache(); + + // read back to avoid drift when hitting limits + // as long as the velocity is less than the delta between the limit and 180, this is fine. + m_aimPitch = -GetPoseParameter( JEEP_GUN_PITCH ); + m_aimYaw = -GetPoseParameter( JEEP_GUN_YAW ); + + // Now draw crosshair for actual aiming point + Vector vecMuzzle, vecMuzzleDir; + QAngle vecMuzzleAng; + + GetAttachment( "Muzzle", vecMuzzle, vecMuzzleAng ); + AngleVectors( vecMuzzleAng, &vecMuzzleDir ); + + trace_t tr; + UTIL_TraceLine( vecMuzzle, vecMuzzle + (vecMuzzleDir * MAX_TRACE_LENGTH), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + // see if we hit something, if so, adjust endPos to hit location + if ( tr.fraction < 1.0 ) + { + m_vecGunCrosshair = vecMuzzle + ( vecMuzzleDir * MAX_TRACE_LENGTH * tr.fraction ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::InitWaterData( void ) +{ + m_WaterData.m_bBodyInWater = false; + m_WaterData.m_bBodyWasInWater = false; + + for ( int iWheel = 0; iWheel < JEEP_WHEEL_COUNT; ++iWheel ) + { + m_WaterData.m_bWheelInWater[iWheel] = false; + m_WaterData.m_bWheelWasInWater[iWheel] = false; + m_WaterData.m_vecWheelContactPoints[iWheel].Init(); + m_WaterData.m_flNextRippleTime[iWheel] = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::HandleWater( void ) +{ + // Only check the wheels and engine in water if we have a driver (player). + if ( !GetDriver() ) + return; + + // Check to see if we are in water. + if ( CheckWater() ) + { + for ( int iWheel = 0; iWheel < JEEP_WHEEL_COUNT; ++iWheel ) + { + // Create an entry/exit splash! + if ( m_WaterData.m_bWheelInWater[iWheel] != m_WaterData.m_bWheelWasInWater[iWheel] ) + { + CreateSplash( m_WaterData.m_vecWheelContactPoints[iWheel] ); + CreateRipple( m_WaterData.m_vecWheelContactPoints[iWheel] ); + } + + // Create ripples. + if ( m_WaterData.m_bWheelInWater[iWheel] && m_WaterData.m_bWheelWasInWater[iWheel] ) + { + if ( m_WaterData.m_flNextRippleTime[iWheel] < gpGlobals->curtime ) + { + // Stagger ripple times + m_WaterData.m_flNextRippleTime[iWheel] = gpGlobals->curtime + RandomFloat( 0.1, 0.3 ); + CreateRipple( m_WaterData.m_vecWheelContactPoints[iWheel] ); + } + } + } + } + + // Save of data from last think. + for ( int iWheel = 0; iWheel < JEEP_WHEEL_COUNT; ++iWheel ) + { + m_WaterData.m_bWheelWasInWater[iWheel] = m_WaterData.m_bWheelInWater[iWheel]; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPropJeep::CheckWater( void ) +{ + bool bInWater = false; + + // Check all four wheels. + for ( int iWheel = 0; iWheel < JEEP_WHEEL_COUNT; ++iWheel ) + { + // Get the current wheel and get its contact point. + IPhysicsObject *pWheel = m_VehiclePhysics.GetWheel( iWheel ); + if ( !pWheel ) + continue; + + // Check to see if we hit water. + if ( pWheel->GetContactPoint( &m_WaterData.m_vecWheelContactPoints[iWheel], NULL ) ) + { + m_WaterData.m_bWheelInWater[iWheel] = ( UTIL_PointContents( m_WaterData.m_vecWheelContactPoints[iWheel] ) & MASK_WATER ) ? true : false; + if ( m_WaterData.m_bWheelInWater[iWheel] ) + { + bInWater = true; + } + } + } + + // Check the body and the BONNET. + int iEngine = LookupAttachment( "vehicle_engine" ); + Vector vecEnginePoint; + QAngle vecEngineAngles; + GetAttachment( iEngine, vecEnginePoint, vecEngineAngles ); + + m_WaterData.m_bBodyInWater = ( UTIL_PointContents( vecEnginePoint ) & MASK_WATER ) ? true : false; + if ( m_WaterData.m_bBodyInWater ) + { + if ( m_bHasPoop ) + { + RemoveAllDecals(); + m_bHasPoop = false; + } + + if ( !m_VehiclePhysics.IsEngineDisabled() ) + { + m_VehiclePhysics.SetDisableEngine( true ); + } + } + else + { + if ( m_VehiclePhysics.IsEngineDisabled() ) + { + m_VehiclePhysics.SetDisableEngine( false ); + } + } + + if ( bInWater ) + { + // Check the player's water level. + CheckWaterLevel(); + } + + return bInWater; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::CheckWaterLevel( void ) +{ + CBaseEntity *pEntity = GetDriver(); + if ( pEntity && pEntity->IsPlayer() ) + { + CBasePlayer *pPlayer = static_cast<CBasePlayer*>( pEntity ); + + Vector vecAttachPoint; + QAngle vecAttachAngles; + + // Check eyes. (vehicle_driver_eyes point) + int iAttachment = LookupAttachment( "vehicle_driver_eyes" ); + GetAttachment( iAttachment, vecAttachPoint, vecAttachAngles ); + + // Add the jeep's Z view offset + Vector vecUp; + AngleVectors( vecAttachAngles, NULL, NULL, &vecUp ); + vecUp.z = clamp( vecUp.z, 0.0f, vecUp.z ); + vecAttachPoint.z += r_JeepViewZHeight.GetFloat() * vecUp.z; + + bool bEyes = ( UTIL_PointContents( vecAttachPoint ) & MASK_WATER ) ? true : false; + if ( bEyes ) + { + pPlayer->SetWaterLevel( WL_Eyes ); + return; + } + + // Check waist. (vehicle_engine point -- see parent function). + if ( m_WaterData.m_bBodyInWater ) + { + pPlayer->SetWaterLevel( WL_Waist ); + return; + } + + // Check feet. (vehicle_feet_passenger0 point) + iAttachment = LookupAttachment( "vehicle_feet_passenger0" ); + GetAttachment( iAttachment, vecAttachPoint, vecAttachAngles ); + bool bFeet = ( UTIL_PointContents( vecAttachPoint ) & MASK_WATER ) ? true : false; + if ( bFeet ) + { + pPlayer->SetWaterLevel( WL_Feet ); + return; + } + + // Not in water. + pPlayer->SetWaterLevel( WL_NotInWater ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::CreateSplash( const Vector &vecPosition ) +{ + // Splash data. + CEffectData data; + data.m_fFlags = 0; + data.m_vOrigin = vecPosition; + data.m_vNormal.Init( 0.0f, 0.0f, 1.0f ); + VectorAngles( data.m_vNormal, data.m_vAngles ); + data.m_flScale = 10.0f + random->RandomFloat( 0, 2 ); + + // Create the splash.. + DispatchEffect( "watersplash", data ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::CreateRipple( const Vector &vecPosition ) +{ + // Ripple data. + CEffectData data; + data.m_fFlags = 0; + data.m_vOrigin = vecPosition; + data.m_vNormal.Init( 0.0f, 0.0f, 1.0f ); + VectorAngles( data.m_vNormal, data.m_vAngles ); + data.m_flScale = 10.0f + random->RandomFloat( 0, 2 ); + if ( GetWaterType() & CONTENTS_SLIME ) + { + data.m_fFlags |= FX_WATER_IN_SLIME; + } + + // Create the ripple. + DispatchEffect( "waterripple", data ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::Think(void) +{ + BaseClass::Think(); + +/* + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + if ( m_bEngineLocked ) + { + m_bUnableToFire = true; + + if ( pPlayer != NULL ) + { + pPlayer->m_Local.m_iHideHUD |= HIDEHUD_VEHICLE_CROSSHAIR; + } + } + else + { + // Start this as false and update it again each frame + m_bUnableToFire = false; + + if ( pPlayer != NULL ) + { + pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_VEHICLE_CROSSHAIR; + } + } +*/ + + // Water!? + HandleWater(); + + SetSimulationTime( gpGlobals->curtime ); + + SetNextThink( gpGlobals->curtime ); + SetAnimatedEveryTick( true ); + + if ( !m_bInitialHandbrake ) // after initial timer expires, set the handbrake + { + m_bInitialHandbrake = true; + m_VehiclePhysics.SetHandbrake( true ); + m_VehiclePhysics.Think(); + } + + // Check overturned status. + if ( !IsOverturned() ) + { + m_flOverturnedTime = 0.0f; + } + else + { + m_flOverturnedTime += gpGlobals->frametime; + } + + // spin gun if charging cannon + //FIXME: Don't bother for E3 + if ( m_bCannonCharging ) + { + m_nSpinPos += JEEP_GUN_SPIN_RATE; + SetPoseParameter( JEEP_GUN_SPIN, m_nSpinPos ); + } + + // Aim gun based on the player view direction. + if ( m_hPlayer && !m_bExitAnimOn && !m_bEnterAnimOn ) + { + Vector vecEyeDir, vecEyePos; + m_hPlayer->EyePositionAndVectors( &vecEyePos, &vecEyeDir, NULL, NULL ); + + // Trace out from the player's eye point. + Vector vecEndPos = vecEyePos + ( vecEyeDir * MAX_TRACE_LENGTH ); + trace_t trace; + UTIL_TraceLine( vecEyePos, vecEndPos, MASK_SHOT, this, COLLISION_GROUP_NONE, &trace ); + + // See if we hit something, if so, adjust end position to hit location. + if ( trace.fraction < 1.0 ) + { + vecEndPos = vecEyePos + ( vecEyeDir * MAX_TRACE_LENGTH * trace.fraction ); + } + + //m_vecLookCrosshair = vecEndPos; + AimGunAt( &vecEndPos, 0.1f ); + } + + StudioFrameAdvance(); + + // If the enter or exit animation has finished, tell the server vehicle + if ( IsSequenceFinished() && (m_bExitAnimOn || m_bEnterAnimOn) ) + { + if ( m_bEnterAnimOn ) + { + m_VehiclePhysics.ReleaseHandbrake(); + StartEngine(); + + // HACKHACK: This forces the jeep to play a sound when it gets entered underwater + if ( m_VehiclePhysics.IsEngineDisabled() ) + { + CBaseServerVehicle *pServerVehicle = dynamic_cast<CBaseServerVehicle *>(GetServerVehicle()); + if ( pServerVehicle ) + { + pServerVehicle->SoundStartDisabled(); + } + } + + // The first few time we get into the jeep, print the jeep help + if ( m_iNumberOfEntries < hud_jeephint_numentries.GetInt() ) + { + UTIL_HudHintText( m_hPlayer, "#Valve_Hint_JeepKeys" ); + m_iNumberOfEntries++; + } + } + + // If we're exiting and have had the tau cannon removed, we don't want to reset the animation + GetServerVehicle()->HandleEntryExitFinish( m_bExitAnimOn, !(m_bExitAnimOn && TauCannonHasBeenCutOff()) ); + } + + // See if the ammo crate needs to close + if ( ( m_flAmmoCrateCloseTime < gpGlobals->curtime ) && ( GetSequence() == LookupSequence( "ammo_open" ) ) ) + { + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0; + SetCycle( 0 ); + ResetSequence( LookupSequence( "ammo_close" ) ); + } + else if ( ( GetSequence() == LookupSequence( "ammo_close" ) ) && IsSequenceFinished() ) + { + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0; + SetCycle( 0 ); + ResetSequence( LookupSequence( "idle" ) ); + + CPASAttenuationFilter sndFilter( this, "PropJeep.AmmoClose" ); + EmitSound( sndFilter, entindex(), "PropJeep.AmmoClose" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &startPos - +// &endPos - +// width - +// useMuzzle - +//----------------------------------------------------------------------------- +void CPropJeep::DrawBeam( const Vector &startPos, const Vector &endPos, float width ) +{ + /* + //Tracer down the middle + UTIL_Tracer( startPos, endPos, 0, TRACER_DONT_USE_ATTACHMENT, 6500, false, "GaussTracer" ); + + //Draw the main beam shaft + CBeam *pBeam = CBeam::BeamCreate( GAUSS_BEAM_SPRITE, 0.5 ); + + pBeam->SetStartPos( startPos ); + pBeam->PointEntInit( endPos, this ); + pBeam->SetEndAttachment( LookupAttachment("Muzzle") ); + pBeam->SetWidth( width ); + pBeam->SetEndWidth( 0.05f ); + pBeam->SetBrightness( 255 ); + pBeam->SetColor( 255, 185+random->RandomInt( -16, 16 ), 40 ); + pBeam->RelinkBeam(); + pBeam->LiveForTime( 0.1f ); + + //Draw electric bolts along shaft + pBeam = CBeam::BeamCreate( GAUSS_BEAM_SPRITE, 3.0f ); + + pBeam->SetStartPos( startPos ); + pBeam->PointEntInit( endPos, this ); + pBeam->SetEndAttachment( LookupAttachment("Muzzle") ); + + pBeam->SetBrightness( random->RandomInt( 64, 255 ) ); + pBeam->SetColor( 255, 255, 150+random->RandomInt( 0, 64 ) ); + pBeam->RelinkBeam(); + pBeam->LiveForTime( 0.1f ); + pBeam->SetNoise( 1.6f ); + pBeam->SetEndWidth( 0.1f ); */ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::FireCannon( void ) +{ + //Don't fire again if it's been too soon + if ( m_flCannonTime > gpGlobals->curtime ) + return; + + if ( m_bUnableToFire ) + return; + + m_flCannonTime = gpGlobals->curtime + 0.2f; + m_bCannonCharging = false; + + //Find the direction the gun is pointing in + Vector aimDir; + GetCannonAim( &aimDir ); + + FireBulletsInfo_t info( 1, m_vecGunOrigin, aimDir, VECTOR_CONE_1DEGREES, MAX_TRACE_LENGTH, m_nAmmoType ); + + info.m_nFlags = FIRE_BULLETS_ALLOW_WATER_SURFACE_IMPACTS; + info.m_pAttacker = m_hPlayer; + + FireBullets( info ); + + // Register a muzzleflash for the AI + if ( m_hPlayer ) + { + m_hPlayer->SetMuzzleFlashTime( gpGlobals->curtime + 0.5 ); + } + + CPASAttenuationFilter sndFilter( this, "PropJeep.FireCannon" ); + EmitSound( sndFilter, entindex(), "PropJeep.FireCannon" ); + + // make cylinders of gun spin a bit + m_nSpinPos += JEEP_GUN_SPIN_RATE; + //SetPoseParameter( JEEP_GUN_SPIN, m_nSpinPos ); //FIXME: Don't bother with this for E3, won't look right +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::FireChargedCannon( void ) +{ +/* bool penetrated = false; + + m_bCannonCharging = false; + m_flCannonTime = gpGlobals->curtime + 0.5f; + + StopChargeSound(); + + CPASAttenuationFilter sndFilter( this, "PropJeep.FireChargedCannon" ); + EmitSound( sndFilter, entindex(), "PropJeep.FireChargedCannon" ); + + //Find the direction the gun is pointing in + Vector aimDir; + GetCannonAim( &aimDir ); + + Vector endPos = m_vecGunOrigin + ( aimDir * MAX_TRACE_LENGTH ); + + //Shoot a shot straight out + trace_t tr; + UTIL_TraceLine( m_vecGunOrigin, endPos, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + ClearMultiDamage(); + + //Find how much damage to do + float flChargeAmount = ( gpGlobals->curtime - m_flCannonChargeStartTime ) / MAX_GAUSS_CHARGE_TIME; + + //Clamp this + if ( flChargeAmount > 1.0f ) + { + flChargeAmount = 1.0f; + } + + //Determine the damage amount + //FIXME: Use ConVars! + float flDamage = 15 + ( ( 250 - 15 ) * flChargeAmount ); + + CBaseEntity *pHit = tr.m_pEnt; + + //Look for wall penetration + if ( tr.DidHitWorld() && !(tr.surface.flags & SURF_SKY) ) + { + //Try wall penetration + UTIL_ImpactTrace( &tr, m_nBulletType, "ImpactJeep" ); + UTIL_DecalTrace( &tr, "RedGlowFade" ); + + CPVSFilter filter( tr.endpos ); + te->GaussExplosion( filter, 0.0f, tr.endpos, tr.plane.normal, 0 ); + + Vector testPos = tr.endpos + ( aimDir * 48.0f ); + + UTIL_TraceLine( testPos, tr.endpos, MASK_SHOT, GetDriver(), COLLISION_GROUP_NONE, &tr ); + + if ( tr.allsolid == false ) + { + UTIL_DecalTrace( &tr, "RedGlowFade" ); + + penetrated = true; + } + } + else if ( pHit != NULL ) + { + CTakeDamageInfo dmgInfo( this, GetDriver(), flDamage, DMG_SHOCK ); + CalculateBulletDamageForce( &dmgInfo, GetAmmoDef()->Index("GaussEnergy"), aimDir, tr.endpos, 1.0f + flChargeAmount * 4.0f ); + + //Do direct damage to anything in our path + pHit->DispatchTraceAttack( dmgInfo, aimDir, &tr ); + } + + ApplyMultiDamage(); + + //Kick up an effect + if ( !(tr.surface.flags & SURF_SKY) ) + { + UTIL_ImpactTrace( &tr, m_nBulletType, "ImpactJeep" ); + + //Do a gauss explosion + CPVSFilter filter( tr.endpos ); + te->GaussExplosion( filter, 0.0f, tr.endpos, tr.plane.normal, 0 ); + } + + //Show the effect + DrawBeam( m_vecGunOrigin, tr.endpos, 9.6 ); + + // Register a muzzleflash for the AI + if ( m_hPlayer ) + { + m_hPlayer->SetMuzzleFlashTime( gpGlobals->curtime + 0.5f ); + } + + //Rock the car + IPhysicsObject *pObj = VPhysicsGetObject(); + + if ( pObj != NULL ) + { + Vector shoveDir = aimDir * -( flDamage * 500.0f ); + + pObj->ApplyForceOffset( shoveDir, m_vecGunOrigin ); + } + + //Do radius damage if we didn't penetrate the wall + if ( penetrated == true ) + { + RadiusDamage( CTakeDamageInfo( this, this, flDamage, DMG_SHOCK ), tr.endpos, 200.0f, CLASS_NONE, NULL ); + } */ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::ChargeCannon( void ) +{ + //Don't fire again if it's been too soon + if ( m_flCannonTime > gpGlobals->curtime ) + return; + + //See if we're starting a charge + if ( m_bCannonCharging == false ) + { + m_flCannonChargeStartTime = gpGlobals->curtime; + m_bCannonCharging = true; + + //Start charging sound + CPASAttenuationFilter filter( this ); + m_sndCannonCharge = (CSoundEnvelopeController::GetController()).SoundCreate( filter, entindex(), CHAN_STATIC, "Jeep.GaussCharge", ATTN_NORM ); + + assert(m_sndCannonCharge!=NULL); + if ( m_sndCannonCharge != NULL ) + { + (CSoundEnvelopeController::GetController()).Play( m_sndCannonCharge, 1.0f, 50 ); + (CSoundEnvelopeController::GetController()).SoundChangePitch( m_sndCannonCharge, 250, 3.0f ); + } + + return; + } + + //TODO: Add muzzle effect? + + //TODO: Check for overcharge and have the weapon simply fire or instead "decharge"? +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::StopChargeSound( void ) +{ + if ( m_sndCannonCharge != NULL ) + { + (CSoundEnvelopeController::GetController()).SoundFadeOut( m_sndCannonCharge, 0.1f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the true aiming position of the gun (looks at what player +// is looking at and adjusts) +// Input : &resultDir - direction to be calculated +//----------------------------------------------------------------------------- +void CPropJeep::GetCannonAim( Vector *resultDir ) +{ + Vector muzzleOrigin; + QAngle muzzleAngles; + + GetAttachment( LookupAttachment("gun_ref"), muzzleOrigin, muzzleAngles ); + + AngleVectors( muzzleAngles, resultDir ); +} + +//----------------------------------------------------------------------------- +// Purpose: If the player uses the jeep while at the back, he gets ammo from the crate instead +//----------------------------------------------------------------------------- +void CPropJeep::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + CBasePlayer *pPlayer = ToBasePlayer( pActivator ); + + if ( pPlayer == NULL) + return; + + // Find out if the player's looking at our ammocrate hitbox + Vector vecForward; + pPlayer->EyeVectors( &vecForward, NULL, NULL ); + + trace_t tr; + Vector vecStart = pPlayer->EyePosition(); + UTIL_TraceLine( vecStart, vecStart + vecForward * 1024, MASK_SOLID | CONTENTS_DEBRIS | CONTENTS_HITBOX, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.m_pEnt == this && tr.hitgroup == JEEP_AMMOCRATE_HITGROUP ) + { + // Player's using the crate. + // Fill up his SMG ammo. + pPlayer->GiveAmmo( 300, "SMG1"); + + if ( ( GetSequence() != LookupSequence( "ammo_open" ) ) && ( GetSequence() != LookupSequence( "ammo_close" ) ) ) + { + // Open the crate + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0; + SetCycle( 0 ); + ResetSequence( LookupSequence( "ammo_open" ) ); + + CPASAttenuationFilter sndFilter( this, "PropJeep.AmmoOpen" ); + EmitSound( sndFilter, entindex(), "PropJeep.AmmoOpen" ); + } + + m_flAmmoCrateCloseTime = gpGlobals->curtime + JEEP_AMMO_CRATE_CLOSE_DELAY; + return; + } + + // Fall back and get in the vehicle instead + BaseClass::Use( pActivator, pCaller, useType, value ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CPropJeep::CanExitVehicle( CBaseEntity *pEntity ) +{ + return ( !m_bEnterAnimOn && !m_bExitAnimOn && !m_bLocked && (m_nSpeed <= g_jeepexitspeed.GetFloat() ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::DampenEyePosition( Vector &vecVehicleEyePos, QAngle &vecVehicleEyeAngles ) +{ + // Get the frametime. (Check to see if enough time has passed to warrent dampening). + float flFrameTime = gpGlobals->frametime; + if ( flFrameTime < JEEP_FRAMETIME_MIN ) + { + vecVehicleEyePos = m_vecLastEyePos; + DampenUpMotion( vecVehicleEyePos, vecVehicleEyeAngles, 0.0f ); + return; + } + + // Keep static the sideways motion. + + // Dampen forward/backward motion. + DampenForwardMotion( vecVehicleEyePos, vecVehicleEyeAngles, flFrameTime ); + + // Blend up/down motion. + DampenUpMotion( vecVehicleEyePos, vecVehicleEyeAngles, flFrameTime ); +} + +//----------------------------------------------------------------------------- +// Use the controller as follows: +// speed += ( pCoefficientsOut[0] * ( targetPos - currentPos ) + pCoefficientsOut[1] * ( targetSpeed - currentSpeed ) ) * flDeltaTime; +//----------------------------------------------------------------------------- +void CPropJeep::ComputePDControllerCoefficients( float *pCoefficientsOut, + float flFrequency, float flDampening, + float flDeltaTime ) +{ + float flKs = 9.0f * flFrequency * flFrequency; + float flKd = 4.5f * flFrequency * flDampening; + + float flScale = 1.0f / ( 1.0f + flKd * flDeltaTime + flKs * flDeltaTime * flDeltaTime ); + + pCoefficientsOut[0] = flKs * flScale; + pCoefficientsOut[1] = ( flKd + flKs * flDeltaTime ) * flScale; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::DampenForwardMotion( Vector &vecVehicleEyePos, QAngle &vecVehicleEyeAngles, float flFrameTime ) +{ + // Get forward vector. + Vector vecForward; + AngleVectors( vecVehicleEyeAngles, &vecForward); + + // Simulate the eye position forward based on the data from last frame + // (assumes no acceleration - it will get that from the "spring"). + Vector vecCurrentEyePos = m_vecLastEyePos + m_vecEyeSpeed * flFrameTime; + + // Calculate target speed based on the current vehicle eye position and the last vehicle eye position and frametime. + Vector vecVehicleEyeSpeed = ( vecVehicleEyePos - m_vecLastEyeTarget ) / flFrameTime; + m_vecLastEyeTarget = vecVehicleEyePos; + + // Calculate the speed and position deltas. + Vector vecDeltaSpeed = vecVehicleEyeSpeed - m_vecEyeSpeed; + Vector vecDeltaPos = vecVehicleEyePos - vecCurrentEyePos; + + // Clamp. + if ( vecDeltaPos.Length() > JEEP_DELTA_LENGTH_MAX ) + { + float flSign = vecForward.Dot( vecVehicleEyeSpeed ) >= 0.0f ? -1.0f : 1.0f; + vecVehicleEyePos += flSign * ( vecForward * JEEP_DELTA_LENGTH_MAX ); + m_vecLastEyePos = vecVehicleEyePos; + m_vecEyeSpeed = vecVehicleEyeSpeed; + return; + } + + // Generate an updated (dampening) speed for use in next frames position extrapolation. + float flCoefficients[2]; + ComputePDControllerCoefficients( flCoefficients, r_JeepViewDampenFreq.GetFloat(), r_JeepViewDampenDamp.GetFloat(), flFrameTime ); + m_vecEyeSpeed += ( ( flCoefficients[0] * vecDeltaPos + flCoefficients[1] * vecDeltaSpeed ) * flFrameTime ); + + // Save off data for next frame. + m_vecLastEyePos = vecCurrentEyePos; + + // Move eye forward/backward. + Vector vecForwardOffset = vecForward * ( vecForward.Dot( vecDeltaPos ) ); + vecVehicleEyePos -= vecForwardOffset; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::DampenUpMotion( Vector &vecVehicleEyePos, QAngle &vecVehicleEyeAngles, float flFrameTime ) +{ + // Get up vector. + Vector vecUp; + AngleVectors( vecVehicleEyeAngles, NULL, NULL, &vecUp ); + vecUp.z = clamp( vecUp.z, 0.0f, vecUp.z ); + vecVehicleEyePos.z += r_JeepViewZHeight.GetFloat() * vecUp.z; + + // NOTE: Should probably use some damped equation here. +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::SetupMove( CBasePlayer *player, CUserCmd *ucmd, IMoveHelper *pHelper, CMoveData *move ) +{ + // If we are overturned and hit any key - leave the vehicle (IN_USE is already handled!). + if ( m_flOverturnedTime > OVERTURNED_EXIT_WAITTIME ) + { + if ( (ucmd->buttons & (IN_FORWARD|IN_BACK|IN_MOVELEFT|IN_MOVERIGHT|IN_SPEED|IN_JUMP|IN_ATTACK|IN_ATTACK2) ) && !m_bExitAnimOn ) + { + // Can't exit yet? We're probably still moving. Swallow the keys. + if ( !CanExitVehicle(player) ) + return; + + if ( !GetServerVehicle()->HandlePassengerExit( m_hPlayer ) && ( m_hPlayer != NULL ) ) + { + m_hPlayer->PlayUseDenySound(); + } + return; + } + } + + // If the throttle is disabled or we're upside-down, don't allow throttling (including turbo) + CUserCmd tmp; + if ( ( m_throttleDisableTime > gpGlobals->curtime ) || ( IsOverturned() ) ) + { + m_bUnableToFire = true; + + tmp = (*ucmd); + tmp.buttons &= ~(IN_FORWARD|IN_BACK|IN_SPEED); + ucmd = &tmp; + } + + BaseClass::SetupMove( player, ucmd, pHelper, move ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::DriveVehicle( float flFrameTime, CUserCmd *ucmd, int iButtonsDown, int iButtonsReleased ) +{ + int iButtons = ucmd->buttons; + + //Adrian: No headlights on Superfly. +/* if ( ucmd->impulse == 100 ) + { + if (HeadlightIsOn()) + { + HeadlightTurnOff(); + } + else + { + HeadlightTurnOn(); + } + }*/ + + // If we're holding down an attack button, update our state + if ( IsOverturned() == false ) + { + if ( iButtons & IN_ATTACK ) + { + if ( m_bCannonCharging ) + { + FireChargedCannon(); + } + else + { + FireCannon(); + } + } + else if ( iButtons & IN_ATTACK2 ) + { + ChargeCannon(); + } + } + + // If we've released our secondary button, fire off our cannon + if ( ( iButtonsReleased & IN_ATTACK2 ) && ( m_bCannonCharging ) ) + { + FireChargedCannon(); + } + + BaseClass::DriveVehicle( flFrameTime, ucmd, iButtonsDown, iButtonsReleased ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +// *pMoveData - +//----------------------------------------------------------------------------- +void CPropJeep::ProcessMovement( CBasePlayer *pPlayer, CMoveData *pMoveData ) +{ + BaseClass::ProcessMovement( pPlayer, pMoveData ); + + // Create dangers sounds in front of the vehicle. + CreateDangerSounds(); +} + +//----------------------------------------------------------------------------- +// Purpose: Create danger sounds in front of the vehicle. +//----------------------------------------------------------------------------- +void CPropJeep::CreateDangerSounds( void ) +{ + QAngle dummy; + GetAttachment( "Muzzle", m_vecGunOrigin, dummy ); + + if ( m_flDangerSoundTime > gpGlobals->curtime ) + return; + + QAngle vehicleAngles = GetLocalAngles(); + Vector vecStart = GetAbsOrigin(); + Vector vecDir, vecRight; + + GetVectors( &vecDir, &vecRight, NULL ); + + const float soundDuration = 0.25; + float speed = m_VehiclePhysics.GetHLSpeed(); + // Make danger sounds ahead of the jeep + if ( fabs(speed) > 120 ) + { + Vector vecSpot; + + float steering = m_VehiclePhysics.GetSteering(); + if ( steering != 0 ) + { + if ( speed > 0 ) + { + vecDir += vecRight * steering * 0.5; + } + else + { + vecDir -= vecRight * steering * 0.5; + } + VectorNormalize(vecDir); + } + const float radius = speed * 0.4; + // 0.3 seconds ahead of the jeep + vecSpot = vecStart + vecDir * (speed * 0.3f); + CSoundEnt::InsertSound( SOUND_DANGER, vecSpot, radius, soundDuration, this, 0 ); + CSoundEnt::InsertSound( SOUND_PHYSICS_DANGER, vecSpot, radius, soundDuration, this, 1 ); + //NDebugOverlay::Box(vecSpot, Vector(-radius,-radius,-radius),Vector(radius,radius,radius), 255, 0, 255, 0, soundDuration); + +#if 0 + trace_t tr; + // put sounds a bit to left and right but slightly closer to Jeep to make a "cone" of sound + // in front of it + vecSpot = vecStart + vecDir * (speed * 0.5f) - vecRight * speed * 0.5; + UTIL_TraceLine( vecStart, vecSpot, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + CSoundEnt::InsertSound( SOUND_DANGER, vecSpot, 400, soundDuration, this, 1 ); + + vecSpot = vecStart + vecDir * (speed * 0.5f) + vecRight * speed * 0.5; + UTIL_TraceLine( vecStart, vecSpot, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + CSoundEnt::InsertSound( SOUND_DANGER, vecSpot, 400, soundDuration, this, 2); +#endif + } + + m_flDangerSoundTime = gpGlobals->curtime + 0.1; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::EnterVehicle( CBasePlayer *pPlayer ) +{ + if ( !pPlayer ) + return; + + CheckWater(); + BaseClass::EnterVehicle( pPlayer ); + + // Start looking for seagulls to land + m_hLastPlayerInVehicle = m_hPlayer; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::ExitVehicle( int nRole ) +{ + HeadlightTurnOff(); + + BaseClass::ExitVehicle( nRole ); + + //If the player has exited, stop charging + StopChargeSound(); + m_bCannonCharging = false; + + // Remember when we last saw the player + m_flPlayerExitedTime = gpGlobals->curtime; + m_flLastSawPlayerAt = gpGlobals->curtime; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::InputStartRemoveTauCannon( inputdata_t &inputdata ) +{ + // Start the gun removal animation + m_flAnimTime = gpGlobals->curtime; + m_flPlaybackRate = 0.0; + SetCycle( 0 ); + ResetSequence( LookupSequence( "tau_levitate" ) ); + + m_bGunHasBeenCutOff = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::InputFinishRemoveTauCannon( inputdata_t &inputdata ) +{ + // Remove & hide the gun + SetBodygroup( 1, false ); + m_bHasGun = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeep::OnRestore( void ) +{ + IServerVehicle *pServerVehicle = GetServerVehicle(); + if ( pServerVehicle != NULL ) + { + // Restore the passenger information we're holding on to + pServerVehicle->RestorePassengerInfo(); + } +} + +//======================================================================================================================================== +// JEEP FOUR WHEEL PHYSICS VEHICLE SERVER VEHICLE +//======================================================================================================================================== +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CJeepFourWheelServerVehicle::NPC_AimPrimaryWeapon( Vector vecTarget ) +{ + ((CPropJeep*)m_pVehicle)->AimGunAt( &vecTarget, 0.1f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &vecEyeExitEndpoint - +// Output : int +//----------------------------------------------------------------------------- +int CJeepFourWheelServerVehicle::GetExitAnimToUse( Vector &vecEyeExitEndpoint, bool &bAllPointsBlocked ) +{ + bAllPointsBlocked = false; + + if ( !m_bParsedAnimations ) + { + // Load the entry/exit animations from the vehicle + ParseEntryExitAnims(); + m_bParsedAnimations = true; + } + + CBaseAnimating *pAnimating = dynamic_cast<CBaseAnimating *>(m_pVehicle); + // If we don't have the gun anymore, we want to get out using the "gun-less" animation + if ( pAnimating && ((CPropJeep*)m_pVehicle)->TauCannonHasBeenCutOff() ) + { + // HACK: We know the tau-cannon removed exit anim uses the first upright anim's exit details + trace_t tr; + + // Convert our offset points to worldspace ones + Vector vehicleExitOrigin = m_ExitAnimations[0].vecExitPointLocal; + QAngle vehicleExitAngles = m_ExitAnimations[0].vecExitAnglesLocal; + UTIL_ParentToWorldSpace( pAnimating, vehicleExitOrigin, vehicleExitAngles ); + + // Ensure the endpoint is clear by dropping a point down from above + vehicleExitOrigin -= VEC_VIEW; + Vector vecMove = Vector(0,0,64); + Vector vecStart = vehicleExitOrigin + vecMove; + Vector vecEnd = vehicleExitOrigin - vecMove; + UTIL_TraceHull( vecStart, vecEnd, VEC_HULL_MIN, VEC_HULL_MAX, MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr ); + + Assert( !tr.startsolid && tr.fraction < 1.0 ); + m_vecCurrentExitEndPoint = vecStart + ((vecEnd - vecStart) * tr.fraction); + vecEyeExitEndpoint = m_vecCurrentExitEndPoint + VEC_VIEW; + m_iCurrentExitAnim = 0; + return pAnimating->LookupSequence( "exit_tauremoved" ); + } + + return BaseClass::GetExitAnimToUse( vecEyeExitEndpoint, bAllPointsBlocked ); +} diff --git a/game/server/cstrike/func_bomb_target.cpp b/game/server/cstrike/func_bomb_target.cpp new file mode 100644 index 0000000..a85d646 --- /dev/null +++ b/game/server/cstrike/func_bomb_target.cpp @@ -0,0 +1,84 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bomb target area +// +//=============================================================================// + +#include "cbase.h" +#include "cs_player.h" +#include "func_bomb_target.h" +#include "cs_gamerules.h" + +LINK_ENTITY_TO_CLASS( func_bomb_target, CBombTarget ); + +BEGIN_DATADESC( CBombTarget ) + DEFINE_FUNCTION( BombTargetTouch ), + DEFINE_FUNCTION( BombTargetUse ), //needed? + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "BombExplode", OnBombExplode ), + DEFINE_INPUTFUNC( FIELD_VOID, "BombPlanted", OnBombPlanted ), + DEFINE_INPUTFUNC( FIELD_VOID, "BombDefused", OnBombDefused ), + + // Outputs + DEFINE_OUTPUT( m_OnBombExplode, "BombExplode" ), + DEFINE_OUTPUT( m_OnBombPlanted, "BombPlanted" ), + DEFINE_OUTPUT( m_OnBombDefused, "BombDefused" ), + DEFINE_KEYFIELD( m_bIsHeistBombTarget, FIELD_BOOLEAN, "heistbomb" ), + DEFINE_KEYFIELD( m_szMountTarget, FIELD_STRING, "bomb_mount_target" ), + +END_DATADESC() + +CBombTarget::CBombTarget( void ) +{ + m_bIsHeistBombTarget = false; + m_szMountTarget = NULL_STRING; +} + +void CBombTarget::Spawn() +{ + InitTrigger(); + SetTouch( &CBombTarget::BombTargetTouch ); + SetUse( &CBombTarget::BombTargetUse ); +} + +void CBombTarget::BombTargetTouch( CBaseEntity* pOther ) +{ + CCSPlayer *p = dynamic_cast< CCSPlayer* >( pOther ); + if ( p ) + { + if ( p->HasC4() && CSGameRules()->m_bBombPlanted == false ) + { + p->m_bInBombZone = true; + p->m_iBombSiteIndex = entindex(); + if ( !(p->m_iDisplayHistoryBits & DHF_IN_TARGET_ZONE) ) + { + p->HintMessage( "#Hint_you_are_in_targetzone", false ); + p->m_iDisplayHistoryBits |= DHF_IN_TARGET_ZONE; + } + } + } +} + +void CBombTarget::BombTargetUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + //SUB_UseTargets( NULL, USE_TOGGLE, 0 ); + DevMsg( 2, "BombTargetUse does nothing\n" ); +} + +// Relay to our outputs +void CBombTarget::OnBombExplode( inputdata_t &inputdata ) +{ + m_OnBombExplode.FireOutput(this, this); +} + +// Relay to our outputs +void CBombTarget::OnBombPlanted( inputdata_t &inputdata ) +{ + m_OnBombPlanted.FireOutput(this, this); +} +// Relay to our outputs +void CBombTarget::OnBombDefused( inputdata_t &inputdata ) +{ + m_OnBombDefused.FireOutput(this, this); +}
\ No newline at end of file diff --git a/game/server/cstrike/func_bomb_target.h b/game/server/cstrike/func_bomb_target.h new file mode 100644 index 0000000..090b81d --- /dev/null +++ b/game/server/cstrike/func_bomb_target.h @@ -0,0 +1,37 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bomb Target Area ent +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "triggers.h" + +class CBombTarget : public CBaseTrigger +{ +public: + DECLARE_CLASS( CBombTarget, CBaseTrigger ); + DECLARE_DATADESC(); + + CBombTarget(); + + void Spawn(); + void EXPORT BombTargetTouch( CBaseEntity* pOther ); + void EXPORT BombTargetUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + + void OnBombExplode( inputdata_t &inputdata ); + void OnBombPlanted( inputdata_t &inputdata ); + void OnBombDefused( inputdata_t &inputdata ); + + bool IsHeistBombTarget( void ) { return m_bIsHeistBombTarget; } + const char *GetBombMountTarget( void ){ return STRING( m_szMountTarget ); } + +private: + COutputEvent m_OnBombExplode; //Fired when the bomb explodes + COutputEvent m_OnBombPlanted; //Fired when the bomb is planted + COutputEvent m_OnBombDefused; //Fired when the bomb is defused + + bool m_bIsHeistBombTarget; + string_t m_szMountTarget; +};
\ No newline at end of file diff --git a/game/server/cstrike/func_buy_zone.cpp b/game/server/cstrike/func_buy_zone.cpp new file mode 100644 index 0000000..a982fe4 --- /dev/null +++ b/game/server/cstrike/func_buy_zone.cpp @@ -0,0 +1,77 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "triggers.h" +#include "cs_player.h" + + +//====================================== +// Bomb target area +// +// + +class CBuyZone : public CBaseTrigger +{ +public: + DECLARE_CLASS( CBuyZone, CBaseTrigger ); + DECLARE_DATADESC(); + + CBuyZone(); + void Spawn(); + void EXPORT BuyZoneTouch( CBaseEntity* pOther ); + +public: + int m_LegacyTeamNum; +}; + + +LINK_ENTITY_TO_CLASS( func_buyzone, CBuyZone ); + +BEGIN_DATADESC( CBuyZone ) + DEFINE_FUNCTION( BuyZoneTouch ), + + // This is here to support maps that haven't updated to using "teamnum" yet. + DEFINE_INPUT( m_LegacyTeamNum, FIELD_INTEGER, "team" ) +END_DATADESC() + + +CBuyZone::CBuyZone() +{ + m_LegacyTeamNum = -1; +} + + +void CBuyZone::Spawn() +{ + InitTrigger(); + SetTouch( &CBuyZone::BuyZoneTouch ); + + // Support for legacy-style teamnums. + if ( m_LegacyTeamNum == 1 ) + { + ChangeTeam( TEAM_TERRORIST ); + } + else if ( m_LegacyTeamNum == 2 ) + { + ChangeTeam( TEAM_CT ); + } +} + + +void CBuyZone::BuyZoneTouch( CBaseEntity* pOther ) +{ + CCSPlayer *p = dynamic_cast< CCSPlayer* >( pOther ); + if ( p ) + { + // compare player team with buy zone team number + if ( p->GetTeamNumber() == GetTeamNumber() ) + { + p->m_bInBuyZone = true; + } + } +} + diff --git a/game/server/cstrike/func_hostage_rescue.cpp b/game/server/cstrike/func_hostage_rescue.cpp new file mode 100644 index 0000000..5a5166f --- /dev/null +++ b/game/server/cstrike/func_hostage_rescue.cpp @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "triggers.h" + +class CHostageRescueZone : public CBaseTrigger +{ +public: + DECLARE_CLASS( CHostageRescueZone, CBaseTrigger ); + DECLARE_DATADESC(); + + void CHostageRescue(); + void Spawn(); + + void HostageRescueTouch( CBaseEntity* pOther ); +}; + + +LINK_ENTITY_TO_CLASS( func_hostage_rescue, CHostageRescueZone ); + + +BEGIN_DATADESC( CHostageRescueZone ) + + //Functions + DEFINE_FUNCTION( HostageRescueTouch ), + +END_DATADESC() + + +void CHostageRescueZone::Spawn() +{ + InitTrigger(); + SetTouch( &CHostageRescueZone::HostageRescueTouch ); +} + +void CHostageRescueZone::HostageRescueTouch( CBaseEntity *pOther ) +{ + variant_t emptyVariant; + pOther->AcceptInput( "OnRescueZoneTouch", NULL, NULL, emptyVariant, 0 ); +} + diff --git a/game/server/cstrike/funfact_cs.cpp b/game/server/cstrike/funfact_cs.cpp new file mode 100644 index 0000000..e880359 --- /dev/null +++ b/game/server/cstrike/funfact_cs.cpp @@ -0,0 +1,793 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_gamestats.h" +#include "funfactmgr_cs.h" +#include "funfact_cs.h" +#include "../../game/shared/cstrike/weapon_csbase.h" +#include "cs_achievement_constants.h" + +#define FIRST_BLOOD_TIME 45.0f +#define FIRST_KILL_TIME 45.0f +#define SHORT_ROUND_TIME 30.0f +#define MIN_SHOTS_FOR_ACCURACY 10 + +enum FunFactId +{ + FUNFACT_CT_WIN_NO_KILLS, + FUNFACT_T_WIN_NO_KILLS, + FUNFACT_KILL_DEFUSER, + FUNFACT_KILL_RESCUER, + FUNFACT_T_WIN_NO_CASUALTIES, + FUNFACT_CT_WIN_NO_CASUALTIES, + FUNFACT_DAMAGE_WITH_GRENADES, + FUNFACT_KILLS_WITH_GRENADES, + FUNFACT_KILLS_WITH_SINGLE_GRENADE, + FUNFACT_DAMAGE_NO_KILLS, + FUNFACT_KILLED_ENEMIES, + FUNFACT_FIRST_KILL, + FUNFACT_FIRST_BLOOD, + FUNFACT_SHORT_ROUND, + FUNFACT_BEST_ACCURACY, + FUNFACT_KNIFE_KILLS, + FUNFACT_BLIND_KILLS, + FUNFACT_KILLS_WITH_LAST_ROUND, + FUNFACT_DONATED_WEAPONS, + FUNFACT_POSTHUMOUS_KILLS_WITH_GRENADE, + FUNFACT_KNIFE_IN_GUNFIGHT, + FUNFACT_NUM_TIMES_JUMPED, + FUNFACT_FALL_DAMAGE, + FUNFACT_ITEMS_PURCHASED, + FUNFACT_WON_AS_LAST_MEMBER, + FUNFACT_NUMBER_OF_OVERKILLS, + FUNFACT_SHOTS_FIRED, + FUNFACT_MONEY_SPENT, + FUNFACT_SURVIVED_MULTIPLE_ATTACKERS, + FUNFACT_DIED_FROM_MULTIPLE_ATTACKERS, + FUNFACT_DAMAGE_MULTIPLE_ENEMIES, + FUNFACT_GRENADES_THROWN, + FUNFACT_USED_ALL_AMMO, + FUNFACT_DEFENDED_BOMB, + FUNFACT_ITEMS_DROPPED_VALUE, + FUNFACT_KILL_WOUNDED_ENEMIES, + FUNFACT_USED_MULTIPLE_WEAPONS, + FUNFACT_TERRORIST_ACCURACY, + FUNFACT_CT_ACCURACY, + FUNFACT_SAME_UNIFORM_TERRORIST, + FUNFACT_SAME_UNIFORM_CT, + FUNFACT_BEST_TERRORIST_ACCURACY, + FUNFACT_BEST_COUNTERTERRORIST_ACCURACY, + FUNFACT_FALLBACK1, + FUNFACT_FALLBACK2, + FUNFACT_KILLS_HEADSHOTS, + FUNFACT_BROKE_WINDOWS, + FUNFACT_NIGHTVISION_DAMAGE, + FUNFACT_DEFUSED_WITH_DROPPED_KIT, + FUNFACT_KILLED_HALF_OF_ENEMIES, +}; + + +CFunFactHelper *CFunFactHelper::s_pFirst = NULL; + + +//============================================================================= +// Generic evaluation Fun Fact +// This fun fact will evaluate the specified function to determine when it is +// valid. This is basically just a glue class for simple evaluation functions. +//============================================================================= + +// Function type that we use to evaluate our fun facts. The data is returned as ints then floats that are passed in as reference parameters +typedef bool (*fFunFactEval)( int &iPlayer, int &data1, int &data2, int &data3 ); + +class CFunFact_GenericEvalFunction : public FunFactEvaluator +{ +public: + CFunFact_GenericEvalFunction(FunFactId id, const char* szLocalizationToken, float fCoolness, fFunFactEval pfnEval ) : + FunFactEvaluator(id, szLocalizationToken, fCoolness), + m_pfnEval(pfnEval) + {} + + virtual bool Evaluate( FunFactVector& results ) const + { + FunFact funfact; + if (m_pfnEval(funfact.iPlayer, funfact.iData1, funfact.iData2, funfact.iData3)) + { + funfact.id = GetId(); + funfact.szLocalizationToken = GetLocalizationToken(); + funfact.fMagnitude = 0.0f; + results.AddToTail(funfact); + return true; + } + else + return false; + } + + private: + fFunFactEval m_pfnEval; +}; +#define DECLARE_FUNFACT_EVALFUNC(funfactId, szLocalizationToken, fCoolness, pfnEval) \ +static FunFactEvaluator *CreateFunFact_##funfactId( void ) \ +{ \ + return new CFunFact_GenericEvalFunction(funfactId, szLocalizationToken, fCoolness, pfnEval);\ +}; \ +static CFunFactHelper g_##funfactId##_Helper( CreateFunFact_##funfactId ); + + +//============================================================================= +// Per-player evaluation Fun Fact +// Evaluate the function per player and generate a fun fact for each valid or +// highest valid player +//============================================================================= + +namespace EvalFlags +{ + enum Type + { + All = 0x00, + TeamCT = 0x01, + TeamTerrorist = 0x02, + HighestOnly = 0x04, // when not set, generates fun facts for all valid testees + Alive = 0x08, + Dead = 0x10, + WinningTeam = 0x20, + LosingTeam = 0x40, + }; +}; + + +bool PlayerQualifies( const CBasePlayer* pPlayer, int flags ) +{ + if ( (flags & EvalFlags::TeamCT) && pPlayer->GetTeamNumber() != TEAM_CT ) + return false; + if ( (flags & EvalFlags::TeamTerrorist) && pPlayer->GetTeamNumber() != TEAM_TERRORIST ) + return false; + if ( (flags & EvalFlags::Dead) && const_cast<CBasePlayer*>(pPlayer)->IsAlive() ) // IsAlive() really isn't const correct + return false; + if ( (flags & EvalFlags::Alive) && !const_cast<CBasePlayer*>(pPlayer)->IsAlive() ) + return false; + if ( (flags & EvalFlags::WinningTeam) && pPlayer->GetTeamNumber() != CSGameRules()->m_iRoundWinStatus ) + return false; + if ( (flags & EvalFlags::LosingTeam) && pPlayer->GetTeamNumber() == CSGameRules()->m_iRoundWinStatus ) + return false; + return true; +} + + +typedef int (*PlayerEvalFunction)(CCSPlayer* pPlayer); + +class CFunFact_PlayerEvalFunction : public FunFactEvaluator +{ +public: + CFunFact_PlayerEvalFunction(FunFactId id, const char* szLocalizationToken, float fCoolness, PlayerEvalFunction pfnEval, + int iMin, int flags ) : + FunFactEvaluator(id, szLocalizationToken, fCoolness), + m_pfnEval(pfnEval), + m_min(iMin), + m_flags(flags) + {} + + virtual bool Evaluate( FunFactVector& results ) const + { + int iBestValue = 0; + int iBestPlayer = 0; + bool bResult = false; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer* pPlayer = ToCSPlayer(UTIL_PlayerByIndex( i ) ); + if ( pPlayer ) + { + if (!PlayerQualifies(pPlayer, m_flags)) + continue; + + int iValue = m_pfnEval(pPlayer); + + if (m_flags & EvalFlags::HighestOnly) + { + if ( iValue > iBestValue ) + { + iBestValue = iValue; + iBestPlayer = i; + } + } + else + { + // generate fun facts for any player who meets the validation requirement + if ( iValue >= m_min ) + { + FunFact funfact; + funfact.id = GetId(); + funfact.szLocalizationToken = GetLocalizationToken(); + funfact.iPlayer = i; + funfact.iData1 = iValue; + funfact.fMagnitude = 1.0f - ((float)m_min / iValue); + results.AddToTail(funfact); + bResult = true; + } + } + } + } + if ( (m_flags & EvalFlags::HighestOnly) && iBestValue >= m_min ) + { + FunFact funfact; + funfact.id = GetId(); + funfact.szLocalizationToken = GetLocalizationToken(); + funfact.iPlayer = iBestPlayer; + funfact.iData1 = iBestValue; + funfact.fMagnitude = 1.0f - ((float)m_min / iBestValue); + + results.AddToTail(funfact); + bResult = true; + } + return bResult; + } + +private: + PlayerEvalFunction m_pfnEval; + int m_min; + int m_flags; +}; +#define DECLARE_FUNFACT_PLAYERFUNC(funfactId, szLocalizationToken, fCoolness, pfnEval, iMin, iFlags) \ +static FunFactEvaluator *CreateFunFact_##funfactId( void ) \ +{ \ + return new CFunFact_PlayerEvalFunction(funfactId, szLocalizationToken, fCoolness, pfnEval, iMin, iFlags); \ +}; \ +static CFunFactHelper g_##funfactId##_Helper( CreateFunFact_##funfactId ); + + +//============================================================================= +// Per-team evaluation Fun Fact +//============================================================================= + +typedef bool (*TeamEvalFunction)(int iTeam, int &data1, int &data2, int &data3); + +class CFunFact_TeamEvalFunction : public FunFactEvaluator +{ +public: + CFunFact_TeamEvalFunction(FunFactId id, const char* szLocalizationToken, float fCoolness, TeamEvalFunction pfnEval, int iTeam ) : + FunFactEvaluator(id, szLocalizationToken, fCoolness), + m_pfnEval(pfnEval), + m_team(iTeam) + {} + + virtual bool Evaluate( FunFactVector& results ) const + { + int iData1, iData2, iData3; + if ( m_pfnEval(m_team, iData1, iData2, iData3) ) + { + FunFact funfact; + funfact.id = GetId(); + funfact.szLocalizationToken = GetLocalizationToken(); + funfact.fMagnitude = 0.0f; + results.AddToTail(funfact); + return true; + } + return false; + } + +private: + TeamEvalFunction m_pfnEval; + int m_team; +}; +#define DECLARE_FUNFACT_TEAMFUNC(funfactId, szLocalizationToken, fCoolness, pfnEval, iTeam) \ +static FunFactEvaluator *CreateFunFact_##funfactId( void ) \ +{ \ + return new CFunFact_TeamEvalFunction(funfactId, szLocalizationToken, fCoolness, pfnEval, iTeam);\ +}; \ +static CFunFactHelper g_##funfactId##_Helper( CreateFunFact_##funfactId ); + + +//============================================================================= +// High Stat-based Fun Fact +// This fun fact will find the player with the highest value for a particular +// stat, and validate when that stat exceeds a specified minimum +//============================================================================= +class CFunFact_StatBest : public FunFactEvaluator +{ +public: + CFunFact_StatBest(FunFactId id, const char* szLocalizationToken, float fCoolness, CSStatType_t statId, int iMin, int flags ) : + FunFactEvaluator(id, szLocalizationToken, fCoolness), + m_statId(statId), + m_min(iMin), + m_flags(flags) + { + V_strncpy(m_singularLocalizationToken, szLocalizationToken, sizeof(m_singularLocalizationToken)); + if (m_min == 1) + { + V_strncat(m_singularLocalizationToken, "_singular", sizeof(m_singularLocalizationToken)); + } + } + + virtual bool Evaluate( FunFactVector& results ) const + { + int iBestValue = 0; + int iBestPlayer = 0; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer ) + { + if (!PlayerQualifies(pPlayer, m_flags)) + continue; + + int iValue = CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[m_statId]; + if ( iValue > iBestValue ) + { + iBestValue = iValue; + iBestPlayer = i; + } + } + } + if ( iBestValue >= m_min ) + { + FunFact funfact; + funfact.id = GetId(); + funfact.szLocalizationToken = iBestValue == 1 ? m_singularLocalizationToken : GetLocalizationToken(); + funfact.iPlayer = iBestPlayer; + funfact.iData1 = iBestValue; + funfact.fMagnitude = 1.0f - ((float)m_min / iBestValue); + + results.AddToTail(funfact); + return true; + } + return false; + } + +private: + CSStatType_t m_statId; + int m_min; + char m_singularLocalizationToken[128]; + int m_flags; + +}; +#define DECLARE_FUNFACT_STATBEST(funfactId, szLocalizationToken, fCoolness, statId, iMin, flags) \ +static FunFactEvaluator *CreateFunFact_##funfactId( void ) \ +{ \ + return new CFunFact_StatBest(funfactId, szLocalizationToken, fCoolness, statId, iMin, flags); \ +}; \ +static CFunFactHelper g_##funfactId##_Helper( CreateFunFact_##funfactId ); + + +//============================================================================= +// Sum-based Fun Fact +// This fun fact will add up a stat for all players, and is valid when the +// sum exceeds a threshold +//============================================================================= +class CFunFact_StatSum : public FunFactEvaluator +{ +public: + CFunFact_StatSum(FunFactId id, const char* szLocalizationToken, float fCoolness, CSStatType_t statId, int iMin, EvalFlags::Type flags ) : + FunFactEvaluator(id, szLocalizationToken, fCoolness), + m_statId(statId), + m_min(iMin), + m_flags(flags) + {} + + virtual bool Evaluate( FunFactVector& results ) const + { + int iSum = 0; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer ) + { + if (!PlayerQualifies(pPlayer, m_flags)) + continue; + + iSum += CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[m_statId]; + } + } + if ( iSum >= m_min ) + { + FunFact funfact; + funfact.id = GetId(); + funfact.szLocalizationToken = GetLocalizationToken(); + funfact.iPlayer = 0; + funfact.iData1 = iSum; + funfact.fMagnitude = 1.0f - ((float)m_min / iSum); + + results.AddToTail(funfact); + return true; + } + return false; + } + +private: + CSStatType_t m_statId; + int m_min; + int m_flags; + +}; +#define DECLARE_FUNFACT_STATSUM(funfactId, szLocalizationToken, fCoolness, statId, iMin, flags) \ +static FunFactEvaluator *CreateFunFact_##funfactId( void ) \ +{ \ + return new CFunFact_StatSum(funfactId, szLocalizationToken, fCoolness, statId, iMin, flags); \ +}; \ +static CFunFactHelper g_##funfactId##_Helper( CreateFunFact_##funfactId ); + + + +//============================================================================= +// Helper function to calculate team accuracy +//============================================================================= + +float GetTeamAccuracy( int teamNumber ) +{ + int teamShots = 0; + int teamHits = 0; + + //Add up hits and shots + CBasePlayer *pPlayer = NULL; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + pPlayer = UTIL_PlayerByIndex( i ); + if (pPlayer) + { + if (pPlayer->GetTeamNumber() == teamNumber) + { + teamShots += CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_FIRED];; + teamHits += CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_HIT];; + } + } + } + + if (teamShots > MIN_SHOTS_FOR_ACCURACY) + return (float)teamHits / teamShots; + + return 0.0f; +} + + + +//============================================================================= +// fun fact explicit evaluation functions +//============================================================================= + +bool FFEVAL_ALWAYS_TRUE( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + return true; +} + +bool FFEVAL_CT_WIN_NO_KILLS( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + return ( CSGameRules()->m_iRoundWinStatus == WINNER_CT && CSGameRules()->m_bNoTerroristsKilled ); +} + +bool FFEVAL_T_WIN_NO_KILLS( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + return ( CSGameRules()->m_iRoundWinStatus == WINNER_TER && CSGameRules()->m_bNoCTsKilled ); +} + +bool FFEVAL_T_WIN_NO_CASUALTIES( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + return ( CSGameRules()->m_iRoundWinStatus == WINNER_TER && CSGameRules()->m_bNoTerroristsKilled ); +} + +bool FFEVAL_CT_WIN_NO_CASUALTIES( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + return ( CSGameRules()->m_iRoundWinStatus == WINNER_CT && CSGameRules()->m_bNoCTsKilled ); +} + +int FFEVAL_KILLED_DEFUSER( CCSPlayer* pPlayer ) +{ + return pPlayer->GetKilledDefuser() ? 1 : 0; +} + +int FFEVAL_KILLED_RESCUER( CCSPlayer* pPlayer ) +{ + return pPlayer->GetKilledRescuer() ? 1 : 0; +} + +int FFEVAL_KILLS_WITH_GRENADE( CCSPlayer* pPlayer ) +{ + return pPlayer->GetMaxGrenadeKills(); +} + +int FFEVAL_DAMAGE_NO_KILLS( CCSPlayer* pPlayer ) +{ + if (CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_KILLS] == 0) + return CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_DAMAGE]; + else + return 0; +} + +int FFEVAL_FIRST_KILL( CCSPlayer* pPlayer ) +{ + if ( pPlayer == CSGameRules()->m_pFirstKill && CSGameRules()->m_firstKillTime < FIRST_KILL_TIME ) + return CSGameRules()->m_firstKillTime; + else + return 0; +} + +int FFEVAL_FIRST_BLOOD( CCSPlayer* pPlayer ) +{ + if ( pPlayer == CSGameRules()->m_pFirstBlood && CSGameRules()->m_firstBloodTime < FIRST_BLOOD_TIME ) + return CSGameRules()->m_firstBloodTime; + else + return 0; +} + +bool FFEVAL_SHORT_ROUND( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + if ( CSGameRules()->GetRoundLength() - CSGameRules()->GetRoundRemainingTime() < SHORT_ROUND_TIME ) + { + data1 = CSGameRules()->GetRoundLength() - CSGameRules()->GetRoundRemainingTime(); + return true; + } + return false; +} + +int FFEVAL_ACCURACY( CCSPlayer* pPlayer ) +{ + float shots = CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_FIRED]; + float hits = CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_HIT]; + if (shots >= MIN_SHOTS_FOR_ACCURACY) + return RoundFloatToInt(100.0f * hits / shots); + return 0; +} + +int FFEVAL_KILLED_HALF_OF_ENEMIES( CCSPlayer* pPlayer ) +{ + return pPlayer->GetPercentageOfEnemyTeamKilled(); +} + +bool FFEVAL_WON_AS_LAST_MEMBER( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + CCSPlayer *pCSPlayer = NULL; + int winningTeam = CSGameRules()->m_iRoundWinStatus; + + if (winningTeam != TEAM_TERRORIST && winningTeam != TEAM_CT) + { + return false; + } + + int losingTeam = (winningTeam == TEAM_TERRORIST) ? TEAM_CT : TEAM_TERRORIST; + + CCSGameRules::TeamPlayerCounts playerCounts[TEAM_MAXCOUNT]; + CSGameRules()->GetPlayerCounts(playerCounts); + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + pCSPlayer = ToCSPlayer(UTIL_PlayerByIndex( i ) ); + if( pCSPlayer && pCSPlayer->GetTeamNumber() == winningTeam && pCSPlayer->IsAlive()) + { + //Check if the player is still the only living member of his team ( on the off chance that a player joins late) + //This check is a little hacky. We make sure that there are no enemies alive. Since the bomb causes the round to end before exploding, + //the only way for only 1 person to be alive at round win time is extermination or defuse (in both cases, the last living player caused the win) + if (playerCounts[winningTeam].totalAlivePlayers == 1 && playerCounts[losingTeam].totalAlivePlayers == 0) + { + const PlayerStats_t& playerStats = CCS_GameStats.FindPlayerStats( pCSPlayer ); + iPlayer = i; + data1 = playerStats.statsCurrentRound[CSSTAT_KILLS_WHILE_LAST_PLAYER_ALIVE]; + if (data1 >= 2) + { + return true; + } + } + } + } + + return false; +} + +int FFEVAL_KNIFE_IN_GUNFIGHT( CCSPlayer* pPlayer ) +{ + return pPlayer->WasWieldingKnifeAndKilledByGun() ? 1 : 0; +} + +int FFEVAL_MULTIPLE_ATTACKER_COUNT( CCSPlayer* pPlayer ) +{ + return pPlayer->GetNumEnemyDamagers(); +} + +int FFEVAL_USED_ALL_AMMO( CCSPlayer* pPlayer ) +{ + CWeaponCSBase *pRifleWeapon = dynamic_cast< CWeaponCSBase * >(pPlayer->Weapon_GetSlot( WEAPON_SLOT_RIFLE )); + CWeaponCSBase *pHandgunWeapon = dynamic_cast< CWeaponCSBase * >(pPlayer->Weapon_GetSlot( WEAPON_SLOT_PISTOL )); + if ( pRifleWeapon && !pRifleWeapon->HasAmmo() && pHandgunWeapon && !pHandgunWeapon->HasAmmo() ) + return 1; + else + return 0; +} + +int FFEVAL_DAMAGE_MULTIPLE_ENEMIES( CCSPlayer* pPlayer ) +{ + return pPlayer->GetNumEnemiesDamaged(); +} + +int FFEVAL_USED_MULTIPLE_WEAPONS( CCSPlayer* pPlayer ) +{ + return pPlayer->GetNumFirearmsUsed(); +} + +int FFEVAL_DEFUSED_WITH_DROPPED_KIT( CCSPlayer* pPlayer ) +{ + return pPlayer->GetDefusedWithPickedUpKit() ? 1 : 0; +} + +bool FFEVAL_TERRORIST_ACCURACY( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + float terroristAccuracy = GetTeamAccuracy(TEAM_TERRORIST); + float ctAccuracy = GetTeamAccuracy(TEAM_CT); + + if (terroristAccuracy > 0.2f && terroristAccuracy > ctAccuracy) + { + data1 = RoundFloatToInt(terroristAccuracy * 100.0f); + return true; + } + return false; +} + +bool FFEVAL_CT_ACCURACY( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + float terroristAccuracy = GetTeamAccuracy(TEAM_TERRORIST); + float ctAccuracy = GetTeamAccuracy(TEAM_CT); + + if (ctAccuracy > 0.2f && ctAccuracy > terroristAccuracy) + { + data1 = RoundFloatToInt(ctAccuracy * 100.0f); + return true; + } + return false; +} + +bool FFEVAL_SAME_UNIFORM( int iTeam, int &iData1, int &iData2, int &iData3 ) +{ + int numberInUniform = 0; + int iUniform = -1; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CCSPlayer *pCSPlayer = ToCSPlayer(UTIL_PlayerByIndex( i ) ); + if ( pCSPlayer && pCSPlayer->GetTeamNumber() == iTeam && pCSPlayer->State_Get() != STATE_PICKINGCLASS) + { + if (iUniform == -1) + { + iUniform = pCSPlayer->PlayerClass(); + } + else if (pCSPlayer->PlayerClass() != iUniform) + { + return false; + } + ++numberInUniform; + } + } + + return numberInUniform >= 3; +} + +bool FFEVAL_BEST_TERRORIST_ACCURACY( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + float fAccuracy = 0.0f, fBestAccuracy = 0.0f; + CBasePlayer *pPlayer = NULL; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + pPlayer = UTIL_PlayerByIndex( i ); + + // Look only at terrorist players + if ( pPlayer && pPlayer->GetTeamNumber() == TEAM_TERRORIST ) + { + // Calculate accuracy the terrorist + float shots = CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_FIRED]; + float hits = CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_HIT]; + if (shots > MIN_SHOTS_FOR_ACCURACY) + { + fAccuracy = (float)hits / shots; + } + + // Track the most accurate terrorist + if ( fAccuracy > fBestAccuracy ) + { + fBestAccuracy = fAccuracy; + iPlayer = i; + } + } + } + + if ( fBestAccuracy - GetTeamAccuracy( TEAM_TERRORIST ) >= 0.10f ) + { + data1 = RoundFloatToInt(fBestAccuracy * 100.0f); + data2 = RoundFloatToInt(GetTeamAccuracy( TEAM_TERRORIST ) * 100.0f); + return true; + } + + return false; +} + +bool FFEVAL_BEST_COUNTERTERRORIST_ACCURACY( int &iPlayer, int &data1, int &data2, int &data3 ) +{ + float fAccuracy = 0.0f, fBestAccuracy = 0.0f; + CBasePlayer *pPlayer = NULL; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + pPlayer = UTIL_PlayerByIndex( i ); + + // Look only at counter-terrorist players + if ( pPlayer && pPlayer->GetTeamNumber() == TEAM_CT ) + { + // Calculate accuracy the counter-terrorist + float shots = CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_FIRED]; + float hits = CCS_GameStats.FindPlayerStats(pPlayer).statsCurrentRound[CSSTAT_SHOTS_HIT]; + if (shots > MIN_SHOTS_FOR_ACCURACY) + { + fAccuracy = (float)hits / shots; + } + + // Track the most accurate counter-terrorist + if ( fAccuracy > fBestAccuracy ) + { + fBestAccuracy = fAccuracy; + iPlayer = i; + } + } + } + + if ( fBestAccuracy - GetTeamAccuracy( TEAM_CT ) >= 0.10f ) + { + data1 = RoundFloatToInt(fBestAccuracy * 100.0f); + data2 = RoundFloatToInt(GetTeamAccuracy( TEAM_CT ) * 100.0f); + return true; + } + + return false; +} + + +//============================================================================= +// Fun Fact Declarations +//============================================================================= + +DECLARE_FUNFACT_STATBEST( FUNFACT_DAMAGE_WITH_GRENADES, "#funfact_damage_with_grenade", 0.5f, CSSTAT_GRENADE_DAMAGE, 200, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_KNIFE_KILLS, "#funfact_knife_kills", 0.5f, CSSTAT_KILLS_KNIFE, 1, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_KILLS_WITH_GRENADES, "#funfact_kills_grenades", 0.7f, CSSTAT_KILLS_HEGRENADE, 2, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_BLIND_KILLS, "#funfact_blind_kills", 0.9f, CSSTAT_KILLS_WHILE_BLINDED, 1, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_KILLED_ENEMIES, "#funfact_killed_enemies", 0.6f, CSSTAT_KILLS, 3, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_KILLS_WITH_LAST_ROUND, "#funfact_kills_with_last_round", 0.6f, CSSTAT_KILLS_WITH_LAST_ROUND, 1, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_DONATED_WEAPONS, "#funfact_donated_weapons", 0.3f, CSSTAT_WEAPONS_DONATED, 2, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_NUM_TIMES_JUMPED, "#funfact_num_times_jumped", 0.2f, CSSTAT_TOTAL_JUMPS, 10, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_FALL_DAMAGE, "#funfact_fall_damage", 0.2f, CSSTAT_FALL_DAMAGE, 50, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_POSTHUMOUS_KILLS_WITH_GRENADE, "#funfact_posthumous_kills_with_grenade", 1.0f, CSSTAT_GRENADE_POSTHUMOUSKILLS, 1, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_ITEMS_PURCHASED, "#funfact_items_purchased", 0.2f, CSSTAT_ITEMS_PURCHASED, 5, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_NUMBER_OF_OVERKILLS, "#funfact_number_of_overkills", 0.5f, CSSTAT_DOMINATION_OVERKILLS, 2, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_MONEY_SPENT, "#funfact_money_spent", 0.2f, CSSTAT_MONEY_SPENT, 5000, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_GRENADES_THROWN, "#funfact_grenades_thrown", 0.3f, CSSTAT_GRENADES_THROWN, 2, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_DEFENDED_BOMB, "#funfact_defended_bomb", 0.5f, CSSTAT_KILLS_WHILE_DEFENDING_BOMB, 2, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_ITEMS_DROPPED_VALUE, "#funfact_items_dropped_value", 0.5f, CSTAT_ITEMS_DROPPED_VALUE, 10000, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_KILL_WOUNDED_ENEMIES, "#funfact_kill_wounded_enemies", 0.4f, CSSTAT_KILLS_ENEMY_WOUNDED, 3, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_KILLS_HEADSHOTS, "#funfact_kills_headshots", 0.7f, CSSTAT_KILLS_HEADSHOT, 3, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_BROKE_WINDOWS, "#funfact_broke_windows", 0.3f, CSSTAT_NUM_BROKEN_WINDOWS, 5, EvalFlags::HighestOnly); +DECLARE_FUNFACT_STATBEST( FUNFACT_NIGHTVISION_DAMAGE, "#funfact_nightvision_damage", 0.5f, CSSTAT_NIGHTVISION_DAMAGE, 100, EvalFlags::HighestOnly); + +DECLARE_FUNFACT_STATSUM( FUNFACT_SHOTS_FIRED, "#funfact_shots_fired", 0.1f, CSSTAT_SHOTS_FIRED, 200, EvalFlags::All); + +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_KILL_DEFUSER, "#funfact_kill_defuser", 0.6f, FFEVAL_KILLED_DEFUSER, 1, EvalFlags::TeamTerrorist | EvalFlags::WinningTeam); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_KILL_RESCUER, "#funfact_kill_rescuer", 0.6f, FFEVAL_KILLED_RESCUER, 1, EvalFlags::TeamTerrorist); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_KILLS_WITH_SINGLE_GRENADE, "#funfact_kills_with_single_grenade", 0.8f, FFEVAL_KILLS_WITH_GRENADE, 2, EvalFlags::HighestOnly); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_DAMAGE_NO_KILLS, "#funfact_damage_no_kills", 0.4f, FFEVAL_DAMAGE_NO_KILLS, 200, EvalFlags::HighestOnly); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_FIRST_KILL, "#funfact_first_kill", 0.2f, FFEVAL_FIRST_KILL, 1, EvalFlags::HighestOnly); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_FIRST_BLOOD, "#funfact_first_blood", 0.2f, FFEVAL_FIRST_BLOOD, 1, EvalFlags::HighestOnly); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_BEST_ACCURACY, "#funfact_best_accuracy", 0.4f, FFEVAL_ACCURACY, 20, EvalFlags::HighestOnly); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_KNIFE_IN_GUNFIGHT, "#funfact_knife_in_gunfight", 0.6f, FFEVAL_KNIFE_IN_GUNFIGHT , 1, EvalFlags::All); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_SURVIVED_MULTIPLE_ATTACKERS, "#funfact_survived_multiple_attackers", 0.3f, FFEVAL_MULTIPLE_ATTACKER_COUNT, 3, EvalFlags::HighestOnly | EvalFlags::Alive); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_DIED_FROM_MULTIPLE_ATTACKERS, "#funfact_died_from_multiple_attackers", 0.5f, FFEVAL_MULTIPLE_ATTACKER_COUNT, 3, EvalFlags::HighestOnly | EvalFlags::Dead); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_USED_ALL_AMMO, "#funfact_used_all_ammo", 0.5f, FFEVAL_USED_ALL_AMMO, 1, EvalFlags::All ); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_DAMAGE_MULTIPLE_ENEMIES, "#funfact_damage_multiple_enemies", 0.5f, FFEVAL_DAMAGE_MULTIPLE_ENEMIES, 3, EvalFlags::HighestOnly); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_USED_MULTIPLE_WEAPONS, "#funfact_used_multiple_weapons", 0.5f, FFEVAL_USED_MULTIPLE_WEAPONS, 4, EvalFlags::HighestOnly); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_DEFUSED_WITH_DROPPED_KIT, "#funfact_defused_with_dropped_kit", 0.4f, FFEVAL_DEFUSED_WITH_DROPPED_KIT, 1, EvalFlags::TeamCT); +DECLARE_FUNFACT_PLAYERFUNC( FUNFACT_KILLED_HALF_OF_ENEMIES, "#funfact_killed_half_of_enemies", 0.5f, FFEVAL_KILLED_HALF_OF_ENEMIES, 50, EvalFlags::WinningTeam | EvalFlags::HighestOnly); + +DECLARE_FUNFACT_EVALFUNC( FUNFACT_CT_WIN_NO_KILLS, "#funfact_ct_win_no_kills", 0.4f, FFEVAL_CT_WIN_NO_KILLS); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_T_WIN_NO_KILLS, "#funfact_t_win_no_kills", 0.4f, FFEVAL_T_WIN_NO_KILLS ); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_T_WIN_NO_CASUALTIES, "#funfact_t_win_no_casualties", 0.2f, FFEVAL_T_WIN_NO_CASUALTIES ); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_CT_WIN_NO_CASUALTIES, "#funfact_ct_win_no_casualties", 0.2f, FFEVAL_CT_WIN_NO_CASUALTIES ); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_SHORT_ROUND, "#funfact_short_round", 0.3f, FFEVAL_SHORT_ROUND ); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_WON_AS_LAST_MEMBER, "#funfact_won_as_last_member", 0.6f, FFEVAL_WON_AS_LAST_MEMBER ); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_TERRORIST_ACCURACY, "#funfact_terrorist_accuracy", 0.2f, FFEVAL_TERRORIST_ACCURACY); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_CT_ACCURACY, "#funfact_ct_accuracy", 0.2f, FFEVAL_CT_ACCURACY); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_BEST_TERRORIST_ACCURACY, "#funfact_best_terrorist_accuracy", 0.3f, FFEVAL_BEST_TERRORIST_ACCURACY); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_BEST_COUNTERTERRORIST_ACCURACY, "#funfact_best_counterterrorist_accuracy", 0.3f, FFEVAL_BEST_COUNTERTERRORIST_ACCURACY); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_FALLBACK1, "#funfact_fallback1", 0.0f, FFEVAL_ALWAYS_TRUE); +DECLARE_FUNFACT_EVALFUNC( FUNFACT_FALLBACK2, "#funfact_fallback2", 0.0f, FFEVAL_ALWAYS_TRUE); + +DECLARE_FUNFACT_TEAMFUNC( FUNFACT_SAME_UNIFORM_TERRORIST, "#funfact_same_uniform_terrorist", 0.5f, FFEVAL_SAME_UNIFORM, TEAM_TERRORIST); +DECLARE_FUNFACT_TEAMFUNC( FUNFACT_SAME_UNIFORM_CT, "#funfact_same_uniform_ct", 0.5f, FFEVAL_SAME_UNIFORM, TEAM_CT); diff --git a/game/server/cstrike/funfact_cs.h b/game/server/cstrike/funfact_cs.h new file mode 100644 index 0000000..bed7bec --- /dev/null +++ b/game/server/cstrike/funfact_cs.h @@ -0,0 +1,71 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef INCLUDED_funfact_cs +#define INCLUDED_funfact_cs +#pragma once + +#include "cs_player.h" + +struct FunFact +{ + FunFact() : + id(-1), + szLocalizationToken(NULL), + iPlayer(0), + iData1(0), + iData2(0), + iData3(0), + fMagnitude(0.0f) + {} + int id; + const char* szLocalizationToken; + int iPlayer; + int iData1; + int iData2; + int iData3; + float fMagnitude; +}; + +typedef CUtlVector<FunFact> FunFactVector; + +class FunFactEvaluator +{ + DECLARE_CLASS_NOBASE( FunFactEvaluator ); +public: + FunFactEvaluator( int id, const char* szLocalizationToken, float fCoolness ) : + m_id(id), + m_pLocalizationToken(szLocalizationToken), + m_fCoolness(fCoolness) + {} + + virtual ~FunFactEvaluator() {} + + int GetId() const { return m_id; } + const char* GetLocalizationToken() const { return m_pLocalizationToken; } + float GetCoolness() const { return m_fCoolness; } + + virtual bool Evaluate( FunFactVector& results ) const = 0; + +private: + int m_id; + const char* m_pLocalizationToken; + float m_fCoolness; +}; + + +typedef FunFactEvaluator* (*funfactCreateFunc) (void); +class CFunFactHelper +{ +public: + CFunFactHelper ( funfactCreateFunc createFunc ) + { + m_pfnCreate = createFunc; + m_pNext = s_pFirst; + s_pFirst = this; + } + funfactCreateFunc m_pfnCreate; + CFunFactHelper *m_pNext; + static CFunFactHelper *s_pFirst; +}; + +#endif // INCLUDED_funfact_cs + diff --git a/game/server/cstrike/funfactmgr_cs.cpp b/game/server/cstrike/funfactmgr_cs.cpp new file mode 100644 index 0000000..4454e08 --- /dev/null +++ b/game/server/cstrike/funfactmgr_cs.cpp @@ -0,0 +1,204 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" +#include "usermessages.h" +#include "funfactmgr_cs.h" + +const float kCooldownRatePlayer = 0.4f; +const float kCooldownRateFunFact = 0.2f; + +const float kWeightPlayerCooldown = 0.8f; +const float kWeightFunFactCooldown = 1.0f; +const float kWeightCoolness = 2.0f; +const float kWeightRarity = 1.0f; + +#define DEBUG_FUNFACT_SCORING 0 + +//----------------------------------------------------------------------------- +// Purpose: constructor +//----------------------------------------------------------------------------- +CCSFunFactMgr::CCSFunFactMgr() : + CAutoGameSystemPerFrame( "CCSFunFactMgr" ), + m_funFactDatabase(0, 100, DefLessFunc(int) ) +{ + for ( int i = 0; i < MAX_PLAYERS; ++i ) + { + m_playerCooldown[i] = 0.0f; + } +} + +CCSFunFactMgr::~CCSFunFactMgr() +{ + Shutdown(); +} + +//----------------------------------------------------------------------------- +// Purpose: Initializes the fun fact manager +//----------------------------------------------------------------------------- +bool CCSFunFactMgr::Init() +{ + ListenForGameEvent( "player_connect" ); + + CFunFactHelper *pFunFactHelper = CFunFactHelper::s_pFirst; + + // create database of all fun fact evaluators (and initial usage metrics) + while ( pFunFactHelper ) + { + FunFactDatabaseEntry entry; + entry.fCooldown = 0.0f; + entry.iOccurrences = 0; + entry.pEvaluator = pFunFactHelper->m_pfnCreate(); + m_funFactDatabase.Insert(entry.pEvaluator->GetId(), entry); + + pFunFactHelper = pFunFactHelper->m_pNext; + } + + for (int i = 0; i < ARRAYSIZE(m_playerCooldown); ++i) + { + m_playerCooldown[i] = 0.0f; + } + + m_numRounds = 0; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Shuts down the fun fact manager +//----------------------------------------------------------------------------- +void CCSFunFactMgr::Shutdown() +{ + FOR_EACH_MAP( m_funFactDatabase, iter ) + { + delete m_funFactDatabase[iter].pEvaluator; + } + m_funFactDatabase.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: Per frame processing +//----------------------------------------------------------------------------- +void CCSFunFactMgr::Update( float frametime ) +{ + +} + +//----------------------------------------------------------------------------- +// Purpose: Listens for game events. Clears out map based stats and player based stats when necessary +//----------------------------------------------------------------------------- +void CCSFunFactMgr::FireGameEvent( IGameEvent *event ) +{ + const char *eventname = event->GetName(); + + if ( Q_strcmp( "player_connect", eventname ) == 0 ) + { + int index = event->GetInt("index");// player slot (entity index-1) + ASSERT( index >= 0 && index < MAX_PLAYERS ); + if( index >= 0 && index < MAX_PLAYERS ) + { + m_playerCooldown[index] = 0.0f; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the best fun fact to display and returns all necessary information through the parameters +//----------------------------------------------------------------------------- +bool CCSFunFactMgr::GetRoundEndFunFact( int iWinningTeam, int iRoundResult, FunFact& funfact ) +{ + FunFactVector validFunFacts; + + // Generate a vector of all valid fun facts for this round + FOR_EACH_MAP( m_funFactDatabase, i ) + { + FunFact funFact; + if ( m_funFactDatabase[i].pEvaluator->Evaluate(validFunFacts) ) + { + m_funFactDatabase[i].iOccurrences++; + } + } + + m_numRounds++; + + if (validFunFacts.Size() == 0) + return false; + + // pick the fun fact with the highest score + float fBestScore = -FLT_MAX; + int iFunFactIndex = -1; + +#if DEBUG_FUNFACT_SCORING + Msg("Scoring fun facts:\n"); +#endif + + FOR_EACH_VEC(validFunFacts, i) + { + float fScore = ScoreFunFact(validFunFacts[i]); + +#if DEBUG_FUNFACT_SCORING + char szPlayerName[64]; + const FunFact& funfact = validFunFacts[i]; + if (funfact.iPlayer > 0) + V_strncpy(szPlayerName, ToCSPlayer(UTIL_PlayerByIndex(funfact.iPlayer))->GetPlayerName(), sizeof(szPlayerName)); + else + V_strcpy(szPlayerName, ""); + + Msg("(%5.4f) %s, %s, %i, %i, %i\n", fScore, funfact.szLocalizationToken, szPlayerName, funfact.iData1, funfact.iData2, funfact.iData3); +#endif + + if (fScore > fBestScore) + { + fBestScore = fScore; + iFunFactIndex = i; + } + } + + if (iFunFactIndex < 0) + return false; + + funfact = validFunFacts[iFunFactIndex]; + + // decay player cooldowns + for (int i = 0; i < MAX_PLAYERS; ++i ) + { + m_playerCooldown[i] *= (1.0f - kCooldownRatePlayer); + } + + // decay funfact cooldowns + FOR_EACH_MAP(m_funFactDatabase, i) + { + m_funFactDatabase[i].fCooldown *= (1.0f - kCooldownRateFunFact); + } + + // set player cooldown for player in funfact + if ( funfact.iPlayer ) + { + m_playerCooldown[funfact.iPlayer - 1] = 1.0f; + } + + // set funfact cooldown for current funfact + m_funFactDatabase[m_funFactDatabase.Find(funfact.id)].fCooldown = 1.0f; + + return true; +} + +float CCSFunFactMgr::ScoreFunFact( const FunFact& funfact ) +{ + float fScore = 0.0f; + const FunFactDatabaseEntry& dbEntry = m_funFactDatabase[m_funFactDatabase.Find(funfact.id)]; + + // add the coolness score for the funfact + fScore += kWeightCoolness * dbEntry.pEvaluator->GetCoolness() * (1.0f + funfact.fMagnitude); + + // subtract the cooldown for the funfact + fScore -= kWeightFunFactCooldown * dbEntry.fCooldown; + + // subtract the cooldown for the player + if ( funfact.iPlayer ) { + fScore -= kWeightPlayerCooldown * m_playerCooldown[funfact.iPlayer - 1]; + } + + // add the rarity bonus + fScore += kWeightRarity * powf((1.0f - (float)dbEntry.iOccurrences / m_numRounds), 2.0f); + + return fScore; +} diff --git a/game/server/cstrike/funfactmgr_cs.h b/game/server/cstrike/funfactmgr_cs.h new file mode 100644 index 0000000..aac715c --- /dev/null +++ b/game/server/cstrike/funfactmgr_cs.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef FUNFACTMGR_H +#define FUNFACTMGR_H +#ifdef _WIN32 +#pragma once +#endif + +#include "GameEventListener.h" +#include "funfact_cs.h" +#include "utlmap.h" + +class FunFactEvaluator; + +class CCSFunFactMgr : public CAutoGameSystemPerFrame, public CGameEventListener +{ +public: + CCSFunFactMgr(); + ~CCSFunFactMgr(); + + virtual bool Init(); + virtual void Shutdown(); + virtual void Update( float frametime ); + + bool GetRoundEndFunFact( int iWinningTeam, int iRoundResult, FunFact& funfact ); + +protected: + float ScoreFunFact( const FunFact& funfact ); + void FireGameEvent( IGameEvent *event ); + +private: + + float m_playerCooldown[MAX_PLAYERS]; // Weights for all players. Updated every round + + struct FunFactDatabaseEntry + { + const FunFactEvaluator* pEvaluator; + int iOccurrences; + float fCooldown; + }; + CUtlMap<int, FunFactDatabaseEntry> m_funFactDatabase; + int m_numRounds; +}; + +#endif + diff --git a/game/server/cstrike/holiday_gift.cpp b/game/server/cstrike/holiday_gift.cpp new file mode 100644 index 0000000..afb2f0a --- /dev/null +++ b/game/server/cstrike/holiday_gift.cpp @@ -0,0 +1,128 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "smokegrenade_projectile.h" +#include "sendproxy.h" +#include "holiday_gift.h" +#include "cs_player.h" +#include "KeyValues.h" +#include "bot_manager.h" +#include "weapon_csbase.h" + +#define CHRISTMAS_MODEL "models/items/cs_gift.mdl" + +LINK_ENTITY_TO_CLASS( holiday_gift, CHolidayGift ); +PRECACHE_WEAPON_REGISTER( holiday_gift ); + +//----------------------------------------------------------------------------- +CHolidayGift* CHolidayGift::Create( const Vector &position, const QAngle &angles, const QAngle &eyeAngles, const Vector &velocity, CBaseCombatCharacter *pOwner ) +{ + CHolidayGift *pGift = (CHolidayGift*)CBaseEntity::Create( "holiday_gift", position, angles, pOwner ); + + if ( pGift ) + { + pGift->AddSpawnFlags( SF_NORESPAWN ); + + Vector vecRight, vecUp; + AngleVectors( eyeAngles, NULL, &vecRight, &vecUp ); + + // Calculate the initial impulse on the gift. + Vector vecImpulse( 0.0f, 0.0f, 0.0f ); + vecImpulse += vecUp * random->RandomFloat( 0, 0.25 ); + vecImpulse += vecRight * random->RandomFloat( -0.25, 0.25 ); + VectorNormalize( vecImpulse ); + vecImpulse *= random->RandomFloat( 100.0, 150.0 ); + vecImpulse += velocity; + + // Cap the impulse. + float flSpeed = vecImpulse.Length(); + if ( flSpeed > 300.0 ) + { + VectorScale( vecImpulse, 300.0 / flSpeed, vecImpulse ); + } + + pGift->SetMoveType( MOVETYPE_FLYGRAVITY ); + pGift->SetAbsVelocity( vecImpulse * 2.f + Vector(0,0,200) ); + pGift->SetAbsAngles( QAngle(0,0,0) ); + pGift->UseClientSideAnimation(); + pGift->ResetSequence( pGift->LookupSequence("idle") ); + + pGift->EmitSound( "Christmas.GiftDrop" ); + + pGift->ActivateWhenAtRest(); + } + + return pGift; +} + +//----------------------------------------------------------------------------- +void CHolidayGift::Precache() +{ + BaseClass::Precache(); + + PrecacheModel( CHRISTMAS_MODEL ); + PrecacheScriptSound( "Christmas.GiftDrop" ); + PrecacheScriptSound( "Christmas.GiftPickup" ); +} + +//----------------------------------------------------------------------------- +void CHolidayGift::Spawn( void ) +{ + BaseClass::Spawn(); + + SetModel( CHRISTMAS_MODEL ); + + // Die in 30 seconds + SetContextThink( &CBaseEntity::SUB_Remove, gpGlobals->curtime + 30, "DIE_THINK" ); + SetContextThink( &CHolidayGift::DropSoundThink, gpGlobals->curtime + 0.2f, "SOUND_THINK" ); +} + +//----------------------------------------------------------------------------- +void CHolidayGift::DropSoundThink( void ) +{ + EmitSound( "Christmas.GiftDrop" ); +} + +//----------------------------------------------------------------------------- +bool CHolidayGift::MyTouch( CBasePlayer *pPlayer ) +{ + if( !pPlayer ) + return false; + + if( !pPlayer->IsAlive() ) + return false; + + if ( pPlayer->IsBot() ) + return false; + + if ( ( pPlayer->GetTeamNumber() != TEAM_CT ) && ( pPlayer->GetTeamNumber() != TEAM_TERRORIST ) ) + return false; + + // Send a message for the achievement tracking. + IGameEvent *event = gameeventmanager->CreateEvent( "christmas_gift_grab" ); + if ( event ) + { + event->SetInt( "userid", pPlayer->GetUserID() ); + gameeventmanager->FireEvent( event ); + } + pPlayer->EmitSound( "Christmas.GiftPickup" ); + + return true; +} + +//----------------------------------------------------------------------------- +void CHolidayGift::ItemTouch( CBaseEntity *pOther ) +{ + if ( pOther->IsWorld() ) + { + Vector absVel = GetAbsVelocity(); + SetAbsVelocity( Vector( 0,0,absVel.z ) ); + return; + } + + BaseClass::ItemTouch( pOther ); +} diff --git a/game/server/cstrike/holiday_gift.h b/game/server/cstrike/holiday_gift.h new file mode 100644 index 0000000..f5987bd --- /dev/null +++ b/game/server/cstrike/holiday_gift.h @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef HOLIDAY_GIFT_H +#define HOLIDAY_GIFT_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "items.h" + + +class CHolidayGift: public CItem +{ +public: + DECLARE_CLASS( CHolidayGift, CItem ); + +public: + + virtual void Precache(); + virtual void Spawn( void ); + virtual bool MyTouch( CBasePlayer *pBasePlayer ); + virtual void ItemTouch( CBaseEntity *pOther ); + void DropSoundThink( void ); + +public: + + static CHolidayGift* Create( const Vector &position, const QAngle &angles, const QAngle &eyeAngles, const Vector &velocity, CBaseCombatCharacter *pOwner ); +}; + + +#endif // HOLIDAY_GIFT_H diff --git a/game/server/cstrike/hostage/cs_simple_hostage.cpp b/game/server/cstrike/hostage/cs_simple_hostage.cpp new file mode 100644 index 0000000..aacab39 --- /dev/null +++ b/game/server/cstrike/hostage/cs_simple_hostage.cpp @@ -0,0 +1,1345 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +// cs_simple_hostage.cpp +// Simple CS1.6 level hostage +// Author: Michael S. Booth and Matt Boone, July 2004 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_player.h" +#include "cs_gamerules.h" +#include "game.h" +#include "bot.h" +#include <KeyValues.h> +#include "obstacle_pushaway.h" +#include "props_shared.h" + +//============================================================================= +// HPE_BEGIN +//============================================================================= +// [dwenger] Necessary for stats tracking +#include "cs_gamestats.h" + +//[tj] Necessary for fast rescue achievement +#include "cs_achievement_constants.h" +//============================================================================= +// HPE_END +//============================================================================= + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define HOSTAGE_THINK_INTERVAL 0.1f + +#define DrawLine( from, to, duration, red, green, blue ) NDebugOverlay::Line( from, to, red, green, blue, true, 0.1f ) +#define HOSTAGE_PUSHAWAY_THINK_CONTEXT "HostagePushawayThink" + +#define HOSTAGE_BBOX_VEC_MIN Vector( -13, -13, 0 ) +#define HOSTAGE_BBOX_VEC_MAX Vector( 13, 13, 72 ) + + +ConVar mp_hostagepenalty( "mp_hostagepenalty", "13", FCVAR_NOTIFY, "Terrorist are kicked for killing too much hostages" ); +ConVar hostage_debug( "hostage_debug", "0", FCVAR_CHEAT, "Show hostage AI debug information" ); + +extern ConVar sv_pushaway_force; +extern ConVar sv_pushaway_min_player_speed; +extern ConVar sv_pushaway_max_force; + +// We need hostage-specific pushaway cvars because the hostage doesn't have the same friction etc as players +ConVar sv_pushaway_hostage_force( "sv_pushaway_hostage_force", "20000", FCVAR_REPLICATED | FCVAR_CHEAT, "How hard the hostage is pushed away from physics objects (falls off with inverse square of distance)." ); +ConVar sv_pushaway_max_hostage_force( "sv_pushaway_max_hostage_force", "1000", FCVAR_REPLICATED | FCVAR_CHEAT, "Maximum of how hard the hostage is pushed away from physics objects." ); + +const int NumHostageModels = 4; +static const char *HostageModel[NumHostageModels] = +{ + "models/Characters/Hostage_01.mdl", + "models/Characters/Hostage_02.mdl", + "models/Characters/hostage_03.mdl", + "models/Characters/hostage_04.mdl", +}; + +Vector DropToGround( CBaseEntity *pMainEnt, const Vector &vPos, const Vector &vMins, const Vector &vMaxs ); + +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( hostage_entity, CHostage ); + + +//----------------------------------------------------------------------------------------------------- +BEGIN_DATADESC( CHostage ) + + DEFINE_INPUTFUNC( FIELD_VOID, "OnRescueZoneTouch", HostageRescueZoneTouch ), + + DEFINE_USEFUNC( HostageUse ), + DEFINE_THINKFUNC( HostageThink ), + +END_DATADESC() + + +//----------------------------------------------------------------------------------------------------- +IMPLEMENT_SERVERCLASS_ST( CHostage, DT_CHostage ) + SendPropExclude( "DT_BaseAnimating", "m_flPoseParameter" ), + SendPropExclude( "DT_BaseAnimating", "m_flPlaybackRate" ), + SendPropExclude( "DT_BaseAnimating", "m_nSequence" ), + SendPropExclude( "DT_BaseAnimating", "m_nNewSequenceParity" ), + SendPropExclude( "DT_BaseAnimating", "m_nResetEventsParity" ), + SendPropExclude( "DT_BaseAnimatingOverlay", "overlay_vars" ), + + // cs_playeranimstate and clientside animation takes care of these on the client + SendPropExclude( "DT_ServerAnimationData" , "m_flCycle" ), + SendPropExclude( "DT_AnimTimeMustBeFirst" , "m_flAnimTime" ), + + SendPropBool( SENDINFO(m_isRescued) ), + SendPropInt( SENDINFO(m_iHealth), 10 ), + SendPropInt( SENDINFO(m_iMaxHealth), 10 ), + SendPropInt( SENDINFO(m_lifeState), 3, SPROP_UNSIGNED ), + + SendPropEHandle( SENDINFO(m_leader) ), + +END_SEND_TABLE() + + +//----------------------------------------------------------------------------------------------------- +CUtlVector< CHostage * > g_Hostages; +static CountdownTimer announceTimer; // used to stop "hostage rescued" announcements from stepping on each other + + +//----------------------------------------------------------------------------------------------------- +CHostage::CHostage() +{ + g_Hostages.AddToTail( this ); + m_PlayerAnimState = CreateHostageAnimState( this, this, LEGANIM_8WAY, false ); + UseClientSideAnimation(); + SetBloodColor( BLOOD_COLOR_RED ); +} + +//----------------------------------------------------------------------------------------------------- +CHostage::~CHostage() +{ + g_Hostages.FindAndRemove( this ); + m_PlayerAnimState->Release(); +} + +CWeaponCSBase* CHostage::CSAnim_GetActiveWeapon() +{ + return NULL; +} + +bool CHostage::CSAnim_CanMove() +{ + return true; +} + +//----------------------------------------------------------------------------------------------------- +void CHostage::Spawn( void ) +{ + Precache(); + + // round-robin through the hostage models + static int index = 0; + int whichModel = index % NumHostageModels; + ++index; + + SetModel( HostageModel[ whichModel ] ); + + + SetHullType( HULL_HUMAN ); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + SetCollisionGroup( COLLISION_GROUP_PLAYER ); + + SetGravity( 1.0 ); + + m_iHealth = 100; + m_iMaxHealth = m_iHealth; + m_takedamage = DAMAGE_YES; + + InitBoneControllers( ); + + // we must set this, because its zero by default thus putting their eyes in their feet + SetViewOffset( Vector( 0, 0, 60 ) ); + + + // set up think callback + SetNextThink( gpGlobals->curtime + HOSTAGE_THINK_INTERVAL ); + SetThink( &CHostage::HostageThink ); + + SetContextThink( &CHostage::PushawayThink, gpGlobals->curtime + PUSHAWAY_THINK_INTERVAL, HOSTAGE_PUSHAWAY_THINK_CONTEXT ); + + SetUse( &CHostage::HostageUse ); + + m_leader = NULL; + m_reuseTimer.Invalidate(); + m_hasBeenUsed = false; + + m_isRescued = false; + + m_vel = Vector( 0, 0, 0 ); + m_accel = Vector( 0, 0, 0 ); + + m_path.Invalidate(); + m_repathTimer.Invalidate(); + + m_pathFollower.Reset(); + m_pathFollower.SetPath( &m_path ); + m_pathFollower.SetImprov( this ); + + m_lastKnownArea = NULL; + + // Need to make sure the hostages are on the ground when they spawn + Vector GroundPos = DropToGround( this, GetAbsOrigin(), HOSTAGE_BBOX_VEC_MIN, HOSTAGE_BBOX_VEC_MAX ); + SetAbsOrigin( GroundPos ); + + if (TheNavMesh) + { + Vector pos = GetAbsOrigin(); + m_lastKnownArea = TheNavMesh->GetNearestNavArea( pos ); + } + + m_isCrouching = false; + m_isRunning = true; + m_jumpTimer.Invalidate(); + m_inhibitObstacleAvoidanceTimer.Invalidate(); + + m_isWaitingForLeader = false; + + m_isAdjusted = false; + + m_lastLeaderID = 0; + + announceTimer.Invalidate(); + m_disappearTime = 0.0f; +} + +//----------------------------------------------------------------------------------------------------- +void CHostage::Precache() +{ + for ( int i=0; i<NumHostageModels; ++i ) + { + PrecacheModel( HostageModel[i] ); + } + + PrecacheScriptSound( "Hostage.StartFollowCT" ); + PrecacheScriptSound( "Hostage.StopFollowCT" ); + PrecacheScriptSound( "Hostage.Pain" ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------------------------------- +int CHostage::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + float actualDamage = info.GetDamage(); + + // say something + EmitSound( "Hostage.Pain" ); + + CCSPlayer *player = ToCSPlayer( info.GetAttacker() ); + + if (player) + { + //============================================================================= + // HPE_BEGIN + // [dwenger] Track which player injured the hostage + //============================================================================= + + player->SetInjuredAHostage(true); + CSGameRules()->HostageInjured(); + + //============================================================================= + // HPE_END + //============================================================================= + + + if ( !( player->m_iDisplayHistoryBits & DHF_HOSTAGE_INJURED ) ) + { + player->HintMessage( "#Hint_careful_around_hostages", FALSE ); + player->m_iDisplayHistoryBits |= DHF_HOSTAGE_INJURED; + } + + IGameEvent *event = gameeventmanager->CreateEvent( "hostage_hurt" ); + if ( event ) + { + event->SetInt( "userid", player->GetUserID() ); + event->SetInt( "hostage", entindex() ); + event->SetInt( "priority", 5 ); + + gameeventmanager->FireEvent( event ); + } + + player->AddAccount( -((int)actualDamage * 20) ); + } + + return BaseClass::OnTakeDamage_Alive( info ); +} + +//----------------------------------------------------------------------------------------------------- +/** + * Modify damage the hostage takes by hitgroup + */ +float CHostage::GetModifiedDamage( float flDamage, int nHitGroup ) +{ + switch ( nHitGroup ) + { + case HITGROUP_GENERIC: flDamage *= 1.75; break; + case HITGROUP_HEAD: flDamage *= 2.5; break; + case HITGROUP_CHEST: flDamage *= 1.5; break; + case HITGROUP_STOMACH: flDamage *= 1.75; break; + case HITGROUP_LEFTARM: flDamage *= 0.75; break; + case HITGROUP_RIGHTARM: flDamage *= 0.75; break; + case HITGROUP_LEFTLEG: flDamage *= 0.6; break; + case HITGROUP_RIGHTLEG: flDamage *= 0.6; break; + default: flDamage *= 1.5; break; + } + + return flDamage; +} + +//----------------------------------------------------------------------------------------------------- +void CHostage::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo scaledInfo = info; + scaledInfo.SetDamage( GetModifiedDamage( info.GetDamage(), ptr->hitgroup ) ); + BaseClass::TraceAttack( scaledInfo, vecDir, ptr, pAccumulator ); +} + +//----------------------------------------------------------------------------------------------------- +/** + * Check for hostage-killer abuse + */ +void CHostage::CheckForHostageAbuse( CCSPlayer *player ) +{ + int hostageKillLimit = mp_hostagepenalty.GetInt(); + + if (hostageKillLimit > 0) + { + player->m_iHostagesKilled++; + + if ( player->m_iHostagesKilled == hostageKillLimit - 1 ) + { + player->HintMessage( "#Hint_removed_for_next_hostage_killed", TRUE ); + } + else if ( player->m_iHostagesKilled >= hostageKillLimit ) + { + Msg( "Kicking client \"%s\" for killing too many hostages\n", player->GetPlayerName() ); + engine->ServerCommand( UTIL_VarArgs( "kickid %d \"For killing too many hostages\"\n", player->GetUserID() ) ); + } + } +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Hostage was killed + */ +void CHostage::Event_Killed( const CTakeDamageInfo &info ) +{ + // tell the game logic that we've died + CSGameRules()->CheckWinConditions(); + + //============================================================================= + // HPE_BEGIN: + // [tj] Let the game know that a hostage has been killed + //============================================================================= + CSGameRules()->HostageKilled(); + //============================================================================= + // HPE_END + //============================================================================= + + CCSPlayer *attacker = ToCSPlayer( info.GetAttacker() ); + + if (attacker) + { + if ( !( attacker->m_iDisplayHistoryBits & DHF_HOSTAGE_KILLED ) ) + { + attacker->HintMessage( "#Hint_lost_money", FALSE ); + attacker->m_iDisplayHistoryBits |= DHF_HOSTAGE_KILLED; + } + + // monetary penalty for killing the hostage + attacker->AddAccount( -( 500 + ((int)info.GetDamage() * 20) ) ); + + // check for hostage-killer abuse + if (attacker->GetTeamNumber() == TEAM_TERRORIST) + { + CheckForHostageAbuse( attacker ); + } + } + + m_lastLeaderID = 0; + + SetUse( NULL ); + BaseClass::Event_Killed( info ); + + IGameEvent *event = gameeventmanager->CreateEvent("hostage_killed"); + if ( event ) + { + event->SetInt( "userid", (attacker)?attacker->GetUserID():0 ); + event->SetInt( "hostage", entindex() ); + event->SetInt( "priority", 6 ); + gameeventmanager->FireEvent( event ); + } +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Invoked when a Hostage touches a Rescue Zone + */ +void CHostage::HostageRescueZoneTouch( inputdata_t &inputdata ) +{ + if (!m_isRescued) + { + m_isRescued = true; + m_lastLeaderID = 0; + + SetSolid( SOLID_NONE ); + SetSolidFlags( 0 ); + + // start fading out + m_disappearTime = gpGlobals->curtime + 3.0f; + + SetUse( NULL ); + m_takedamage = DAMAGE_NO; + + // give rescuer a cash bonus + CCSPlayer *player = GetLeader(); + if (player) + { + const int rescuerCashBonus = 1000; + player->AddAccount( rescuerCashBonus ); + } + + Idle(); + + // tell the bots someone has rescued a hostage + IGameEvent *event = gameeventmanager->CreateEvent( "hostage_rescued" ); + if ( event ) + { + event->SetInt( "userid", player ? player->GetUserID() : (-1) ); + event->SetInt( "hostage", entindex() ); + event->SetInt( "site", 0 ); // TODO add site index + event->SetInt( "priority", 9 ); + gameeventmanager->FireEvent( event ); + } + + // update game rules + CSGameRules()->m_iHostagesRescued++; + + //============================================================================= + // HPE_BEGIN + // [dwenger] Hostage rescue achievement processing + //============================================================================= + + // Track last rescuer + if ( CSGameRules()->m_pLastRescuer == NULL ) + { + // No rescuer yet, so assign one & set rescuer count to 1 + CSGameRules()->m_pLastRescuer = player; + CSGameRules()->m_iNumRescuers = 1; + } + else + { + if ( CSGameRules()->m_pLastRescuer != player ) + { + // Rescuer changed + CSGameRules()->m_pLastRescuer = player; + CSGameRules()->m_iNumRescuers++; + } + } + + bool roundIsAlreadyOver = (CSGameRules()->m_iRoundWinStatus != WINNER_NONE); + + //============================================================================= + // HPE_END + //============================================================================= + + if (CSGameRules()->CheckWinConditions() == false) + { + // this hostage didn't win the round, so announce its rescue to everyone + if (announceTimer.IsElapsed()) + { + CSGameRules()->BroadcastSound( "Event.HostageRescued" ); + } + + // avoid having the announcer talk over himself + announceTimer.Start( 2.0f ); + } + //============================================================================= + // HPE_BEGIN + // [dwenger] Awarding of hostage rescue achievement + //============================================================================= + + else + { + //Check hostage rescue achievements + if ( CSGameRules()->m_iNumRescuers == 1 && !CSGameRules()->WasHostageKilled()) + { + //check for unrescued hostages + bool allHostagesRescued = true; + CHostage* hostage = NULL; + int iNumHostages = g_Hostages.Count(); + + for ( int i = 0 ; i < iNumHostages; i++ ) + { + hostage = g_Hostages[i]; + + if ( hostage->m_iHealth > 0 && !hostage->IsRescued() ) + { + allHostagesRescued = false; + break; + } + } + + if (allHostagesRescued) + { + player->AwardAchievement(CSRescueAllHostagesInARound); + + //[tj] fast version + if (gpGlobals->curtime - CSGameRules()->GetRoundStartTime() < AchievementConsts::FastHostageRescue_Time) + { + if ( player ) + { + player->AwardAchievement(CSFastHostageRescue); + } + } + } + } + //============================================================================= + // HPE_BEGIN: + // [menglish] If this rescue ended the round give an mvp to the rescuer + //============================================================================= + + if ( player && !roundIsAlreadyOver) + { + player->IncrementNumMVPs( CSMVP_HOSTAGERESCUE ); + } + + //============================================================================= + // HPE_END + //============================================================================= + } + + if ( player ) + { + CCS_GameStats.Event_HostageRescued( player ); + } + + //============================================================================= + // HPE_END + //============================================================================= + } +} + + +//----------------------------------------------------------------------------------------------------- +/** + * In contact with "other" + */ +void CHostage::Touch( CBaseEntity *other ) +{ + BaseClass::Touch( other ); + + // allow players and other hostages to push me around + if ( ( other->IsPlayer() && other->GetTeamNumber() == TEAM_CT ) || FClassnameIs( other, "hostage_entity" ) ) + { + // only push in 2D + Vector to = GetAbsOrigin() - other->GetAbsOrigin(); + to.z = 0.0f; + to.NormalizeInPlace(); + + const float pushForce = 500.0f; + ApplyForce( pushForce * to ); + } + else if ( m_inhibitDoorTimer.IsElapsed() && + ( other->ClassMatches( "func_door*" ) || other->ClassMatches( "prop_door*" ) ) ) + { + m_inhibitDoorTimer.Start( 3.0f ); + other->Use( this, this, USE_TOGGLE, 0.0f ); + } +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Hostage is stuck - attempt to wiggle out + */ +void CHostage::Wiggle( void ) +{ + if (m_wiggleTimer.IsElapsed()) + { + m_wiggleDirection = (NavRelativeDirType)RandomInt( 0, 3 ); + m_wiggleTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + } + + Vector dir, lat; + AngleVectors( GetAbsAngles(), &dir, &lat, NULL ); + + const float speed = 500.0f; + + switch( m_wiggleDirection ) + { + case LEFT: + ApplyForce( speed * lat ); + break; + + case RIGHT: + ApplyForce( -speed * lat ); + break; + + case FORWARD: + ApplyForce( speed * dir ); + break; + + case BACKWARD: + ApplyForce( -speed * dir ); + break; + } + + const float minStuckJumpTime = 0.25f; + if (m_pathFollower.GetStuckDuration() > minStuckJumpTime) + { + Jump(); + } +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Do following behavior + */ +void CHostage::UpdateFollowing( float deltaT ) +{ + if ( !IsFollowingSomeone() && m_lastLeaderID != 0 ) + { + // emit hostage_stops_following event + IGameEvent *event = gameeventmanager->CreateEvent( "hostage_stops_following" ); + if ( event ) + { + event->SetInt( "userid", m_lastLeaderID ); + event->SetInt( "hostage", entindex() ); + event->SetInt( "priority", 6 ); + gameeventmanager->FireEvent( event ); + } + + m_lastLeaderID = 0; + } + + // if we have a leader, follow him + CCSPlayer *leader = GetLeader(); + if (leader) + { + // if leader is dead, stop following him + if (!leader->IsAlive()) + { + Idle(); + return; + } + + // if leader has moved, repath + if (m_path.IsValid()) + { + Vector pathError = leader->GetAbsOrigin() - m_path.GetEndpoint(); + + const float repathRange = 100.0f; + if (pathError.IsLengthGreaterThan( repathRange )) + { + m_path.Invalidate(); + } + } + + + // build a path to our leader + if (!m_path.IsValid() && m_repathTimer.IsElapsed()) + { + const float repathInterval = 0.5f; + m_repathTimer.Start( repathInterval ); + + Vector from = GetAbsOrigin(); + Vector to = leader->GetAbsOrigin(); + HostagePathCost pathCost; + + m_path.Compute( from, to, pathCost ); + m_pathFollower.Reset(); + } + + + // if our rescuer is too far away, give up + const float giveUpRange = 2000.0f; + const float maxPathLength = 4000.0f; + Vector toLeader = leader->GetAbsOrigin() - GetAbsOrigin(); + if (toLeader.IsLengthGreaterThan( giveUpRange ) || (m_path.IsValid() && m_path.GetLength() > maxPathLength)) + { + if ( hostage_debug.GetInt() < 2 ) + { + Idle(); + } + return; + } + + + // don't crowd the leader + if (m_isWaitingForLeader) + { + // we are close to our leader and waiting for him to move + const float waitRange = 150.0f; + if (toLeader.IsLengthGreaterThan( waitRange )) + { + // leader has moved away - follow him + m_isWaitingForLeader = false; + } + + // face the leader + //FaceTowards( leader->GetAbsOrigin(), deltaT ); + } + else + { + // we are far from our leader, and need to check if we're close enough to wait + const float nearRange = 125.0f; + + if (toLeader.IsLengthLessThan( nearRange )) + { + // we are close to the leader - wait for him to move + m_isWaitingForLeader = true; + } + } + + if (!m_isWaitingForLeader) + { + // move along path towards the leader + m_pathFollower.Update( deltaT, m_inhibitObstacleAvoidanceTimer.IsElapsed() ); + + if (hostage_debug.GetBool()) + { + m_pathFollower.Debug( true ); + } + + if (m_pathFollower.IsStuck()) + { + Wiggle(); + } + + if (hostage_debug.GetBool()) + { + m_path.Draw(); + } + } + } +} + + + +//----------------------------------------------------------------------------------------------------- +void CHostage::AvoidPhysicsProps( void ) +{ + if ( m_lifeState == LIFE_DEAD ) + return; + + CBaseEntity *props[512]; + int nEnts = GetPushawayEnts( this, props, ARRAYSIZE( props ), 0.0f, PARTITION_ENGINE_SOLID_EDICTS ); + + for ( int i=0; i < nEnts; i++ ) + { + // Don't respond to this entity on the client unless it has PHYSICS_MULTIPLAYER_FULL set. + IMultiplayerPhysics *pInterface = dynamic_cast<IMultiplayerPhysics*>( props[i] ); + if ( pInterface && pInterface->GetMultiplayerPhysicsMode() != PHYSICS_MULTIPLAYER_SOLID ) + continue; + + const float minMass = 10.0f; // minimum mass that can push a player back + const float maxMass = 30.0f; // cap at a decently large value + float mass = maxMass; + if ( pInterface ) + { + mass = pInterface->GetMass(); + } + mass = MIN( mass, maxMass ); + mass -= minMass; + mass = MAX( mass, 0 ); + mass /= (maxMass-minMass); // bring into a 0..1 range + + // Push away from the collision point. The closer our center is to the collision point, + // the harder we push away. + Vector vPushAway = (WorldSpaceCenter() - props[i]->WorldSpaceCenter()); + float flDist = VectorNormalize( vPushAway ); + flDist = MAX( flDist, 1 ); + + float flForce = sv_pushaway_hostage_force.GetFloat() / flDist * mass; + flForce = MIN( flForce, sv_pushaway_max_hostage_force.GetFloat() ); + vPushAway *= flForce; + + ApplyForce( vPushAway ); + } + + // + // Handle step and ledge "step-up" movement here, before m_accel is zero'd + // + if ( !m_accel.IsZero() ) + { + trace_t trace; + Vector start = GetAbsOrigin(); + Vector forward = m_accel; + forward.NormalizeInPlace(); + UTIL_TraceEntity( this, start, start + forward, MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER, &trace ); + if ( !trace.startsolid && trace.fraction < 1.0f && trace.plane.normal.z < 0.7f ) + { + float groundFraction = trace.fraction; + start.z += StepHeight; + UTIL_TraceEntity( this, start, start + forward, MASK_PLAYERSOLID, this, COLLISION_GROUP_PLAYER, &trace ); + if ( !trace.startsolid && trace.fraction > groundFraction ) + { + SetAbsOrigin( start ); + } + } + } +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Push physics objects away from the hostage + */ +void CHostage::PushawayThink( void ) +{ + PerformObstaclePushaway( this ); + SetNextThink( gpGlobals->curtime + PUSHAWAY_THINK_INTERVAL, HOSTAGE_PUSHAWAY_THINK_CONTEXT ); +} + +//----------------------------------------------------------------------------------------------------- +/** + * @TODO imitate player movement: + * MoveHelperServer()->SetHost( this ); + * this->PlayerRunCommand( &cmd, MoveHelperServer() ); + */ +void CHostage::PhysicsSimulate( void ) +{ + BaseClass::PhysicsSimulate(); + + SetAbsVelocity( m_vel ); +} + +//----------------------------------------------------------------------------------------------------- +/** + * Update Hostage behaviors + */ +void CHostage::HostageThink( void ) +{ + if (!m_isAdjusted) + { + m_isAdjusted = true; + + // HACK - figure out why the default bbox is 6 units too low + SetCollisionBounds( HOSTAGE_BBOX_VEC_MIN, HOSTAGE_BBOX_VEC_MAX ); + } + + const float deltaT = HOSTAGE_THINK_INTERVAL; + SetNextThink( gpGlobals->curtime + deltaT ); + + // keep track of which Navigation Area we are in (or were in, if we're "off the mesh" right now) + CNavArea *area = TheNavMesh->GetNavArea( GetAbsOrigin() ); + if (area != NULL && area != m_lastKnownArea) + { + // entered a new nav area + m_lastKnownArea = area; + } + + // do leader-following behavior, if necessary + UpdateFollowing( deltaT ); + + AvoidPhysicsProps(); + + // update hostage velocity in the XY plane + const float damping = 2.0f; + m_vel += deltaT * (m_accel - damping * m_vel); + + // leave Z component untouched + m_vel.z = GetAbsVelocity().z; + + if ( m_accel.IsZero() && m_vel.AsVector2D().IsZero( 1.0f ) ) + { + m_vel.x = 0.0f; + m_vel.y = 0.0f; + } + + m_accel = Vector( 0, 0, 0 ); + + // set animation to idle for now + StudioFrameAdvance(); + + int sequence = SelectWeightedSequence( ACT_IDLE ); + if (GetSequence() != sequence) + { + SetSequence( sequence ); + } + + m_PlayerAnimState->Update( GetAbsAngles()[YAW], GetAbsAngles()[PITCH] ); + + + if ( m_disappearTime && m_disappearTime < gpGlobals->curtime ) + { + // finished fading - remove us completely + AddEffects( EF_NODRAW ); + + SetSolid( SOLID_NONE ); + SetSolidFlags( 0 ); + m_disappearTime = 0.0f; + } +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsFollowingSomeone( void ) +{ + return (m_leader.m_Value != NULL); +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsFollowing( const CBaseEntity *entity ) +{ + return (m_leader.m_Value == entity); +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsValid( void ) const +{ + return (m_iHealth > 0 && !IsRescued()); +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsRescuable( void ) const +{ + return (m_iHealth > 0 && !IsRescued()); +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsRescued( void ) const +{ + return m_isRescued; +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsOnGround( void ) const +{ + return (GetFlags() & FL_ONGROUND); +} + +//----------------------------------------------------------------------------------------------------- +/** + * Return true if hostage can see position + */ +bool CHostage::IsVisible( const Vector &pos, bool testFOV ) const +{ + trace_t result; + UTIL_TraceLine( EyePosition(), pos, CONTENTS_SOLID, this, COLLISION_GROUP_NONE, &result ); + return (result.fraction >= 1.0f); +} + +//----------------------------------------------------------------------------------------------------- +/** + * Give bonus to CT's for talking to a hostage + */ +void CHostage::GiveCTUseBonus( CCSPlayer *rescuer ) +{ + // money to team + const int teamBonus = 100; + CSGameRules()->m_iAccountCT += teamBonus; + + // money to rescuer + const int rescuerBonus = 150; + rescuer->AddAccount( rescuerBonus ); +} + +//----------------------------------------------------------------------------------------------------- +/** + * Stand idle + */ +void CHostage::Idle( void ) +{ + m_leader = NULL; +} + +//----------------------------------------------------------------------------------------------------- +/** + * Begin following "leader" + */ +void CHostage::Follow( CCSPlayer *leader ) +{ + //============================================================================= + // HPE_BEGIN + // [dwenger] Set variable to track whether player is currently rescuing hostages + //============================================================================= + + if ( leader ) + { + leader->IncrementNumFollowers(); + leader->SetIsRescuing(true); + } + + //============================================================================= + // HPE_END + //============================================================================= + + m_leader = leader; + m_isWaitingForLeader = false; + m_lastLeaderID = (leader) ? leader->GetUserID() : 0; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Return our leader, or NULL + */ +CCSPlayer *CHostage::GetLeader( void ) const +{ + return ToCSPlayer( m_leader.m_Value ); +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Invoked when a Hostage is "used" by a player + */ +void CHostage::HostageUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + Vector to = pActivator->GetAbsOrigin() - GetAbsOrigin(); + + // limit use range + float useRange = 1000.0f; + if (to.IsLengthGreaterThan( useRange )) + { + return; + } + + // TODO: check line of sight to hostage + + + CCSPlayer *user = ToCSPlayer( pActivator ); + if (user == NULL) + { + return; + } + + // only members of the CT team can use hostages (no T's or spectators) + if (!hostage_debug.GetBool() && user->GetTeamNumber() != TEAM_CT) + { + if ( user->GetTeamNumber() == TEAM_TERRORIST ) + { + if ( !(user->m_iDisplayHistoryBits & DHF_HOSTAGE_CTMOVE) ) + { + user->m_iDisplayHistoryBits |= DHF_HOSTAGE_CTMOVE; + user->HintMessage( "#Only_CT_Can_Move_Hostages", false, true ); + } + } + + return; + } + + CCSPlayer *leader = GetLeader(); + if( leader && !leader->IsAlive() ) + { + Idle(); + leader = NULL; + } + + // throttle how often leader can change + if (!m_reuseTimer.IsElapsed()) + { + return; + } + + // give money bonus to first CT touching this hostage + if (!m_hasBeenUsed) + { + m_hasBeenUsed = true; + + GiveCTUseBonus( user ); + + CSGameRules()->HostageTouched(); + } + + // if we are already following the player who used us, stop following + if (IsFollowing( user )) + { + Idle(); + + // say something + EmitSound( "Hostage.StopFollowCT" ); + } + else + { + // if we're already following a CT, ignore new uses + if (IsFollowingSomeone()) + { + return; + } + + // start following + Follow( user ); + + // say something + EmitSound( "Hostage.StartFollowCT" ); + + // emit hostage_follows event + IGameEvent *event = gameeventmanager->CreateEvent( "hostage_follows" ); + if ( event ) + { + event->SetInt( "userid", user->GetUserID() ); + event->SetInt( "hostage", entindex() ); + event->SetInt( "priority", 6 ); + gameeventmanager->FireEvent( event ); + } + + if ( !(user->m_iDisplayHistoryBits & DHF_HOSTAGE_USED) ) + { + user->m_iDisplayHistoryBits |= DHF_HOSTAGE_USED; + user->HintMessage( "#Hint_lead_hostage_to_rescue_point", false ); + } + } + + m_reuseTimer.Start( 1.0f ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Rotate body to face towards "target" + */ +void CHostage::FaceTowards( const Vector &target, float deltaT ) +{ + Vector to = target - GetFeet(); + to.z = 0.0f; + + QAngle desiredAngles; + VectorAngles( to, desiredAngles ); + + QAngle angles = GetAbsAngles(); + + const float turnSpeed = 250.0f; + angles.y = ApproachAngle( desiredAngles.y, angles.y, turnSpeed * deltaT ); + + SetAbsAngles( angles ); +} + +//----------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------------- + +const Vector &CHostage::GetCentroid( void ) const +{ + static Vector centroid; + + centroid = GetFeet(); + centroid.z += HalfHumanHeight; + + return centroid; +} + +//----------------------------------------------------------------------------------------------------- +/** + * Return position of "feet" - point below centroid of improv at feet level + */ +const Vector &CHostage::GetFeet( void ) const +{ + static Vector feet; + + feet = GetAbsOrigin(); + + return feet; +} + +//----------------------------------------------------------------------------------------------------- +const Vector &CHostage::GetEyes( void ) const +{ + static Vector eyes; + + eyes = EyePosition(); + + return eyes; +} + +//----------------------------------------------------------------------------------------------------- +/** + * Return direction of movement + */ +float CHostage::GetMoveAngle( void ) const +{ + return GetAbsAngles().y; +} + +//----------------------------------------------------------------------------------------------------- +CNavArea *CHostage::GetLastKnownArea( void ) const +{ + return m_lastKnownArea; +} + +//----------------------------------------------------------------------------------------------------- +/** + * Find "simple" ground height, treating current nav area as part of the floo + */ +bool CHostage::GetSimpleGroundHeightWithFloor( const Vector &pos, float *height, Vector *normal ) +{ + if (TheNavMesh->GetSimpleGroundHeight( pos, height, normal )) + { + // our current nav area also serves as a ground polygon + if (m_lastKnownArea && m_lastKnownArea->IsOverlapping( pos )) + { + *height = MAX( (*height), m_lastKnownArea->GetZ( pos ) ); + } + + return true; + } + + return false; +} + +//----------------------------------------------------------------------------------------------------- +void CHostage::Crouch( void ) +{ + m_isCrouching = true; +} + +//----------------------------------------------------------------------------------------------------- +/** + * un-crouch + */ +void CHostage::StandUp( void ) +{ + m_isCrouching = false; +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsCrouching( void ) const +{ + return m_isCrouching; +} + +//----------------------------------------------------------------------------------------------------- +/** + * Initiate a jump + */ +void CHostage::Jump( void ) +{ + if (m_jumpTimer.IsElapsed() && IsOnGround()) + { + const float minJumpInterval = 0.5f; + m_jumpTimer.Start( minJumpInterval ); + + Vector vel = GetAbsVelocity(); + vel.z += 200.0f; + SetAbsVelocity( vel ); + } +} + + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsJumping( void ) const +{ + return !m_jumpTimer.IsElapsed(); +} + +//----------------------------------------------------------------------------------------------------- +/** + * Set movement speed to running + */ +void CHostage::Run( void ) +{ + m_isRunning = true; +} + + +//----------------------------------------------------------------------------------------------------- +/** + * Set movement speed to walking + */ +void CHostage::Walk( void ) +{ + m_isRunning = false; +} + + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsRunning( void ) const +{ + return m_isRunning; +} + +//----------------------------------------------------------------------------------------------------- +/** + * Invoked when a ladder is encountered while following a path + */ +void CHostage::StartLadder( const CNavLadder *ladder, NavTraverseType how, const Vector &approachPos, const Vector &departPos ) +{ +} + +//----------------------------------------------------------------------------------------------------- +/** + * Traverse given ladder + */ +bool CHostage::TraverseLadder( const CNavLadder *ladder, NavTraverseType how, const Vector &approachPos, const Vector &departPos, float deltaT ) +{ + return false; +} + +//----------------------------------------------------------------------------------------------------- +bool CHostage::IsUsingLadder( void ) const +{ + return false; +} + +//----------------------------------------------------------------------------------------------------- +/** + * Move Hostage directly toward "pathGoal", causing Hostage to track the current path. + */ +void CHostage::TrackPath( const Vector &pathGoal, float deltaT ) +{ + // face in the direction of our motion + FaceTowards( GetAbsOrigin() + 10.0f * m_vel, deltaT ); + + if (GetFlags() & FL_ONGROUND) + { + // on the ground - move towards pathGoal + Vector to = pathGoal - GetFeet(); + to.z = 0.0f; + to.NormalizeInPlace(); + + const float speed = 1000.0f; + ApplyForce( speed * to ); + } + else + { + // in the air - continue forward motion + Vector to; + QAngle angles = GetAbsAngles(); + AngleVectors( angles, &to ); + + const float airSpeed = 350.0f; + ApplyForce( airSpeed * to ); + } +} + +//----------------------------------------------------------------------------------------------------- +/** + * Invoked when an improv reaches its MoveTo goal + */ +void CHostage::OnMoveToSuccess( const Vector &goal ) +{ +} + +//----------------------------------------------------------------------------------------------------- +/** + * Invoked when an improv fails to reach a MoveTo goal + */ +void CHostage::OnMoveToFailure( const Vector &goal, MoveToFailureType reason ) +{ +} + + +unsigned int CHostage::PhysicsSolidMaskForEntity() const +{ + return MASK_PLAYERSOLID; +} + diff --git a/game/server/cstrike/hostage/cs_simple_hostage.h b/game/server/cstrike/hostage/cs_simple_hostage.h new file mode 100644 index 0000000..cdc6544 --- /dev/null +++ b/game/server/cstrike/hostage/cs_simple_hostage.h @@ -0,0 +1,256 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// cs_simple_hostage.h +// Simple CS1.6 level hostage +// Author: Michael S. Booth, July 2004 + +#ifndef _CS_SIMPLE_HOSTAGE_H_ +#define _CS_SIMPLE_HOSTAGE_H_ + +#include "nav_mesh.h" +#include "cs_nav_path.h" +#include "cs_nav_pathfind.h" +#include "improv_locomotor.h" +#include "cs_playeranimstate.h" + +class CCSPlayer; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * A Counter-Strike Hostage + */ +class CHostage : public CBaseCombatCharacter, public CImprovLocomotor, public ICSPlayerAnimStateHelpers +{ +public: + DECLARE_CLASS( CHostage, CBaseCombatCharacter ); + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + + CHostage( void ); + virtual ~CHostage(); + + +public: + virtual void Spawn( void ); + virtual void Precache(); + int ObjectCaps( void ) { return (BaseClass::ObjectCaps() | FCAP_IMPULSE_USE); } // make hostage "useable" + + virtual void PhysicsSimulate( void ); + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void Touch( CBaseEntity *other ); // in contact with "other" + + void HostageRescueZoneTouch( inputdata_t &inputdata ); // invoked when hostage touches a rescue zone + + void HostageThink( void ); // periodic update to initiate behaviors + + void GiveCTUseBonus( CCSPlayer *rescuer ); // give bonus to CT's for talking to a hostage + void CheckForHostageAbuse( CCSPlayer *player ); // check for hostage-killer abuse + + // queries + bool IsFollowingSomeone( void ); + bool IsFollowing( const CBaseEntity *entity ); + bool IsValid( void ) const; + bool IsRescuable( void ) const; + bool IsRescued( void ) const; + bool IsOnGround( void ) const; + + bool IsVisible( const Vector &pos, bool testFOV = false ) const; // return true if hostage can see position + + // hostage states + void Idle( void ); // stand idle + void Follow( CCSPlayer *leader ); // begin following "leader" + CCSPlayer *GetLeader( void ) const; // return our leader, or NULL + + void FaceTowards( const Vector &target, float deltaT ); // rotate body to face towards "target" + void ApplyForce( const Vector &force ) { m_accel += force; } // apply a force to the hostage + + unsigned int PhysicsSolidMaskForEntity() const; + Class_T Classify ( void ) { return CLASS_PLAYER_ALLY; } + +public: + // begin CImprovLocomotor ----------------------------------------------------------------------------------------------------------------- + virtual const Vector &GetCentroid( void ) const; + virtual const Vector &GetFeet( void ) const; // return position of "feet" - point below centroid of improv at feet level + virtual const Vector &GetEyes( void ) const; + virtual float GetMoveAngle( void ) const; // return direction of movement + + virtual CNavArea *GetLastKnownArea( void ) const; + virtual bool GetSimpleGroundHeightWithFloor( const Vector &pos, float *height, Vector *normal = NULL ); // find "simple" ground height, treating current nav area as part of the floor + + virtual void Crouch( void ); + virtual void StandUp( void ); // "un-crouch" + virtual bool IsCrouching( void ) const; + + virtual void Jump( void ); // initiate a jump + virtual bool IsJumping( void ) const; + + virtual void Run( void ); // set movement speed to running + virtual void Walk( void ); // set movement speed to walking + virtual bool IsRunning( void ) const; + + virtual void StartLadder( const CNavLadder *ladder, NavTraverseType how, const Vector &approachPos, const Vector &departPos ); // 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 ); // traverse given ladder + virtual bool IsUsingLadder( void ) const; + + virtual void TrackPath( const Vector &pathGoal, float deltaT ); // 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 + // end CImprovLocomotor ------------------------------------------------------------------------------------------------------------------- + +// ICSPlayerAnimState overrides. +public: + virtual CWeaponCSBase* CSAnim_GetActiveWeapon(); + virtual bool CSAnim_CanMove(); + + + +protected: + virtual void HostageUse( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + +private: + float GetModifiedDamage( float flDamage, int nHitGroup ); + + ICSPlayerAnimState *m_PlayerAnimState; + + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_iMaxHealth ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_iHealth ); + IMPLEMENT_NETWORK_VAR_FOR_DERIVED( m_lifeState ); + CNetworkVar( bool, m_isRescued ); // true if the hostage has been rescued + + CNetworkVar( EHANDLE, m_leader ); // the player we are following + void UpdateFollowing( float deltaT ); // do following behavior + + int m_lastLeaderID; + + CountdownTimer m_reuseTimer; // to throttle how often hostage can be used + bool m_hasBeenUsed; // flag to give first rescuer bonus money + + Vector m_vel; + Vector m_accel; + + bool m_isRunning; // true if hostage move speed is to run (walk if false) + bool m_isCrouching; // true if hostage is crouching + CountdownTimer m_jumpTimer; // if zero, we can jump + + bool m_isWaitingForLeader; // true if we are waiting for our rescuer to move + + CCSNavPath m_path; // current path to follow + CountdownTimer m_repathTimer; // throttle pathfinder + + CountdownTimer m_inhibitDoorTimer; + + CNavPathFollower m_pathFollower; // path tracking mechanism + CountdownTimer m_inhibitObstacleAvoidanceTimer; // when active, turn off path following feelers + + CNavArea *m_lastKnownArea; // last area we were in + + void Wiggle( void ); // attempt to wiggle-out of begin stuck + CountdownTimer m_wiggleTimer; // for wiggling + NavRelativeDirType m_wiggleDirection; + + bool m_isAdjusted; // hack for adjusting bounding box + float m_disappearTime; // has finished fading, remove me + + void PushawayThink( void ); // pushes physics objects away from the hostage + void AvoidPhysicsProps( void ); // guides the hostage away from physics props +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor used with NavAreaBuildPath() for building Hostage paths. + * Once we hook up crouching and ladders, this can be removed and ShortestPathCost() can be used instead. + */ +class HostagePathCost +{ +public: + + // HPE_TODO[pmf]: check that these new parameters are okay to be ignored + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) + { + if (fromArea == NULL) + { + // first area in path, no cost + return 0.0f; + } + else + { + // if this area isn't available to hostages, skip it + if ( area->GetAttributes() & NAV_MESH_NO_HOSTAGES ) + { + return -1.0f; + } + + // compute distance travelled along path so far + float dist; + + if (ladder) + { + // can't traverse ladders + return -1.0f; + } + else + { + dist = (area->GetCenter() - fromArea->GetCenter()).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + if (area->GetAttributes() & NAV_MESH_CROUCH) // && !(area->GetAttributes() & NAV_MESH_JUMP)) + { + // can't traverse areas that require crouching + return -1.0f; + } + + // if this is a "jump" area, add penalty + if (area->GetAttributes() & NAV_MESH_JUMP) + { + const float jumpPenalty = 5.0f; + cost += jumpPenalty * dist; + } + + return cost; + } + } +}; + + +//-------------------------------------------------------------------------------------------------------------- +// All the hostage entities. +extern CUtlVector< CHostage * > g_Hostages; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Iterate over all active hostages in the game, invoking functor on each. + * If functor returns false, stop iteration and return false. + */ +template < typename Functor > +bool ForEachHostage( Functor &func ) +{ + for( int i=0; i<g_Hostages.Count(); ++i ) + { + CHostage *hostage = g_Hostages[i]; + + if ( hostage == NULL || !hostage->IsValid() ) + continue; + + if ( func( hostage ) == false ) + return false; + } + + return true; +} + + +#endif // _CS_SIMPLE_HOSTAGE_H_ diff --git a/game/server/cstrike/info_view_parameters.cpp b/game/server/cstrike/info_view_parameters.cpp new file mode 100644 index 0000000..d4579fe --- /dev/null +++ b/game/server/cstrike/info_view_parameters.cpp @@ -0,0 +1,18 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "info_view_parameters.h" + + +BEGIN_DATADESC( CInfoViewParameters ) + DEFINE_KEYFIELD( m_nViewMode, FIELD_INTEGER, "ViewMode" ) +END_DATADESC() + + +LINK_ENTITY_TO_CLASS( info_view_parameters, CInfoViewParameters ); + + diff --git a/game/server/cstrike/info_view_parameters.h b/game/server/cstrike/info_view_parameters.h new file mode 100644 index 0000000..0e1d38f --- /dev/null +++ b/game/server/cstrike/info_view_parameters.h @@ -0,0 +1,24 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef INFO_VIEW_PARAMETERS_H +#define INFO_VIEW_PARAMETERS_H +#ifdef _WIN32 +#pragma once +#endif + + +class CInfoViewParameters : public CBaseEntity +{ +public: + DECLARE_CLASS( CInfoViewParameters, CBaseEntity ); + DECLARE_DATADESC(); + + int m_nViewMode; +}; + + +#endif // INFO_VIEW_PARAMETERS_H diff --git a/game/server/cstrike/item_ammo.cpp b/game/server/cstrike/item_ammo.cpp new file mode 100644 index 0000000..7b09cc5 --- /dev/null +++ b/game/server/cstrike/item_ammo.cpp @@ -0,0 +1,147 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "items.h" +#include "cs_player.h" +#include "weapon_csbase.h" +#include "cs_ammodef.h" + + +//----------------------------------------------------------------------------- +class CItemAmmo : public CItem +{ +public: + DECLARE_CLASS( CItemAmmo, CItem ); + + bool MyTouch( CBasePlayer *pBasePlayer ) + { + CCSPlayer *pPlayer = dynamic_cast< CCSPlayer* >( pBasePlayer ); + if ( !pPlayer ) + { + Assert( false ); + return false; + } + + int ammoIndex = GetCSAmmoDef()->Index( GetAmmoName() ); + if ( ammoIndex < 0 ) + { + Assert( false ); + return false; + } + + pPlayer->GiveAmmo( GetCSAmmoDef()->GetBuySize( ammoIndex ), ammoIndex ); + + return true; + } + + virtual const char * GetAmmoName( void ) const { return NULL; } +}; + +//----------------------------------------------------------------------------- +class CItemAmmo50AE : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo50AE, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_50AE; } +}; + +LINK_ENTITY_TO_CLASS( ammo_50ae, CItemAmmo50AE ); + +//----------------------------------------------------------------------------- +class CItemAmmo762MM : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo762MM, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_762MM; } +}; + +LINK_ENTITY_TO_CLASS( ammo_762mm, CItemAmmo762MM ); + +//----------------------------------------------------------------------------- +class CItemAmmo556MM : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo556MM, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_556MM; } +}; + +LINK_ENTITY_TO_CLASS( ammo_556mm, CItemAmmo556MM ); + +//----------------------------------------------------------------------------- +class CItemAmmo556MM_BOX : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo556MM_BOX, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_556MM_BOX; } +}; + +LINK_ENTITY_TO_CLASS( ammo_556mm_box, CItemAmmo556MM_BOX ); + +//----------------------------------------------------------------------------- +class CItemAmmo338MAG : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo338MAG, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_338MAG; } +}; + +LINK_ENTITY_TO_CLASS( ammo_338mag, CItemAmmo338MAG ); + +//----------------------------------------------------------------------------- +class CItemAmmo9MM : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo9MM, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_9MM; } +}; + +LINK_ENTITY_TO_CLASS( ammo_9mm, CItemAmmo9MM ); + +//----------------------------------------------------------------------------- +class CItemAmmoBuckshot : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmoBuckshot, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_BUCKSHOT; } +}; + +LINK_ENTITY_TO_CLASS( ammo_buckshot, CItemAmmoBuckshot ); + +//----------------------------------------------------------------------------- +class CItemAmmo45ACP : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo45ACP, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_45ACP; } +}; + +LINK_ENTITY_TO_CLASS( ammo_45acp, CItemAmmo45ACP ); + +//----------------------------------------------------------------------------- +class CItemAmmo357SIG : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo357SIG, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_357SIG; } +}; + +LINK_ENTITY_TO_CLASS( ammo_357sig, CItemAmmo357SIG ); + +//----------------------------------------------------------------------------- +class CItemAmmo57MM : public CItemAmmo +{ +public: + DECLARE_CLASS( CItemAmmo57MM, CItemAmmo ); + virtual const char * GetAmmoName( void ) const { return BULLET_PLAYER_57MM; } +}; + +LINK_ENTITY_TO_CLASS( ammo_57mm, CItemAmmo57MM ); + +//----------------------------------------------------------------------------- + diff --git a/game/server/cstrike/item_assaultsuit.cpp b/game/server/cstrike/item_assaultsuit.cpp new file mode 100644 index 0000000..da36de5 --- /dev/null +++ b/game/server/cstrike/item_assaultsuit.cpp @@ -0,0 +1,56 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "items.h" +#include "cs_player.h" + + +class CItemAssaultSuit : public CItem +{ + void Spawn( void ) + { + Precache( ); + CItem::Spawn( ); + } + + void Precache( void ) + { + PrecacheScriptSound( "BaseCombatCharacter.ItemPickup2" ); + } + + bool MyTouch( CBasePlayer *pBasePlayer ) + { + CCSPlayer *pPlayer = dynamic_cast< CCSPlayer* >( pBasePlayer ); + if ( !pPlayer ) + { + Assert( false ); + return false; + } + + pPlayer->m_bHasHelmet = true; + pPlayer->SetArmorValue( 100 ); + + if ( pPlayer->IsDead() == false ) + { + CPASAttenuationFilter filter( pBasePlayer ); + EmitSound( filter, entindex(), "BaseCombatCharacter.ItemPickup2" ); + + CSingleUserRecipientFilter user( pPlayer ); + UserMessageBegin( user, "ItemPickup" ); + WRITE_STRING( "item_assaultsuit" ); + MessageEnd(); + } + + return true; + } +}; + +LINK_ENTITY_TO_CLASS( item_assaultsuit, CItemAssaultSuit ); + + diff --git a/game/server/cstrike/item_defuser.cpp b/game/server/cstrike/item_defuser.cpp new file mode 100644 index 0000000..b6f3343 --- /dev/null +++ b/game/server/cstrike/item_defuser.cpp @@ -0,0 +1,107 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Defuser kit that drops from counter-strike CTS +// +//=============================================================================// + +#include "cbase.h" +#include "items.h" +#include "cs_player.h" + +class CItemDefuser : public CItem +{ +public: + DECLARE_CLASS( CItemDefuser, CItem ); + + void Spawn( void ); + void Precache( void ); + void DefuserTouch( CBaseEntity *pOther ); + void ActivateThink( void ); + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( item_defuser, CItemDefuser ); +PRECACHE_REGISTER(item_defuser); + + +BEGIN_DATADESC( CItemDefuser ) + + //Functions + DEFINE_THINKFUNC( ActivateThink ), + DEFINE_ENTITYFUNC( DefuserTouch ), + +END_DATADESC() + + +void CItemDefuser::Spawn( void ) +{ + Precache( ); + SetModel( "models/weapons/w_defuser.mdl" ); + BaseClass::Spawn(); + + SetNextThink( gpGlobals->curtime + 0.5f ); + SetThink( &CItemDefuser::ActivateThink ); + + SetTouch( NULL ); +} + +void CItemDefuser::Precache( void ) +{ + PrecacheModel( "models/weapons/w_defuser.mdl" ); + + PrecacheScriptSound( "BaseCombatCharacter.ItemPickup2" ); +} + +void CItemDefuser::ActivateThink( void ) +{ + //since we can't stop the item from being touched while its in the air, + //activate 1 second after being dropped + + SetTouch( &CItemDefuser::DefuserTouch ); + SetThink( NULL ); +} + +void CItemDefuser::DefuserTouch( CBaseEntity *pOther ) +{ + if ( !pOther->IsPlayer() ) + { + return; + } + + //if( GetFlags() & FL_ONGROUND ) + { + CCSPlayer *pPlayer = (CCSPlayer *)pOther; + + if ( !pPlayer ) + { + Assert( false ); + return; + } + + if( pPlayer->GetTeamNumber() == TEAM_CT && !pPlayer->HasDefuser() ) + { + //============================================================================= + // HPE_BEGIN: + // [dwenger] Added for fun-fact support + //============================================================================= + + pPlayer->GiveDefuser( true ); + + //============================================================================= + // HPE_END + //============================================================================= + + if ( pPlayer->IsDead() == false ) + { + CPASAttenuationFilter filter( pPlayer ); + EmitSound( filter, entindex(), "BaseCombatCharacter.ItemPickup2" ); + } + + UTIL_Remove( this ); + return; + } + } +} + + diff --git a/game/server/cstrike/item_kevlar.cpp b/game/server/cstrike/item_kevlar.cpp new file mode 100644 index 0000000..bbb12e6 --- /dev/null +++ b/game/server/cstrike/item_kevlar.cpp @@ -0,0 +1,59 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "items.h" +#include "cs_player.h" + + +class CItemKevlar : public CItem +{ +public: + + DECLARE_CLASS( CItemKevlar, CItem ); + + void Spawn( void ) + { + Precache( ); + BaseClass::Spawn( ); + } + + void Precache( void ) + { + PrecacheScriptSound( "BaseCombatCharacter.ItemPickup2" ); + } + + bool MyTouch( CBasePlayer *pBasePlayer ) + { + CCSPlayer *pPlayer = dynamic_cast< CCSPlayer* >( pBasePlayer ); + if ( !pPlayer ) + { + Assert( false ); + return false; + } + + pPlayer->SetArmorValue( 100 ); + + if ( pPlayer->IsDead() == false ) + { + CPASAttenuationFilter filter( pBasePlayer ); + EmitSound( filter, entindex(), "BaseCombatCharacter.ItemPickup2" ); + + CSingleUserRecipientFilter user( pPlayer ); + UserMessageBegin( user, "ItemPickup" ); + WRITE_STRING( "item_kevlar" ); + MessageEnd(); + } + + return true; + } +}; + +LINK_ENTITY_TO_CLASS( item_kevlar, CItemKevlar ); + + diff --git a/game/server/cstrike/item_nvgs.cpp b/game/server/cstrike/item_nvgs.cpp new file mode 100644 index 0000000..b013280 --- /dev/null +++ b/game/server/cstrike/item_nvgs.cpp @@ -0,0 +1,59 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "cbase.h" +#include "items.h" +#include "cs_player.h" + + +class CItemNvgs : public CItem +{ +public: + + DECLARE_CLASS( CItemNvgs, CItem ); + + void Spawn( void ) + { + Precache( ); + BaseClass::Spawn( ); + } + + void Precache( void ) + { + PrecacheScriptSound( "BaseCombatCharacter.ItemPickup2" ); + } + + bool MyTouch( CBasePlayer *pBasePlayer ) + { + CCSPlayer *pPlayer = dynamic_cast< CCSPlayer* >( pBasePlayer ); + if ( !pPlayer ) + { + Assert( false ); + return false; + } + + pPlayer->m_bHasNightVision = true; + + if ( pPlayer->IsDead() == false ) + { + CPASAttenuationFilter filter( pPlayer ); + EmitSound( filter, entindex(), "BaseCombatCharacter.ItemPickup2" ); + + CSingleUserRecipientFilter user( pPlayer ); + UserMessageBegin( user, "ItemPickup" ); + WRITE_STRING( "item_nvgs" ); + MessageEnd(); + } + + return true; + } +}; + +LINK_ENTITY_TO_CLASS( item_nvgs, CItemNvgs ); + + diff --git a/game/server/cstrike/mapinfo.cpp b/game/server/cstrike/mapinfo.cpp new file mode 100644 index 0000000..bb2a20a --- /dev/null +++ b/game/server/cstrike/mapinfo.cpp @@ -0,0 +1,76 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "mapinfo.h" +#include "cs_gamerules.h" + +LINK_ENTITY_TO_CLASS( info_map_parameters, CMapInfo ); + +BEGIN_DATADESC( CMapInfo ) + + DEFINE_INPUTFUNC( FIELD_INTEGER, "FireWinCondition", InputFireWinCondition ), + +END_DATADESC() + +CMapInfo *g_pMapInfo = NULL; + + +CMapInfo::CMapInfo() +{ + m_flBombRadius = 500.0f; + m_iBuyingStatus = 0; + + if ( g_pMapInfo ) + { + // Should only be one of these. + Warning( "Warning: Multiple info_map_parameters entities in map!\n" ); + } + else + { + g_pMapInfo = this; + } +} + + +CMapInfo::~CMapInfo() +{ + if ( g_pMapInfo == this ) + g_pMapInfo = NULL; +} + + +bool CMapInfo::KeyValue( const char *szKeyName, const char *szValue ) +{ + if (FStrEq(szKeyName, "buying")) + { + m_iBuyingStatus = atoi(szValue); + return true; + } + else if (FStrEq(szKeyName, "bombradius")) + { + m_flBombRadius = (float)(atoi(szValue)); + if (m_flBombRadius > 2048) + m_flBombRadius = 2048; + + return true; + } + + return BaseClass::KeyValue( szKeyName, szValue ); +} + + +void CMapInfo::Spawn( void ) +{ + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_NONE ); + AddEffects( EF_NODRAW ); +} + +void CMapInfo::InputFireWinCondition(inputdata_t &inputdata ) +{ + CSGameRules()->TerminateRound( 5, inputdata.value.Int() ); +} diff --git a/game/server/cstrike/mapinfo.h b/game/server/cstrike/mapinfo.h new file mode 100644 index 0000000..bd3cb5d --- /dev/null +++ b/game/server/cstrike/mapinfo.h @@ -0,0 +1,42 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef MAPINFO_H +#define MAPINFO_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "baseentity.h" + + +class CMapInfo : public CPointEntity +{ +public : + + DECLARE_DATADESC(); + DECLARE_CLASS( CMapInfo, CPointEntity ); + + CMapInfo(); + virtual ~CMapInfo(); + + bool KeyValue( const char *szKeyName, const char *szValue ); + void Spawn(); + + void InputFireWinCondition( inputdata_t &inputdata ); + +public: + int m_iBuyingStatus; + float m_flBombRadius; +}; + + +// The info_map_parameters entity in this map (only one is allowed for). +extern CMapInfo *g_pMapInfo; + + +#endif // MAPINFO_H diff --git a/game/server/cstrike/point_surroundtest.cpp b/game/server/cstrike/point_surroundtest.cpp new file mode 100644 index 0000000..8703827 --- /dev/null +++ b/game/server/cstrike/point_surroundtest.cpp @@ -0,0 +1,71 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "mapinfo.h" + + +class CSurroundTest : public CPointEntity +{ +public: + DECLARE_CLASS( CSurroundTest, CPointEntity ); + + void FireCorrectOutput( inputdata_t &inputdata ); + void Spawn( void ); + +private: + + COutputEvent m_On2Speakers; + COutputEvent m_On4Speakers; + COutputEvent m_On51Speakers; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( point_surroundtest, CSurroundTest ); + +BEGIN_DATADESC( CSurroundTest ) + DEFINE_INPUTFUNC( FIELD_VOID, "FireCorrectOutput", FireCorrectOutput ), + DEFINE_OUTPUT( m_On2Speakers, "On2Speakers" ), + DEFINE_OUTPUT( m_On4Speakers, "On4Speakers" ), + DEFINE_OUTPUT( m_On51Speakers, "On51Speakers" ), +END_DATADESC() + +enum +{ + SND_SURROUND_HEADPHONES = 0, + SND_SURROUND_2SPEAKERS = 2, + SND_SURROUND_4SPEAKERS = 4, + SND_SURROUND_51SPEAKERS, +}; + +void CSurroundTest::FireCorrectOutput( inputdata_t &inputdata ) +{ + ConVar const *pSurroundCVar = cvar->FindVar( "snd_surround_speakers" ); + + if ( pSurroundCVar ) + { + int iSetting = pSurroundCVar->GetInt(); + + if ( iSetting == SND_SURROUND_HEADPHONES || iSetting == SND_SURROUND_2SPEAKERS ) + { + m_On2Speakers.FireOutput( this, this ); + } + else if ( iSetting == SND_SURROUND_4SPEAKERS ) + { + m_On4Speakers.FireOutput( this, this ); + } + else if ( iSetting == SND_SURROUND_51SPEAKERS ) + { + m_On51Speakers.FireOutput( this, this ); + } + } +} + +void CSurroundTest::Spawn( void ) +{ + BaseClass::Spawn(); +}
\ No newline at end of file diff --git a/game/server/cstrike/smokegrenade_projectile.cpp b/game/server/cstrike/smokegrenade_projectile.cpp new file mode 100644 index 0000000..82d8d7c --- /dev/null +++ b/game/server/cstrike/smokegrenade_projectile.cpp @@ -0,0 +1,189 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "smokegrenade_projectile.h" +#include "sendproxy.h" +#include "particle_smokegrenade.h" +#include "cs_player.h" +#include "KeyValues.h" +#include "bot_manager.h" +#include "weapon_csbase.h" + +#define GRENADE_MODEL "models/Weapons/w_eq_smokegrenade_thrown.mdl" + + +LINK_ENTITY_TO_CLASS( smokegrenade_projectile, CSmokeGrenadeProjectile ); +PRECACHE_WEAPON_REGISTER( smokegrenade_projectile ); + +BEGIN_DATADESC( CSmokeGrenadeProjectile ) + DEFINE_THINKFUNC( Think_Detonate ), + DEFINE_THINKFUNC( Think_Fade ), + DEFINE_THINKFUNC( Think_Remove ) +END_DATADESC() + + +CSmokeGrenadeProjectile* CSmokeGrenadeProjectile::Create( + const Vector &position, + const QAngle &angles, + const Vector &velocity, + const AngularImpulse &angVelocity, + CBaseCombatCharacter *pOwner ) +{ + CSmokeGrenadeProjectile *pGrenade = (CSmokeGrenadeProjectile*)CBaseEntity::Create( "smokegrenade_projectile", position, angles, pOwner ); + + // Set the timer for 1 second less than requested. We're going to issue a SOUND_DANGER + // one second before detonation. + pGrenade->SetTimer( 1.5 ); + pGrenade->SetAbsVelocity( velocity ); + pGrenade->SetupInitialTransmittedGrenadeVelocity( velocity ); + pGrenade->SetThrower( pOwner ); + pGrenade->SetGravity( 0.55 ); + pGrenade->SetFriction( 0.7 ); + pGrenade->m_flDamage = 100; + pGrenade->ChangeTeam( pOwner->GetTeamNumber() ); + pGrenade->ApplyLocalAngularVelocityImpulse( angVelocity ); + pGrenade->SetTouch( &CBaseGrenade::BounceTouch ); + + pGrenade->SetGravity( BaseClass::GetGrenadeGravity() ); + pGrenade->SetFriction( BaseClass::GetGrenadeFriction() ); + pGrenade->SetElasticity( BaseClass::GetGrenadeElasticity() ); + pGrenade->m_bDidSmokeEffect = false; + + pGrenade->m_pWeaponInfo = GetWeaponInfo( WEAPON_SMOKEGRENADE ); + + return pGrenade; +} + + +void CSmokeGrenadeProjectile::SetTimer( float timer ) +{ + SetThink( &CSmokeGrenadeProjectile::Think_Detonate ); + SetNextThink( gpGlobals->curtime + timer ); + + TheBots->SetGrenadeRadius( this, 0.0f ); +} + +void CSmokeGrenadeProjectile::Think_Detonate() +{ + if ( GetAbsVelocity().Length() > 0.1 ) + { + // Still moving. Don't detonate yet. + SetNextThink( gpGlobals->curtime + 0.2 ); + return; + } + + TheBots->SetGrenadeRadius( this, SmokeGrenadeRadius ); + + // Ok, we've stopped rolling or whatever. Now detonate. + ParticleSmokeGrenade *pGren = (ParticleSmokeGrenade*)CBaseEntity::Create( PARTICLESMOKEGRENADE_ENTITYNAME, GetAbsOrigin(), QAngle(0,0,0), NULL ); + if ( pGren ) + { + pGren->FillVolume(); + pGren->SetFadeTime( 15, 20 ); + pGren->SetAbsOrigin( GetAbsOrigin() ); + + //tell the hostages about the smoke! + CBaseEntity *pEntity = NULL; + variant_t var; //send the location of the smoke? + var.SetVector3D( GetAbsOrigin() ); + while ( ( pEntity = gEntList.FindEntityByClassname( pEntity, "hostage_entity" ) ) != NULL) + { + //send to hostages that have a resonable chance of being in it while its still smoking + if( (GetAbsOrigin() - pEntity->GetAbsOrigin()).Length() < 1000 ) + pEntity->AcceptInput( "smokegrenade", this, this, var, 0 ); + } + + // tell the bots a smoke grenade has exploded + CCSPlayer *player = ToCSPlayer(GetThrower()); + if ( player ) + { + IGameEvent * event = gameeventmanager->CreateEvent( "smokegrenade_detonate" ); + if ( event ) + { + event->SetInt( "userid", player->GetUserID() ); + event->SetFloat( "x", GetAbsOrigin().x ); + event->SetFloat( "y", GetAbsOrigin().y ); + event->SetFloat( "z", GetAbsOrigin().z ); + gameeventmanager->FireEvent( event ); + } + } + } + + m_hSmokeEffect = pGren; + m_bDidSmokeEffect = true; + + EmitSound( "BaseSmokeEffect.Sound" ); + + m_nRenderMode = kRenderTransColor; + SetNextThink( gpGlobals->curtime + 5 ); + SetThink( &CSmokeGrenadeProjectile::Think_Fade ); +} + + +// Fade the projectile out over time before making it disappear +void CSmokeGrenadeProjectile::Think_Fade() +{ + SetNextThink( gpGlobals->curtime ); + + color32 c = GetRenderColor(); + c.a -= 1; + SetRenderColor( c.r, c.b, c.g, c.a ); + + if ( !c.a ) + { + TheBots->RemoveGrenade( this ); + + SetModelName( NULL_STRING );//invisible + SetNextThink( gpGlobals->curtime + 20 ); + SetThink( &CSmokeGrenadeProjectile::Think_Remove ); // Spit out smoke for 10 seconds. + SetSolid( SOLID_NONE ); + } +} + + +void CSmokeGrenadeProjectile::Think_Remove() +{ + if ( m_hSmokeEffect.Get() ) + UTIL_Remove( m_hSmokeEffect ); + + TheBots->RemoveGrenade( this ); + + SetModelName( NULL_STRING );//invisible + SetSolid( SOLID_NONE ); + SetMoveType( MOVETYPE_NONE ); +} + +//Implement this so we never call the base class, +//but this should never be called either. +void CSmokeGrenadeProjectile::Detonate( void ) +{ + Assert(!"Smoke grenade handles its own detonation"); +} + + +void CSmokeGrenadeProjectile::Spawn() +{ + SetModel( GRENADE_MODEL ); + BaseClass::Spawn(); +} + + +void CSmokeGrenadeProjectile::Precache() +{ + PrecacheModel( GRENADE_MODEL ); + PrecacheScriptSound( "BaseSmokeEffect.Sound" ); + PrecacheScriptSound( "SmokeGrenade.Bounce" ); + BaseClass::Precache(); +} + +void CSmokeGrenadeProjectile::BounceSound( void ) +{ + if ( !m_bDidSmokeEffect ) + { + EmitSound( "SmokeGrenade.Bounce" ); + } +} diff --git a/game/server/cstrike/smokegrenade_projectile.h b/game/server/cstrike/smokegrenade_projectile.h new file mode 100644 index 0000000..ea6ab1c --- /dev/null +++ b/game/server/cstrike/smokegrenade_projectile.h @@ -0,0 +1,53 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef HEGRENADE_PROJECTILE_H +#define HEGRENADE_PROJECTILE_H +#ifdef _WIN32 +#pragma once +#endif + + +#include "basecsgrenade_projectile.h" + + +class CSmokeGrenadeProjectile : public CBaseCSGrenadeProjectile +{ +public: + DECLARE_CLASS( CSmokeGrenadeProjectile, CBaseCSGrenadeProjectile ); + DECLARE_DATADESC(); + +// Overrides. +public: + + virtual void Spawn(); + virtual void Precache(); + virtual void Detonate(); + virtual void BounceSound( void ); + + void Think_Detonate(); + void Think_Fade(); + void Think_Remove(); + + +// Grenade stuff. +public: + + static CSmokeGrenadeProjectile* Create( + const Vector &position, + const QAngle &angles, + const Vector &velocity, + const AngularImpulse &angVelocity, + CBaseCombatCharacter *pOwner ); + + void SetTimer( float timer ); + + EHANDLE m_hSmokeEffect; + bool m_bDidSmokeEffect; +}; + + +#endif // HEGRENADE_PROJECTILE_H diff --git a/game/server/cstrike/te_radioicon.cpp b/game/server/cstrike/te_radioicon.cpp new file mode 100644 index 0000000..771ce9f --- /dev/null +++ b/game/server/cstrike/te_radioicon.cpp @@ -0,0 +1,77 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" + +//----------------------------------------------------------------------------- +// Purpose: Dispatches blood stream tempentity +//----------------------------------------------------------------------------- +class CTERadioIcon : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTERadioIcon, CBaseTempEntity ); + + CTERadioIcon( const char *name ); + virtual ~CTERadioIcon( void ); + + void Precache( void ); + + DECLARE_SERVERCLASS(); + +public: + + CNetworkVar( int, m_iAttachToClient ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTERadioIcon::CTERadioIcon( const char *name ) : + CBaseTempEntity( name ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTERadioIcon::~CTERadioIcon( void ) +{ +} + +void CTERadioIcon::Precache( void ) +{ + CBaseEntity::PrecacheModel("sprites/radio.vmt"); +} + +IMPLEMENT_SERVERCLASS_ST(CTERadioIcon, DT_TERadioIcon) + SendPropInt( SENDINFO(m_iAttachToClient), 8, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton to fire StickyBolt objects +static CTERadioIcon g_TERadioIcon( "RadioIcon" ); + +//----------------------------------------------------------------------------- +// Purpose: +// Input : filter - +// delay - +// pPlayer - +//----------------------------------------------------------------------------- +void TE_RadioIcon( IRecipientFilter& filter, float delay, CBaseEntity *pPlayer ) +{ + g_TERadioIcon.m_iAttachToClient = pPlayer->entindex(); + + // Send it over the wire + g_TERadioIcon.Create( filter, delay ); +} diff --git a/game/server/cstrike/te_shotgun_shot.cpp b/game/server/cstrike/te_shotgun_shot.cpp new file mode 100644 index 0000000..d730f18 --- /dev/null +++ b/game/server/cstrike/te_shotgun_shot.cpp @@ -0,0 +1,161 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" +#include "basetempentity.h" +#include "fx_cs_shared.h" + + +#define NUM_BULLET_SEED_BITS 8 + + +//----------------------------------------------------------------------------- +// Purpose: Display's a blood sprite +//----------------------------------------------------------------------------- +class CTEFireBullets : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEFireBullets, CBaseTempEntity ); + DECLARE_SERVERCLASS(); + + CTEFireBullets( const char *name ); + virtual ~CTEFireBullets( void ); + +public: + CNetworkVar( int, m_iPlayer ); + CNetworkVector( m_vecOrigin ); + CNetworkQAngle( m_vecAngles ); + CNetworkVar( int, m_iWeaponID ); + CNetworkVar( int, m_iMode ); + CNetworkVar( int, m_iSeed ); + CNetworkVar( float, m_fInaccuracy ); + CNetworkVar( float, m_fSpread ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEFireBullets::CTEFireBullets( const char *name ) : + CBaseTempEntity( name ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEFireBullets::~CTEFireBullets( void ) +{ +} + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CTEFireBullets, DT_TEFireBullets) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD ), + SendPropAngle( SENDINFO_VECTORELEM( m_vecAngles, 0 ), 13, 0 ), + SendPropAngle( SENDINFO_VECTORELEM( m_vecAngles, 1 ), 13, 0 ), + SendPropInt( SENDINFO( m_iWeaponID ), 5, SPROP_UNSIGNED ), // max 31 weapons + SendPropInt( SENDINFO( m_iMode ), 1, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iSeed ), NUM_BULLET_SEED_BITS, SPROP_UNSIGNED ), + SendPropInt( SENDINFO( m_iPlayer ), 6, SPROP_UNSIGNED ), // max 64 players, see MAX_PLAYERS + SendPropFloat( SENDINFO( m_fInaccuracy ), 10, 0, 0, 1 ), + SendPropFloat( SENDINFO( m_fSpread ), 8, 0, 0, 0.1f ), +END_SEND_TABLE() + + +// Singleton +static CTEFireBullets g_TEFireBullets( "Shotgun Shot" ); + + +void TE_FireBullets( + int iPlayerIndex, + const Vector &vOrigin, + const QAngle &vAngles, + int iWeaponID, + int iMode, + int iSeed, + float fInaccuracy, + float fSpread + ) +{ + CPASFilter filter( vOrigin ); + filter.UsePredictionRules(); + + g_TEFireBullets.m_iPlayer = iPlayerIndex-1; + g_TEFireBullets.m_vecOrigin = vOrigin; + g_TEFireBullets.m_vecAngles = vAngles; + g_TEFireBullets.m_iSeed = iSeed; + g_TEFireBullets.m_fInaccuracy = fInaccuracy; + g_TEFireBullets.m_fSpread = fSpread; + g_TEFireBullets.m_iMode = iMode; + g_TEFireBullets.m_iWeaponID = iWeaponID; + + Assert( iSeed < (1 << NUM_BULLET_SEED_BITS) ); + + g_TEFireBullets.Create( filter, 0 ); +} + + + + +//----------------------------------------------------------------------------- +// Purpose: Displays a bomb plant animation +//----------------------------------------------------------------------------- +class CTEPlantBomb : public CBaseTempEntity +{ +public: + DECLARE_CLASS( CTEPlantBomb, CBaseTempEntity ); + DECLARE_SERVERCLASS(); + + CTEPlantBomb( const char *name ); + virtual ~CTEPlantBomb( void ); + +public: + CNetworkVar( int, m_iPlayer ); + CNetworkVector( m_vecOrigin ); + CNetworkVar( PlantBombOption_t, m_option ); +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +//----------------------------------------------------------------------------- +CTEPlantBomb::CTEPlantBomb( const char *name ) : + CBaseTempEntity( name ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTEPlantBomb::~CTEPlantBomb( void ) +{ +} + +IMPLEMENT_SERVERCLASS_ST_NOBASE(CTEPlantBomb, DT_TEPlantBomb) + SendPropVector( SENDINFO(m_vecOrigin), -1, SPROP_COORD ), + SendPropInt( SENDINFO( m_iPlayer ), 6, SPROP_UNSIGNED ), // max 64 players, see MAX_PLAYERS + SendPropInt( SENDINFO( m_option ), 1, SPROP_UNSIGNED ), +END_SEND_TABLE() + + +// Singleton +static CTEPlantBomb g_TEPlantBomb( "Bomb Plant" ); + + +void TE_PlantBomb( int iPlayerIndex, const Vector &vOrigin, PlantBombOption_t option ) +{ + CPASFilter filter( vOrigin ); + filter.UsePredictionRules(); + + g_TEPlantBomb.m_iPlayer = iPlayerIndex-1; + g_TEPlantBomb.m_option = option; + g_TEPlantBomb.Create( filter, 0 ); +} diff --git a/game/server/cstrike/te_shotgun_shot.h b/game/server/cstrike/te_shotgun_shot.h new file mode 100644 index 0000000..8d72e9c --- /dev/null +++ b/game/server/cstrike/te_shotgun_shot.h @@ -0,0 +1,28 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef TE_SHOTGUN_SHOT_H +#define TE_SHOTGUN_SHOT_H +#ifdef _WIN32 +#pragma once +#endif + + +void TE_FireBullets( + int iPlayerIndex, + const Vector &vOrigin, + const QAngle &vAngles, + int iWeaponID, + int iMode, + int iSeed, + float fInaccuracy, + float fSpread + ); + +void TE_PlantBomb( int iPlayerIndex, const Vector &vOrigin, PlantBombOption_t option ); + + +#endif // TE_SHOTGUN_SHOT_H |