diff options
Diffstat (limited to 'game/server/tf/nav_mesh')
| -rw-r--r-- | game/server/tf/nav_mesh/tf_nav_area.cpp | 537 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_nav_area.h | 299 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_nav_interface.cpp | 45 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_nav_mesh.cpp | 2450 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_nav_mesh.h | 235 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_nav_mesh_edit.cpp | 306 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_nav_mesh_edit.h | 11 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_path_follower.cpp | 158 | ||||
| -rw-r--r-- | game/server/tf/nav_mesh/tf_path_follower.h | 58 |
9 files changed, 4099 insertions, 0 deletions
diff --git a/game/server/tf/nav_mesh/tf_nav_area.cpp b/game/server/tf/nav_mesh/tf_nav_area.cpp new file mode 100644 index 0000000..2ca96a2 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_nav_area.cpp @@ -0,0 +1,537 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_nav_area.h +// TF specific nav area +// Michael Booth, February 2009 + +#include "cbase.h" +#include "tf_nav_mesh.h" +#include "tf_nav_area.h" +#include "tf_gamerules.h" +#include "bot/tf_bot.h" +#include "nav_pathfind.h" + +ConVar tf_nav_show_incursion_distance( "tf_nav_show_incursion_distance", "0", FCVAR_CHEAT, "Display travel distances from current spawn room (1=red, 2=blue)" ); +ConVar tf_nav_show_bomb_target_distance( "tf_nav_show_bomb_target_distance", "0", FCVAR_CHEAT, "Display travel distances to bomb target (MvM mode)" ); +ConVar tf_nav_show_turf_ownership( "tf_nav_show_turf_ownership", "0", FCVAR_CHEAT, "Color nav area by smallest incursion distance" ); + +ConVar tf_nav_in_combat_duration( "tf_nav_in_combat_duration", "30", FCVAR_CHEAT, "How long after gunfire occurs is this area still considered to be 'in combat'" ); + +ConVar tf_nav_combat_build_rate( "tf_nav_combat_build_rate", "0.05", FCVAR_CHEAT, "Gunfire/second increase (combat caps at 1.0)" ); +ConVar tf_nav_combat_decay_rate( "tf_nav_combat_decay_rate", "0.022", FCVAR_CHEAT, "Decay/second toward zero" ); + +ConVar tf_show_sniper_areas( "tf_show_sniper_areas", "0", FCVAR_CHEAT ); +ConVar tf_show_sniper_areas_safety_range( "tf_show_sniper_areas_safety_range", "1000", FCVAR_CHEAT ); + +ConVar tf_show_incursion_range( "tf_show_incursion_range", "0", FCVAR_CHEAT, "1 = red, 2 = blue" ); +ConVar tf_show_incursion_range_min( "tf_show_incursion_range_min", "0", FCVAR_CHEAT, "Highlight areas with incursion distances between min and max cvar values" ); +ConVar tf_show_incursion_range_max( "tf_show_incursion_range_max", "0", FCVAR_CHEAT, "Highlight areas with incursion distances between min and max cvar values" ); + + +//------------------------------------------------------------------------------------------------ +CTFNavArea::CTFNavArea( void ) +{ + m_attributeFlags = 0; + m_wanderCount = 0; + m_combatIntensity = 0.0f; + m_distanceToBombTarget = 0.0f; + m_TFMark = 0; + m_invasionSearchMarker = (unsigned int)-1; +} + + +//------------------------------------------------------------------------------------------------ +/** + * (EXTEND) invoked when map is initially loaded + */ +void CTFNavArea::OnServerActivate( void ) +{ + BaseClass::OnServerActivate(); + + ClearAllPotentiallyVisibleActors(); +} + +//------------------------------------------------------------------------------------------------ +/** + * (EXTEND) invoked for each area when the round restarts + */ +void CTFNavArea::OnRoundRestart( void ) +{ + BaseClass::OnRoundRestart(); + + ClearAllPotentiallyVisibleActors(); + + m_combatIntensity = 0.0f; +} + +//------------------------------------------------------------------------------------------------ +/** + * For game-specific analysis + */ +void CTFNavArea::CustomAnalysis( bool isIncremental ) +{ + +} + + +//------------------------------------------------------------------------------------------------ +/** + * Draw area for debugging & editing + */ +void CTFNavArea::Draw( void ) const +{ + CNavArea::Draw(); + +#ifdef TF_RAID_MODE + if ( TFGameRules()->IsRaidMode() && m_wanderCount > 0 ) + { + NDebugOverlay::Text( GetCenter(), UTIL_VarArgs( "%d", m_wanderCount ), false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } +#endif // TF_RAID_MODE + + if ( tf_nav_show_incursion_distance.GetBool() ) + { + NDebugOverlay::Text( GetCenter(), UTIL_VarArgs( "R:%3.1f B:%3.1f", GetIncursionDistance( TF_TEAM_RED ), GetIncursionDistance( TF_TEAM_BLUE ) ), false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + + if ( tf_nav_show_bomb_target_distance.GetBool() ) + { + NDebugOverlay::Text( GetCenter(), UTIL_VarArgs( "%3.1f", GetTravelDistanceToBombTarget() ), false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + + if ( tf_show_sniper_areas.GetBool() ) + { + bool redSniper = IsAwayFromInvasionAreas( TF_TEAM_RED, tf_show_sniper_areas_safety_range.GetFloat() ); + bool blueSniper = IsAwayFromInvasionAreas( TF_TEAM_BLUE, tf_show_sniper_areas_safety_range.GetFloat() ); + + if ( blueSniper ) + { + if ( redSniper ) + { + // both teams like this spot? + DrawFilled( 255, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + // blue sniper area + DrawFilled( 0, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + else if ( redSniper ) + { + // red sniper area + DrawFilled( 255, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + + int rangeTeam = tf_show_incursion_range.GetInt(); + if ( rangeTeam > 0 ) + { + rangeTeam += ( TF_TEAM_RED - 1); + + float range = GetIncursionDistance( rangeTeam ); + if ( range >= tf_show_incursion_range_min.GetFloat() && range <= tf_show_incursion_range_max.GetFloat() ) + { + DrawFilled( 0, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } +} + + +//------------------------------------------------------------------------------------------------ +/** + * Return adjacent area with largest increase in incursion distance + */ +CTFNavArea *CTFNavArea::GetNextIncursionArea( int team ) const +{ + CTFNavArea *nextIncursionArea = NULL; + float nextIncursionDistance = GetIncursionDistance( team ); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + const NavConnectVector *adjVector = GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + CTFNavArea *adjArea = static_cast< CTFNavArea * >( (*adjVector)[ bit ].area ); + + if ( adjArea->GetIncursionDistance( team ) > nextIncursionDistance ) + { + nextIncursionArea = adjArea; + nextIncursionDistance = adjArea->GetIncursionDistance( team ); + } + } + } + + return nextIncursionArea; +} + + +//----------------------------------------------------------------------------- +// Populate 'priorVector' with a collection of adjacent areas that have a lower incursion distance that this area +void CTFNavArea::CollectPriorIncursionAreas( int team, CUtlVector< CTFNavArea * > *priorVector ) +{ + float myIncursionDistance = GetIncursionDistance( team ); + + priorVector->RemoveAll(); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + const NavConnectVector *adjVector = GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + CTFNavArea *adjArea = static_cast< CTFNavArea * >( (*adjVector)[ bit ].area ); + + if ( adjArea->GetIncursionDistance( team ) < myIncursionDistance ) + { + priorVector->AddToTail( adjArea ); + } + } + } +} + + +//----------------------------------------------------------------------------- +// Populate 'priorVector' with a collection of adjacent areas that have a higher incursion distance that this area +void CTFNavArea::CollectNextIncursionAreas( int team, CUtlVector< CTFNavArea * > *priorVector ) +{ + float myIncursionDistance = GetIncursionDistance( team ); + + priorVector->RemoveAll(); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + const NavConnectVector *adjVector = GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + CTFNavArea *adjArea = static_cast< CTFNavArea * >( (*adjVector)[ bit ].area ); + + if ( adjArea->GetIncursionDistance( team ) > myIncursionDistance ) + { + priorVector->AddToTail( adjArea ); + } + } + } +} + + +//----------------------------------------------------------------------------- +/** + * Return true if this area is at least safetyRange units away from all invasion areas + */ +bool CTFNavArea::IsAwayFromInvasionAreas( int myTeam, float safetyRange ) const +{ + const CUtlVector< CTFNavArea * > &invasionVector = GetEnemyInvasionAreaVector( myTeam ); + FOR_EACH_VEC( invasionVector, vit ) + { + CTFNavArea *invasionArea = invasionVector[ vit ]; + + if ( ( invasionArea->GetCenter() - GetCenter() ).IsLengthLessThan( safetyRange ) ) + { + // too close to incoming enemy route to snipe + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +class MarkVisibleSet +{ +public: + MarkVisibleSet( unsigned int marker ) + { + m_marker = marker; + } + + bool operator() ( CNavArea *baseArea ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( baseArea ); + area->SetInvasionSearchMarker( m_marker ); + return true; + } + + unsigned int m_marker; +}; + + +//----------------------------------------------------------------------------- +class CollectInvasionAreas +{ +public: + CollectInvasionAreas( unsigned int marker, CTFNavArea *homeArea, CUtlVector< CTFNavArea * > *redInvasionAreaVector, CUtlVector< CTFNavArea * > *blueInvasionAreaVector ) + { + m_homeArea = homeArea; + m_visibleMarker = marker; + m_redInvasionAreaVector = redInvasionAreaVector; + m_blueInvasionAreaVector = blueInvasionAreaVector; + } + + void FilterArea( CTFNavArea *area, CTFNavArea *adjArea ) + { + if ( adjArea->IsInvasionSearchMarked( m_visibleMarker ) ) + { + // also in PVS - can't be invasion area + return; + } + + const float behindTolerance = 100.0; + + // adjacent area is not in PVS, test if adjacent area not penetrated as far, if so it is an invasion area + if ( area->GetIncursionDistance( TF_TEAM_BLUE ) > adjArea->GetIncursionDistance( TF_TEAM_BLUE ) ) + { + if ( area->GetIncursionDistance( TF_TEAM_BLUE ) > m_homeArea->GetIncursionDistance( TF_TEAM_BLUE ) + behindTolerance ) + { + // this area is farther "in" than we are - don't search further + return; + } + + m_redInvasionAreaVector->AddToTail( adjArea ); + } + + if ( area->GetIncursionDistance( TF_TEAM_RED ) > adjArea->GetIncursionDistance( TF_TEAM_RED ) ) + { + if ( area->GetIncursionDistance( TF_TEAM_RED ) > m_homeArea->GetIncursionDistance( TF_TEAM_RED ) + behindTolerance ) + { + // this area is farther "in" than we are - don't search further + return; + } + + m_blueInvasionAreaVector->AddToTail( adjArea ); + } + } + + bool operator() ( CNavArea *baseArea ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( baseArea ); + + // explore adjacent floor areas + int dir; + for( dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + int count = area->GetAdjacentCount( (NavDirType)dir ); + for( int i=0; i<count; ++i ) + { + CTFNavArea *adjArea = static_cast< CTFNavArea * >( area->GetAdjacentArea( (NavDirType)dir, i ) ); + + FilterArea( area, adjArea ); + } + } + + // include areas that connect TO this area via a one-way link, since the enemy is coming TO us + for( dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + const NavConnectVector *list = area->GetIncomingConnections( (NavDirType)dir ); + + FOR_EACH_VEC( (*list), it ) + { + NavConnect connect = (*list)[ it ]; + + FilterArea( area, static_cast< CTFNavArea * >( connect.area ) ); + } + } + + return true; + } + + CTFNavArea *m_homeArea; + CUtlVector< CTFNavArea * > *m_redInvasionAreaVector; + CUtlVector< CTFNavArea * > *m_blueInvasionAreaVector; + unsigned int m_visibleMarker; +}; + + +//------------------------------------------------------------------------------------------------ +/** + * Find invasion areas where enemies enter from + */ +void CTFNavArea::ComputeInvasionAreaVectors( void ) +{ + static unsigned int searchMarker = RandomInt( 0, 1024*1024 ); + + for( int i=0; i<TF_TEAM_COUNT; ++i ) + { + m_invasionAreaVector[ i ].RemoveAll(); + } + + ++searchMarker; + + // mark all potentially visible areas for quick testing during the search + MarkVisibleSet marker( searchMarker ); + ForAllCompletelyVisibleAreas( marker ); + + // search boundary of potentially visible area set for area pairs where + // the area in the PVS has a higher incursion distance than an adjacent + // area outside of the PVS - an invasion area + + CollectInvasionAreas collector( searchMarker, this, &m_invasionAreaVector[ TF_TEAM_RED ], &m_invasionAreaVector[ TF_TEAM_BLUE ] ); + ForAllCompletelyVisibleAreas( collector ); +} + + +//------------------------------------------------------------------------------------------------ +bool CTFNavArea::IsBlocked( int teamID, bool ignoreNavBlockers ) const +{ + if ( HasAttributeTF( TF_NAV_UNBLOCKABLE ) ) + return false; + + if ( HasAttributeTF( TF_NAV_BLOCKED ) ) + return true; + + // temporary fix: + if ( teamID == TF_TEAM_RED && HasAttributeTF( TF_NAV_BLUE_ONE_WAY_DOOR ) ) + return true; + + if ( teamID == TF_TEAM_BLUE && HasAttributeTF( TF_NAV_RED_ONE_WAY_DOOR ) ) + return true; + + return CNavArea::IsBlocked( teamID, ignoreNavBlockers ); +} + + +//------------------------------------------------------------------------------------------------ +void CTFNavArea::Save( CUtlBuffer &fileBuffer, unsigned int version ) const +{ + CNavArea::Save( fileBuffer, version ); + + // save attribute flags + unsigned int attributes = m_attributeFlags & TF_NAV_PERSISTENT_ATTRIBUTES; + fileBuffer.PutUnsignedInt( attributes ); +} + + +//------------------------------------------------------------------------------------------------ +NavErrorType CTFNavArea::Load( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ) +{ + // load base class data + CNavArea::Load( fileBuffer, version, subVersion ); + + if ( subVersion > TheNavMesh->GetSubVersionNumber() ) + { + Warning( "Unknown NavArea sub-version number\n" ); + return NAV_INVALID_FILE; + } + else if ( subVersion <= 1 ) + { + // no data + m_attributeFlags = 0; + return NAV_OK; + } + + m_attributeFlags = fileBuffer.GetUnsignedInt(); + if ( !fileBuffer.IsValid() ) + { + Warning( "Can't read TF-specific attributes\n" ); + return NAV_INVALID_FILE; + } + + return NAV_OK; +} + + +//-------------------------------------------------------------------------------------------------------- +unsigned int CTFNavArea::m_masterTFMark = 1; + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavArea::MakeNewTFMarker( void ) +{ + ++m_masterTFMark; +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavArea::ResetTFMarker( void ) +{ + m_masterTFMark = 1; +} + + +//-------------------------------------------------------------------------------------------------------- +bool CTFNavArea::IsTFMarked( void ) const +{ + return ( m_TFMark == m_masterTFMark ); +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavArea::TFMark( void ) +{ + m_TFMark = m_masterTFMark; +} + + +//-------------------------------------------------------------------------------------------------------- +bool CTFNavArea::IsValidForWanderingPopulation( void ) const +{ + if ( HasAttributeTF( TF_NAV_BLOCKED | TF_NAV_SPAWN_ROOM_RED | TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_NO_SPAWNING | TF_NAV_RESCUE_CLOSET ) ) + return false; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavArea::AddPotentiallyVisibleActor( CBaseCombatCharacter *who ) +{ + if ( who == NULL ) + { + return; + } + + int team = who->GetTeamNumber(); + if ( team < 0 || team >= TF_TEAM_COUNT ) + return; + + CTFBot *bot = ToTFBot( who ); + if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) + return; + + if ( m_potentiallyVisibleActor[ team ].Find( who ) == m_potentiallyVisibleActor[ team ].InvalidIndex() ) + { + m_potentiallyVisibleActor[ team ].AddToTail( who ); + } +} + + + +//-------------------------------------------------------------------------------------------------------- +float CTFNavArea::GetCombatIntensity( void ) const +{ + if ( !m_combatTimer.HasStarted() ) + { + return 0.0f; + } + + float actualIntensity = m_combatIntensity - m_combatTimer.GetElapsedTime() * tf_nav_combat_decay_rate.GetFloat(); + + if ( actualIntensity < 0.0f ) + { + actualIntensity = 0.0f; + } + + return actualIntensity; +} + + +//-------------------------------------------------------------------------------------------------------- +// Invoked when combat happens in/near this area +void CTFNavArea::OnCombat( void ) +{ + m_combatIntensity += tf_nav_combat_build_rate.GetFloat(); + if ( m_combatIntensity > 1.0f ) + { + m_combatIntensity = 1.0f; + } + + m_combatTimer.Start(); +} + + +//-------------------------------------------------------------------------------------------------------- +bool CTFNavArea::IsInCombat( void ) const +{ + return GetCombatIntensity() > 0.01f; +} + + diff --git a/game/server/tf/nav_mesh/tf_nav_area.h b/game/server/tf/nav_mesh/tf_nav_area.h new file mode 100644 index 0000000..2954688 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_nav_area.h @@ -0,0 +1,299 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_nav_area.h +// TF specific nav area +// Michael Booth, February 2009 + +#ifndef TF_NAV_AREA_H +#define TF_NAV_AREA_H + +#include "nav_area.h" +#include "tf_shareddefs.h" + +enum TFNavAttributeType +{ + TF_NAV_INVALID = 0x00000000, + + // Also look for NAV_MESH_NAV_BLOCKER (w/ nav_debug_blocked ConVar). + TF_NAV_BLOCKED = 0x00000001, // blocked for some TF-specific reason + TF_NAV_SPAWN_ROOM_RED = 0x00000002, + TF_NAV_SPAWN_ROOM_BLUE = 0x00000004, + TF_NAV_SPAWN_ROOM_EXIT = 0x00000008, + TF_NAV_HAS_AMMO = 0x00000010, + TF_NAV_HAS_HEALTH = 0x00000020, + TF_NAV_CONTROL_POINT = 0x00000040, + + TF_NAV_BLUE_SENTRY_DANGER = 0x00000080, // sentry can potentially fire upon enemies in this area + TF_NAV_RED_SENTRY_DANGER = 0x00000100, + + TF_NAV_BLUE_SETUP_GATE = 0x00000800, // this area is blocked until the setup period is over + TF_NAV_RED_SETUP_GATE = 0x00001000, // this area is blocked until the setup period is over + TF_NAV_BLOCKED_AFTER_POINT_CAPTURE = 0x00002000, // this area becomes blocked after the first point is capped + TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE = 0x00004000, // this area is blocked until the first point is capped, then is unblocked + TF_NAV_BLUE_ONE_WAY_DOOR = 0x00008000, + TF_NAV_RED_ONE_WAY_DOOR = 0x00010000, + + TF_NAV_WITH_SECOND_POINT = 0x00020000, // modifier for BLOCKED_*_POINT_CAPTURE + TF_NAV_WITH_THIRD_POINT = 0x00040000, // modifier for BLOCKED_*_POINT_CAPTURE + TF_NAV_WITH_FOURTH_POINT = 0x00080000, // modifier for BLOCKED_*_POINT_CAPTURE + TF_NAV_WITH_FIFTH_POINT = 0x00100000, // modifier for BLOCKED_*_POINT_CAPTURE + + TF_NAV_SNIPER_SPOT = 0x00200000, // this is a good place for a sniper to lurk + TF_NAV_SENTRY_SPOT = 0x00400000, // this is a good place to build a sentry + + TF_NAV_ESCAPE_ROUTE = 0x00800000, // for Raid mode + TF_NAV_ESCAPE_ROUTE_VISIBLE = 0x01000000, // all areas that have visibility to the escape route + + TF_NAV_NO_SPAWNING = 0x02000000, // don't spawn bots in this area + + TF_NAV_RESCUE_CLOSET = 0x04000000, // for respawning friends in Raid mode + + TF_NAV_BOMB_CAN_DROP_HERE = 0x08000000, // the bomb can be dropped here and reached by the invaders in MvM + + TF_NAV_DOOR_NEVER_BLOCKS = 0x10000000, + TF_NAV_DOOR_ALWAYS_BLOCKS = 0x20000000, + + TF_NAV_UNBLOCKABLE = 0x40000000, // this area cannot be blocked + + // save/load these manually set flags, and don't clear them between rounds + TF_NAV_PERSISTENT_ATTRIBUTES = TF_NAV_SNIPER_SPOT | TF_NAV_SENTRY_SPOT | TF_NAV_NO_SPAWNING | TF_NAV_BLUE_SETUP_GATE | TF_NAV_RED_SETUP_GATE | TF_NAV_BLOCKED_AFTER_POINT_CAPTURE | TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE | TF_NAV_BLUE_ONE_WAY_DOOR | TF_NAV_RED_ONE_WAY_DOOR | TF_NAV_DOOR_NEVER_BLOCKS | TF_NAV_DOOR_ALWAYS_BLOCKS | TF_NAV_UNBLOCKABLE | TF_NAV_WITH_SECOND_POINT | TF_NAV_WITH_THIRD_POINT | TF_NAV_WITH_FOURTH_POINT | TF_NAV_WITH_FIFTH_POINT | TF_NAV_RESCUE_CLOSET +}; + + + +class CTFNavArea : public CNavArea +{ +public: + DECLARE_CLASS( CTFNavArea, CNavArea ); + + CTFNavArea( void ); + + 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 CustomAnalysis( bool isIncremental = false ); // for game-specific analysis + virtual void Draw( void ) const; // draw area for debugging & editing + + virtual void UpdateBlocked( bool force = false, int teamID = TEAM_ANY ) { } // we'll handle managing blocked status directly + virtual bool IsBlocked( int teamID, bool ignoreNavBlockers = false ) const; + + virtual void Save( CUtlBuffer &fileBuffer, unsigned int version ) const; // (EXTEND) + virtual NavErrorType Load( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ); // (EXTEND) + + float GetIncursionDistance( int team ) const; // return travel distance from the team's active spawn room to this area, -1 for invalid + CTFNavArea *GetNextIncursionArea( int team ) const; // return adjacent area with largest increase in incursion distance + bool IsReachableByTeam( int team ) const; // return true if the given team can reach this area + void CollectPriorIncursionAreas( int team, CUtlVector< CTFNavArea * > *priorVector ); // populate 'priorVector' with a collection of adjacent areas that have a lower incursion distance that this area + void CollectNextIncursionAreas( int team, CUtlVector< CTFNavArea * > *priorVector ); // populate 'priorVector' with a collection of adjacent areas that have a higher incursion distance that this area + + const CUtlVector< CTFNavArea * > &GetEnemyInvasionAreaVector( int myTeam ) const; // given OUR team index, return list of areas the enemy is invading from + bool IsAwayFromInvasionAreas( int myTeam, float safetyRange = 1000.0f ) const; // return true if this area is at least safetyRange units away from all invasion areas + + void ComputeInvasionAreaVectors( void ); + void SetInvasionSearchMarker( unsigned int marker ); + bool IsInvasionSearchMarked( unsigned int marker ) const; + + void SetAttributeTF( int flags ); + void ClearAttributeTF( int flags ); + bool HasAttributeTF( int flags ) const; + + void AddPotentiallyVisibleActor( CBaseCombatCharacter *who ); + void RemovePotentiallyVisibleActor( CBaseCombatCharacter *who ); + void ClearAllPotentiallyVisibleActors( void ); + bool IsPotentiallyVisibleToActor( CBaseCombatCharacter *who ) const; // return true if the given actor has potential visibility to this area + + virtual bool IsPotentiallyVisibleToTeam( int team ) const; // return true if any portion of this area is visible to anyone on the given team (very fast) + + class IForEachPotentiallyVisibleActor + { + public: + virtual bool Inspect( CBaseCombatCharacter *who ) = 0; + }; + bool ForEachPotentiallyVisibleActor( IForEachPotentiallyVisibleActor &func, int team = TEAM_ANY ); + + void OnCombat( void ); // invoked when combat happens in/near this area + bool IsInCombat( void ) const; // return true if this area has seen combat recently + float GetCombatIntensity( void ) const; // 1 = in active combat, 0 = quiet + + static void MakeNewTFMarker( void ); + static void ResetTFMarker( void ); + bool IsTFMarked( void ) const; + void TFMark( void ); + + // Raid mode ------------------------------------------------- + void AddToWanderCount( int count ); + void SetWanderCount( int count ); + int GetWanderCount( void ) const; + + bool IsValidForWanderingPopulation( void ) const; + // Raid mode ------------------------------------------------- + + // Distance for MvM bomb delivery + float GetTravelDistanceToBombTarget( void ) const; + +private: + friend class CTFNavMesh; + + float m_distanceFromSpawnRoom[ TF_TEAM_COUNT ]; + CUtlVector< CTFNavArea * > m_invasionAreaVector[ TF_TEAM_COUNT ]; // use our team as index to get list of areas the enemy is invading from + unsigned int m_invasionSearchMarker; + + unsigned int m_attributeFlags; + + CUtlVector< CHandle< CBaseCombatCharacter > > m_potentiallyVisibleActor[ TF_TEAM_COUNT ]; + + float m_combatIntensity; + IntervalTimer m_combatTimer; + + static unsigned int m_masterTFMark; + unsigned int m_TFMark; // this area's mark + + // Raid mode ------------------------------------------------- + int m_wanderCount; // how many wandering defenders to populate here + // Raid mode ------------------------------------------------- + + float m_distanceToBombTarget; +}; + + +inline float CTFNavArea::GetTravelDistanceToBombTarget( void ) const +{ + return m_distanceToBombTarget; +} + +inline void CTFNavArea::AddToWanderCount( int count ) +{ + m_wanderCount += count; +} + +inline void CTFNavArea::SetWanderCount( int count ) +{ + m_wanderCount = count; +} + +inline int CTFNavArea::GetWanderCount( void ) const +{ + return m_wanderCount; +} + + + +inline bool CTFNavArea::IsPotentiallyVisibleToActor( CBaseCombatCharacter *who ) const +{ + if ( who == NULL ) + return false; + + int team = who->GetTeamNumber(); + if ( team < 0 || team >= TF_TEAM_COUNT ) + return false; + + return m_potentiallyVisibleActor[ team ].Find( who ) != m_potentiallyVisibleActor[ team ].InvalidIndex(); +} + + +inline bool CTFNavArea::IsPotentiallyVisibleToTeam( int team ) const +{ + return team >= 0 && team < TF_TEAM_COUNT && m_potentiallyVisibleActor[ team ].Count() > 0; +} + + +inline bool CTFNavArea::ForEachPotentiallyVisibleActor( CTFNavArea::IForEachPotentiallyVisibleActor &func, int team ) +{ + if ( team == TEAM_ANY ) + { + for( int t=0; t<TF_TEAM_COUNT; ++t ) + { + for( int i=0; i<m_potentiallyVisibleActor[ t ].Count(); ++i ) + { + CBaseCombatCharacter *who = m_potentiallyVisibleActor[ t ][ i ]; + + if ( who && func.Inspect( who ) == false ) + { + return false; + } + } + } + } + else if ( team >= 0 && team < TF_TEAM_COUNT ) + { + for( int i=0; i<m_potentiallyVisibleActor[ team ].Count(); ++i ) + { + CBaseCombatCharacter *who = m_potentiallyVisibleActor[ team ][ i ]; + + if ( who && func.Inspect( who ) == false ) + { + return false; + } + } + } + + return true; +} + +inline void CTFNavArea::RemovePotentiallyVisibleActor( CBaseCombatCharacter *who ) +{ + for( int i=0; i<TF_TEAM_COUNT; ++i ) + m_potentiallyVisibleActor[i].FindAndFastRemove( who ); +} + +inline void CTFNavArea::ClearAllPotentiallyVisibleActors( void ) +{ + for( int i=0; i<TF_TEAM_COUNT; ++i ) + m_potentiallyVisibleActor[i].RemoveAll(); +} + +inline float CTFNavArea::GetIncursionDistance( int team ) const +{ + if ( team < 0 || team >= TF_TEAM_COUNT ) + { + return -1.0f; + } + + return m_distanceFromSpawnRoom[ team ]; +} + +inline bool CTFNavArea::IsReachableByTeam( int team ) const +{ + if ( team < 0 || team >= TF_TEAM_COUNT ) + { + return false; + } + + return m_distanceFromSpawnRoom[ team ] >= 0.0f; +} + +inline const CUtlVector< CTFNavArea * > &CTFNavArea::GetEnemyInvasionAreaVector( int myTeam ) const +{ + if ( myTeam < 0 || myTeam >= TF_TEAM_COUNT ) + { + myTeam = 0.0f; + } + + return m_invasionAreaVector[ myTeam ]; +} + +inline void CTFNavArea::SetInvasionSearchMarker( unsigned int marker ) +{ + m_invasionSearchMarker = marker; +} + +inline bool CTFNavArea::IsInvasionSearchMarked( unsigned int marker ) const +{ + return marker == m_invasionSearchMarker; +} + +inline void CTFNavArea::SetAttributeTF( int flags ) +{ + m_attributeFlags |= flags; +} + +inline void CTFNavArea::ClearAttributeTF( int flags ) +{ + m_attributeFlags &= ~flags; +} + +inline bool CTFNavArea::HasAttributeTF( int flags ) const +{ + return ( m_attributeFlags & flags ) ? true : false; +} + +#endif // TF_NAV_AREA_H
\ No newline at end of file diff --git a/game/server/tf/nav_mesh/tf_nav_interface.cpp b/game/server/tf/nav_mesh/tf_nav_interface.cpp new file mode 100644 index 0000000..c720db3 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_nav_interface.cpp @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Implements nav interface entity. Used by maps to do various things +// with the nav mesh +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "tf_nav_mesh.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CPointNavInterface : public CPointEntity +{ + DECLARE_CLASS( CPointNavInterface, CPointEntity ); + +public: + + // Input handlers + void RecomputeBlockers(inputdata_t &inputdata); + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CPointNavInterface ) + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "RecomputeBlockers", RecomputeBlockers ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( tf_point_nav_interface, CPointNavInterface ); + + +void CPointNavInterface::RecomputeBlockers( inputdata_t &inputdata ) +{ + CTFNavMesh* pTFNavMesh = dynamic_cast<CTFNavMesh*>( TheNavMesh ); + Assert( pTFNavMesh ); + if( pTFNavMesh ) + { + pTFNavMesh->ScheduleRecomputationOfInternalData( CTFNavMesh::MAP_LOGIC ); + } +} diff --git a/game/server/tf/nav_mesh/tf_nav_mesh.cpp b/game/server/tf/nav_mesh/tf_nav_mesh.cpp new file mode 100644 index 0000000..84a0ad8 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_nav_mesh.cpp @@ -0,0 +1,2450 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_nav_mesh.cpp +// TF specific nav mesh +// Michael Booth, February 2009 + +#include "cbase.h" +#include "tf_nav_mesh.h" +#include "bot/tf_bot.h" +#include "bot/tf_bot_manager.h" +#include "tf_obj.h" +#include "tf_obj_sentrygun.h" +#include "team_control_point_master.h" +#include "team_train_watcher.h" +#include "tf_gamerules.h" +#include "func_respawnroom.h" +#include "doors.h" +#include "props.h" +#include "filters.h" +#include "NextBotUtil.h" + +// NOTE: nav_debug_blocked ConVar is also use for debugging NAV_MESH_NAV_BLOCKER and TF_NAV_BLOCKED... + +ConVar tf_show_in_combat_areas( "tf_show_in_combat_areas", "0", FCVAR_CHEAT ); +ConVar tf_show_enemy_invasion_areas( "tf_show_enemy_invasion_areas", "0", FCVAR_CHEAT, "Highlight areas where the enemy team enters the visible environment of the local player" ); +ConVar tf_show_blocked_areas( "tf_show_blocked_areas", "0", FCVAR_CHEAT, "Highlight areas that are considered blocked for TF-specific reasons" ); +ConVar tf_show_incursion_flow( "tf_show_incursion_flow", "0", FCVAR_CHEAT ); +ConVar tf_show_incursion_flow_range( "tf_show_incursion_flow_range", "150", FCVAR_CHEAT, "1 = red, 2 = blue" ); +ConVar tf_show_incursion_flow_gradient( "tf_show_incursion_flow_gradient", "0", FCVAR_CHEAT, "1 = red, 2 = blue" ); +ConVar tf_show_mesh_decoration( "tf_show_mesh_decoration", "0", FCVAR_CHEAT, "Highlight special areas" ); +ConVar tf_show_mesh_decoration_manual( "tf_show_mesh_decoration_manual", "0", FCVAR_CHEAT, "Highlight special areas marked by hand" ); +// Method 1 & 2 should be exactly the same for tf_show_sentry_danger. +ConVar tf_show_sentry_danger( "tf_show_sentry_danger", "0", FCVAR_CHEAT, "Show sentry danger areas. 1:Use m_sentryAreas. 2:Check all nav areas." ); +ConVar tf_show_actor_potential_visibility( "tf_show_actor_potential_visibility", "0", FCVAR_CHEAT ); +ConVar tf_show_control_points( "tf_show_control_points", "0", FCVAR_CHEAT ); +ConVar tf_show_bomb_drop_areas( "tf_show_bomb_drop_areas", "0", FCVAR_CHEAT ); + +ConVar tf_bot_min_setup_gate_defend_range( "tf_bot_min_setup_gate_defend_range", "750", FCVAR_CHEAT, "How close from the setup gate(s) defending bots can take up positions. Areas closer than this will be in cover to ambush." ); +ConVar tf_bot_max_setup_gate_defend_range( "tf_bot_max_setup_gate_defend_range", "2000", FCVAR_CHEAT, "How far from the setup gate(s) defending bots can take up positions" ); +ConVar tf_bot_min_setup_gate_sniper_defend_range( "tf_bot_min_setup_gate_sniper_defend_range", "1500", FCVAR_CHEAT, "How far from the setup gate(s) a defending sniper will take up position" ); +ConVar tf_show_gate_defense_areas( "tf_show_gate_defense_areas", "0", FCVAR_CHEAT ); +ConVar tf_show_point_defense_areas( "tf_show_point_defense_areas", "0", FCVAR_CHEAT ); + + +extern ConVar tf_bot_debug_select_defense_area; +extern ConVar tf_nav_in_combat_duration; +extern ConVar mp_teams_unbalance_limit; +extern ConVar mp_autoteambalance; +extern ConVar sv_alltalk; +extern ConVar mp_timelimit; + + +//-------------------------------------------------------------------------------------------------------------- +ConVar tf_select_ambush_areas_radius( "tf_select_ambush_areas_radius", "750", FCVAR_CHEAT ); +ConVar tf_select_ambush_areas_close_range( "tf_select_ambush_areas_close_range", "300", FCVAR_CHEAT ); +ConVar tf_select_ambush_areas_max_enemy_exposure_area( "tf_select_ambush_areas_max_enemy_exposure_area", "500000", FCVAR_CHEAT ); + +class ScanSelectAmbushAreas +{ +public: + ScanSelectAmbushAreas( CUtlVector< CTFNavArea * > *ambushAreaVector, int teamToAmbush, float enemyIncursionLimit ) + { + m_ambushAreaVector = ambushAreaVector; + m_teamToAmbush = teamToAmbush; + m_enemyIncursionLimit = enemyIncursionLimit; + } + + bool operator() ( CNavArea *baseArea ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( baseArea ); + + // no drop-downs or jumps + if ( area->GetParent() && !area->GetParent()->IsContiguous( area ) ) + return false; + + float enemyIncursionDistanceAtArea = area->GetIncursionDistance( m_teamToAmbush ); + + if ( enemyIncursionDistanceAtArea > m_enemyIncursionLimit ) + return false; + + int wallCount = 0; + int dir; + for( dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + if ( area->GetAdjacentCount( (NavDirType)dir ) == 0 ) + { + // wall (or dropoff) on this side + ++wallCount; + } + } + + if ( wallCount >= 1 ) + { + // good cover, are we also right next to enemy incursion areas? + const CUtlVector< CTFNavArea * > &invasionVector = area->GetEnemyInvasionAreaVector( GetEnemyTeam( m_teamToAmbush ) ); + + // don't use areas that are in plain sight of large amounts of incoming enemy space + NavAreaCollector collector( true ); + area->ForAllPotentiallyVisibleAreas( collector ); + + float totalVisibleThreatArea = 0.0f; + FOR_EACH_VEC( collector.m_area, it ) + { + CTFNavArea *visArea = static_cast< CTFNavArea * >( collector.m_area[ it ] ); + + if ( visArea->GetIncursionDistance( m_teamToAmbush ) < enemyIncursionDistanceAtArea ) + { + totalVisibleThreatArea += visArea->GetSizeX() * visArea->GetSizeY(); + } + } + + if ( totalVisibleThreatArea > tf_select_ambush_areas_max_enemy_exposure_area.GetFloat() ) + { + // too exposed + return true; + } + + float nearRangeSq = tf_select_ambush_areas_close_range.GetFloat(); + nearRangeSq *= nearRangeSq; + + FOR_EACH_VEC( invasionVector, it ) + { + CTFNavArea *invasionArea = invasionVector[ it ]; + + if ( invasionArea->GetIncursionDistance( m_teamToAmbush ) < enemyIncursionDistanceAtArea ) + { + // the enemy will go through invasionArea before they reach the candidate area + float rangeSq = ( invasionArea->GetCenter() - area->GetCenter() ).LengthSqr(); + if ( rangeSq < nearRangeSq ) + { + // there is at least one nearby invasion area + m_ambushAreaVector->AddToTail( area ); + break; + } + } + } + } + + return true; + } + + int m_teamToAmbush; + float m_enemyIncursionLimit; + CUtlVector< CTFNavArea * > *m_ambushAreaVector; +}; + +void CMD_SelectAmbushAreas( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return; + + CTFNavArea *searchSourceArea = static_cast< CTFNavArea * >( player->GetLastKnownArea() ); + + int teamToAmbush = GetEnemyTeam( player->GetTeamNumber() ); + + CUtlVector< CTFNavArea * > ambushAreaVector; + ScanSelectAmbushAreas selector( &ambushAreaVector, teamToAmbush, searchSourceArea->GetIncursionDistance( teamToAmbush ) + 300.0f ); + SearchSurroundingAreas( searchSourceArea, searchSourceArea->GetCenter(), selector, tf_select_ambush_areas_radius.GetFloat() ); + + FOR_EACH_VEC( ambushAreaVector, it ) + { + TheNavMesh->AddToSelectedSet( ambushAreaVector[ it ] ); + } +} +static ConCommand tf_select_ambush_areas( "tf_select_ambush_areas", CMD_SelectAmbushAreas, "Add good ambush spots to the selected set. For debugging.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +#ifdef SKIPME +//------------------------------------------------------------------------- +void CMD_SelectIncursionZone( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return; + + const CUtlVector< CTFNavArea * > *pointAreaVector = TheTFNavMesh()->GetControlPointAreas(); + if ( !pointAreaVector ) + return; + + int i; + float incursionAtPoint = 0.0f; + float maxInvaderTravelDistance = 2000.0f; + + for( i=0; i<pointAreaVector->Count(); ++i ) + { + if ( pointAreaVector->Element(i)->GetIncursionDistance( TF_TEAM_BLUE ) > incursionAtPoint ) + { + incursionAtPoint = pointAreaVector->Element(i)->GetIncursionDistance( TF_TEAM_BLUE ); + } + } + + for( i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + + float inc = area->GetIncursionDistance( TF_TEAM_BLUE ); + if ( inc > 0.0f && inc < incursionAtPoint && inc > incursionAtPoint - maxInvaderTravelDistance ) + { + NDebugOverlay::Cross3D( area->GetCenter(), 5.0f, 255, 255, 0, true, 99999.9f ); + //TheNavMesh->AddToSelectedSet( area ); + } + } +} +static ConCommand tf_select_incursion_zone( "tf_select_incursion_zone", CMD_SelectIncursionZone, "Select areas where invading team approaches the objective. For debugging.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + + +//------------------------------------------------------------------------- +void CMD_SelectControlPointIncursionAreas( void ) +{ + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return; + + const CUtlVector< CTFNavArea * > *pointAreaVector = TheTFNavMesh()->GetControlPointAreas(); + + for( int i=0; i<pointAreaVector->Count(); ++i ) + { + CTFNavArea *pointArea = (CTFNavArea *)pointAreaVector->Element(i); + + for( i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + + if ( area->GetIncursionDistance( TF_TEAM_BLUE ) > pointArea->GetIncursionDistance( TF_TEAM_BLUE ) ) + continue; + + if ( pointArea->IsPotentiallyVisible( area ) ) + { + // the point is visible from this area + + // if no prior areas can see the point, we have a point incursion area + CUtlVector< CTFNavArea * > priorVector; + area->CollectPriorIncursionAreas( TF_TEAM_BLUE, &priorVector ); + + int j; + for( j=0; j<priorVector.Count(); ++j ) + { + if ( pointArea->IsPotentiallyVisible( priorVector[j] ) ) + { + break; + } + } + + if ( j == priorVector.Count() && j > 0 ) + { + // no prior areas can see the point + TheNavMesh->AddToSelectedSet( area ); + } + } + } + } +} +static ConCommand tf_select_control_point_incursion_areas( "tf_select_control_point_incursion_areas", CMD_SelectControlPointIncursionAreas, "Select areas where invading team leaves cover near the objective. For debugging.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + +//------------------------------------------------------------------------- +CON_COMMAND_F( tf_assign_territory, "Divvy up the mesh into red and blue territories. For debugging.", FCVAR_GAMEDLL ) +{ + // Listenserver host or rcon access only! + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + int i; + + // clear all territory markings + for( i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + area->ClearAttributeTF( TF_NAV_RED_TERRITORY | TF_NAV_BLUE_TERRITORY ); + area->SetParent( NULL ); + } + + const CUtlVector< CTFNavArea * > *pointAreaVector = TheTFNavMesh()->GetControlPointAreas(); + + if ( !pointAreaVector || pointAreaVector->Count() <= 0 ) + return; + + // find centermost point area, and mark all contested point areas as owned by red + Vector center = vec3_origin; + for( i=0; i<pointAreaVector->Count(); ++i ) + { + center += pointAreaVector->Element(i)->GetCenter(); + pointAreaVector->Element(i)->SetAttributeTF( TF_NAV_RED_TERRITORY ); + } + center /= pointAreaVector->Count(); + + CTFNavArea *pointArea = pointAreaVector->Element(0); + for( i=0; i<pointAreaVector->Count(); ++i ) + { + if ( pointAreaVector->Element(i)->IsOverlapping( center ) ) + { + pointArea = pointAreaVector->Element(i); + break; + } + } + + // spread red's territory to surround the contested area a bit + const float surroundRange = 1000.0f; + CUtlVector< CNavArea * > surroundingVector; + CollectSurroundingAreas( &surroundingVector, pointArea, surroundRange ); + + for( int t=0; t<surroundingVector.Count(); ++t ) + { + CTFNavArea *area = (CTFNavArea *)surroundingVector[t]; + area->ClearAttributeTF( TF_NAV_BLUE_TERRITORY ); + area->SetAttributeTF( TF_NAV_RED_TERRITORY ); + } + + + // do a breadth first search out from control point center + // when a spawn room is reached, mark it and all its parent areas as belonging to the team of the spawn room + CNavArea::ClearSearchLists(); + + pointArea->AddToOpenList(); + pointArea->Mark(); + pointArea->SetParent( NULL ); + + CUtlVectorFixedGrowable< const NavConnect *, 64 > adjAreaVector; + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CTFNavArea *area = static_cast< CTFNavArea * >( CNavArea::PopOpenList() ); + + // ignore setup gates, since they will be open after the setup time + if ( !area->HasAttributeTF( TF_NAV_BLUE_SETUP_GATE | TF_NAV_RED_SETUP_GATE ) && ( area->IsBlocked( TF_TEAM_RED ) || area->IsBlocked( TF_TEAM_BLUE ) ) ) + { + // don't pass through blocked areas + continue; + } + + // explore adjacent floor areas + adjAreaVector.RemoveAll(); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + // collect all OUTGOING links from this area to adjacent areas + const NavConnectVector *adjVector = area->GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + adjAreaVector.AddToTail( &(*adjVector)[ bit ] ); + } + } + + FOR_EACH_VEC( adjAreaVector, vit ) + { + const NavConnect *connect = adjAreaVector[ vit ]; + CTFNavArea *adjArea = static_cast< CTFNavArea * >( connect->area ); + + if ( adjArea->ComputeAdjacentConnectionHeightChange( area ) > TF_PLAYER_JUMP_HEIGHT || + area->ComputeAdjacentConnectionHeightChange( adjArea ) > TF_PLAYER_JUMP_HEIGHT ) + { + // don't go up ledges too high to jump + continue; + } + + if ( !adjArea->IsMarked() ) + { + adjArea->Mark(); + adjArea->SetParent( area ); + + // if this area is in a spawn room, mark path we took to get here as the appropriate team's territory + if ( adjArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED ) ) + { + for( CTFNavArea *pathArea = adjArea; pathArea; pathArea = (CTFNavArea *)pathArea->GetParent() ) + { + pathArea->SetAttributeTF( TF_NAV_RED_TERRITORY ); + } + } + else if ( adjArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) + { + for( CTFNavArea *pathArea = adjArea; pathArea; pathArea = (CTFNavArea *)pathArea->GetParent() ) + { + pathArea->SetAttributeTF( TF_NAV_BLUE_TERRITORY ); + } + } + + adjArea->AddToOpenListTail(); + } + } + } + + if ( args.ArgC() == 1 ) + { + return; + } + + // iterate over all areas, spreading territory out from found routes into unclaimed areas + CUtlVector< CTFNavArea * > spreadVector; + + while( true ) + { + spreadVector.RemoveAll(); + + for( int i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + CTFNavArea *parent = (CTFNavArea *)area->GetParent(); + + // if this area has no territory affiliation but its parent does, inherit it and iterate again + if ( !area->HasAttributeTF( TF_NAV_RED_TERRITORY | TF_NAV_BLUE_TERRITORY ) && parent && parent->HasAttributeTF( TF_NAV_RED_TERRITORY | TF_NAV_BLUE_TERRITORY ) ) + { + spreadVector.AddToTail( area ); + } + } + + if ( spreadVector.Count() == 0 ) + { + // finished spreading + break; + } + + // spread the territory influence one step out + for( int j=0; j<spreadVector.Count(); ++j ) + { + CTFNavArea *area = spreadVector[j]; + CTFNavArea *parent = (CTFNavArea *)area->GetParent(); + + if ( parent->HasAttributeTF( TF_NAV_RED_TERRITORY ) ) + { + area->SetAttributeTF( TF_NAV_RED_TERRITORY ); + } + + if ( parent->HasAttributeTF( TF_NAV_BLUE_TERRITORY ) ) + { + area->SetAttributeTF( TF_NAV_BLUE_TERRITORY ); + } + } + } +} +#endif // SKIPME + + +//------------------------------------------------------------------------- +CTFNavMesh::CTFNavMesh( void ) +{ + for( int j=0; j<MAX_CONTROL_POINTS; ++j ) + { + m_controlPointAreaVector[j].RemoveAll(); + m_controlPointCenterAreaVector[j] = NULL; + } + + ListenForGameEvent( "teamplay_setup_finished" ); + ListenForGameEvent( "teamplay_point_captured" ); + ListenForGameEvent( "teamplay_point_unlocked" ); + + ListenForGameEvent( "player_builtobject" ); + ListenForGameEvent( "player_dropobject" ); + ListenForGameEvent( "player_carryobject" ); + ListenForGameEvent( "object_detonated" ); + ListenForGameEvent( "object_destroyed" ); + + m_priorBotCount = 0; + + m_recomputeInternalDataTimer.Invalidate(); +} + + +//------------------------------------------------------------------------- +CTFNavArea *CTFNavMesh::CreateArea( void ) const +{ + return new CTFNavArea; +} + + +//------------------------------------------------------------------------- +/** + * Invoked on each game frame + */ +void CTFNavMesh::Update( void ) +{ + CNavMesh::Update(); + + if ( !TheNavAreas.Count() ) + return; + + UpdateDebugDisplay(); + + if ( TheNextBots().GetNextBotCount() > 0 ) + { + if ( m_priorBotCount == 0 ) + { + // the first bot was just added + ScheduleRecomputationOfInternalData( RESET ); + } + + // we use a timer here to give the map logic a few moments to settle out before inspecting it + if ( m_recomputeInternalDataTimer.HasStarted() && m_recomputeInternalDataTimer.IsElapsed() ) + { + m_recomputeInternalDataTimer.Invalidate(); + RecomputeInternalData(); + } + + if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT && m_watchCartTimer.IsElapsed() ) + { + // the cart may have moved, recompute new sniper spots + m_watchCartTimer.Start( 3.0f ); + } + } + + m_priorBotCount = TheNextBots().GetNextBotCount(); +} + + +//------------------------------------------------------------------------- +/** + * (EXTEND) invoked when server loads a new map + */ +void CTFNavMesh::OnServerActivate( void ) +{ + CNavMesh::OnServerActivate(); + + m_sentryAreas.RemoveAll(); + + ResetMeshAttributes( true ); + m_priorBotCount = 0; + + m_setupGateDefenseAreaVector.RemoveAll(); + + m_redSpawnRoomAreaVector.RemoveAll(); + m_blueSpawnRoomAreaVector.RemoveAll(); + + m_redSpawnRoomExitAreaVector.RemoveAll(); + m_blueSpawnRoomExitAreaVector.RemoveAll(); + for( int i=0; i<MAX_CONTROL_POINTS; ++i ) + { + m_controlPointAreaVector[i].RemoveAll(); + m_controlPointCenterAreaVector[i] = NULL; + } +} + + +//------------------------------------------------------------------------- +/** + * Invoked when a game round restarts + */ +void CTFNavMesh::OnRoundRestart( void ) +{ + CNavMesh::OnRoundRestart(); + + ResetMeshAttributes( true ); + + // nasty hack + TheTFBots().OnRoundRestart(); + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + RecomputeInternalData(); + } + + DevMsg( "CTFNavMesh: %d nav areas in mesh.\n", GetNavAreaCount() ); +} + + +//------------------------------------------------------------------------- +/** + * One or more areas may have become blocked or are no longer blocked. + * Recompute dependent mesh data. + */ +void CTFNavMesh::OnBlockedAreasChanged( void ) +{ + VPROF_BUDGET( "CTFNavMesh::OnBlockedAreasChanged", "NextBot" ); + + if ( TheNextBots().GetNextBotCount() == 0 ) + return; + + ScheduleRecomputationOfInternalData( BLOCKED_STATUS_CHANGED ); +} + + +//------------------------------------------------------------------------- +void TestAndBlockOverlappingAreas( CBaseEntity *entity ) +{ + Ray_t ray; + trace_t trace; + NextBotTraceFilterIgnoreActors filter( NULL, COLLISION_GROUP_NONE ); + + const float crouchHeight = 30.0f; + Vector hullMin, hullMax; + Vector traceFrom, traceTo; + + Extent extent; + extent.Init( entity ); + + CUtlVector< CNavArea * > overlapVector; + TheNavMesh->CollectAreasOverlappingExtent( extent, &overlapVector ); + + for( int i=0; i<overlapVector.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)overlapVector[i]; + + const float tolerance = 1.0f; + if ( fabs( area->GetCorner( NORTH_WEST ).z - area->GetCorner( NORTH_EAST ).z ) < tolerance ) + { + // flat along X, potentially varies along Y + hullMin.x = 0.0f; + hullMin.y = 0.0f; + hullMin.z = StepHeight; + + hullMax.x = area->GetSizeX(); + hullMax.y = 0.0f; + hullMax.z = crouchHeight; + + traceFrom = area->GetCorner( NORTH_WEST ); + traceTo = area->GetCorner( SOUTH_WEST ); + } + else if ( fabs( area->GetCorner( NORTH_WEST ).z - area->GetCorner( SOUTH_WEST ).z ) < tolerance ) + { + // flat along Y, potentially varies along X + hullMin.x = 0.0f; + hullMin.y = 0.0f; + hullMin.z = StepHeight; + + hullMax.x = 0.0f; + hullMax.y = area->GetSizeY(); + hullMax.z = crouchHeight; + + traceFrom = area->GetCorner( NORTH_WEST ); + traceTo = area->GetCorner( NORTH_EAST ); + } + else + { + // varies along both X and Y + hullMin.x = 0.0f; + hullMin.y = 0.0f; + hullMin.z = StepHeight; + + hullMax.x = 1.0f; + hullMax.y = 1.0f; + hullMax.z = crouchHeight; + + traceFrom = area->GetCorner( NORTH_WEST ); + traceTo = area->GetCorner( SOUTH_EAST ); + } + + // need to trace from high to low to avoid interpenetration + if ( traceFrom.z < traceTo.z ) + { + Vector tmp = traceFrom; + traceFrom = traceTo; + traceTo = tmp; + } + + ray.Init( traceFrom, traceTo, hullMin, hullMax ); + enginetrace->TraceRay( ray, MASK_PLAYERSOLID, &filter, &trace ); + + // NDebugOverlay::SweptBox( traceFrom, traceTo, hullMin, hullMax, vec3_angle, 255, 255, 0, 255, 99999.9f ); + + if ( trace.DidHit() ) + { + if ( trace.m_pEnt && trace.m_pEnt->ShouldBlockNav() ) + { + area->MarkAsBlocked( TEAM_ANY, entity ); + } + } + } +} + + +//------------------------------------------------------------------------- +void CTFNavMesh::ComputeBlockedAreas( void ) +{ + // clear all blocked state + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + area->UnblockArea(); + } + +#ifdef TF_CREEP_MODE + if ( TFGameRules()->IsCreepWaveMode() ) + { + // no blocking for creeps + return; + } +#endif + + // block mesh under solid brushes + CFuncBrush *brush = NULL; + while( ( brush = (CFuncBrush *)gEntList.FindEntityByClassname( brush, "func_brush" ) ) != NULL ) + { + if ( brush->IsSolid() ) // && !brush->m_iDisabled ) // "disabled" seems to be overridden by solidity + { + // this brush is potentially blocking navigation + TestAndBlockOverlappingAreas( brush ); + } + } + + + // Find all func_doors in the map. If a func_door is surrounded by a trigger_multiple, + // the trigger controls access to the door. If the func_door is bare, the door itself + // determines access. + CBaseDoor *door = NULL; + while( ( door = (CBaseDoor *)gEntList.FindEntityByClassname( door, "func_door*" ) ) != NULL ) + { + // if a closed door is not controlled by a trigger assume it doesn't open at all until the scenario changes and map logic opens it + bool isDoorClosed = ( door->m_toggle_state == TS_AT_BOTTOM || door->m_toggle_state == TS_GOING_DOWN ); + + int doorOwnedByTeam = TEAM_UNASSIGNED; + + bool isDoorTriggerControlled = false; + + Extent triggerExtent, doorExtent; + doorExtent.Init( door ); + + CTriggerMultiple *trigger = NULL; + while( ( trigger = (CTriggerMultiple *)gEntList.FindEntityByClassname( trigger, "trigger_multiple" ) ) != NULL ) + { + triggerExtent.Init( trigger ); + + // just check overlapping, not encompassing, since some door triggers only are player height tall (cp_gravelpit) + if ( triggerExtent.IsOverlapping( doorExtent ) ) + { + if ( !trigger->m_bDisabled ) + { + // this trigger contains this door, and thus controls it + isDoorTriggerControlled = true; + + // look for a filter attached to this trigger that limits access to one team + if ( trigger->m_hFilter != NULL && FClassnameIs( trigger->m_hFilter, "filter_activator_tfteam" ) ) + { + doorOwnedByTeam = trigger->m_hFilter->GetTeamNumber(); + } + } + } + } + + // is this door acting like a wall? + bool isDoorWall = isDoorTriggerControlled ? false : isDoorClosed; + + // set the blocked status of all areas overlapping this door + NavAreaCollector doorAreas; + TheNavMesh->ForAllAreasOverlappingExtent( doorAreas, doorExtent ); + + int blockedTeam = ( doorOwnedByTeam == TEAM_UNASSIGNED ) ? TEAM_ANY : ( ( doorOwnedByTeam == TF_TEAM_RED ) ? TF_TEAM_BLUE : TF_TEAM_RED ); + + for( int i=0; i<doorAreas.m_area.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)doorAreas.m_area[i]; + + bool isDoorBlocking; + if ( area->HasAttributeTF( TF_NAV_DOOR_ALWAYS_BLOCKS ) ) + { + // closed doors always block + isDoorBlocking = isDoorClosed; + } + else + { + // untriggered closed doors, or team-owned doors block + isDoorBlocking = ( isDoorWall || doorOwnedByTeam != TEAM_UNASSIGNED ); + } + + if ( isDoorBlocking ) + { + // this door is blocking navigation for at least one team + if ( !area->HasAttributeTF( TF_NAV_DOOR_NEVER_BLOCKS ) ) + { + area->MarkAsBlocked( blockedTeam, door ); + } + } + else + { + // we need to UN-block these areas to account for legacy func_brushes + // used inside of cosmetic doors as a collision proxy that have marked + // these areas as blocked + area->UnblockArea( blockedTeam ); + } + } + } + +#ifdef DONT_USE_BLOCKS_TOO_MUCH + // Find all prop_dynamic entities in the map and block areas they overlap + CDynamicProp *prop = NULL; + while( ( prop = (CDynamicProp *)gEntList.FindEntityByClassname( prop, "prop_dynamic" ) ) != NULL ) + { + if ( prop->IsSolid() ) + { + // if this prop is parented to a door, ignore it - it has already been handled by the door code above + CBaseDoor *parentDoor = dynamic_cast< CBaseDoor * >( prop->GetParent() ); + if ( !parentDoor ) + { + // this prop is potentially blocking navigation + TestAndBlockOverlappingAreas( prop ); + } + } + } +#endif // DONT_USE_BLOCKS_TOO_MUCH +} + + +//------------------------------------------------------------------------- +void CTFNavMesh::CollectControlPointAreas( void ) +{ + for( int i=0; i<MAX_CONTROL_POINTS; ++i ) + { + m_controlPointAreaVector[i].RemoveAll(); + m_controlPointCenterAreaVector[i] = NULL; + } + + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster ) + { + CBaseEntity *trigger = NULL; + while( ( trigger = gEntList.FindEntityByClassname( trigger, "trigger_capture_area*" ) ) != NULL ) + { + CTeamControlPoint *point = ((CTriggerAreaCapture *)trigger)->GetControlPoint(); + + if ( point ) + { + Extent extent; + extent.Init( trigger ); + + // expand extent a bit to make sure it intersects ground below (koth_viaduct) + extent.lo.z -= HalfHumanHeight; + extent.hi.z += HalfHumanHeight; + + CUtlVector< CTFNavArea * > *pointAreaVector = &m_controlPointAreaVector[ point->GetPointIndex() ]; + TheNavMesh->CollectAreasOverlappingExtent< CTFNavArea >( extent, pointAreaVector ); + + // find area closest to the control point's center + m_controlPointCenterAreaVector[ point->GetPointIndex() ] = NULL; + float closeRangeSq = FLT_MAX; + + for( int i=0; i<pointAreaVector->Count(); ++i ) + { + CTFNavArea *area = pointAreaVector->Element(i); + + float rangeSq = ( area->GetCenter() - trigger->WorldSpaceCenter() ).Length2DSqr(); + if ( rangeSq < closeRangeSq ) + { + m_controlPointCenterAreaVector[ point->GetPointIndex() ] = area; + closeRangeSq = rangeSq; + } + } + } + } + } +} + + +//------------------------------------------------------------------------- +// For MvM mode. Mark all nav areas where the bomb can drop and the invaders can reach it. +void CTFNavMesh::ComputeLegalBombDropAreas( void ) +{ + if ( !TFGameRules()->IsMannVsMachineMode() ) + { + return; + } + + CTFNavArea *startArea = NULL; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) + { + startArea = area; + } + + area->ClearAttributeTF( TF_NAV_BOMB_CAN_DROP_HERE ); + } + + if ( startArea == NULL ) + { + Warning( "Can't find blue spawn room nav areas. No legal bomb drop areas are marked" ); + return; + } + + CNavArea::ClearSearchLists(); + + startArea->AddToOpenList(); + startArea->Mark(); + startArea->SetParent( NULL ); + + CUtlVectorFixedGrowable< const NavConnect *, 64 > adjAreaVector; + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CTFNavArea *area = static_cast< CTFNavArea * >( CNavArea::PopOpenList() ); + + // explore adjacent floor areas + adjAreaVector.RemoveAll(); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + // collect all OUTGOING links from this area to adjacent areas + const NavConnectVector *adjVector = area->GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + adjAreaVector.AddToTail( &(*adjVector)[ bit ] ); + } + } + + FOR_EACH_VEC( adjAreaVector, vit ) + { + const NavConnect *connect = adjAreaVector[ vit ]; + CTFNavArea *adjArea = static_cast< CTFNavArea * >( connect->area ); + + if ( adjArea->IsMarked() ) + { + continue; + } + + if ( area->ComputeAdjacentConnectionHeightChange( adjArea ) > StepHeight ) + { + // don't go up ledges higher than a legal step + continue; + } + + if ( !adjArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // this area can be reached by walking from the spawn, so it's legal to drop the bomb here + adjArea->SetAttributeTF( TF_NAV_BOMB_CAN_DROP_HERE ); + } + + adjArea->Mark(); + adjArea->SetParent( area ); + + if ( !adjArea->IsOpen() ) + { + // Since we're doing a breadth-first search, this area will end up at the end of the list. + // Adding it to the tail explicitly saves us a bunch of list traversals. + adjArea->AddToOpenListTail(); + } + } + } +} + + +//------------------------------------------------------------------------- +// For MvM mode. Mark all nav areas where the bomb can drop and the invaders can reach it. +void CTFNavMesh::ComputeBombTargetDistance() +{ + if ( !TFGameRules()->IsMannVsMachineMode() ) + { + return; + } + + CCaptureZone *zone = NULL; + for( int i=0; i<ICaptureZoneAutoList::AutoList().Count(); ++i ) + { + zone = static_cast< CCaptureZone* >( ICaptureZoneAutoList::AutoList()[i] ); + if ( zone->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + break; + } + } + + if ( zone == NULL ) + { + Warning( "Can't find bomb delivery zone." ); + return; + } + + CTFNavArea *zoneArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( zone->WorldSpaceCenter(), false, 500.0f, true ); + if ( !zoneArea ) + { + Warning( "No nav area for bomb delivery zone." ); + return; + } + + // invalidate all travel distances + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + area->m_distanceToBombTarget = -1.0f; + } + + CNavArea::ClearSearchLists(); + + zoneArea->AddToOpenList(); + zoneArea->Mark(); + zoneArea->SetParent( NULL ); + zoneArea->m_distanceToBombTarget = 0.0f; + + CUtlVectorFixedGrowable< const NavConnect *, 64 > adjAreaVector; + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CTFNavArea *area = static_cast< CTFNavArea * >( CNavArea::PopOpenList() ); + + // explore adjacent floor areas + adjAreaVector.RemoveAll(); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + // collect all OUTGOING links from this area to adjacent areas + const NavConnectVector *adjVector = area->GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + adjAreaVector.AddToTail( &(*adjVector)[ bit ] ); + } + } + + FOR_EACH_VEC( adjAreaVector, vit ) + { + const NavConnect *connect = adjAreaVector[ vit ]; + CTFNavArea *adjArea = static_cast< CTFNavArea * >( connect->area ); + + if ( area->ComputeAdjacentConnectionHeightChange( adjArea ) > TF_PLAYER_JUMP_HEIGHT ) + { + // don't go up ledges too high to jump + continue; + } + + // compute travel distance + float newTravelDistance = 0.0f; + + float between = connect->length; + newTravelDistance = area->m_distanceToBombTarget + between; + + float adjacentTravelDistance = adjArea->m_distanceToBombTarget; + + // Found a shortcut to our neighbor passing through this area? + // Use a tolernace. Without it, floating point math can make this loop go on forever, + // because intermediate results are stored at a different precision + float flTol = .001f; + if ( adjacentTravelDistance < 0.0f || adjacentTravelDistance > newTravelDistance + flTol ) + { + adjArea->m_distanceToBombTarget = newTravelDistance; + adjArea->Mark(); + adjArea->SetParent( area ); + + if ( !adjArea->IsOpen() ) + { + // Since we're doing a breadth-first search, this area will end up at the end of the list. + // Adding it to the tail explicitly saves us a bunch of list traversals. + adjArea->AddToOpenListTail(); + } + } + else + { + // Found a shortcut this area that passes through the neighbor? + float newTravelDistanceFromAdjacent = adjacentTravelDistance + between; + if ( newTravelDistanceFromAdjacent + flTol < area->m_distanceToBombTarget ) + { + // check if the reverse direction is cheaper (for the case of jumping off edges) + area->m_distanceToBombTarget = newTravelDistanceFromAdjacent; + area->Mark(); + area->SetParent( adjArea ); + if ( !area->IsOpen() ) + { + // found a cheaper path, try to traverse backward + area->AddToOpenListTail(); + } + } + } + } + } +} + + +//------------------------------------------------------------------------- +void CTFNavMesh::RecomputeInternalData( void ) +{ + CollectControlPointAreas(); + RemoveAllMeshDecoration(); + DecorateMesh(); + ComputeBlockedAreas(); // relies on DecorateMesh() being complete + ComputeIncursionDistances(); + ComputeInvasionAreas(); + ComputeLegalBombDropAreas(); + ComputeBombTargetDistance(); // for MvM + + if ( m_recomputeReason == RESET || m_recomputeReason == SETUP_FINISHED ) + { + // update point-conditionally blocked areas + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->HasAttributeTF( TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE ) ) + { + area->SetAttributeTF( TF_NAV_BLOCKED ); + } + } + } + + if ( m_recomputeReason == POINT_CAPTURED ) + { + // update point-conditionally blocked areas + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->HasAttributeTF( TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE ) ) + { + // which point unblocks us? + + // if no modifier given, unblock after first capture + bool isUnblocked = true; + + if ( area->HasAttributeTF( TF_NAV_WITH_SECOND_POINT ) ) + { + isUnblocked = (m_recomputeReasonWhichPoint >= 1); + } + else if ( area->HasAttributeTF( TF_NAV_WITH_THIRD_POINT ) ) + { + isUnblocked = (m_recomputeReasonWhichPoint >= 2); + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FOURTH_POINT ) ) + { + isUnblocked = (m_recomputeReasonWhichPoint >= 3); + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FIFTH_POINT ) ) + { + isUnblocked = (m_recomputeReasonWhichPoint >= 4); + } + + if ( isUnblocked ) + { + area->ClearAttributeTF( TF_NAV_BLOCKED ); + } + } + else if ( area->HasAttributeTF( TF_NAV_BLOCKED_AFTER_POINT_CAPTURE ) ) + { + // which point blocks us? + + // if no modifier given, block after first capture + bool isBlocked = true; + + if ( area->HasAttributeTF( TF_NAV_WITH_SECOND_POINT ) ) + { + isBlocked = ( m_recomputeReasonWhichPoint >= 1 ); + } + else if ( area->HasAttributeTF( TF_NAV_WITH_THIRD_POINT ) ) + { + isBlocked = ( m_recomputeReasonWhichPoint >= 2 ); + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FOURTH_POINT ) ) + { + isBlocked = ( m_recomputeReasonWhichPoint >= 3 ); + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FIFTH_POINT ) ) + { + isBlocked = ( m_recomputeReasonWhichPoint >= 4 ); + } + + if ( isBlocked ) + { + area->SetAttributeTF( TF_NAV_BLOCKED ); + } + } + } + } + + m_recomputeInternalDataTimer.Invalidate(); +} + +//------------------------------------------------------------------------- +// Re-calculate sentry danger attributes. +void CTFNavMesh::OnObjectChanged() +{ + // Clear all sentry danger attributes. + ResetMeshAttributes( false ); + + CUtlVector< CBaseObject * > ActiveSentries; + ActiveSentries.EnsureCapacity( 16 ); + + // Get a list of all sentries that aren't being carried or dying. + for ( int oit = 0; oit < IBaseObjectAutoList::AutoList().Count(); ++oit ) + { + CBaseObject* obj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[ oit ] ); + + if ( obj->ObjectType() == OBJ_SENTRYGUN ) + { + if ( !obj->IsDying() && !obj->IsCarried() ) + ActiveSentries.AddToTail( obj ); + } + } + + // Only go through the NavAreas if we found some live sentries. Hopefully some of these + // sentries will be able to shoot some spies in the face. + if ( ActiveSentries.Count() ) + { + // We must iterate all of the nav areas because we're testing visibility + // and arbitrary switchback routes make the use of SearchSurroundingAreas + // not useful. + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea *>( TheNavAreas[ it ] ); + + // Check all active sentries against this area. + FOR_EACH_VEC( ActiveSentries, oit ) + { + const CBaseObject* obj = ActiveSentries[ oit ]; + + // If this area in range of this sentry? + Vector close; + area->GetClosestPointOnArea( obj->GetAbsOrigin(), &close ); + + if ( ( obj->GetAbsOrigin() - close ).IsLengthLessThan( SENTRY_MAX_RANGE ) ) + { + // Can this sentry reach this area? + if ( area->IsPartiallyVisible( obj->GetAbsOrigin() + Vector( 0, 0, 30.0f ), obj ) ) + { + // If this area wasn't already added to m_sentryAreas, do it now. + if ( !area->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER | TF_NAV_RED_SENTRY_DANGER ) ) + m_sentryAreas.AddToTail( area ); + + // Mark this area as being potentially dangerous. + area->SetAttributeTF( ( obj->GetTeamNumber() == TF_TEAM_RED ) ? TF_NAV_RED_SENTRY_DANGER : TF_NAV_BLUE_SENTRY_DANGER ); + } + } + } + } + } + + if ( tf_show_sentry_danger.GetBool() ) + DevMsg( "%s: sentries:%d areas count:%d\n", __FUNCTION__, ActiveSentries.Count(), m_sentryAreas.Count() ); +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Return true if a Sentry Gun has been built in the given area + */ +bool CTFNavMesh::IsSentryGunHere( CTFNavArea *area ) const +{ + // Check to see if the area is on the highway to the danger zone. + // If it isn't then there shouldn't be a sentry gun here. + if ( area->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER | TF_NAV_RED_SENTRY_DANGER ) ) + { + // Walk through all the objects built by players + for ( int oit = 0; oit < IBaseObjectAutoList::AutoList().Count(); ++oit ) + { + CBaseObject* obj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[ oit ] ); + + if ( obj->ObjectType() == OBJ_SENTRYGUN ) + { + // If this object is a sentry gun, and it's in this nav area, return true. + if ( GetNearestNavArea( obj ) == area ) + return true; + } + } + } + + return false; +} + + +//------------------------------------------------------------------------- +// Fill given vector will all objects on the given team +void CTFNavMesh::CollectBuiltObjects( CUtlVector< CBaseObject * > *collectionVector, int team ) +{ + collectionVector->RemoveAll(); + + // check all active sentries against this area + for ( int oit = 0; oit < IBaseObjectAutoList::AutoList().Count(); ++oit ) + { + CBaseObject* obj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[ oit ] ); + + if ( team == TEAM_ANY || obj->GetTeamNumber() == team ) + { + collectionVector->AddToTail( obj ); + } + } +} + + +//------------------------------------------------------------------------- +void CTFNavMesh::FireGameEvent( IGameEvent *event ) +{ + CNavMesh::FireGameEvent( event ); + + const CUtlString eventName( event->GetName() ); + + if ( eventName == "teamplay_point_captured" ) + { + int whichPoint = event->GetInt( "cp" ); + + ScheduleRecomputationOfInternalData( POINT_CAPTURED, whichPoint ); + } + else if ( eventName == "teamplay_setup_finished" ) + { + ScheduleRecomputationOfInternalData( SETUP_FINISHED ); + } + else if ( eventName == "teamplay_point_unlocked" ) + { + // recompute since doors may have opened/etc (koth_nucleus) + int whichPoint = event->GetInt( "cp" ); + + ScheduleRecomputationOfInternalData( POINT_UNLOCKED, whichPoint ); + } + else if ( eventName == "player_builtobject" || + eventName == "player_carryobject" || + eventName == "object_detonated" || + eventName == "object_destroyed" ) + { + // We don't need "player_dropobject" as "player_builtobject" is sent right after. + // Some message have "object", some have "objectid" - use the one that is set. + int objecttype = !event->IsEmpty( "objecttype" ) ? event->GetInt( "objecttype" ) : event->GetInt( "object" ); + if ( objecttype == OBJ_SENTRYGUN ) + { + if ( tf_show_sentry_danger.GetBool() ) + DevMsg( "%s: Got sentrygun %s event\n", __FUNCTION__, eventName.Get() ); + + OnObjectChanged(); + } + } +} + + +//------------------------------------------------------------------------- +void CTFNavMesh::BeginCustomAnalysis( bool bIncremental ) +{ + +} + + +//------------------------------------------------------------------------- +// invoked when custom analysis step is complete +void CTFNavMesh::PostCustomAnalysis( void ) +{ + +} + + +//------------------------------------------------------------------------- +void CTFNavMesh::EndCustomAnalysis() +{ + +} + + +//------------------------------------------------------------------------- +/** + * Returns sub-version number of data format used by derived classes + */ +unsigned int CTFNavMesh::GetSubVersionNumber( void ) const +{ + // 1: initial implementation + // 2: added TF-specific attribute flags + return 2; +} + + +//------------------------------------------------------------------------- +/** + * Store custom mesh data for derived classes + */ +void CTFNavMesh::SaveCustomData( CUtlBuffer &fileBuffer ) const +{ + +} + + +//------------------------------------------------------------------------- +/** + * Load custom mesh data for derived classes + */ +void CTFNavMesh::LoadCustomData( CUtlBuffer &fileBuffer, unsigned int subVersion ) +{ + +} + + +//------------------------------------------------------------------------- +/** + * Recompute travel distance from each team's spawn room for each nav area + */ +void CTFNavMesh::ComputeIncursionDistances( void ) +{ + VPROF_BUDGET( "CTFNavMesh::ComputeIncursionDistances", "NextBot" ); + + // invalidate all travel distances + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + for( int i=0; i<TF_TEAM_COUNT; ++i ) + { + area->m_distanceFromSpawnRoom[i] = -1.0f; + } + } + + bool isRedComputed = false; + bool isBlueComputed = false; + for ( int i=0; i<IFuncRespawnRoomAutoList::AutoList().Count(); ++i ) + { + CFuncRespawnRoom *spawnRoom = static_cast< CFuncRespawnRoom* >( IFuncRespawnRoomAutoList::AutoList()[i] ); + + if ( !spawnRoom->GetActive() ) + continue; + + if ( spawnRoom->m_bDisabled ) + continue; + + // find a spawn point inside this room + for ( int i=0; i<ITFTeamSpawnAutoList::AutoList().Count(); ++i ) + { + CTFTeamSpawn *spawnSpot = static_cast< CTFTeamSpawn* >( ITFTeamSpawnAutoList::AutoList()[i] ); + + if ( !spawnSpot->IsTriggered( NULL ) ) + continue; + + if ( spawnSpot->IsDisabled() ) + continue; + + if ( spawnSpot->GetTeamNumber() == TF_TEAM_RED && isRedComputed ) + continue; + + if ( spawnSpot->GetTeamNumber() == TF_TEAM_BLUE && isBlueComputed ) + continue; + + if ( spawnRoom->PointIsWithin( spawnSpot->GetAbsOrigin() ) ) + { + // found a valid spawn spot in an active spawn room, compute travel distances throughout the nav mesh + CTFNavArea *spawnArea = static_cast< CTFNavArea * >( TheTFNavMesh()->GetNearestNavArea( spawnSpot ) ); + if ( spawnArea ) + { + ComputeIncursionDistances( spawnArea, spawnSpot->GetTeamNumber() ); + + if ( spawnSpot->GetTeamNumber() == TF_TEAM_RED ) + { + isRedComputed = true; + } + else + { + isBlueComputed = true; + } + + break; + } + } + } + } + + if ( !isRedComputed ) + { + Warning( "Can't compute incursion distances from the Red spawn room(s). Bots will perform poorly. This is caused by either a missing func_respawnroom, or missing info_player_teamspawn entities within the func_respawnroom.\n" ); + } + + if ( !isBlueComputed ) + { + Warning( "Can't compute incursion distances from the Blue spawn room(s). Bots will perform poorly. This is caused by either a missing func_respawnroom, or missing info_player_teamspawn entities within the func_respawnroom.\n" ); + } + + if ( !TFGameRules()->IsMannVsMachineMode() ) + { + // In Raid mode, the Red (bot) team has no spawn room. + // So, we'll assume the Red incursion distance is the inverse of the Blue incursion distance for now. + // @TODO: Use the Boss battle room as the anchor for computing Red incursion distances + float maxBlueIncursionDistance = 0.0f; + + for( int i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + + if ( area->GetIncursionDistance( TF_TEAM_BLUE ) > maxBlueIncursionDistance ) + { + maxBlueIncursionDistance = area->GetIncursionDistance( TF_TEAM_BLUE ); + } + } + + for( int i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + + if ( area->GetIncursionDistance( TF_TEAM_BLUE ) >= 0.0f ) + { + area->m_distanceFromSpawnRoom[ TF_TEAM_RED ] = maxBlueIncursionDistance - area->GetIncursionDistance( TF_TEAM_BLUE ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Flood-fill outwards, marking flow distance as we go. + * When we reach an area, stop if it already has a lesser travel distance + */ +void CTFNavMesh::ComputeIncursionDistances( CTFNavArea *spawnArea, int team ) +{ + if ( spawnArea == NULL || team < 0 || team >= TF_TEAM_COUNT ) + { + return; + } + + CNavArea::ClearSearchLists(); + + spawnArea->m_distanceFromSpawnRoom[ team ] = 0.0f; + spawnArea->AddToOpenList(); + spawnArea->Mark(); + spawnArea->SetParent( NULL ); + + CUtlVectorFixedGrowable< const NavConnect *, 64 > adjAreaVector; + //TFNavAttributeType teamSpawnRoom = ( team == TF_TEAM_RED ) ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE; + + while( !CNavArea::IsOpenListEmpty() ) + { + // get next area to check + CTFNavArea *area = static_cast< CTFNavArea * >( CNavArea::PopOpenList() ); + + bool bIgnoreBlockedAreas = false; + +#ifdef TF_RAID_MODE + // TODO: Raid mode ignores blocked areas for now (cap gates break this) + if ( TFGameRules()->IsRaidMode() ) + { + bIgnoreBlockedAreas = true; + } +#endif // TF_RAID_MODE + + // TODO: Ditto for Mann Vs Machine mode + if ( TFGameRules()->IsMannVsMachineMode() ) + { + bIgnoreBlockedAreas = true; + } + + if ( !bIgnoreBlockedAreas ) + { + // ignore spawn room exits, since they presumably will be open + // ignore setup gates, since they will be open after the setup time + if ( !area->HasAttributeTF( TF_NAV_SPAWN_ROOM_EXIT | TF_NAV_BLUE_SETUP_GATE | TF_NAV_RED_SETUP_GATE ) && area->IsBlocked( team ) ) + { + // don't pass through blocked areas + continue; + } + } + + // explore adjacent floor areas + adjAreaVector.RemoveAll(); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + // collect all OUTGOING links from this area to adjacent areas + const NavConnectVector *adjVector = area->GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + adjAreaVector.AddToTail( &(*adjVector)[ bit ] ); + } + } + + FOR_EACH_VEC( adjAreaVector, vit ) + { + const NavConnect *connect = adjAreaVector[ vit ]; + CTFNavArea *adjArea = static_cast< CTFNavArea * >( connect->area ); + + if ( area->ComputeAdjacentConnectionHeightChange( adjArea ) > TF_PLAYER_JUMP_HEIGHT ) + { + // don't go up ledges too high to jump + continue; + } + + // compute travel distance + float newTravelDistance = 0.0f; + + // travel distance is zero in all areas of our spawn room + // if ( !adjArea->HasAttributeTF( teamSpawnRoom ) ) + { + float between = connect->length; + newTravelDistance = area->m_distanceFromSpawnRoom[ team ] + between; + } + + float adjacentTravelDistance = adjArea->m_distanceFromSpawnRoom[ team ]; + + if ( adjacentTravelDistance < 0.0f || adjacentTravelDistance > newTravelDistance ) + { + adjArea->m_distanceFromSpawnRoom[ team ] = newTravelDistance; + adjArea->Mark(); + adjArea->SetParent( area ); + + if ( !adjArea->IsOpen() ) + { + // Since we're doing a breadth-first search, this area will end up at the end of the list. + // Adding it to the tail explicitly saves us a bunch of list traversals. + adjArea->AddToOpenListTail(); + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavMesh::ComputeInvasionAreas( void ) +{ + VPROF_BUDGET( "CTFNavMesh::ComputeInvasionAreas", "NextBot" ); + + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + area->ComputeInvasionAreaVectors(); + } +} + + +//-------------------------------------------------------------------------------------------------------- +class CCollectAndLabelSpawnRoomAreas +{ +public: + CCollectAndLabelSpawnRoomAreas( void ) + { + m_room = NULL; + } + + void Init( CFuncRespawnRoom *room, int team, CUtlVector< CTFNavArea * > *areaVector ) + { + m_room = room; + m_team = team; + m_areaVector = areaVector; + } + + bool operator() ( CNavArea *baseArea ) + { + static Vector stepHeight( 0.0f, 0.0f, 18.0f ); + + if ( !m_room ) + return true; + + if ( m_room->PointIsWithin( baseArea->GetCenter() + stepHeight ) || + m_room->PointIsWithin( baseArea->GetCorner( NORTH_WEST ) + stepHeight ) || + m_room->PointIsWithin( baseArea->GetCorner( NORTH_EAST ) + stepHeight ) || + m_room->PointIsWithin( baseArea->GetCorner( SOUTH_WEST ) + stepHeight ) || + m_room->PointIsWithin( baseArea->GetCorner( SOUTH_EAST ) + stepHeight ) ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + area->SetAttributeTF( ( m_team == TF_TEAM_RED ) ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE ); + + m_areaVector->AddToTail( area ); + } + + return true; + } + + CFuncRespawnRoom *m_room; + int m_team; + CUtlVector< CTFNavArea * > *m_areaVector; +}; + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavMesh::CollectAndMarkSpawnRoomExits( CTFNavArea *area, CUtlVector< CTFNavArea * > *exitAreaVector ) +{ + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + const NavConnectVector *connect = area->GetAdjacentAreas( (NavDirType)dir ); + if ( connect ) + { + FOR_EACH_VEC( (*connect), cit ) + { + CTFNavArea *adjArea = (CTFNavArea *)connect->Element(cit).area; + + if ( !adjArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // adjacent area leads out of spawn room - this is an exit + area->SetAttributeTF( TF_NAV_SPAWN_ROOM_EXIT ); + exitAreaVector->AddToTail( area ); + return; + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavMesh::DecorateMesh( void ) +{ + VPROF_BUDGET( "CTFNavMesh::DecorateMesh", "NextBot" ); + + CBaseEntity *entity = NULL; + CCollectAndLabelSpawnRoomAreas collectAndLabel; + Extent extent; + + // mark spawn rooms + m_redSpawnRoomAreaVector.RemoveAll(); + m_blueSpawnRoomAreaVector.RemoveAll(); + + for ( int iFuncRespawnRoom=0; iFuncRespawnRoom<IFuncRespawnRoomAutoList::AutoList().Count(); ++iFuncRespawnRoom ) + { + CFuncRespawnRoom *respawnRoom = static_cast< CFuncRespawnRoom* >( IFuncRespawnRoomAutoList::AutoList()[iFuncRespawnRoom] ); + + if ( !respawnRoom->GetActive() ) + continue; + + if ( respawnRoom->m_bDisabled ) + continue; + + // func_respawn rooms only enforce spawn room rules. We need to search for enabled + // info_player_teamspawn entities contained within an active func_respawnroom in + // order to locate the current set of active spawn rooms + // find a spawn point inside this room + for ( int iTFTeamSpawn=0; iTFTeamSpawn<ITFTeamSpawnAutoList::AutoList().Count(); ++iTFTeamSpawn ) + { + CTFTeamSpawn *spawnSpot = static_cast< CTFTeamSpawn* >( ITFTeamSpawnAutoList::AutoList()[iTFTeamSpawn] ); + + if ( !spawnSpot->IsTriggered( NULL ) ) + continue; + + if ( spawnSpot->IsDisabled() ) + continue; + + if ( respawnRoom->PointIsWithin( spawnSpot->GetAbsOrigin() ) ) + { + // found a valid spawn spot in an active spawn room + collectAndLabel.Init( respawnRoom, spawnSpot->GetTeamNumber(), spawnSpot->GetTeamNumber() == TF_TEAM_RED ? &m_redSpawnRoomAreaVector : &m_blueSpawnRoomAreaVector ); + extent.Init( respawnRoom ); + + TheNavMesh->ForAllAreasOverlappingExtent( collectAndLabel, extent ); + } + } + } + + // mark each spawn room area adjacent to a non-spawn room area as an exit + m_redSpawnRoomExitAreaVector.RemoveAll(); + m_blueSpawnRoomExitAreaVector.RemoveAll(); + + FOR_EACH_VEC( m_redSpawnRoomAreaVector, rit ) + { + CollectAndMarkSpawnRoomExits( m_redSpawnRoomAreaVector[ rit ], &m_redSpawnRoomExitAreaVector ); + } + + FOR_EACH_VEC( m_blueSpawnRoomAreaVector, bit ) + { + CollectAndMarkSpawnRoomExits( m_blueSpawnRoomAreaVector[ bit ], &m_blueSpawnRoomExitAreaVector ); + } + + + // mark ammo areas + entity = NULL; + while( ( entity = gEntList.FindEntityByClassname( entity, "item_ammopack*" ) ) != NULL ) + { + CTFNavArea *area = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( entity->GetAbsOrigin() ); + if ( area ) + { + area->SetAttributeTF( TF_NAV_HAS_AMMO ); + } + } + + // mark health areas + entity = NULL; + while( ( entity = gEntList.FindEntityByClassname( entity, "item_healthkit*" ) ) != NULL ) + { + CTFNavArea *area = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( entity->GetAbsOrigin() ); + if ( area ) + { + area->SetAttributeTF( TF_NAV_HAS_HEALTH ); + } + } + + // mark control points + for( int p=0; p<MAX_CONTROL_POINTS; ++p ) + { + CUtlVector< CTFNavArea * > *pointAreaVector = &m_controlPointAreaVector[ p ]; + + for( int i=0; i<pointAreaVector->Count(); ++i ) + { + pointAreaVector->Element(i)->SetAttributeTF( TF_NAV_CONTROL_POINT ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavMesh::RemoveAllMeshDecoration( void ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + // wipe all non-persistent attributes + area->ClearAttributeTF( (TFNavAttributeType)( ~TF_NAV_PERSISTENT_ATTRIBUTES ) ); + } + + // We just cleared all our SENTRY_DANGER attributes. Wipe m_sentryAreas and recompute. + m_sentryAreas.RemoveAll(); + OnObjectChanged(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CTFNavMesh::ResetMeshAttributes( bool bScheduleRecomputation ) +{ + // Clear all sentry danger attributes. + FOR_EACH_VEC( m_sentryAreas, nit ) + { + // One of the sentry danger attributes should be set. + Assert( bScheduleRecomputation || m_sentryAreas[ nit ]->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER | TF_NAV_RED_SENTRY_DANGER ) ); + m_sentryAreas[ nit ]->ClearAttributeTF( TF_NAV_BLUE_SENTRY_DANGER | TF_NAV_RED_SENTRY_DANGER ); + } + m_sentryAreas.RemoveAll(); + +#ifdef DBGFLAG_ASSERT + FOR_EACH_VEC( TheNavAreas, it ) + { + // Sentry danger attributes should not be set anywhere. + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + Assert( !area->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER | TF_NAV_RED_SENTRY_DANGER ) ); + } +#endif + + if ( bScheduleRecomputation ) + { + ScheduleRecomputationOfInternalData( RESET ); + } +} + + +//-------------------------------------------------------------------------------------------------------- +class DrawIncursionFlow +{ +public: + bool operator() ( CNavArea *baseArea ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( baseArea ); + + int team = ( tf_show_incursion_flow.GetInt() == 1 ) ? TF_TEAM_RED : TF_TEAM_BLUE; + + const float cycleRange = 2500.0f; + const float cycleRate = 0.333f; // cycles/sec + + float baseFlow = area->GetIncursionDistance( team ); + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + const NavConnectVector *adjVector = area->GetAdjacentAreas( (NavDirType)dir ); + FOR_EACH_VEC( (*adjVector), bit ) + { + CTFNavArea *adjArea = static_cast< CTFNavArea * >( (*adjVector)[ bit ].area ); + + if ( area->ComputeAdjacentConnectionHeightChange( adjArea ) > TF_PLAYER_JUMP_HEIGHT ) + { + // don't go up ledges too high to jump + continue; + } + + float adjFlow = adjArea->GetIncursionDistance( team ); + + if ( adjFlow > baseFlow ) + { + float cycle = fmod( adjFlow - ( gpGlobals->curtime * cycleRate * cycleRange ), cycleRange ); + float t = 2.0f * cycle / cycleRange; + if ( t > 1.0f ) + { + t = 2.0f - t; + } + + int r, g, b; + if ( team == TF_TEAM_RED ) + { + r = 255 * t; + g = 0; + b = 0; + } + else + { + r = 0; + g = 0; + b = 255 * t; + } + + NDebugOverlay::HorzArrow( area->GetCenter(), adjArea->GetCenter(), 5.0f, r, g, b, 255, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + + return true; + } +}; + + +void CTFNavMesh::UpdateDebugDisplay( void ) const +{ + // avoid Warning() spam from UTIL_GetListenServerHost when on a dedicated server + if ( engine->IsDedicatedServer() ) + return; + + CBasePlayer *player = UTIL_GetListenServerHost(); + if ( player == NULL ) + return; + + + if ( tf_show_in_combat_areas.GetBool() ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->IsInCombat() ) + { + float t = area->GetCombatIntensity(); + area->DrawFilled( t * 255, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } + + if ( tf_show_enemy_invasion_areas.GetBool() ) + { + CTFNavArea *myArea = static_cast< CTFNavArea * >( player->GetLastKnownArea() ); + + if ( myArea ) + { + const CUtlVector< CTFNavArea * > &invasionAreaVector = myArea->GetEnemyInvasionAreaVector( player->GetTeamNumber() ); + + FOR_EACH_VEC( invasionAreaVector, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( invasionAreaVector[ it ] ); + + area->DrawFilled( 255, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } + + if ( tf_show_bomb_drop_areas.GetBool() ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->HasAttributeTF( TF_NAV_BOMB_CAN_DROP_HERE ) ) + { + area->DrawFilled( 0, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } + + if ( tf_show_blocked_areas.GetBool() ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + const char *describe = ""; + + if ( area->HasAttributeTF( TF_NAV_BLOCKED ) ) + { + area->DrawFilled( 255, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true, 0.0f ); + } + + if ( area->IsBlocked( TF_TEAM_RED ) ) + { + if ( area->IsBlocked( TF_TEAM_BLUE ) ) + { + area->DrawFilled( 100, 0, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Blocked for All"; + } + else + { + area->DrawFilled( 100, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Blocked for Red"; + } + } + else if ( area->IsBlocked( TF_TEAM_BLUE ) ) + { + area->DrawFilled( 0, 0, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Blocked for Blue"; + } + + if ( describe && TheNavMesh->GetSelectedArea() == area ) + { + NDebugOverlay::Text( area->GetCenter(), describe, false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + + if ( tf_show_incursion_flow.GetInt() > 0 || tf_show_incursion_flow_gradient.GetInt() > 0 ) + { + Vector forward; + AngleVectors( player->EyeAngles() + player->GetPunchAngle(), &forward ); + + float maxRange = 2000.0f; + Vector to = player->EyePosition() + maxRange * forward; + + trace_t result; + CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING ); + UTIL_TraceLine( player->EyePosition(), to, MASK_NPCSOLID, &filter, &result ); + + CTFNavArea *selectedArea = static_cast< CTFNavArea * >( TheNavMesh->GetNearestNavArea( result.endpos, false, 500.0f ) ); + + if ( selectedArea ) + { + if ( tf_show_incursion_flow.GetInt() > 0 ) + { + DrawIncursionFlow draw; + SearchSurroundingAreas( selectedArea, selectedArea->GetCenter(), draw, tf_show_incursion_flow_range.GetFloat() ); + } + else if ( tf_show_incursion_flow_gradient.GetInt() > 0 ) + { + int myTeam; + int r,g,b; + if ( tf_show_incursion_flow_gradient.GetInt() == 1 ) + { + myTeam = TF_TEAM_RED; + r = 255; + g = 0; + b = 0; + } + else + { + myTeam = TF_TEAM_BLUE; + r = 0; + g = 0; + b = 255; + } + + selectedArea->DrawFilled( r, g, b, 255 ); + + CUtlVector< CTFNavArea * > areaVector; + selectedArea->CollectPriorIncursionAreas( myTeam, &areaVector ); + FOR_EACH_VEC( areaVector, p ) + { + areaVector[p]->DrawFilled( r/2, g/2, b/2, 255 ); + } + + selectedArea->CollectNextIncursionAreas( myTeam, &areaVector ); + FOR_EACH_VEC( areaVector, n ) + { + areaVector[n]->DrawFilled( MIN( r+100, 255 ), MIN( g+100, 255 ), MIN( b+100, 255 ), 255 ); + } + } + } + } + + if ( tf_show_mesh_decoration.GetBool() && !tf_show_mesh_decoration_manual.GetBool() ) + { + // render these from cached vectors to verify their data + int i; + const CUtlVector< CTFNavArea * > *areaVector; + + areaVector = GetSpawnRoomAreas( TF_TEAM_BLUE ); + if ( areaVector ) + { + for( i=0; i<areaVector->Count(); ++i ) + { + CTFNavArea *area = areaVector->Element(i); + + if ( !area->HasAttributeTF( TF_NAV_SPAWN_ROOM_EXIT ) ) + { + area->DrawFilled( 0, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + + if ( TheNavMesh->GetSelectedArea() == area ) + { + NDebugOverlay::Text( area->GetCenter(), "Blue Spawn Room", false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + } + + areaVector = GetSpawnRoomExitAreas( TF_TEAM_BLUE ); + if ( areaVector ) + { + for( i=0; i<areaVector->Count(); ++i ) + { + CTFNavArea *area = areaVector->Element(i); + + area->DrawFilled( 150, 150, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + + if ( TheNavMesh->GetSelectedArea() == area ) + { + NDebugOverlay::Text( area->GetCenter(), "Blue Spawn Exit", false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + + areaVector = GetSpawnRoomAreas( TF_TEAM_RED ); + if ( areaVector ) + { + for( i=0; i<areaVector->Count(); ++i ) + { + CTFNavArea *area = areaVector->Element(i); + + if ( !area->HasAttributeTF( TF_NAV_SPAWN_ROOM_EXIT ) ) + { + area->DrawFilled( 255, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + + if ( TheNavMesh->GetSelectedArea() == area ) + { + NDebugOverlay::Text( area->GetCenter(), "Red Spawn Room", false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + } + + areaVector = GetSpawnRoomExitAreas( TF_TEAM_RED ); + if ( areaVector ) + { + for( i=0; i<areaVector->Count(); ++i ) + { + CTFNavArea *area = areaVector->Element(i); + + area->DrawFilled( 255, 150, 150, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + + if ( TheNavMesh->GetSelectedArea() == area ) + { + NDebugOverlay::Text( area->GetCenter(), "Red Spawn Exit", false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + } + + + if ( tf_show_mesh_decoration.GetBool() || tf_show_mesh_decoration_manual.GetBool() ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + const char *describe = ""; + + if ( !tf_show_mesh_decoration_manual.GetBool() ) + { + if ( area->HasAttributeTF( TF_NAV_HAS_AMMO ) && area->HasAttributeTF( TF_NAV_HAS_HEALTH ) ) + { + area->DrawFilled( 255, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Health & Ammo"; + } + else + { + if ( area->HasAttributeTF( TF_NAV_HAS_AMMO ) ) + { + area->DrawFilled( 100, 100, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Ammo"; + } + else if ( area->HasAttributeTF( TF_NAV_HAS_HEALTH ) ) + { + area->DrawFilled( 255, 150, 150, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Health"; + } + } + + if ( area->HasAttributeTF( TF_NAV_CONTROL_POINT ) ) + { + area->DrawFilled( 0, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Control Point"; + } + + if ( area->HasAttributeTF( TF_NAV_BLUE_ONE_WAY_DOOR ) ) + { + area->DrawFilled( 100, 100, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + + if ( area->HasAttributeTF( TF_NAV_RED_ONE_WAY_DOOR ) ) + { + area->DrawFilled( 255, 100, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + + if ( area->HasAttributeTF( TF_NAV_SNIPER_SPOT ) ) + { + area->DrawFilled( 255, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Sniper Spot"; + } + + if ( area->HasAttributeTF( TF_NAV_SENTRY_SPOT ) ) + { + area->DrawFilled( 255, 100, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Sentry Spot"; + } + + if ( area->HasAttributeTF( TF_NAV_NO_SPAWNING ) ) + { + area->DrawFilled( 100, 100, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "No Spawning"; + } + + if ( area->HasAttributeTF( TF_NAV_RESCUE_CLOSET ) ) + { + area->DrawFilled( 0, 255, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Rescue Closet"; + } + + if ( area->HasAttributeTF( TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE ) ) + { + area->DrawFilled( 0, 255, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + + if ( area->HasAttributeTF( TF_NAV_WITH_SECOND_POINT ) ) + { + describe = "Blocked Until Second Point Captured"; + } + else if ( area->HasAttributeTF( TF_NAV_WITH_THIRD_POINT ) ) + { + describe = "Blocked Until Third Point Captured"; + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FOURTH_POINT ) ) + { + describe = "Blocked Until Fourth Point Captured"; + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FIFTH_POINT ) ) + { + describe = "Blocked Until Fifth Point Captured"; + } + else + { + describe = "Blocked Until First Point Captured"; + } + } + + if ( area->HasAttributeTF( TF_NAV_BLOCKED_AFTER_POINT_CAPTURE ) ) + { + area->DrawFilled( 255, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + + if ( area->HasAttributeTF( TF_NAV_WITH_SECOND_POINT ) ) + { + describe = "Blocked After Second Point Captured"; + } + else if ( area->HasAttributeTF( TF_NAV_WITH_THIRD_POINT ) ) + { + describe = "Blocked After Third Point Captured"; + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FOURTH_POINT ) ) + { + describe = "Blocked After Fourth Point Captured"; + } + else if ( area->HasAttributeTF( TF_NAV_WITH_FIFTH_POINT ) ) + { + describe = "Blocked After Fifth Point Captured"; + } + else + { + describe = "Blocked After First Point Captured"; + } + } + + if ( area->HasAttributeTF( TF_NAV_BLUE_SETUP_GATE ) ) + { + area->DrawFilled( 0, 0, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Blue Setup Gate"; + } + + if ( area->HasAttributeTF( TF_NAV_RED_SETUP_GATE ) ) + { + area->DrawFilled( 100, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Red Setup Gate"; + } + + if ( area->HasAttributeTF( TF_NAV_DOOR_ALWAYS_BLOCKS ) ) + { + area->DrawFilled( 100, 0, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Door Always Blocks"; + } + + if ( area->HasAttributeTF( TF_NAV_DOOR_NEVER_BLOCKS ) ) + { + area->DrawFilled( 0, 100, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Door Never Blocks"; + } + + if ( area->HasAttributeTF( TF_NAV_UNBLOCKABLE ) ) + { + area->DrawFilled( 0, 200, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + describe = "Unblockable"; + } + + if ( describe && TheNavMesh->GetSelectedArea() == area ) + { + NDebugOverlay::Text( area->GetCenter(), describe, false, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + + if ( tf_show_sentry_danger.GetBool() ) + { + if ( tf_show_sentry_danger.GetInt() == 2 ) + { + // Walk all TheNavAreas entries. Left this code in to help debug in case + // TheNavAreas is never not _exactly_ the same as m_sentryAreas. + FOR_EACH_VEC( TheNavAreas, it ) + { + const CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + int r = area->HasAttributeTF( TF_NAV_RED_SENTRY_DANGER ) * 255; + int b = area->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER ) * 255; + + if ( r || b ) + { + area->DrawFilled( r, 0, b, 80, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } + else + { + // Only go through the m_SentryAreas entries. Should be the same as walking the + // entire TheNavAreas, but a lot faster. + FOR_EACH_VEC( m_sentryAreas, nit ) + { + const CTFNavArea *area = m_sentryAreas[ nit ]; + int r = area->HasAttributeTF( TF_NAV_RED_SENTRY_DANGER ) * 255; + int b = area->HasAttributeTF( TF_NAV_BLUE_SENTRY_DANGER ) * 255; + + if ( r || b ) + { + area->DrawFilled( r, 0, b, 80, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } + } + + if ( tf_show_actor_potential_visibility.GetBool() ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) + { + if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_RED ) ) + { + area->DrawFilled( 255, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + else + { + area->DrawFilled( 0, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + else if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_RED ) ) + { + area->DrawFilled( 255, 0, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } + +/* + if ( tf_show_gate_defense_areas.GetBool() ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->HasAttributeTF( TF_NAV_DEFEND_SETUP_GATES ) ) + { + if ( area->HasAttributeTF( TF_NAV_DEFEND_VIA_SNIPING ) ) + area->DrawFilled( 0, 255, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + else if ( area->HasAttributeTF( TF_NAV_DEFEND_VIA_AMBUSH ) ) + area->DrawFilled( 255, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + else + area->DrawFilled( 0, 0, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } + + if ( tf_show_point_defense_areas.GetBool() ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ it ] ); + + if ( area->HasAttributeTF( TF_NAV_DEFEND_POINT ) ) + { + if ( area->HasAttributeTF( TF_NAV_DEFEND_VIA_SNIPING ) ) + area->DrawFilled( 0, 255, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + else if ( area->HasAttributeTF( TF_NAV_DEFEND_VIA_AMBUSH ) ) + area->DrawFilled( 255, 150, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + else + area->DrawFilled( 0, 150, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, true ); + } + } + } +*/ + + if ( tf_show_control_points.GetBool() ) + { + for( int which=0; which<MAX_CONTROL_POINTS; ++which ) + { + for( int i=0; i<m_controlPointAreaVector[ which ].Count(); ++i ) + { + CTFNavArea *area = m_controlPointAreaVector[ which ][ i ]; + + if ( m_controlPointCenterAreaVector[ which ] == area ) + { + area->DrawFilled( 255, 255, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + else + { + area->DrawFilled( 255, 150, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Populate the given "ambushVector" with good areas to lurk in ambush for the invading enemy team + */ +void CTFNavMesh::CollectAmbushAreas( CUtlVector< CTFNavArea * > *ambushVector, CTFNavArea *startArea, int teamToAmbush, float searchRadius, float incursionTolerance ) const +{ + ScanSelectAmbushAreas selector( ambushVector, teamToAmbush, startArea->GetIncursionDistance( teamToAmbush ) + incursionTolerance ); + SearchSurroundingAreas( startArea, startArea->GetCenter(), selector, searchRadius ); +} + + +//-------------------------------------------------------------------------------------------------------- +/** + * Populate the given vector with areas that are just outside of the given team's spawn room(s) + */ +void CTFNavMesh::CollectSpawnRoomThresholdAreas( CUtlVector< CTFNavArea * > *spawnExitAreaVector, int team ) const +{ + const CUtlVector< CTFNavArea * > *exitAreaVector = GetSpawnRoomExitAreas( team ); + + if ( !exitAreaVector ) + return; + + for( int i=0; i<exitAreaVector->Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + + // find largest non-spawn-room area connected to this exit + CTFNavArea *exitArea = NULL; + float exitAreaSize = 0.0f; + + for( int dir=0; dir<NUM_DIRECTIONS; ++dir ) + { + const NavConnectVector *adjConnect = area->GetAdjacentAreas( (NavDirType)dir ); + + for( int j=0; j<adjConnect->Count(); ++j ) + { + CTFNavArea *adjArea = (CTFNavArea *)adjConnect->Element(j).area; + + if ( !adjArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_RED | TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_EXIT ) ) + { + // this area is outside of the spawn room + float size = adjArea->GetSizeX() * adjArea->GetSizeY(); + if ( size > exitAreaSize ) + { + exitArea = adjArea; + exitAreaSize = size; + } + } + } + } + + if ( exitArea ) + { + spawnExitAreaVector->AddToTail( exitArea ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------- +// Populate the given vector with areas that have a bomb travel distance within the given range +void CTFNavMesh::CollectAreaWithinBombTravelRange( CUtlVector< CTFNavArea * > *spawnExitAreaVector, float minTravel, float maxTravel ) const +{ + for( int i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( TheNavAreas[ i ] ); + + float travelDistance = area->GetTravelDistanceToBombTarget(); + + if ( travelDistance >= minTravel && travelDistance <= maxTravel ) + { + spawnExitAreaVector->AddToTail( area ); + } + } +} diff --git a/game/server/tf/nav_mesh/tf_nav_mesh.h b/game/server/tf/nav_mesh/tf_nav_mesh.h new file mode 100644 index 0000000..19ae078 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_nav_mesh.h @@ -0,0 +1,235 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_nav_mesh.h +// TF specific nav mesh +// Michael Booth, February 2009 + +#ifndef TF_NAV_MESH_H +#define TF_NAV_MESH_H + +#include "nav_mesh.h" +#include "tf_nav_area.h" +#include "tf_obj_teleporter.h" + +#define TF_PLAYER_JUMP_HEIGHT 45.0f // non crouch-jumping + +class CBaseObject; +class CObjectTeleporter; +class CTFPlayer; + +//------------------------------------------------------------------------- +// General purpose collector class for ForAllArea-style functor methods +class CTFAreaCollector +{ +public: + bool operator() ( CNavArea *area ) + { + m_vector.AddToTail( (CTFNavArea *)area ); + return true; + } + + CUtlVector< CTFNavArea * > m_vector; +}; + + +//------------------------------------------------------------------------- +class CTFNavMesh : public CNavMesh +{ +public: + CTFNavMesh( void ); + + virtual CTFNavArea *CreateArea( void ) const; // CNavArea factory + + virtual void Update( void ); // invoked on each game frame + + 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 OnServerActivate( void ); // (EXTEND) invoked when server loads a new map + virtual void OnRoundRestart( void ); // invoked when a game round restarts + + virtual void FireGameEvent( IGameEvent *event ); + + /** + * Return true if nav mesh can be trusted for all climbing/jumping decisions because game environment is fairly simple. + * Authoritative meshes mean path followers can skip CPU intesive realtime scanning of unpredictable geometry. + */ + virtual bool IsAuthoritative( void ) const { return true; } // TF2 has nice clean environments + + virtual unsigned int GetGenerationTraceMask( void ) const; // return the mask used by traces when generating the mesh + + void OnObjectChanged(); + bool IsSentryGunHere( CTFNavArea *area ) const; // return true if a Sentry Gun has been built in the given area + + void CollectBuiltObjects( CUtlVector< CBaseObject * > *collectionVector, int team = TEAM_ANY ); // fill given vector will all objects on the given team + + struct BallisticLaunchInfo + { + Vector m_launchSpot; // where to stand + float m_aimYaw; // how to aim + float m_aimPitch; // how to aim + float m_chargeTime; // how long to charge weapon + }; + + // populate the given vector with ways to launch grenades to hit the given building + void CollectBallisticAttackInfo( CBaseObject *building, CUtlVector< BallisticLaunchInfo > *infoVector ) const; + + void ResetMeshAttributes( bool bScheduleRecomputation ); + + void CollectControlPointAreas( void ); + + void DecorateMesh( void ); + void DecorateMeshTacticalHints( void ); + void RemoveAllMeshDecoration( void ); + + // populate the given "ambushVector" with good areas to lurk in ambush for the invading enemy team + void CollectAmbushAreas( CUtlVector< CTFNavArea * > *ambushVector, CTFNavArea *startArea, int teamToAmbush, float searchRadius, float incursionTolerance = 300.0f ) const; + + // populate the given vector with areas that are just outside of the given team's spawn room(s) + void CollectSpawnRoomThresholdAreas( CUtlVector< CTFNavArea * > *spawnExitAreaVector, int team ) const; + + // populate the given vector with areas that have a bomb travel distance within the given range + void CollectAreaWithinBombTravelRange( CUtlVector< CTFNavArea * > *spawnExitAreaVector, float minTravel, float maxTravel ) const; + + const CUtlVector< CTFNavArea * > *GetSetupGateDefenseAreas( void ) const; // return vector of areas that are good for defending enemies coming out of the blue setup gates + const CUtlVector< CTFNavArea * > *GetControlPointAreas( int pointIndex ) const; // return vector of areas overlapping the given control point + CTFNavArea *GetControlPointCenterArea( int pointIndex ) const; // return area overlapping the center of the given control point + const CUtlVector< CTFNavArea * > *GetSpawnRoomAreas( int team ) const; // return vector of areas within the given team spawn room(s) + const CUtlVector< CTFNavArea * > *GetSpawnRoomExitAreas( int team ) const; // return vector of areas where the given team exits their spawn room(s) + + enum RecomputeReasonType + { + RESET, + SETUP_FINISHED, + POINT_CAPTURED, + POINT_UNLOCKED, + BLOCKED_STATUS_CHANGED, + MAP_LOGIC + }; + void ScheduleRecomputationOfInternalData( RecomputeReasonType reason, int whichPoint ); + + +protected: + virtual void BeginCustomAnalysis( bool bIncremental ); + virtual void PostCustomAnalysis( void ); // invoked when custom analysis step is complete + virtual void EndCustomAnalysis(); + +private: + void ComputeIncursionDistances( void ); // recompute travel distance from each team's spawn room for each nav area + void ComputeIncursionDistances( CTFNavArea *spawnArea, int team ); + void ComputeInvasionAreas( void ); + void ComputeLegalBombDropAreas( void ); + void ComputeBombTargetDistance(); + + void UpdateDebugDisplay( void ) const; + + void OnBlockedAreasChanged( void ); + + void ComputeBlockedAreas( void ); + + CountdownTimer m_recomputeInternalDataTimer; // if started, when counts down recompute internal data to give various map logic time to complete + RecomputeReasonType m_recomputeReason; + int m_recomputeReasonWhichPoint; + void RecomputeInternalData( void ); + + // Array of areas with sentry danger attributes set. + CUtlVector< CTFNavArea * > m_sentryAreas; + + CUtlVector< CTFNavArea * > m_setupGateDefenseAreaVector; + + CUtlVector< CTFNavArea * > m_controlPointAreaVector[ MAX_CONTROL_POINTS ]; + CTFNavArea *m_controlPointCenterAreaVector[ MAX_CONTROL_POINTS ]; + + CUtlVector< CTFNavArea * > m_redSpawnRoomAreaVector; + CUtlVector< CTFNavArea * > m_blueSpawnRoomAreaVector; + + CUtlVector< CTFNavArea * > m_redSpawnRoomExitAreaVector; + CUtlVector< CTFNavArea * > m_blueSpawnRoomExitAreaVector; + void CollectAndMarkSpawnRoomExits( CTFNavArea *area, CUtlVector< CTFNavArea * > *exitAreaVector ); + + CountdownTimer m_watchCartTimer; + + int m_priorBotCount; +}; + + +inline void CTFNavMesh::ScheduleRecomputationOfInternalData( CTFNavMesh::RecomputeReasonType reason, int whichPoint = 0 ) +{ + m_recomputeInternalDataTimer.Start( 2.0f ); + m_recomputeReason = reason; + m_recomputeReasonWhichPoint = whichPoint; +} + + +inline const CUtlVector< CTFNavArea * > *CTFNavMesh::GetSpawnRoomAreas( int team ) const +{ + if ( team == TF_TEAM_RED ) + { + return &m_redSpawnRoomAreaVector; + } + + if ( team == TF_TEAM_BLUE ) + { + return &m_blueSpawnRoomAreaVector; + } + + return NULL; +} + +inline const CUtlVector< CTFNavArea * > *CTFNavMesh::GetSpawnRoomExitAreas( int team ) const +{ + if ( team == TF_TEAM_RED ) + { + return &m_redSpawnRoomExitAreaVector; + } + + if ( team == TF_TEAM_BLUE ) + { + return &m_blueSpawnRoomExitAreaVector; + } + + return NULL; +} + +inline const CUtlVector< CTFNavArea * > *CTFNavMesh::GetControlPointAreas( int pointIndex ) const +{ + if ( pointIndex < 0 || pointIndex >= MAX_CONTROL_POINTS ) + { + return NULL; + } + + return &m_controlPointAreaVector[ pointIndex ]; +} + +inline CTFNavArea *CTFNavMesh::GetControlPointCenterArea( int pointIndex ) const +{ + if ( pointIndex < 0 || pointIndex >= MAX_CONTROL_POINTS ) + { + return NULL; + } + + return m_controlPointCenterAreaVector[ pointIndex ]; +} + +inline const CUtlVector< CTFNavArea * > *CTFNavMesh::GetSetupGateDefenseAreas( void ) const +{ + return &m_setupGateDefenseAreaVector; +} + + +inline unsigned int CTFNavMesh::GetGenerationTraceMask( void ) const +{ + return MASK_PLAYERSOLID_BRUSHONLY; +} + + +inline CTFNavMesh *TheTFNavMesh( void ) +{ + return reinterpret_cast< CTFNavMesh * >( TheNavMesh ); +} + + +extern TFNavAttributeType NameToTFAttribute( const char *name ); +extern const char *TFAttributeToName( TFNavAttributeType attribute ); + +#endif // TF_NAV_MESH_H
\ No newline at end of file diff --git a/game/server/tf/nav_mesh/tf_nav_mesh_edit.cpp b/game/server/tf/nav_mesh/tf_nav_mesh_edit.cpp new file mode 100644 index 0000000..ee9baee --- /dev/null +++ b/game/server/tf/nav_mesh/tf_nav_mesh_edit.cpp @@ -0,0 +1,306 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_nav_mesh_edit.cpp +// TF specific nav mesh editing +// Michael Booth, May 2009 + +#include "cbase.h" +#include "tf_nav_mesh.h" + + +//-------------------------------------------------------------------------------------------------------- +class CTFAttributeClearer +{ +public: + CTFAttributeClearer( TFNavAttributeType attribute ) + { + m_attribute = attribute; + } + + bool operator() ( CNavArea *baseArea ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + area->ClearAttributeTF( m_attribute ); + + return true; + } + + TFNavAttributeType m_attribute; +}; + +void TF_EditClearAllAttributes( void ) +{ + CTFAttributeClearer clear( (TFNavAttributeType)0xFFFFFFFF ); + TheNavMesh->ForAllSelectedAreas( clear ); + TheNavMesh->ClearSelectedSet(); +} +static ConCommand ClearAllAttributes( "tf_wipe_attributes", TF_EditClearAllAttributes, "Clear all TF-specific attributes of selected area.", FCVAR_CHEAT ); + + +//-------------------------------------------------------------------------------------------------------- +struct AttributeLookup +{ + const char *name; + TFNavAttributeType attribute; +}; + +static AttributeLookup s_TFAttributeTable[] = +{ + { "BLUE_SETUP_GATE", TF_NAV_BLUE_SETUP_GATE }, + { "RED_SETUP_GATE", TF_NAV_RED_SETUP_GATE }, + { "BLOCKED_AFTER_POINT_CAPTURE", TF_NAV_BLOCKED_AFTER_POINT_CAPTURE }, + { "BLOCKED_UNTIL_POINT_CAPTURE", TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE }, + { "BLUE_ONE_WAY_DOOR", TF_NAV_BLUE_ONE_WAY_DOOR }, + { "RED_ONE_WAY_DOOR", TF_NAV_RED_ONE_WAY_DOOR }, + + { "SNIPER_SPOT", TF_NAV_SNIPER_SPOT }, + { "SENTRY_SPOT", TF_NAV_SENTRY_SPOT }, + { "NO_SPAWNING", TF_NAV_NO_SPAWNING }, + { "RESCUE_CLOSET", TF_NAV_RESCUE_CLOSET }, + + { "DOOR_ALWAYS_BLOCKS", TF_NAV_DOOR_ALWAYS_BLOCKS }, + { "DOOR_NEVER_BLOCKS", TF_NAV_DOOR_NEVER_BLOCKS }, + { "UNBLOCKABLE", TF_NAV_UNBLOCKABLE }, + + { "WITH_SECOND_POINT", TF_NAV_WITH_SECOND_POINT }, + { "WITH_THIRD_POINT", TF_NAV_WITH_THIRD_POINT }, + { "WITH_FOURTH_POINT", TF_NAV_WITH_FOURTH_POINT }, + { "WITH_FIFTH_POINT", TF_NAV_WITH_FIFTH_POINT }, + + { NULL, TF_NAV_INVALID } +}; + + +/** + * Can be used with any command that takes an attribute as its 2nd argument + */ +static int AttributeAutocomplete( const char *input, char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] ) +{ + if ( Q_strlen( input ) >= COMMAND_COMPLETION_ITEM_LENGTH ) + { + return 0; + } + + char command[ COMMAND_COMPLETION_ITEM_LENGTH+1 ]; + Q_strncpy( command, input, sizeof( command ) ); + + // skip to start of argument + char *partialArg = Q_strrchr( command, ' ' ); + if ( partialArg == NULL ) + { + return 0; + } + + // chop command from partial argument + *partialArg = '\000'; + ++partialArg; + + int partialArgLength = Q_strlen( partialArg ); + + int count = 0; + for( unsigned int i=0; s_TFAttributeTable[i].name && count < COMMAND_COMPLETION_MAXITEMS; ++i ) + { + if ( !Q_strnicmp( s_TFAttributeTable[i].name, partialArg, partialArgLength ) ) + { + // Add to the autocomplete array + Q_snprintf( commands[ count++ ], COMMAND_COMPLETION_ITEM_LENGTH, "%s %s", command, s_TFAttributeTable[i].name ); + } + } + +/* all of these are deprecated + for( unsigned int i=0; TheNavAttributeTable[i].name && count < COMMAND_COMPLETION_MAXITEMS; ++i ) + { + if ( !Q_strnicmp( TheNavAttributeTable[i].name, partialArg, partialArgLength ) ) + { + // Add to the autocomplete array + Q_snprintf( commands[ count++ ], COMMAND_COMPLETION_ITEM_LENGTH, "%s %s", command, TheNavAttributeTable[i].name ); + } + } +*/ + + return count; +} + + +TFNavAttributeType NameToTFAttribute( const char *name ) +{ + for( unsigned int i=0; s_TFAttributeTable[i].name; ++i ) + { + if ( !Q_stricmp( s_TFAttributeTable[i].name, name ) ) + { + return s_TFAttributeTable[i].attribute; + } + } + + return TF_NAV_INVALID; +} + + +const char *TFAttributeToName( TFNavAttributeType attribute ) +{ + for( unsigned int i=0; s_TFAttributeTable[i].name; ++i ) + { + if ( s_TFAttributeTable[i].attribute == attribute ) + { + return s_TFAttributeTable[i].name; + } + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------- +void TF_EditClearAttribute( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Usage: %s <attribute1> [attribute2...]\n", args[0] ); + return; + } + + for ( int i = 1; i < args.ArgC(); ++i ) + { + TFNavAttributeType spawnAttribute = NameToTFAttribute( args[i] ); + NavAttributeType navAttribute = NameToNavAttribute( args[i] ); + + if ( spawnAttribute != TF_NAV_INVALID ) + { + CTFAttributeClearer clear( spawnAttribute ); + TheNavMesh->ForAllSelectedAreas( clear ); + } + else if ( navAttribute != NAV_MESH_INVALID ) + { + NavAttributeClearer clear( navAttribute ); + TheNavMesh->ForAllSelectedAreas( clear ); + } + else + { + Msg( "Unknown attribute '%s'", args[i] ); + } + } + + TheNavMesh->ClearSelectedSet(); +} +static ConCommand ClearAttributeTF( "tf_clear_attribute", TF_EditClearAttribute, "Remove given attribute from all areas in the selected set.", FCVAR_CHEAT, AttributeAutocomplete ); + + +//-------------------------------------------------------------------------------------------------------- +class CTFAttributeToggler +{ +public: + CTFAttributeToggler( TFNavAttributeType attribute ) + { + m_attribute = attribute; + } + + bool operator() ( CNavArea *baseArea ) + { + CTFNavArea *area = (CTFNavArea *)baseArea; + + // only toggle if dealing with a single selected area + if ( TheNavMesh->IsSelectedSetEmpty() && area->HasAttributeTF( m_attribute ) ) + { + area->ClearAttributeTF( m_attribute ); + } + else + { + area->SetAttributeTF( m_attribute ); + } + + return true; + } + + TFNavAttributeType m_attribute; +}; + + +//-------------------------------------------------------------------------------------------------------- +void TF_EditMarkAttribute( const CCommand &args ) +{ + if ( args.ArgC() < 2 ) + { + Msg( "Usage: %s <attribute> [attribute2...]\n", args[0] ); + return; + } + + for ( int i = 1; i < args.ArgC(); ++i ) + { + TFNavAttributeType spawnAttribute = NameToTFAttribute( args[i] ); + NavAttributeType navAttribute = NameToNavAttribute( args[i] ); + + if ( spawnAttribute != TF_NAV_INVALID ) + { + CTFAttributeToggler toggle( spawnAttribute ); + TheNavMesh->ForAllSelectedAreas( toggle ); + } + else if ( navAttribute != NAV_MESH_INVALID ) + { + NavAttributeToggler clear( navAttribute ); + TheNavMesh->ForAllSelectedAreas( clear ); + } + else + { + Msg( "Unknown attribute '%s'", args[i] ); + } + } + + TheNavMesh->ClearSelectedSet(); +} +static ConCommand MarkAttribute( "tf_mark", TF_EditMarkAttribute, "Set attribute of selected area.", FCVAR_CHEAT, AttributeAutocomplete ); + + +//-------------------------------------------------------------------------------------------------------- +void TF_EditSelectWithAttribute( const CCommand &args ) +{ + TheNavMesh->ClearSelectedSet(); + + if ( args.ArgC() != 2 ) + { + Msg( "Usage: %s <attribute>\n", args[0] ); + return; + } + + TFNavAttributeType spawnAttribute = NameToTFAttribute( args[1] ); + + int count = 0; + + if ( spawnAttribute != TF_NAV_INVALID ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CTFNavArea *area = (CTFNavArea *)TheNavAreas[ it ]; + + if ( area->HasAttributeTF( spawnAttribute ) ) + { + TheNavMesh->AddToSelectedSet( area ); + ++count; + } + } + } + else + { + NavAttributeType navAttribute = NameToNavAttribute( args[1] ); + if ( navAttribute != NAV_MESH_INVALID ) + { + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( area->GetAttributes() & navAttribute ) + { + TheNavMesh->AddToSelectedSet( area ); + ++count; + } + } + } + else + { + Msg( "Unknown attribute '%s'", args[1] ); + } + } + + Msg( "%d areas added to selection\n", count ); +} +static ConCommand SelectWithAttribute( "tf_select_with_attribute", TF_EditSelectWithAttribute, "Selects areas with the given attribute.", FCVAR_CHEAT, AttributeAutocomplete ); + diff --git a/game/server/tf/nav_mesh/tf_nav_mesh_edit.h b/game/server/tf/nav_mesh/tf_nav_mesh_edit.h new file mode 100644 index 0000000..c6717d9 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_nav_mesh_edit.h @@ -0,0 +1,11 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_nav_mesh_edit.h +// TF specific nav mesh editing +// Michael Booth, May 2009 + +#ifndef TF_NAV_MESH_EDIT_H +#define TF_NAV_MESH_EDIT_H + + + +#endif // TF_NAV_MESH_EDIT_H
\ No newline at end of file diff --git a/game/server/tf/nav_mesh/tf_path_follower.cpp b/game/server/tf/nav_mesh/tf_path_follower.cpp new file mode 100644 index 0000000..f566330 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_path_follower.cpp @@ -0,0 +1,158 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_path_follower.cpp +// Simplified path following for TF2 +// Author: Michael Booth, November 2010 + +#include "cbase.h" + +#include "NextBotManager.h" +#include "tf_path_follower.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor + */ +CTFPathFollower::CTFPathFollower( void ) +{ + m_goal = NULL; + m_minLookAheadRange = 300.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +CTFPathFollower::~CTFPathFollower() +{ + // allow bots to detach pointer to me + CUtlVector< INextBot * > botVector; + TheNextBots().CollectAllBots( &botVector ); + + for( int i=0; i<botVector.Count(); ++i ) + { + botVector[i]->NotifyPathDestruction( this ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When the path is invalidated, the follower is also reset + */ +void CTFPathFollower::Invalidate( void ) +{ + // extend + Path::Invalidate(); + + m_goal = NULL; + MoveCursorToStart(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when the path is (re)computed (path is valid at the time of this call) + */ +void CTFPathFollower::OnPathChanged( INextBot *bot, Path::ResultType result ) +{ + // start from the beginning + m_goal = FirstSegment(); + MoveCursorToStart(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move mover along path + */ +void CTFPathFollower::Update( INextBot *bot ) +{ + VPROF_BUDGET( "CTFPathFollower::Update", "NextBot" ); + ILocomotion *mover = bot->GetLocomotionInterface(); + + // track most recent path followed + bot->SetCurrentPath( this ); + + if ( !IsValid() || m_goal == NULL ) + { + return; + } + + // check if we've reached the end of the path + const float nearRange = 25.0f; + if ( mover->IsOnGround() && ( GetEndPosition() - mover->GetFeet() ).AsVector2D().IsLengthLessThan( nearRange ) ) + { + // the end of the path has been reached + mover->GetBot()->OnMoveToSuccess( this ); + + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + DevMsg( "CTFPathFollower: OnMoveToSuccess\n" ); + } + + // don't invalidate if OnMoveToSuccess just recomputed a new path + if ( GetAge() > 0.0f ) + { + Invalidate(); + } + + return; + } + + // move along the path + MoveCursorToClosestPosition( mover->GetFeet(), SEEK_AHEAD ); + float myCursorPosition = GetCursorPosition(); + const Path::Data &data = GetCursorData(); + + if ( !data.segmentPrior ) + { + // this shouldn't happen + mover->GetBot()->OnMoveToFailure( this, FAIL_STUCK ); + Invalidate(); + return; + } + + // set goal to be just ahead of wherever we happen to be on the path + m_goal = NextSegment( data.segmentPrior ); + + if ( !m_goal ) + { + m_goal = data.segmentPrior; + } + + // find actual move-to position farther down the path + Vector moveToPos = m_goal->pos; + + // follow point farther down the path to smooth out our movement + for( float ahead = m_minLookAheadRange; ahead > 0.0f; ahead -= 50.0f ) + { + MoveCursor( myCursorPosition, PATH_ABSOLUTE_DISTANCE ); + MoveCursor( ahead, PATH_RELATIVE_DISTANCE ); + + // get path data at this lookahead point + const Path::Data &data = GetCursorData(); + + if ( mover->IsPotentiallyTraversable( mover->GetFeet(), data.pos ) ) + { + moveToPos = data.pos; + break; + } + } + + // move bot along path + mover->FaceTowards( moveToPos ); + mover->Approach( moveToPos ); + + // debug display + if ( bot->IsDebugging( NEXTBOT_PATH ) ) + { + Path::Draw(); + + NDebugOverlay::Cross3D( moveToPos, 5.0f, 150, 150, 255, true, 0.1f ); + NDebugOverlay::Line( bot->GetEntity()->WorldSpaceCenter(), moveToPos, 255, 255, 0, true, 0.1f ); + } +} + diff --git a/game/server/tf/nav_mesh/tf_path_follower.h b/game/server/tf/nav_mesh/tf_path_follower.h new file mode 100644 index 0000000..7f75913 --- /dev/null +++ b/game/server/tf/nav_mesh/tf_path_follower.h @@ -0,0 +1,58 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// tf_path_follower.h +// Simplified path following for TF2 +// Author: Michael Booth, November 2010 + +#ifndef TF_PATH_FOLLOWER_H +#define TF_PATH_FOLLOWER_H + +#include "nav_mesh.h" +#include "nav_pathfind.h" +#include "Path/NextBotPathFollow.h" + +class INextBot; +class ILocomotion; + + +//-------------------------------------------------------------------------------------------------------- +/** + * This is a simplified path follower that doesn't care about ladders, climbing, hindrances, etc. + */ +class CTFPathFollower : public PathFollower +{ +public: + CTFPathFollower( void ); + virtual ~CTFPathFollower(); + + virtual void Invalidate( void ); // (EXTEND) cause the path to become invalid + virtual void OnPathChanged( INextBot *bot, Path::ResultType result ); // invoked when the path is (re)computed (path is valid at the time of this call) + + virtual void Update( INextBot *bot ); // move bot along path + + virtual const Path::Segment *GetCurrentGoal( void ) const; // return current goal along the path we are trying to reach + + virtual void SetMinLookAheadDistance( float value ); // minimum range movement goal must be along path + +private: + const Path::Segment *m_goal; // our current goal along the path + float m_minLookAheadRange; + +// bool CheckProgress( INextBot *bot ); +// bool IsAtGoal( INextBot *bot ) const; // return true if reached current path goal +}; + + +inline const Path::Segment *CTFPathFollower::GetCurrentGoal( void ) const +{ + return m_goal; +} + + +inline void CTFPathFollower::SetMinLookAheadDistance( float value ) +{ + m_minLookAheadRange = value; +} + +#endif // TF_PATH_FOLLOWER_H + + |