summaryrefslogtreecommitdiff
path: root/game/server/tf/nav_mesh
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/nav_mesh')
-rw-r--r--game/server/tf/nav_mesh/tf_nav_area.cpp537
-rw-r--r--game/server/tf/nav_mesh/tf_nav_area.h299
-rw-r--r--game/server/tf/nav_mesh/tf_nav_interface.cpp45
-rw-r--r--game/server/tf/nav_mesh/tf_nav_mesh.cpp2450
-rw-r--r--game/server/tf/nav_mesh/tf_nav_mesh.h235
-rw-r--r--game/server/tf/nav_mesh/tf_nav_mesh_edit.cpp306
-rw-r--r--game/server/tf/nav_mesh/tf_nav_mesh_edit.h11
-rw-r--r--game/server/tf/nav_mesh/tf_path_follower.cpp158
-rw-r--r--game/server/tf/nav_mesh/tf_path_follower.h58
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
+
+