aboutsummaryrefslogtreecommitdiff
path: root/mp/src/game/server/nav_generate.cpp
diff options
context:
space:
mode:
authorJørgen P. Tjernø <[email protected]>2013-12-02 19:31:46 -0800
committerJørgen P. Tjernø <[email protected]>2013-12-02 19:46:31 -0800
commitf56bb35301836e56582a575a75864392a0177875 (patch)
treede61ddd39de3e7df52759711950b4c288592f0dc /mp/src/game/server/nav_generate.cpp
parentMark some more files as text. (diff)
downloadsource-sdk-2013-f56bb35301836e56582a575a75864392a0177875.tar.xz
source-sdk-2013-f56bb35301836e56582a575a75864392a0177875.zip
Fix line endings. WHAMMY.
Diffstat (limited to 'mp/src/game/server/nav_generate.cpp')
-rw-r--r--mp/src/game/server/nav_generate.cpp9868
1 files changed, 4934 insertions, 4934 deletions
diff --git a/mp/src/game/server/nav_generate.cpp b/mp/src/game/server/nav_generate.cpp
index fe577273..c9d2ea4b 100644
--- a/mp/src/game/server/nav_generate.cpp
+++ b/mp/src/game/server/nav_generate.cpp
@@ -1,4934 +1,4934 @@
-//========= Copyright Valve Corporation, All rights reserved. ============//
-//
-// Purpose:
-//
-// $NoKeywords: $
-//
-//=============================================================================//
-// nav_generate.cpp
-// Auto-generate a Navigation Mesh by sampling the current map
-// Author: Michael S. Booth ([email protected]), 2003
-
-#include "cbase.h"
-#include "util_shared.h"
-#include "nav_mesh.h"
-#include "nav_node.h"
-#include "nav_pathfind.h"
-#include "viewport_panel_names.h"
-//#include "terror/TerrorShared.h"
-#include "fmtstr.h"
-
-#ifdef TERROR
-#include "func_simpleladder.h"
-#endif
-
-// NOTE: This has to be the last file included!
-#include "tier0/memdbgon.h"
-
-
-enum { MAX_BLOCKED_AREAS = 256 };
-static unsigned int blockedID[ MAX_BLOCKED_AREAS ];
-static int blockedIDCount = 0;
-static float lastMsgTime = 0.0f;
-
-bool TraceAdjacentNode( int depth, const Vector& start, const Vector& end, trace_t *trace, float zLimit = DeathDrop );
-bool StayOnFloor( trace_t *trace, float zLimit = DeathDrop );
-
-ConVar nav_slope_limit( "nav_slope_limit", "0.7", FCVAR_CHEAT, "The ground unit normal's Z component must be greater than this for nav areas to be generated." );
-ConVar nav_slope_tolerance( "nav_slope_tolerance", "0.1", FCVAR_CHEAT, "The ground unit normal's Z component must be this close to the nav area's Z component to be generated." );
-ConVar nav_displacement_test( "nav_displacement_test", "10000", FCVAR_CHEAT, "Checks for nodes embedded in displacements (useful for in-development maps)" );
-ConVar nav_generate_fencetops( "nav_generate_fencetops", "1", FCVAR_CHEAT, "Autogenerate nav areas on fence and obstacle tops" );
-ConVar nav_generate_fixup_jump_areas( "nav_generate_fixup_jump_areas", "1", FCVAR_CHEAT, "Convert obsolete jump areas into 2-way connections" );
-ConVar nav_generate_incremental_range( "nav_generate_incremental_range", "2000", FCVAR_CHEAT );
-ConVar nav_generate_incremental_tolerance( "nav_generate_incremental_tolerance", "0", FCVAR_CHEAT, "Z tolerance for adding new nav areas." );
-ConVar nav_area_max_size( "nav_area_max_size", "50", FCVAR_CHEAT, "Max area size created in nav generation" );
-
-// Common bounding box for traces
-Vector NavTraceMins( -0.45, -0.45, 0 );
-Vector NavTraceMaxs( 0.45, 0.45, HumanCrouchHeight );
-bool FindGroundForNode( Vector *pos, Vector *normal ); // find a ground Z for pos that is clear for NavTraceMins -> NavTraceMaxs
-
-const float MaxTraversableHeight = StepHeight; // max internal obstacle height that can occur between nav nodes and safely disregarded
-const float MinObstacleAreaWidth = 10.0f; // min width of a nav area we will generate on top of an obstacle
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Shortest path cost, paying attention to "blocked" areas
- */
-class ApproachAreaCost
-{
-public:
- float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator )
- {
- // check if this area is "blocked"
- for( int i=0; i<blockedIDCount; ++i )
- {
- if (area->GetID() == blockedID[i])
- {
- return -1.0f;
- }
- }
-
- if (fromArea == NULL)
- {
- // first area in path, no cost
- return 0.0f;
- }
- else
- {
- // compute distance traveled along path so far
- float dist;
-
- if (ladder)
- {
- dist = ladder->m_length;
- }
- else
- {
- dist = (area->GetCenter() - fromArea->GetCenter()).Length();
- }
-
- float cost = dist + fromArea->GetCostSoFar();
-
- return cost;
- }
- }
-};
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Start at given position and find first area in given direction
- */
-inline CNavArea *findFirstAreaInDirection( const Vector *start, NavDirType dir, float range, float beneathLimit, CBaseEntity *traceIgnore = NULL, Vector *closePos = NULL )
-{
- CNavArea *area = NULL;
-
- Vector pos = *start;
-
- int end = (int)((range / GenerationStepSize) + 0.5f);
-
- for( int i=1; i<=end; i++ )
- {
- AddDirectionVector( &pos, dir, GenerationStepSize );
-
- // make sure we dont look thru the wall
- trace_t result;
-
- UTIL_TraceHull( *start, pos, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), traceIgnore, COLLISION_GROUP_NONE, &result );
-
- if (result.fraction < 1.0f)
- break;
-
- area = TheNavMesh->GetNavArea( pos, beneathLimit );
- if (area)
- {
- if (closePos)
- {
- closePos->x = pos.x;
- closePos->y = pos.y;
- closePos->z = area->GetZ( pos.x, pos.y );
- }
-
- break;
- }
- }
-
- return area;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * For each ladder in the map, create a navigation representation of it.
- */
-void CNavMesh::BuildLadders( void )
-{
- // remove any left-over ladders
- DestroyLadders();
-
-#ifdef TERROR
- CFuncSimpleLadder *ladder = NULL;
- while( (ladder = dynamic_cast< CFuncSimpleLadder * >(gEntList.FindEntityByClassname( ladder, "func_simpleladder" ))) != NULL )
- {
- Vector mins, maxs;
- ladder->CollisionProp()->WorldSpaceSurroundingBounds( &mins, &maxs );
- CreateLadder( mins, maxs, 0.0f );
- }
-#endif
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Create a navigation representation of a ladder.
- */
-void CNavMesh::CreateLadder( const Vector& absMin, const Vector& absMax, float maxHeightAboveTopArea )
-{
- CNavLadder *ladder = new CNavLadder;
-
- // compute top & bottom of ladder
- ladder->m_top.x = (absMin.x + absMax.x) / 2.0f;
- ladder->m_top.y = (absMin.y + absMax.y) / 2.0f;
- ladder->m_top.z = absMax.z;
-
- ladder->m_bottom.x = ladder->m_top.x;
- ladder->m_bottom.y = ladder->m_top.y;
- ladder->m_bottom.z = absMin.z;
-
- // determine facing - assumes "normal" runged ladder
- float xSize = absMax.x - absMin.x;
- float ySize = absMax.y - absMin.y;
- trace_t result;
- if (xSize > ySize)
- {
- // ladder is facing north or south - determine which way
- // "pull in" traceline from bottom and top in case ladder abuts floor and/or ceiling
- Vector from = ladder->m_bottom + Vector( 0.0f, GenerationStepSize, GenerationStepSize/2 );
- Vector to = ladder->m_top + Vector( 0.0f, GenerationStepSize, -GenerationStepSize/2 );
-
- UTIL_TraceLine( from, to, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
-
- if (result.fraction != 1.0f || result.startsolid)
- ladder->SetDir( NORTH );
- else
- ladder->SetDir( SOUTH );
-
- ladder->m_width = xSize;
- }
- else
- {
- // ladder is facing east or west - determine which way
- Vector from = ladder->m_bottom + Vector( GenerationStepSize, 0.0f, GenerationStepSize/2 );
- Vector to = ladder->m_top + Vector( GenerationStepSize, 0.0f, -GenerationStepSize/2 );
-
- UTIL_TraceLine( from, to, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
-
- if (result.fraction != 1.0f || result.startsolid)
- ladder->SetDir( WEST );
- else
- ladder->SetDir( EAST );
-
- ladder->m_width = ySize;
- }
-
- // adjust top and bottom of ladder to make sure they are reachable
- // (cs_office has a crate right in front of the base of a ladder)
- Vector along = ladder->m_top - ladder->m_bottom;
- float length = along.NormalizeInPlace();
- Vector on, out;
- const float minLadderClearance = 32.0f;
-
- // adjust bottom to bypass blockages
- const float inc = 10.0f;
- float t;
- for( t = 0.0f; t <= length; t += inc )
- {
- on = ladder->m_bottom + t * along;
-
- out = on + ladder->GetNormal() * minLadderClearance;
-
- UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
-
- if (result.fraction == 1.0f && !result.startsolid)
- {
- // found viable ladder bottom
- ladder->m_bottom = on;
- break;
- }
- }
-
- // adjust top to bypass blockages
- for( t = 0.0f; t <= length; t += inc )
- {
- on = ladder->m_top - t * along;
-
- out = on + ladder->GetNormal() * minLadderClearance;
-
- UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
-
- if (result.fraction == 1.0f && !result.startsolid)
- {
- // found viable ladder top
- ladder->m_top = on;
- break;
- }
- }
-
- ladder->m_length = (ladder->m_top - ladder->m_bottom).Length();
-
- ladder->SetDir( ladder->GetDir() ); // now that we've adjusted the top and bottom, re-check the normal
-
- ladder->m_bottomArea = NULL;
- ladder->m_topForwardArea = NULL;
- ladder->m_topLeftArea = NULL;
- ladder->m_topRightArea = NULL;
- ladder->m_topBehindArea = NULL;
- ladder->ConnectGeneratedLadder( maxHeightAboveTopArea );
-
- // add ladder to global list
- m_ladders.AddToTail( ladder );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Create a navigation representation of a ladder.
- */
-void CNavMesh::CreateLadder( const Vector &top, const Vector &bottom, float width, const Vector2D &ladderDir, float maxHeightAboveTopArea )
-{
- CNavLadder *ladder = new CNavLadder;
-
- ladder->m_top = top;
- ladder->m_bottom = bottom;
- ladder->m_width = width;
- if ( fabs( ladderDir.x ) > fabs( ladderDir.y ) )
- {
- if ( ladderDir.x > 0.0f )
- {
- ladder->SetDir( EAST );
- }
- else
- {
- ladder->SetDir( WEST );
- }
- }
- else
- {
- if ( ladderDir.y > 0.0f )
- {
- ladder->SetDir( SOUTH );
- }
- else
- {
- ladder->SetDir( NORTH );
- }
- }
-
- // adjust top and bottom of ladder to make sure they are reachable
- // (cs_office has a crate right in front of the base of a ladder)
- Vector along = ladder->m_top - ladder->m_bottom;
- float length = along.NormalizeInPlace();
- Vector on, out;
- const float minLadderClearance = 32.0f;
-
- // adjust bottom to bypass blockages
- const float inc = 10.0f;
- float t;
- trace_t result;
- for( t = 0.0f; t <= length; t += inc )
- {
- on = ladder->m_bottom + t * along;
-
- out = on + ladder->GetNormal() * minLadderClearance;
-
- UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
-
- if (result.fraction == 1.0f && !result.startsolid)
- {
- // found viable ladder bottom
- ladder->m_bottom = on;
- break;
- }
- }
-
- // adjust top to bypass blockages
- for( t = 0.0f; t <= length; t += inc )
- {
- on = ladder->m_top - t * along;
-
- out = on + ladder->GetNormal() * minLadderClearance;
-
- UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
-
- if (result.fraction == 1.0f && !result.startsolid)
- {
- // found viable ladder top
- ladder->m_top = on;
- break;
- }
- }
-
- ladder->m_length = (ladder->m_top - ladder->m_bottom).Length();
-
- ladder->SetDir( ladder->GetDir() ); // now that we've adjusted the top and bottom, re-check the normal
-
- ladder->m_bottomArea = NULL;
- ladder->m_topForwardArea = NULL;
- ladder->m_topLeftArea = NULL;
- ladder->m_topRightArea = NULL;
- ladder->m_topBehindArea = NULL;
- ladder->ConnectGeneratedLadder( maxHeightAboveTopArea );
-
- // add ladder to global list
- m_ladders.AddToTail( ladder );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-void CNavLadder::ConnectGeneratedLadder( float maxHeightAboveTopArea )
-{
- const float nearLadderRange = 75.0f; // 50
-
- //
- // Find naviagtion area at bottom of ladder
- //
-
- // get approximate postion of player on ladder
- Vector center = m_bottom + Vector( 0, 0, GenerationStepSize );
- AddDirectionVector( &center, m_dir, HalfHumanWidth );
-
- m_bottomArea = TheNavMesh->GetNearestNavArea( center, true );
- if (!m_bottomArea)
- {
- DevMsg( "ERROR: Unconnected ladder bottom at ( %g, %g, %g )\n", m_bottom.x, m_bottom.y, m_bottom.z );
- }
- else
- {
- // store reference to ladder in the area
- m_bottomArea->AddLadderUp( this );
- }
-
- //
- // Find adjacent navigation areas at the top of the ladder
- //
-
- // get approximate postion of player on ladder
- center = m_top + Vector( 0, 0, GenerationStepSize );
- AddDirectionVector( &center, m_dir, HalfHumanWidth );
-
- float beneathLimit = MIN( 120.0f, m_top.z - m_bottom.z + HalfHumanWidth );
-
- // find "ahead" area
- m_topForwardArea = findFirstAreaInDirection( &center, OppositeDirection( m_dir ), nearLadderRange, beneathLimit, NULL );
- if (m_topForwardArea == m_bottomArea)
- m_topForwardArea = NULL;
-
- // find "left" area
- m_topLeftArea = findFirstAreaInDirection( &center, DirectionLeft( m_dir ), nearLadderRange, beneathLimit, NULL );
- if (m_topLeftArea == m_bottomArea)
- m_topLeftArea = NULL;
-
- // find "right" area
- m_topRightArea = findFirstAreaInDirection( &center, DirectionRight( m_dir ), nearLadderRange, beneathLimit, NULL );
- if (m_topRightArea == m_bottomArea)
- m_topRightArea = NULL;
-
- // find "behind" area - must look farther, since ladder is against the wall away from this area
- m_topBehindArea = findFirstAreaInDirection( &center, m_dir, 2.0f*nearLadderRange, beneathLimit, NULL );
- if (m_topBehindArea == m_bottomArea)
- m_topBehindArea = NULL;
-
- // can't include behind area, since it is not used when going up a ladder
- if (!m_topForwardArea && !m_topLeftArea && !m_topRightArea)
- DevMsg( "ERROR: Unconnected ladder top at ( %g, %g, %g )\n", m_top.x, m_top.y, m_top.z );
-
- // store reference to ladder in the area(s)
- if (m_topForwardArea)
- m_topForwardArea->AddLadderDown( this );
-
- if (m_topLeftArea)
- m_topLeftArea->AddLadderDown( this );
-
- if (m_topRightArea)
- m_topRightArea->AddLadderDown( this );
-
- if (m_topBehindArea)
- {
- m_topBehindArea->AddLadderDown( this );
- Disconnect( m_topBehindArea );
- }
-
- // adjust top of ladder to highest connected area
- float topZ = m_bottom.z + 5.0f;
- bool topAdjusted = false;
- CNavArea *topAreaList[4];
- topAreaList[0] = m_topForwardArea;
- topAreaList[1] = m_topLeftArea;
- topAreaList[2] = m_topRightArea;
- topAreaList[3] = m_topBehindArea;
-
- for( int a=0; a<4; ++a )
- {
- CNavArea *topArea = topAreaList[a];
- if (topArea == NULL)
- continue;
-
- Vector close;
- topArea->GetClosestPointOnArea( m_top, &close );
- if (topZ < close.z)
- {
- topZ = close.z;
- topAdjusted = true;
- }
- }
-
- if (topAdjusted)
- {
- if ( maxHeightAboveTopArea > 0.0f )
- {
- m_top.z = MIN( topZ + maxHeightAboveTopArea, m_top.z );
- }
- else
- {
- m_top.z = topZ; // not manually specifying a top, so snap exactly
- }
- }
-
- //
- // Determine whether this ladder is "dangling" or not
- // "Dangling" ladders are too high to go up
- //
- if (m_bottomArea)
- {
- Vector bottomSpot;
- m_bottomArea->GetClosestPointOnArea( m_bottom, &bottomSpot );
- if (m_bottom.z - bottomSpot.z > HumanHeight)
- {
- m_bottomArea->Disconnect( this );
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------
-class JumpConnector
-{
-public:
- bool operator()( CNavArea *jumpArea )
- {
- if ( !(jumpArea->GetAttributes() & NAV_MESH_JUMP) )
- {
- return true;
- }
-
- for ( int i=0; i<NUM_DIRECTIONS; ++i )
- {
- NavDirType incomingDir = (NavDirType)i;
- NavDirType outgoingDir = OppositeDirection( incomingDir );
-
- const NavConnectVector *incoming = jumpArea->GetIncomingConnections( incomingDir );
- const NavConnectVector *from = jumpArea->GetAdjacentAreas( incomingDir );
- const NavConnectVector *dest = jumpArea->GetAdjacentAreas( outgoingDir );
-
- TryToConnect( jumpArea, incoming, dest, outgoingDir );
- TryToConnect( jumpArea, from, dest, outgoingDir );
- }
-
- return true;
- }
-
-private:
- struct Connection
- {
- CNavArea *source;
- CNavArea *dest;
- NavDirType direction;
- };
-
- void TryToConnect( CNavArea *jumpArea, const NavConnectVector *source, const NavConnectVector *dest, NavDirType outgoingDir )
- {
- FOR_EACH_VEC( (*source), sourceIt )
- {
- CNavArea *sourceArea = const_cast< CNavArea * >( (*source)[ sourceIt ].area );
- if ( !sourceArea->IsConnected( jumpArea, outgoingDir ) )
- {
- continue;
- }
-
- if ( sourceArea->HasAttributes( NAV_MESH_JUMP ) )
- {
- NavDirType incomingDir = OppositeDirection( outgoingDir );
- const NavConnectVector *in1 = sourceArea->GetIncomingConnections( incomingDir );
- const NavConnectVector *in2 = sourceArea->GetAdjacentAreas( incomingDir );
-
- TryToConnect( jumpArea, in1, dest, outgoingDir );
- TryToConnect( jumpArea, in2, dest, outgoingDir );
-
- continue;
- }
-
- TryToConnect( jumpArea, sourceArea, dest, outgoingDir );
- }
- }
-
- void TryToConnect( CNavArea *jumpArea, CNavArea *sourceArea, const NavConnectVector *dest, NavDirType outgoingDir )
- {
- FOR_EACH_VEC( (*dest), destIt )
- {
- CNavArea *destArea = const_cast< CNavArea * >( (*dest)[ destIt ].area );
- if ( destArea->HasAttributes( NAV_MESH_JUMP ) )
- {
- // Don't connect areas across 2 jump areas. This means we'll have some missing links due to sampling errors.
- // This is preferable to generating incorrect links across multiple jump areas, which is far more common.
- continue;
- }
-
- Vector center;
- float halfWidth;
- sourceArea->ComputePortal( destArea, outgoingDir, &center, &halfWidth );
-
- // Don't create corner-to-corner connections
- if ( halfWidth <= 0.0f )
- {
- continue;
- }
-
- Vector dir( vec3_origin );
- AddDirectionVector( &dir, outgoingDir, 5.0f );
-
- if ( halfWidth > 0.0f )
- {
- Vector sourcePos, destPos;
- sourceArea->GetClosestPointOnArea( center, &sourcePos );
- destArea->GetClosestPointOnArea( center, &destPos );
-
- // No jumping up from stairs.
- if ( sourceArea->HasAttributes( NAV_MESH_STAIRS ) && sourcePos.z + StepHeight < destPos.z )
- {
- continue;
- }
-
- if ( (sourcePos-destPos).AsVector2D().IsLengthLessThan( GenerationStepSize * 3 ) )
- {
- sourceArea->ConnectTo( destArea, outgoingDir );
-// DevMsg( "Connected %d->%d via %d (len %f)\n",
-// sourceArea->GetID(), destArea->GetID(), jumpArea->GetID(), sourcePos.DistTo( destPos ) );
- }
- }
- }
- }
-};
-
-//--------------------------------------------------------------------------------------------------------------
-void CNavMesh::MarkPlayerClipAreas( void )
-{
-#ifdef TERROR
- FOR_EACH_VEC( TheNavAreas, it )
- {
- TerrorNavArea *area = static_cast< TerrorNavArea * >(TheNavAreas[it]);
-
- // Trace upward a bit from our center point just colliding wtih PLAYERCLIP to see if we're in one, if we are, mark us as accordingly.
- trace_t trace;
- Vector start = area->GetCenter() + Vector(0.0f, 0.0f, 16.0f );
- Vector end = area->GetCenter() + Vector(0.0f, 0.0f, 32.0f );
- UTIL_TraceHull( start, end, Vector(0,0,0), Vector(0,0,0), CONTENTS_PLAYERCLIP, NULL, &trace);
-
- if ( trace.fraction < 1.0 )
- {
- area->SetAttributes( area->GetAttributes() | TerrorNavArea::NAV_PLAYERCLIP );
- }
- }
-#endif
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Mark all areas that require a jump to get through them.
- * This currently relies on jump areas having extreme slope.
- */
-void CNavMesh::MarkJumpAreas( void )
-{
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
- if ( !area->HasNodes() )
- continue;
-
- Vector normal, otherNormal;
- area->ComputeNormal( &normal );
- area->ComputeNormal( &otherNormal, true );
-
- float lowestNormalZ = MIN( normal.z, otherNormal.z );
- if (lowestNormalZ < nav_slope_limit.GetFloat())
- {
- // The area is a jump area, and we don't merge jump areas together
- area->SetAttributes( area->GetAttributes() | NAV_MESH_JUMP | NAV_MESH_NO_MERGE );
- }
- else if ( lowestNormalZ < nav_slope_limit.GetFloat() + nav_slope_tolerance.GetFloat() )
- {
- Vector testPos = area->GetCenter();
- testPos.z += HalfHumanHeight;
- Vector groundNormal;
- float dummy;
- if ( GetSimpleGroundHeight( testPos, &dummy, &groundNormal ) )
- {
- // If the ground normal is divergent from the area's normal, mark it as a jump area - it's not
- // really representative of the ground.
- float deltaNormalZ = fabs( groundNormal.z - lowestNormalZ );
- if ( deltaNormalZ > nav_slope_tolerance.GetFloat() )
- {
- // The area is a jump area, and we don't merge jump areas together
- area->SetAttributes( area->GetAttributes() | NAV_MESH_JUMP | NAV_MESH_NO_MERGE );
- }
- }
- }
- }
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Remove all areas marked as jump areas and connect the areas connecting to them
-*
-*/
-void CNavMesh::StichAndRemoveJumpAreas( void )
-{
- // Now, go through and remove jump areas, connecting areas to make up for it
- JumpConnector connector;
- ForAllAreas( connector );
- RemoveJumpAreas();
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Adjusts obstacle start and end distances such that obstacle width (end-start) is not less than MinObstacleAreaWidth,
-* and end distance is not greater than maxAllowedDist
-*/
-void AdjustObstacleDistances( float *pObstacleStartDist, float *pObstacleEndDist, float maxAllowedDist )
-{
- float obstacleWidth = *pObstacleEndDist - *pObstacleStartDist;
- // is the obstacle width too narrow?
- if ( obstacleWidth < MinObstacleAreaWidth )
- {
- float halfDelta = ( MinObstacleAreaWidth - obstacleWidth ) /2;
- // move start so it's half of min width from center, but no less than zero
- *pObstacleStartDist = MAX( *pObstacleStartDist - halfDelta, 0 );
- // move end so it's min width from start
- *pObstacleEndDist = *pObstacleStartDist + MinObstacleAreaWidth;
-
- // if this pushes the end past max allowed distance, pull start and end back so that end is within allowed distance
- if ( *pObstacleEndDist > maxAllowedDist )
- {
- float delta = *pObstacleEndDist - maxAllowedDist;
- *pObstacleStartDist -= delta;
- *pObstacleEndDist -= delta;
- }
- }
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Makes sure tall, slim obstacles like fencetops, railings and narrow walls have nav areas placed on top of them
-* to allow climbing & traversal
-*/
-void CNavMesh::HandleObstacleTopAreas( void )
-{
- if ( !nav_generate_fencetops.GetBool() )
- return;
-
- // For any 1x1 area that is internally blocked by an obstacle, raise it on top of the obstacle and size to fit.
- RaiseAreasWithInternalObstacles();
-
- // Create new areas as required
- CreateObstacleTopAreas();
-
- // It's possible for obstacle top areas to wind up overlapping one another, fix any such cases
- RemoveOverlappingObstacleTopAreas();
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* For any nav area that has internal obstacles between its corners of greater than traversable height,
-* raise that nav area to sit at the top of the obstacle, and shrink it to fit the obstacle. Such nav
-* areas are already restricted to be 1x1 so this will only be performed on areas that are already small.
-*/
-void CNavMesh::RaiseAreasWithInternalObstacles()
-{
- // obstacle areas next to stairs are bad - delete them
- CUtlVector< CNavArea * > areasToDelete;
-
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
-
- // any nav area with internal obstacles will be 1x1 (width and height = GenerationStepSize), so
- // only need to consider areas of that size
- if ( ( area->GetSizeX() != GenerationStepSize ) || (area->GetSizeY() != GenerationStepSize ) )
- continue;
-
- float obstacleZ[2] = { -FLT_MAX, -FLT_MAX };
- float obstacleZMax = -FLT_MAX;
- NavDirType obstacleDir = NORTH;
- float obstacleStartDist = GenerationStepSize;
- float obstacleEndDist = 0;
-
- bool isStairNeighbor = false;
-
- // Look at all 4 directions and determine if there are obstacles in that direction. Find the direction with the highest obstacle, if any.
- for ( int i = 0; i < NUM_DIRECTIONS; i++ )
- {
- NavDirType dir = (NavDirType) i;
-
- // For this direction, look at the left and right edges of the nav area relative to this direction and determined if they are both blocked
- // by obstacles. We only consider this area obstructed if both edges are blocked (e.g. fence runs all the way through it).
-
- NavCornerType corner[2];
- int iEdgesBlocked = 0;
- corner[0] = (NavCornerType) ( ( i + 3 ) % NUM_CORNERS ); // lower left-hand corner relative to current direction
- corner[1] = (NavCornerType) ( ( i + 2 ) % NUM_CORNERS ); // lower right-hand corner relative to current direction
- float obstacleZThisDir[2] = { -FLT_MAX, -FLT_MAX }; // absolute Z pos of obstacle for left and right edge in this direction
- float obstacleStartDistThisDir = GenerationStepSize; // closest obstacle start distance in this direction
- float obstacleEndDistThisDir = 0; // farthest obstacle end distance in this direction
-
- // consider left and right edges of nav area relative to current direction
- for ( int iEdge = 0; iEdge < 2; iEdge++ )
- {
- NavCornerType cornerType = corner[iEdge];
- CNavNode *nodeFrom = area->m_node[cornerType];
- if ( nodeFrom )
- {
- // is there an obstacle going from corner to corner along this edge?
- float obstacleHeight = nodeFrom->m_obstacleHeight[dir];
- if ( obstacleHeight > MaxTraversableHeight )
- {
- // yes, this edge is blocked
- iEdgesBlocked++;
- // keep track of obstacle height and start and end distance for this edge
- float obstacleZ = nodeFrom->GetPosition()->z + obstacleHeight;
- if ( obstacleZ > obstacleZThisDir[iEdge] )
- {
- obstacleZThisDir[iEdge] = obstacleZ;
- }
- obstacleStartDistThisDir = MIN( nodeFrom->m_obstacleStartDist[dir], obstacleStartDistThisDir );
- obstacleEndDistThisDir = MAX( nodeFrom->m_obstacleEndDist[dir], obstacleEndDistThisDir );
- }
- }
- }
-
- int BlockedEdgeCutoff = 2;
- const NavConnectVector *connections = area->GetAdjacentAreas( dir );
- if ( connections )
- {
- for ( int conIndex=0; conIndex<connections->Count(); ++conIndex )
- {
- const CNavArea *connectedArea = connections->Element( conIndex ).area;
- if ( connectedArea && connectedArea->HasAttributes( NAV_MESH_STAIRS ) )
- {
- isStairNeighbor = true;
- BlockedEdgeCutoff = 1; // one blocked edge is already too much when we're next to a stair
- break;
- }
- }
- }
-
- // are both edged blocked in this direction, and is the obstacle height in this direction the tallest we've seen?
- if ( (iEdgesBlocked >= BlockedEdgeCutoff ) && ( MAX( obstacleZThisDir[0], obstacleZThisDir[1] ) ) > obstacleZMax )
- {
- // this is the tallest obstacle we've encountered so far, remember its details
- obstacleZ[0] = obstacleZThisDir[0];
- obstacleZ[1] = obstacleZThisDir[1];
- obstacleZMax = MAX( obstacleZ[0], obstacleZ[1] );
- obstacleDir = dir;
- obstacleStartDist = obstacleStartDistThisDir;
- obstacleEndDist = obstacleStartDistThisDir;
- }
- }
-
- if ( isStairNeighbor && obstacleZMax > -FLT_MAX )
- {
- areasToDelete.AddToTail( area );
- continue;
- }
-
- // if we found an obstacle, raise this nav areas and size it to fit
- if ( obstacleZMax > -FLT_MAX )
- {
- // enforce minimum obstacle width so we don't shrink to become a teensy nav area
- AdjustObstacleDistances( &obstacleStartDist, &obstacleEndDist, GenerationStepSize );
- Assert( obstacleEndDist - obstacleStartDist >= MinObstacleAreaWidth );
-
- // get current corner coords
- Vector corner[4];
- for ( int i = NORTH_WEST; i < NUM_CORNERS; i++ )
- {
- corner[i] = area->GetCorner( (NavCornerType) i );
- }
-
- // adjust our size to fit the obstacle
- switch ( obstacleDir )
- {
- case NORTH:
- corner[NORTH_WEST].y = corner[SOUTH_WEST].y - obstacleEndDist;
- corner[NORTH_EAST].y = corner[SOUTH_EAST].y - obstacleEndDist;
- corner[SOUTH_WEST].y -= obstacleStartDist;
- corner[SOUTH_EAST].y -= obstacleStartDist;
- break;
- case SOUTH:
- corner[SOUTH_WEST].y = corner[NORTH_WEST].y + obstacleEndDist;
- corner[SOUTH_EAST].y = corner[NORTH_EAST].y + obstacleEndDist;
- corner[NORTH_WEST].y += obstacleStartDist;
- corner[NORTH_EAST].y += obstacleStartDist;
- ::V_swap( obstacleZ[0], obstacleZ[1] ); // swap left and right Z heights for obstacle so we can run common code below
- break;
- case EAST:
- corner[NORTH_EAST].x = corner[NORTH_WEST].x + obstacleEndDist;
- corner[SOUTH_EAST].x = corner[SOUTH_WEST].x + obstacleEndDist;
- corner[NORTH_WEST].x += obstacleStartDist;
- corner[SOUTH_WEST].x += obstacleStartDist;
- case WEST:
- corner[NORTH_WEST].x = corner[NORTH_EAST].x - obstacleEndDist;
- corner[SOUTH_WEST].x = corner[SOUTH_EAST].x - obstacleEndDist;
- corner[NORTH_EAST].x -= obstacleStartDist;
- corner[SOUTH_EAST].x -= obstacleStartDist;
- ::V_swap( obstacleZ[0], obstacleZ[1] ); // swap left and right Z heights for obstacle so we can run common code below
- break;
- }
-
- // adjust Z positions to be z pos of obstacle top
- corner[NORTH_WEST].z = obstacleZ[0];
- corner[NORTH_EAST].z = obstacleZ[1];
- corner[SOUTH_EAST].z = obstacleZ[1];
- corner[SOUTH_WEST].z = obstacleZ[0];
-
- // move the area
- RemoveNavArea( area );
- area->Build( corner[NORTH_WEST], corner[NORTH_EAST], corner[SOUTH_EAST], corner[SOUTH_WEST] );
- Assert( !area->IsDegenerate() );
- AddNavArea( area );
-
- // remove side-to-side connections if there are any so AI does try to do things like run along fencetops
- area->RemoveOrthogonalConnections( obstacleDir );
- area->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE | NAV_MESH_OBSTACLE_TOP );
- area->SetAttributes( area->GetAttributes() & ( ~NAV_MESH_JUMP ) );
- // clear out the nodes associated with this area's corners -- corners don't match the node positions any more
- for ( int i = 0; i < NUM_CORNERS; i++ )
- {
- area->m_node[i] = NULL;
- }
- }
- }
-
- for ( int i=0; i<areasToDelete.Count(); ++i )
- {
- TheNavAreas.FindAndRemove( areasToDelete[i] );
- DestroyArea( areasToDelete[i] );
- }
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* For any two nav areas that have an obstacle between them such as a fence, railing or small wall, creates a new
-* nav area on top of the obstacle and connects it between the areas
-*/
-void CNavMesh::CreateObstacleTopAreas()
-{
- // enumerate all areas
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
-
- // if this is a jump node (which will ultimately get removed) or is an obstacle top, ignore it
- if ( area->GetAttributes() & ( NAV_MESH_JUMP | NAV_MESH_OBSTACLE_TOP ) )
- return;
-
- // Look in all directions
- for ( int i = NORTH; i < NUM_DIRECTIONS; i++ )
- {
- NavDirType dir = (NavDirType) i;
-
- // Look at all adjacent areas in this direction
- int iConnections = area->GetAdjacentCount( dir );
- for ( int j = 0; j < iConnections; j++ )
- {
- CNavArea *areaOther = area->GetAdjacentArea( dir, j );
- // if this is a jump node (which will ultimately get removed) or is an obstacle top, ignore it
- if ( areaOther->GetAttributes() & ( NAV_MESH_JUMP | NAV_MESH_OBSTACLE_TOP ) )
- continue;
-
- // create an obstacle top if there is a one-node separation between the areas and there is an intra-node obstacle within that separation
- if ( !CreateObstacleTopAreaIfNecessary( area, areaOther, dir, false ) )
- {
- // if not, create an obstacle top if there is a two-node separation between the areas and the intermediate node is significantly
- // higher than the two areas, which means there's some geometry there that causes the middle node to be higher
- CreateObstacleTopAreaIfNecessary( area, areaOther, dir, true );
- }
- }
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Creates a new nav area if an obstacle exists between the two nav areas. If bMultiNode is false, this checks
-* if there's a one-node separation between the areas, and if so if there is an obstacle detected between the nodes.
-* If bMultiNode is true, checks if there is a two-node separation between the areas, and if so if the middle node is
-* higher than the two areas, suggesting an obstacle in the middle.
-*/
-bool CNavMesh::CreateObstacleTopAreaIfNecessary( CNavArea *area, CNavArea *areaOther, NavDirType dir, bool bMultiNode )
-{
- float obstacleHeightMin = FLT_MAX;
- float obstacleHeightMax = 0;
- float obstacleHeightStart = 0;
- float obstacleHeightEnd = 0;
- float obstacleDistMin = GenerationStepSize;
- float obstacleDistMax = 0;
-
- Vector center;
- float halfPortalWidth;
-
- area->ComputePortal( areaOther, dir, &center, &halfPortalWidth );
-
- if ( halfPortalWidth > 0 )
- {
- // get the corners to left and right of direction toward other area
- NavCornerType cornerStart = (NavCornerType) dir;
- NavCornerType cornerEnd = (NavCornerType) ( ( dir + 1 ) % NUM_CORNERS );
- CNavNode *node = area->m_node[cornerStart];
- CNavNode *nodeEnd = area->m_node[cornerEnd];
- NavDirType dirEdge = (NavDirType) ( ( dir + 1 ) % NUM_DIRECTIONS );
- obstacleHeightMin = FLT_MAX;
- float zStart = 0, zEnd = 0;
- // along the edge of this area that faces the other area, look at every node that's in the portal between the two
- while ( node )
- {
- Vector vecToPortalCenter = *node->GetPosition() - center;
- vecToPortalCenter.z = 0;
- if ( vecToPortalCenter.IsLengthLessThan( halfPortalWidth + 1.0f ) )
- {
- // this node is in the portal
-
- float obstacleHeight = 0;
- float obstacleDistStartCur = node->m_obstacleStartDist[dir];
- float obstacleDistEndCur = node->m_obstacleEndDist[dir];
-
- if ( !bMultiNode )
- {
- // use the inter-node obstacle height from this node toward the next area
- obstacleHeight = node->m_obstacleHeight[dir];
- }
- else
- {
- if ( !areaOther->Contains( *node->GetPosition() ) )
- {
- // step one node toward the other area
- CNavNode *nodeTowardOtherArea = node->GetConnectedNode( dir );
- if ( nodeTowardOtherArea )
- {
- // see if that step took us upward a significant amount
- float deltaZ = nodeTowardOtherArea->GetPosition()->z - node->GetPosition()->z;
- if ( deltaZ > MaxTraversableHeight )
- {
- // see if we've arrived in the other area
- bool bInOtherArea = false;
- if ( areaOther->Contains( *nodeTowardOtherArea->GetPosition() ) )
- {
- float z = areaOther->GetZ( nodeTowardOtherArea->GetPosition()->x, nodeTowardOtherArea->GetPosition()->y );
- float deltaZ = fabs( nodeTowardOtherArea->GetPosition()->z - z );
- if ( deltaZ < 2.0f )
- {
- bInOtherArea = true;
- }
- }
-
- // if we have not arrived in the other area yet, take one more step in the same direction
- if ( !bInOtherArea )
- {
- CNavNode *nodeTowardOtherArea2 = nodeTowardOtherArea->GetConnectedNode( dir );
- if ( nodeTowardOtherArea2 && areaOther->Contains( *nodeTowardOtherArea2->GetPosition() ) )
- {
- float areaDeltaZ = node->GetPosition()->z - nodeTowardOtherArea2->GetPosition()->z;
- if ( fabs( areaDeltaZ ) <= MaxTraversableHeight )
- {
- // if we arrived in the other area, the obstacle height to get here was the peak deltaZ of the node above to get here
- obstacleHeight = deltaZ;
- // make a nav area MinObstacleAreaWidth wide centered on the peak node, which is GenerationStepSize away from where we started
- obstacleDistStartCur = GenerationStepSize - (MinObstacleAreaWidth / 2);
- obstacleDistEndCur = GenerationStepSize + (MinObstacleAreaWidth / 2);
- }
- }
- }
- }
- }
- }
- }
-
- obstacleHeightMin = MIN( obstacleHeight, obstacleHeightMin );
- obstacleHeightMax = MAX( obstacleHeight, obstacleHeightMax );
- obstacleDistMin = MIN( obstacleDistStartCur, obstacleDistMin );
- obstacleDistMax = MAX( obstacleDistEndCur, obstacleDistMax );
-
- if ( obstacleHeightStart == 0 )
- {
- // keep track of the obstacle height and node z pos at the start of the edge
- obstacleHeightStart = obstacleHeight;
- zStart = node->GetPosition()->z;
- }
- // keep track of the obstacle height and node z pos at the end of the edge
- obstacleHeightEnd = obstacleHeight;
- zEnd = node->GetPosition()->z;
-
- }
- if ( node == nodeEnd )
- break;
-
- node = node->GetConnectedNode( dirEdge );
- }
-
-
-
-
- // if we had some obstacle height from EVERY node along the portal, then getting from this area to the other requires scaling an obstacle,
- // need to generate a nav area on top of it
- if ( ( obstacleHeightMax > MaxTraversableHeight ) && ( obstacleHeightMin > MaxTraversableHeight ) )
- {
- // If the maximum obstacle height was greater than both the height at start and end of the edge, then the obstacle is highest somewhere
- // in the middle. Use that as the height of both ends.
- if ( ( obstacleHeightMax > obstacleHeightStart ) && ( obstacleHeightMax > obstacleHeightEnd ) )
- {
- obstacleHeightStart = obstacleHeightMax;
- obstacleHeightEnd = obstacleHeightMax;
- }
-
- // for south and west, swap "start" and "end" values of edges so we can use common code below
- if ( dir == SOUTH || dir == WEST )
- {
- ::V_swap( obstacleHeightStart, obstacleHeightEnd );
- ::V_swap( zStart, zEnd );
- }
-
- // Enforce min area width for new area
- AdjustObstacleDistances( &obstacleDistMin, &obstacleDistMax, bMultiNode ? GenerationStepSize * 2 : GenerationStepSize );
- Assert( obstacleDistMin < obstacleDistMax );
- Assert( obstacleDistMax - obstacleDistMin >= MinObstacleAreaWidth );
- float newAreaWidth = obstacleDistMax - obstacleDistMin;
- Assert( newAreaWidth > 0 );
-
- // Calculate new area coordinates
- AddDirectionVector( &center, dir, obstacleDistMin + (newAreaWidth/2) );
-
- Vector cornerNW, cornerNE, cornerSE, cornerSW;
- switch ( dir )
- {
- case NORTH:
- case SOUTH:
- cornerNW.Init( center.x - halfPortalWidth, center.y - (newAreaWidth/2), zStart + obstacleHeightStart );
- cornerNE.Init( center.x + halfPortalWidth, center.y - (newAreaWidth/2), zEnd + obstacleHeightEnd );
- cornerSE.Init( center.x + halfPortalWidth, center.y + (newAreaWidth/2), zEnd + obstacleHeightEnd );
- cornerSW.Init( center.x - halfPortalWidth, center.y + (newAreaWidth/2), zStart + obstacleHeightStart );
- break;
- case EAST:
- case WEST:
- cornerNW.Init( center.x - (newAreaWidth/2), center.y - halfPortalWidth, zStart + obstacleHeightStart );
- cornerNE.Init( center.x + (newAreaWidth/2), center.y - halfPortalWidth, zEnd + obstacleHeightEnd );
- cornerSE.Init( center.x + (newAreaWidth/2), center.y + halfPortalWidth, zEnd + obstacleHeightEnd );
- cornerSW.Init( center.x - (newAreaWidth/2), center.y + halfPortalWidth, zStart + obstacleHeightStart );
- break;
- }
-
- CNavArea *areaNew = CreateArea();
- areaNew->Build( cornerNW, cornerNE, cornerSE, cornerSW );
-
- // add it to the nav area list
- TheNavAreas.AddToTail( areaNew );
- AddNavArea( areaNew );
-
- Assert( !areaNew->IsDegenerate() );
-
- Msg( "Created new fencetop area %d(%x) between %d(%x) and %d(%x)\n", areaNew->GetID(), areaNew->GetDebugID(), area->GetID(), area->GetDebugID(), areaOther->GetID(), areaOther->GetDebugID() );
-
- areaNew->SetAttributes( area->GetAttributes() );
- areaNew->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE | NAV_MESH_OBSTACLE_TOP );
-
- area->Disconnect( areaOther );
- area->ConnectTo( areaNew, dir );
-
- areaNew->ConnectTo( area, OppositeDirection( dir ) );
- areaNew->ConnectTo( areaOther, dir );
- if ( areaOther->IsConnected( area, OppositeDirection( dir ) ) )
- {
- areaOther->Disconnect( area );
- areaOther->ConnectTo( areaNew, OppositeDirection( dir ) );
- }
-// AddToSelectedSet( areaNew );
- return true;
- }
- }
-
- return false;
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Remove any obstacle top areas which overlap.
-*/
-void CNavMesh::RemoveOverlappingObstacleTopAreas()
-{
- // What we really want is the union of all obstacle top areas that get generated. That would be hard to compute exactly,
- // so instead we'll just remove any that overlap. The obstacle top areas don't have to be exact, we just need enough of
- // them so there is generally a path to get over any obstacle.
-
- // make a list of just the obstacle top areas to reduce the N of the N squared operation we're about to do
- CUtlVector<CNavArea *> vecObstacleTopAreas;
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
- if ( area->GetAttributes() & NAV_MESH_OBSTACLE_TOP )
- {
- vecObstacleTopAreas.AddToTail( area );
- }
- }
-
- // look at every pair of obstacle top areas
- CUtlVector<CNavArea *> vecAreasToRemove;
- FOR_EACH_VEC( vecObstacleTopAreas, it )
- {
- CNavArea *area = vecObstacleTopAreas[it];
-
- Vector normal, otherNormal;
- area->ComputeNormal( &normal );
- area->ComputeNormal( &otherNormal, true );
-
- // Remove any obstacle areas that are steep enough to be jump areas
- float lowestNormalZ = MIN( normal.z, otherNormal.z );
- if ( lowestNormalZ < nav_slope_limit.GetFloat() )
- {
- vecAreasToRemove.AddToTail( area );
- }
-
- for ( int it2 = it+1; it2 < vecObstacleTopAreas.Count(); it2++ )
- {
- CNavArea *areaOther = vecObstacleTopAreas[it2];
- if ( area->IsOverlapping( areaOther ) )
- {
- if ( area->Contains( areaOther ) )
- {
- // if one entirely contains the other, mark the other for removal
- vecAreasToRemove.AddToTail( areaOther );
- }
- else if ( areaOther->Contains( area ) )
- {
- // if one entirely contains the other, mark the other for removal
- vecAreasToRemove.AddToTail( area );
- }
- else
- {
- // if they overlap without one being a superset of the other, just remove the smaller area
- CNavArea *areaToRemove = ( area->GetSizeX() * area->GetSizeY() > areaOther->GetSizeX() * areaOther->GetSizeY() ? areaOther : area );
- vecAreasToRemove.AddToTail( areaToRemove );
- }
- }
- }
- }
-
- // now go delete all the areas we want to remove
- while ( vecAreasToRemove.Count() > 0 )
- {
- CNavArea *areaToDelete = vecAreasToRemove[0];
- RemoveFromSelectedSet( areaToDelete );
- TheNavMesh->OnEditDestroyNotify( areaToDelete );
- TheNavAreas.FindAndRemove( areaToDelete );
- TheNavMesh->DestroyArea( areaToDelete );
-
- // remove duplicates so we don't double-delete
- while ( vecAreasToRemove.FindAndRemove( areaToDelete ) );
- }
-
-}
-
-static void CommandNavCheckStairs( void )
-{
- TheNavMesh->MarkStairAreas();
-}
-static ConCommand nav_check_stairs( "nav_check_stairs", CommandNavCheckStairs, "Update the nav mesh STAIRS attribute" );
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Mark all areas that are on stairs.
- */
-void CNavMesh::MarkStairAreas( void )
-{
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
- area->TestStairs();
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-enum StairTestType
-{
- STAIRS_NO,
- STAIRS_YES,
- STAIRS_MAYBE,
-};
-
-
-//--------------------------------------------------------------------------------------------------------
-// Test if a line across a nav area could be part of a stairway
-StairTestType IsStairs( const Vector &start, const Vector &end, StairTestType ret )
-{
- if ( ret == STAIRS_NO )
- return ret;
-
- const float inc = 5.0f;
-
- // the minimum height change each step to be a step and not a slope
- const float minStepZ = inc * tan( acos( nav_slope_limit.GetFloat() ) );
- const float MinStairNormal = 0.97f; // we don't care about ramps, just actual flat steps
-
- float t;
- Vector pos, normal;
- float height, priorHeight;
-
- // walk the line, checking for step height discontinuities
- float length = start.AsVector2D().DistTo( end.AsVector2D() );
-
- trace_t trace;
- CTraceFilterNoNPCsOrPlayer filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT );
- Vector hullMins( -inc/2, -inc/2, 0 );
- Vector hullMaxs( inc/2, inc/2, 0 );
- hullMaxs.z = 1; // don't care about vertical clearance
-
- if ( fabs( start.x - end.x ) > fabs( start.y - end.y ) )
- {
- hullMins.x = -8;
- hullMaxs.x = 8;
- }
- else
- {
- hullMins.y = -8;
- hullMaxs.y = 8;
- }
-
- Vector traceOffset( 0, 0, VEC_DUCK_HULL_MAX.z );
-
- // total height change must exceed a single step to be stairs
- if ( abs( start.z - end.z ) > StepHeight )
- {
- // initialize the height delta
- UTIL_TraceHull( start + traceOffset, start - traceOffset, hullMins, hullMaxs, MASK_NPCSOLID, &filter, &trace );
- if ( trace.startsolid || trace.IsDispSurface() )
- {
- return STAIRS_NO;
- }
- priorHeight = trace.endpos.z;
-
- // Save a copy for debug overlays
- Vector prevGround = start;
- prevGround.z = priorHeight;
-
- float traceIncrement = inc / length;
- for( t = 0.0f; t <= 1.0f; t += traceIncrement )
- {
- pos = start + t * ( end - start );
-
- UTIL_TraceHull( pos + traceOffset, pos - traceOffset, hullMins, hullMaxs, MASK_NPCSOLID, &filter, &trace );
- if ( trace.startsolid || trace.IsDispSurface() )
- {
- return STAIRS_NO;
- }
- height = trace.endpos.z;
- normal = trace.plane.normal;
-
- // Save a copy for debug overlays
- Vector ground( pos );
- ground.z = height;
- //NDebugOverlay::Cross3D( ground, 3, 0, 0, 255, true, 100.0f );
- //NDebugOverlay::Box( ground, hullMins, hullMaxs, 0, 0, 255, 0.0f, 100.0f );
-
- if ( t == 0.0f && fabs( height - start.z ) > StepHeight )
- {
- // Discontinuity at start
- return STAIRS_NO;
- }
-
- if ( t == 1.0f && fabs( height - end.z ) > StepHeight )
- {
- // Discontinuity at end
- return STAIRS_NO;
- }
-
- if ( normal.z < MinStairNormal )
- {
- // too steep here
- return STAIRS_NO;
- }
-
-
- float deltaZ = abs( height - priorHeight );
-
- if ( deltaZ >= minStepZ && deltaZ <= StepHeight )
- {
- // found a step
- ret = STAIRS_YES;
- }
- else if ( deltaZ > StepHeight )
- {
- // too steep here
- //NDebugOverlay::Cross3D( ground, 5, 255, 0, 0, true, 10.0f );
- //NDebugOverlay::Cross3D( prevGround, 5, 0, 255, 0, true, 10.0f );
- return STAIRS_NO;
- }
-
- // Save a copy for debug overlays
- prevGround = pos;
- prevGround.z = height;
-
- priorHeight = height;
- }
- }
-
- return ret;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Test an area for being on stairs
- * NOTE: This assumes a globally constant "step height",
- * and walkable surface normal, which really should be locomotor-specific.
- */
-bool CNavArea::TestStairs( void )
-{
- // clear STAIRS attribute
- SetAttributes( GetAttributes() & ~NAV_MESH_STAIRS );
-
- if ( GetSizeX() <= GenerationStepSize && GetSizeY() <= GenerationStepSize )
- {
- // Don't bother with stairs on small areas
- return false;
- }
-
- const float MatchingNormalDot = 0.95f;
- Vector firstNormal, secondNormal;
- ComputeNormal( &firstNormal );
- ComputeNormal( &secondNormal, true );
- if ( firstNormal.Dot( secondNormal ) < MatchingNormalDot )
- {
- // area corners aren't coplanar - no stairs
- return false;
- }
-
- // test center and edges north-to-south, and east-to-west
- StairTestType ret = STAIRS_MAYBE;
- Vector from, to;
-
- const float inset = 5.0f; // inset to keep the tests completely inside the nav area
-
- from = GetCorner( NORTH_WEST ) + Vector( inset, inset, 0 );
- to = GetCorner( NORTH_EAST ) + Vector( -inset, inset, 0 );
- ret = IsStairs( from, to, ret );
-
- from = GetCorner( SOUTH_WEST ) + Vector( inset, -inset, 0 );
- to = GetCorner( SOUTH_EAST ) + Vector( -inset, -inset, 0 );
- ret = IsStairs( from, to, ret );
-
- from = GetCorner( NORTH_WEST ) + Vector( inset, inset, 0 );
- to = GetCorner( SOUTH_WEST ) + Vector( inset, -inset, 0 );
- ret = IsStairs( from, to, ret );
-
- from = GetCorner( NORTH_EAST ) + Vector( -inset, inset, 0 );
- to = GetCorner( SOUTH_EAST ) + Vector( -inset, -inset, 0 );
- ret = IsStairs( from, to, ret );
-
- from = ( GetCorner( NORTH_WEST ) + GetCorner( NORTH_EAST ) ) / 2.0f + Vector( 0, inset, 0 );
- to = ( GetCorner( SOUTH_WEST ) + GetCorner( SOUTH_EAST ) ) / 2.0f + Vector( 0, -inset, 0 );
- ret = IsStairs( from, to, ret );
-
- from = ( GetCorner( NORTH_EAST ) + GetCorner( SOUTH_EAST ) ) / 2.0f + Vector( -inset, 0, 0 );
- to = ( GetCorner( NORTH_WEST ) + GetCorner( SOUTH_WEST ) ) / 2.0f + Vector( inset, 0, 0 );
- ret = IsStairs( from, to, ret );
-
- if ( ret == STAIRS_YES )
- {
- SetAttributes( NAV_MESH_STAIRS );
- return true;
- }
-
- return false;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-CON_COMMAND_F( nav_test_stairs, "Test the selected set for being on stairs", FCVAR_CHEAT )
-{
- int count = 0;
-
- const NavAreaVector &selectedSet = TheNavMesh->GetSelectedSet();
- for ( int i=0; i<selectedSet.Count(); ++i )
- {
- CNavArea *area = selectedSet[i];
- if ( area->TestStairs() )
- {
- ++count;
- }
- }
-
- Msg( "Marked %d areas as stairs\n", count );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Jump areas aren't used by the NextBot. Delete them, connecting adjacent areas.
- */
-void CNavMesh::RemoveJumpAreas( void )
-{
- if ( !nav_generate_fixup_jump_areas.GetBool() )
- {
- return;
- }
-
- CUtlVector< CNavArea * > unusedAreas;
-
- int i;
- for ( i=0; i<TheNavAreas.Count(); ++i )
- {
- CNavArea *testArea = TheNavAreas[i];
- if ( !(testArea->GetAttributes() & NAV_MESH_JUMP) )
- {
- continue;
- }
-
- unusedAreas.AddToTail( testArea );
- }
-
- for ( i=0; i<unusedAreas.Count(); ++i )
- {
- CNavArea *areaToDelete = unusedAreas[i];
- TheNavMesh->OnEditDestroyNotify( areaToDelete );
- TheNavAreas.FindAndRemove( areaToDelete );
- TheNavMesh->DestroyArea( areaToDelete );
- }
-
- StripNavigationAreas();
-
- SetMarkedArea( NULL ); // unmark the mark area
- m_markedCorner = NUM_CORNERS; // clear the corner selection
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-void CNavMesh::CommandNavRemoveJumpAreas( void )
-{
- JumpConnector connector;
- ForAllAreas( connector );
-
- int before = TheNavAreas.Count();
- RemoveJumpAreas();
- int after = TheNavAreas.Count();
-
- Msg( "Removed %d jump areas\n", before - after );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Recursively chop area in half along X until child areas are roughly square
- */
-static void splitX( CNavArea *area )
-{
- if (area->IsRoughlySquare())
- return;
-
- float split = area->GetSizeX();
- split /= 2.0f;
- split += area->GetCorner( NORTH_WEST ).x;
-
- split = TheNavMesh->SnapToGrid( split );
-
- const float epsilon = 0.1f;
- if (fabs(split - area->GetCorner( NORTH_WEST ).x) < epsilon ||
- fabs(split - area->GetCorner( SOUTH_EAST ).x) < epsilon)
- {
- // too small to subdivide
- return;
- }
-
- CNavArea *alpha, *beta;
- if (area->SplitEdit( false, split, &alpha, &beta ))
- {
- // split each new area until square
- splitX( alpha );
- splitX( beta );
- }
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Recursively chop area in half along Y until child areas are roughly square
- */
-static void splitY( CNavArea *area )
-{
- if (area->IsRoughlySquare())
- return;
-
- float split = area->GetSizeY();
- split /= 2.0f;
- split += area->GetCorner( NORTH_WEST ).y;
-
- split = TheNavMesh->SnapToGrid( split );
-
- const float epsilon = 0.1f;
- if (fabs(split - area->GetCorner( NORTH_WEST ).y) < epsilon ||
- fabs(split - area->GetCorner( SOUTH_EAST ).y) < epsilon)
- {
- // too small to subdivide
- return;
- }
-
- CNavArea *alpha, *beta;
- if (area->SplitEdit( true, split, &alpha, &beta ))
- {
- // split each new area until square
- splitY( alpha );
- splitY( beta );
- }
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Split any long, thin, areas into roughly square chunks.
- */
-void CNavMesh::SquareUpAreas( void )
-{
- int it = 0;
-
- while( it < TheNavAreas.Count() )
- {
- CNavArea *area = TheNavAreas[ it ];
-
- // move the iterator in case the current area is split and deleted
- ++it;
-
- if (area->HasNodes() && !area->IsRoughlySquare())
- {
- // chop this area into square pieces
- if (area->GetSizeX() > area->GetSizeY())
- splitX( area );
- else
- splitY( area );
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-static bool testStitchConnection( CNavArea *source, CNavArea *target, const Vector &sourcePos, const Vector &targetPos )
-{
- trace_t result;
- Vector from( sourcePos );
- Vector pos( targetPos );
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
- Vector to, toNormal;
- bool success = false;
- if ( TraceAdjacentNode( 0, from, pos, &result ) )
- {
- to = result.endpos;
- toNormal = result.plane.normal;
- success = true;
- }
- else
- {
- // test going up ClimbUpHeight
- bool success = false;
- for ( float height = StepHeight; height <= ClimbUpHeight; height += 1.0f )
- {
- trace_t tr;
- Vector start( from );
- Vector end( pos );
- start.z += height;
- end.z += height;
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, &tr );
- if ( !tr.startsolid && tr.fraction == 1.0f )
- {
- if ( !StayOnFloor( &tr ) )
- {
- break;
- }
-
- to = tr.endpos;
- toNormal = tr.plane.normal;
-
- start = end = from;
- end.z += height;
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, &tr );
- if ( tr.fraction < 1.0f )
- {
- break;
- }
-
- success = true;
- break;
- }
- }
- }
-
- return success;
-}
-
-
-
-
-//--------------------------------------------------------------------------------------------------------
-class IncrementallyGeneratedAreas
-{
-public:
- bool operator()( CNavArea *area )
- {
- return area->HasNodes();
- }
-};
-
-
-//--------------------------------------------------------------------------------------------------------
-/**
- * Incremental generation fixup for where edges lap up against the existing nav mesh:
- * we have nodes, but the surrounding areas don't. So, we trace outward, to see if we
- * can walk/fall to an adjacent area. This handles dropping down into existing areas etc.
- * TODO: test pre-existing areas for drop-downs into the newly-generated areas.
- */
-void CNavMesh::StitchGeneratedAreas( void )
-{
- if ( m_generationMode == GENERATE_INCREMENTAL )
- {
- IncrementallyGeneratedAreas incrementalAreas;
- StitchMesh( incrementalAreas );
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------
-class AreaSet
-{
-public:
- AreaSet( CUtlVector< CNavArea * > *areas )
- {
- m_areas = areas;
- }
-
- bool operator()( CNavArea *area )
- {
- return ( m_areas->HasElement( area ) );
- }
-
-private:
- CUtlVector< CNavArea * > *m_areas;
-};
-
-
-//--------------------------------------------------------------------------------------------------------
-/**
- * Stitches an arbitrary set of areas (newly-merged, for example) into the existing mesh
- */
-void CNavMesh::StitchAreaSet( CUtlVector< CNavArea * > *areas )
-{
- AreaSet areaSet( areas );
- StitchMesh( areaSet );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Determine if we can "jump down" from given point
- */
-inline bool testJumpDown( const Vector *fromPos, const Vector *toPos )
-{
- float dz = fromPos->z - toPos->z;
-
- // drop can't be too far, or too short (or nonexistant)
- if (dz <= JumpCrouchHeight || dz >= DeathDrop)
- return false;
-
- //
- // Check LOS out and down
- //
- // +-----+
- // | |
- // F |
- // |
- // T
- //
-
- Vector from, to;
- float up;
- trace_t result;
-
- // Try to go up and out, up to ClimbUpHeight, to get over obstacles
- for ( up=1.0f; up<=ClimbUpHeight; up += 1.0f )
- {
- from = *fromPos;
- to.Init( fromPos->x, fromPos->y, fromPos->z + up );
-
- UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
- if (result.fraction <= 0.0f || result.startsolid)
- continue;
-
- from.Init( fromPos->x, fromPos->y, result.endpos.z - 0.5f );
- to.Init( toPos->x, toPos->y, from.z );
-
- UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
- if (result.fraction != 1.0f || result.startsolid)
- continue;
-
- // Success!
- break;
- }
-
- if ( up > ClimbUpHeight )
- return false;
-
- // We've made it up and out, so see if we can drop down
- from = to;
- to.z = toPos->z + 2.0f;
- UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
- if (result.fraction <= 0.0f || result.startsolid)
- return false;
-
- // Allow a little fudge so we can drop down onto stairs
- if ( result.endpos.z > to.z + StepHeight )
- return false;
-
- return true;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-inline CNavArea *findJumpDownArea( const Vector *fromPos, NavDirType dir )
-{
- Vector start( fromPos->x, fromPos->y, fromPos->z + HalfHumanHeight );
- AddDirectionVector( &start, dir, GenerationStepSize/2.0f );
-
- Vector toPos;
- CNavArea *downArea = findFirstAreaInDirection( &start, dir, 4.0f * GenerationStepSize, DeathDrop, NULL, &toPos );
-
- if (downArea && testJumpDown( fromPos, &toPos ))
- return downArea;
-
- return NULL;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-template < typename Functor >
-void CNavMesh::StitchAreaIntoMesh( CNavArea *area, NavDirType dir, Functor &func )
-{
- Vector corner1, corner2;
- switch ( dir )
- {
- case NORTH:
- corner1 = area->GetCorner( NORTH_WEST );
- corner2 = area->GetCorner( NORTH_EAST );
- break;
- case SOUTH:
- corner1 = area->GetCorner( SOUTH_WEST );
- corner2 = area->GetCorner( SOUTH_EAST );
- break;
- case EAST:
- corner1 = area->GetCorner( NORTH_EAST );
- corner2 = area->GetCorner( SOUTH_EAST );
- break;
- case WEST:
- corner1 = area->GetCorner( NORTH_WEST );
- corner2 = area->GetCorner( SOUTH_WEST );
- break;
- }
-
- Vector edgeDir = corner2 - corner1;
- edgeDir.z = 0.0f;
-
- float edgeLength = edgeDir.NormalizeInPlace();
-
- for ( float n=0; n<edgeLength - 1.0f; n += GenerationStepSize )
- {
- Vector sourcePos = corner1 + edgeDir * ( n + 0.5f );
- sourcePos.z += HalfHumanHeight;
-
- Vector targetPos = sourcePos;
- switch ( dir )
- {
- case NORTH: targetPos.y -= GenerationStepSize * 0.5f; break;
- case SOUTH: targetPos.y += GenerationStepSize * 0.5f; break;
- case EAST: targetPos.x += GenerationStepSize * 0.5f; break;
- case WEST: targetPos.x -= GenerationStepSize * 0.5f; break;
- }
-
- CNavArea *targetArea = TheNavMesh->GetNavArea( targetPos );
- if ( targetArea && !func( targetArea ) )
- {
- targetPos.z = targetArea->GetZ( targetPos.x, targetPos.y ) + HalfHumanHeight;
-
- // outgoing connection
- if ( testStitchConnection( area, targetArea, sourcePos, targetPos ) )
- {
- area->ConnectTo( targetArea, dir );
- }
-
- // incoming connection
- if ( testStitchConnection( targetArea, area, targetPos, sourcePos ) )
- {
- targetArea->ConnectTo( area, OppositeDirection( dir ) );
- }
- }
- else
- {
- sourcePos.z -= HalfHumanHeight;
- sourcePos.z += 1;
- CNavArea *downArea = findJumpDownArea( &sourcePos, dir );
- if ( downArea && downArea != area && !func( downArea ) )
- {
- area->ConnectTo( downArea, dir );
- }
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Checks to see if there is a cliff - a drop of at least CliffHeight - in specified direction.
-*/
-inline bool CheckCliff( const Vector *fromPos, NavDirType dir, bool bExhaustive = true )
-{
- // cliffs are half-baked, not used by any existing AI, and create poorly behaved nav areas (ie: long, thin, strips) (MSB 8/7/09)
- return false;
-
-
- Vector toPos( fromPos->x, fromPos->y, fromPos->z );
- AddDirectionVector( &toPos, dir, GenerationStepSize );
-
- trace_t trace;
- // trace a step in specified direction and see where we'd find up
- if ( TraceAdjacentNode( 0, *fromPos, toPos, &trace, DeathDrop * 10 ) && !trace.allsolid && !trace.startsolid )
- {
- float deltaZ = fromPos->z - trace.endpos.z;
- // would we fall off a cliff?
- if ( deltaZ > CliffHeight )
- return true;
-
- // if not, special case for south and east. South and east edges are not considered part of a nav area, so
- // we look ahead two steps for south and east. This ensures that the n-1th row and column of nav nodes
- // on the south and east sides of a nav area reflect any cliffs on the nth row and column.
-
- // if we're looking to south or east, and the first node we found was approximately flat, and this is the top-level
- // call, recurse one level to check one more step in this direction
- if ( ( dir == SOUTH || dir == EAST ) && ( fabs( deltaZ ) < StepHeight ) && bExhaustive )
- {
- return CheckCliff( &trace.endpos, dir, false );
- }
- }
- return false;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Define connections between adjacent generated areas
- */
-void CNavMesh::ConnectGeneratedAreas( void )
-{
- Msg( "Connecting navigation areas...\n" );
-
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
-
- // scan along edge nodes, stepping one node over into the next area
- // for now, only use bi-directional connections
-
- // north edge
- CNavNode *node;
- for( node = area->m_node[ NORTH_WEST ]; node != area->m_node[ NORTH_EAST ]; node = node->GetConnectedNode( EAST ) )
- {
- CNavNode *adj = node->GetConnectedNode( NORTH );
-
- if (adj && adj->GetArea() && adj->GetConnectedNode( SOUTH ) == node )
- {
- area->ConnectTo( adj->GetArea(), NORTH );
- }
- else
- {
- CNavArea *downArea = findJumpDownArea( node->GetPosition(), NORTH );
- if (downArea && downArea != area)
- area->ConnectTo( downArea, NORTH );
- }
- }
-
- // west edge
- for( node = area->m_node[ NORTH_WEST ]; node != area->m_node[ SOUTH_WEST ]; node = node->GetConnectedNode( SOUTH ) )
- {
- CNavNode *adj = node->GetConnectedNode( WEST );
-
- if (adj && adj->GetArea() && adj->GetConnectedNode( EAST ) == node )
- {
- area->ConnectTo( adj->GetArea(), WEST );
- }
- else
- {
- CNavArea *downArea = findJumpDownArea( node->GetPosition(), WEST );
- if (downArea && downArea != area)
- area->ConnectTo( downArea, WEST );
- }
- }
-
- // south edge - this edge's nodes are actually part of adjacent areas
- // move one node north, and scan west to east
- /// @todo This allows one-node-wide areas - do we want this?
- node = area->m_node[ SOUTH_WEST ];
- if ( node ) // pre-existing areas in incremental generates won't have nodes
- {
- node = node->GetConnectedNode( NORTH );
- }
- if (node)
- {
- CNavNode *end = area->m_node[ SOUTH_EAST ]->GetConnectedNode( NORTH );
- /// @todo Figure out why cs_backalley gets a NULL node in here...
- for( ; node && node != end; node = node->GetConnectedNode( EAST ) )
- {
- CNavNode *adj = node->GetConnectedNode( SOUTH );
-
- if (adj && adj->GetArea() && adj->GetConnectedNode( NORTH ) == node )
- {
- area->ConnectTo( adj->GetArea(), SOUTH );
- }
- else
- {
- CNavArea *downArea = findJumpDownArea( node->GetPosition(), SOUTH );
- if (downArea && downArea != area)
- area->ConnectTo( downArea, SOUTH );
- }
- }
- }
-
- // south edge part 2 - scan the actual south edge. If the node is not part of an adjacent area, then it
- // really belongs to us. This will happen if our area runs right up against a ledge.
- for( node = area->m_node[ SOUTH_WEST ]; node != area->m_node[ SOUTH_EAST ]; node = node->GetConnectedNode( EAST ) )
- {
- if ( node->GetArea() )
- continue; // some other area owns this node, pay no attention to it
-
- CNavNode *adj = node->GetConnectedNode( SOUTH );
-
- if ( node->IsBlockedInAnyDirection() || (adj && adj->IsBlockedInAnyDirection()) )
- continue; // The space around this node is blocked, so don't connect across it
-
- // Don't directly connect to adj's area, since it's already 1 cell removed from our area.
- // There was no area in between, presumably for good reason. Only look for jump down links.
- if ( !adj || !adj->GetArea() )
- {
- CNavArea *downArea = findJumpDownArea( node->GetPosition(), SOUTH );
- if (downArea && downArea != area)
- area->ConnectTo( downArea, SOUTH );
- }
- }
-
- // east edge - this edge's nodes are actually part of adjacent areas
- node = area->m_node[ NORTH_EAST ];
- if ( node ) // pre-existing areas in incremental generates won't have nodes
- {
- node = node->GetConnectedNode( WEST );
- }
- if (node)
- {
- CNavNode *end = area->m_node[ SOUTH_EAST ]->GetConnectedNode( WEST );
- for( ; node && node != end; node = node->GetConnectedNode( SOUTH ) )
- {
- CNavNode *adj = node->GetConnectedNode( EAST );
-
- if (adj && adj->GetArea() && adj->GetConnectedNode( WEST ) == node )
- {
- area->ConnectTo( adj->GetArea(), EAST );
- }
- else
- {
- CNavArea *downArea = findJumpDownArea( node->GetPosition(), EAST );
- if (downArea && downArea != area)
- area->ConnectTo( downArea, EAST );
- }
- }
- }
-
- // east edge part 2 - scan the actual east edge. If the node is not part of an adjacent area, then it
- // really belongs to us. This will happen if our area runs right up against a ledge.
- for( node = area->m_node[ NORTH_EAST ]; node != area->m_node[ SOUTH_EAST ]; node = node->GetConnectedNode( SOUTH ) )
- {
- if ( node->GetArea() )
- continue; // some other area owns this node, pay no attention to it
-
- CNavNode *adj = node->GetConnectedNode( EAST );
-
- if ( node->IsBlockedInAnyDirection() || (adj && adj->IsBlockedInAnyDirection()) )
- continue; // The space around this node is blocked, so don't connect across it
-
- // Don't directly connect to adj's area, since it's already 1 cell removed from our area.
- // There was no area in between, presumably for good reason. Only look for jump down links.
- if ( !adj || !adj->GetArea() )
- {
- CNavArea *downArea = findJumpDownArea( node->GetPosition(), EAST );
- if (downArea && downArea != area)
- area->ConnectTo( downArea, EAST );
- }
- }
- }
-
- StitchGeneratedAreas();
-}
-
-//--------------------------------------------------------------------------------------------------------------
-bool CNavArea::IsAbleToMergeWith( CNavArea *other ) const
-{
- if ( !HasNodes() || ( GetAttributes() & NAV_MESH_NO_MERGE ) )
- return false;
-
- if ( !other->HasNodes() || ( other->GetAttributes() & NAV_MESH_NO_MERGE ) )
- return false;
-
- return true;
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Merge areas together to make larger ones (must remain rectangular - convex).
- * Areas can only be merged if their attributes match.
- */
-void CNavMesh::MergeGeneratedAreas( void )
-{
- Msg( "Merging navigation areas...\n" );
-
- bool merged;
-
- do
- {
- merged = false;
-
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
- if ( !area->HasNodes() || ( area->GetAttributes() & NAV_MESH_NO_MERGE ) )
- continue;
-
- // north edge
- FOR_EACH_VEC( area->m_connect[ NORTH ], nit )
- {
- CNavArea *adjArea = area->m_connect[ NORTH ][ nit ].area;
- if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
- continue;
-
- if ( area->GetSizeY() + adjArea->GetSizeY() > GenerationStepSize * nav_area_max_size.GetInt() )
- continue;
-
- if (area->m_node[ NORTH_WEST ] == adjArea->m_node[ SOUTH_WEST ] &&
- area->m_node[ NORTH_EAST ] == adjArea->m_node[ SOUTH_EAST ] &&
- area->GetAttributes() == adjArea->GetAttributes() &&
- area->IsCoplanar( adjArea ))
- {
- // merge vertical
- area->m_node[ NORTH_WEST ] = adjArea->m_node[ NORTH_WEST ];
- area->m_node[ NORTH_EAST ] = adjArea->m_node[ NORTH_EAST ];
-
- merged = true;
- //CONSOLE_ECHO( " Merged (north) areas #%d and #%d\n", area->m_id, adjArea->m_id );
-
- area->FinishMerge( adjArea );
-
- // restart scan - iterator is invalidated
- break;
- }
- }
-
- if (merged)
- break;
-
- // south edge
- FOR_EACH_VEC( area->m_connect[ SOUTH ], sit )
- {
- CNavArea *adjArea = area->m_connect[ SOUTH ][ sit ].area;
- if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
- continue;
-
- if ( area->GetSizeY() + adjArea->GetSizeY() > GenerationStepSize * nav_area_max_size.GetInt() )
- continue;
-
- if (adjArea->m_node[ NORTH_WEST ] == area->m_node[ SOUTH_WEST ] &&
- adjArea->m_node[ NORTH_EAST ] == area->m_node[ SOUTH_EAST ] &&
- area->GetAttributes() == adjArea->GetAttributes() &&
- area->IsCoplanar( adjArea ))
- {
- // merge vertical
- area->m_node[ SOUTH_WEST ] = adjArea->m_node[ SOUTH_WEST ];
- area->m_node[ SOUTH_EAST ] = adjArea->m_node[ SOUTH_EAST ];
-
- merged = true;
- //CONSOLE_ECHO( " Merged (south) areas #%d and #%d\n", area->m_id, adjArea->m_id );
-
- area->FinishMerge( adjArea );
-
- // restart scan - iterator is invalidated
- break;
- }
-
- }
-
- if (merged)
- break;
-
-
- // west edge
- FOR_EACH_VEC( area->m_connect[ WEST ], wit )
- {
- CNavArea *adjArea = area->m_connect[ WEST ][ wit ].area;
- if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
- continue;
-
- if ( area->GetSizeX() + adjArea->GetSizeX() > GenerationStepSize * nav_area_max_size.GetInt() )
- continue;
-
- if (area->m_node[ NORTH_WEST ] == adjArea->m_node[ NORTH_EAST ] &&
- area->m_node[ SOUTH_WEST ] == adjArea->m_node[ SOUTH_EAST ] &&
- area->GetAttributes() == adjArea->GetAttributes() &&
- area->IsCoplanar( adjArea ))
- {
- // merge horizontal
- area->m_node[ NORTH_WEST ] = adjArea->m_node[ NORTH_WEST ];
- area->m_node[ SOUTH_WEST ] = adjArea->m_node[ SOUTH_WEST ];
-
- merged = true;
- //CONSOLE_ECHO( " Merged (west) areas #%d and #%d\n", area->m_id, adjArea->m_id );
-
- area->FinishMerge( adjArea );
-
- // restart scan - iterator is invalidated
- break;
- }
-
- }
-
- if (merged)
- break;
-
- // east edge
- FOR_EACH_VEC( area->m_connect[ EAST ], eit )
- {
- CNavArea *adjArea = area->m_connect[ EAST ][ eit ].area;
- if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
- continue;
-
- if ( area->GetSizeX() + adjArea->GetSizeX() > GenerationStepSize * nav_area_max_size.GetInt() )
- continue;
-
- if (adjArea->m_node[ NORTH_WEST ] == area->m_node[ NORTH_EAST ] &&
- adjArea->m_node[ SOUTH_WEST ] == area->m_node[ SOUTH_EAST ] &&
- area->GetAttributes() == adjArea->GetAttributes() &&
- area->IsCoplanar( adjArea ))
- {
- // merge horizontal
- area->m_node[ NORTH_EAST ] = adjArea->m_node[ NORTH_EAST ];
- area->m_node[ SOUTH_EAST ] = adjArea->m_node[ SOUTH_EAST ];
-
- merged = true;
- //CONSOLE_ECHO( " Merged (east) areas #%d and #%d\n", area->m_id, adjArea->m_id );
-
- area->FinishMerge( adjArea );
-
- // restart scan - iterator is invalidated
- break;
- }
- }
-
- if (merged)
- break;
- }
- }
- while( merged );
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Given arbitrary corners of a compass grid-aligned rectangle, classify them by compass direction.
-* Input: vec[4]: arbitrary corners
-* Output: vecNW, vecNE, vecSE, vecSW: filled in with which corner is in which compass direction
-*/
-void ClassifyCorners( Vector vec[4], Vector &vecNW, Vector &vecNE, Vector &vecSE, Vector &vecSW )
-{
- vecNW = vecNE = vecSE = vecSW = vec[0];
-
- for ( int i = 0; i < 4; i++ )
- {
- if ( ( vec[i].x <= vecNW.x ) && ( vec[i].y <= vecNW.y ) )
- {
- vecNW = vec[i];
- }
- if ( ( vec[i].x >= vecNE.x ) && ( vec[i].y <= vecNE.y ) )
- {
- vecNE = vec[i];
- }
- if ( ( vec[i].x >= vecSE.x ) && ( vec[i].y >= vecSE.y ) )
- {
- vecSE = vec[i];
- }
- if ( ( vec[i].x <= vecSW.x ) && ( vec[i].y >= vecSW.y ) )
- {
- vecSW = vec[i];
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Perform miscellaneous fixups to generated mesh
-*/
-void CNavMesh::FixUpGeneratedAreas( void )
-{
- FixCornerOnCornerAreas();
- FixConnections();
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-void CNavMesh::FixConnections( void )
-{
- // Test the steep sides of stairs for any outgoing links that cross nodes that were partially obstructed.
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
- if ( !area->HasAttributes( NAV_MESH_STAIRS ) )
- continue;
-
- if ( !area->HasNodes() )
- continue;
-
- for ( int dir=0; dir<NUM_DIRECTIONS; ++dir )
- {
- NavCornerType cornerType[2];
- GetCornerTypesInDirection( (NavDirType)dir, &cornerType[0], &cornerType[1] );
-
- // Flat edges of stairs need to connect. It's the slopes we don't want to climb over things for.
- float cornerDeltaZ = fabs( area->GetCorner( cornerType[0] ).z - area->GetCorner( cornerType[1] ).z );
- if ( cornerDeltaZ < StepHeight )
- continue;
-
- const NavConnectVector *connectedAreas = area->GetAdjacentAreas( (NavDirType)dir );
- CUtlVector< CNavArea * > areasToDisconnect;
- for ( int i=0; i<connectedAreas->Count(); ++i )
- {
- CNavArea *adjArea = connectedAreas->Element(i).area;
- if ( !adjArea->HasNodes() )
- continue;
-
- Vector pos, adjPos;
- float width;
- area->ComputePortal( adjArea, (NavDirType)dir, &pos, &width );
- adjArea->GetClosestPointOnArea( pos, &adjPos );
-
- CNavNode *node = area->FindClosestNode( pos, (NavDirType)dir );
- CNavNode *adjNode = adjArea->FindClosestNode( adjPos, OppositeDirection( (NavDirType)dir ) );
- pos = *node->GetPosition();
- adjPos = *adjNode->GetPosition();
-
- if ( !node || !adjNode )
- continue;
-
- NavCornerType adjCornerType[2];
- GetCornerTypesInDirection( OppositeDirection((NavDirType)dir), &adjCornerType[0], &adjCornerType[1] );
-
- // From the stair's perspective, we can't go up more than step height to reach the adjacent area.
- // Also, if the adjacent area has to jump up higher than StepHeight above the stair area to reach the stairs,
- // there's an obstruction close to the adjacent area that could prevent walking from the stairs down.
- if ( node->GetGroundHeightAboveNode( cornerType[0] ) > StepHeight )
- {
- areasToDisconnect.AddToTail( adjArea );
- }
- else if ( node->GetGroundHeightAboveNode( cornerType[1] ) > StepHeight )
- {
- areasToDisconnect.AddToTail( adjArea );
- }
- else if ( adjPos.z + adjNode->GetGroundHeightAboveNode( adjCornerType[0] ) > pos.z + StepHeight )
- {
- areasToDisconnect.AddToTail( adjArea );
- }
- else if ( adjPos.z + adjNode->GetGroundHeightAboveNode( adjCornerType[1] ) > pos.z + StepHeight )
- {
- areasToDisconnect.AddToTail( adjArea );
- }
- }
-
- for ( int i=0; i<areasToDisconnect.Count(); ++i )
- {
- area->Disconnect( areasToDisconnect[i] );
- }
- }
- }
-
- // Test to prevent A->C if A->B->C. This can happen in doorways and dropdowns from rooftops.
- // @TODO: find the root cause of A->C links.
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
- CUtlVector< CNavArea * > areasToDisconnect;
- for ( int dir=0; dir<NUM_DIRECTIONS; ++dir )
- {
- const NavConnectVector *connectedAreas = area->GetAdjacentAreas( (NavDirType)dir );
- for ( int i=0; i<connectedAreas->Count(); ++i )
- {
- CNavArea *adjArea = connectedAreas->Element(i).area;
- const NavConnectVector *adjConnectedAreas = adjArea->GetAdjacentAreas( (NavDirType)dir );
- for ( int j=0; j<adjConnectedAreas->Count(); ++j )
- {
- CNavArea *farArea = adjConnectedAreas->Element(j).area;
-
- if ( area->IsConnected( farArea, (NavDirType)dir ) )
- {
- areasToDisconnect.AddToTail( farArea );
- }
- }
- }
- }
-
- for ( int i=0; i<areasToDisconnect.Count(); ++i )
- {
- area->Disconnect( areasToDisconnect[i] );
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Fix any spots where we there are nav nodes touching only corner-on-corner but we intend bots to be able to traverse
-*/
-void CNavMesh::FixCornerOnCornerAreas( void )
-{
- const float MaxDrop = StepHeight; // don't make corner on corner areas that are too steep
-
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
-
- // determine if we have any corners where the only nav area we touch is diagonally corner-to-corner.
- // if there are, generate additional small (0.5 x 0.5 grid size) nav areas in the corners between
- // them if map geometry allows and make connections in cardinal compass directions to create a path
- // between the two areas.
-
-//
-// XXXXXXXXX XXXXXXXXX
-// X X X X
-// X other X ****X other X
-// X X *newX X
-// XXXXXXXXXXXXXXXXX => XXXXXXXXXXXXXXXXX
-// X X X Xnew*
-// X area X X area X****
-// X X X X
-// XXXXXXXXX XXXXXXXXX
-//
-
- // check each corner
- for ( int iCorner = NORTH_WEST; iCorner < NUM_CORNERS; iCorner++ )
- {
- // get cardinal direction to right and left of this corner
- NavDirType dirToRight = (NavDirType) iCorner;
- NavDirType dirToLeft = (NavDirType) ( ( iCorner+3 ) % NUM_DIRECTIONS );
-
- // if we have any connections on cardinal compass directions on edge on either side of corner we're OK, skip this nav area
- if ( area->GetAdjacentCount( dirToLeft ) > 0 || area->GetAdjacentCount( dirToRight ) > 0 ||
- area->GetIncomingConnections( dirToLeft )->Count() > 0 || area->GetIncomingConnections( dirToRight )->Count() > 0 )
- continue;
-
- Vector cornerPos = area->GetCorner( (NavCornerType) iCorner );
- NavDirType dirToRightTwice = DirectionRight( dirToRight );
- NavDirType dirToLeftTwice = DirectionLeft( dirToLeft );
- NavDirType dirsAlongOtherEdge[2] = { dirToLeft, dirToRight };
- NavDirType dirsAlongOurEdge[2] = { dirToLeftTwice, dirToRightTwice };
-
- // consider 2 potential new nav areas, to left and right of the corner we're considering
- for ( int iDir = 0; iDir < ARRAYSIZE( dirsAlongOtherEdge ); iDir++ )
- {
- NavDirType dirAlongOtherEdge = dirsAlongOtherEdge[iDir];
- NavDirType dirAlongOurEdge = dirsAlongOurEdge[iDir];
-
- // look at the point 0.5 grid units along edge of other nav area
- Vector vecDeltaOtherEdge;
- DirectionToVector2D( dirAlongOtherEdge, (Vector2D *) &vecDeltaOtherEdge );
- vecDeltaOtherEdge.z = 0;
- vecDeltaOtherEdge *= GenerationStepSize * 0.5;
- Vector vecOtherEdgePos = cornerPos + vecDeltaOtherEdge;
-
- // see if there is a nav area at that location
- CNavArea *areaOther = GetNavArea( vecOtherEdgePos );
- Assert( areaOther != area );
- if ( !areaOther )
- continue; // no other area in that location, we're not touching on corner
-
- // see if we can move from our corner in that direction
- trace_t result;
- if ( !TraceAdjacentNode( 0, cornerPos, vecOtherEdgePos, &result, MaxDrop ) )
- continue; // something is blocking movement, don't create additional nodes to aid movement
-
- // get the corner of the other nav area that might touch our corner
- int iCornerOther = ( ( iCorner + 2 ) % NUM_CORNERS );
- Vector cornerPosOther = areaOther->GetCorner( (NavCornerType) iCornerOther );
-
- if ( cornerPos != cornerPosOther )
- continue; // that nav area does not touch us on corner
-
- // we are touching corner-to-corner with the other nav area and don't have connections in cardinal directions around
- // the corner that touches, this is a candidate to generate new small helper nav areas.
-
- // calculate the corners of the 0.5 x 0.5 nav area we would consider building between us and the other nav area whose corner we touch
- Vector vecDeltaOurEdge;
- DirectionToVector2D( dirAlongOurEdge, (Vector2D *) &vecDeltaOurEdge );
- vecDeltaOurEdge.z = 0;
- vecDeltaOurEdge *= GenerationStepSize * 0.5;
- Vector vecOurEdgePos = cornerPos + vecDeltaOurEdge;
- Vector vecCorner[4];
- vecCorner[0] = cornerPos + vecDeltaOtherEdge + vecDeltaOurEdge; // far corner of new nav area
- vecCorner[1] = cornerPos + vecDeltaOtherEdge; // intersection of far edge of new nav area with other nav area we touch
- vecCorner[2] = cornerPos; // common corner of this nav area, nav area we touch, and new nav area
- vecCorner[3] = cornerPos + vecDeltaOurEdge; // intersection of far edge of new nav area with this nav area
-
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
- if ( !TraceAdjacentNode( 0, vecCorner[1], vecCorner[0], &result, MaxDrop ) || // can we move from edge of other area to far corner of new node
- !TraceAdjacentNode( 0, vecCorner[3], vecCorner[0], &result, MaxDrop ) ) // can we move from edge of this area to far corner of new node
- continue; // new node would not fit
-
- // as sanity check, make sure there's not already a nav area there, shouldn't be
- CNavArea *areaTest = GetNavArea( vecCorner[0] );
- Assert ( !areaTest );
- if ( areaTest )
- continue;
-
- vecCorner[0] = result.endpos;
-
- // create a new nav area
- CNavArea *areaNew = CreateArea();
-
- // arrange the corners of the new nav area by compass direction
- Vector vecNW, vecNE, vecSE, vecSW;
- ClassifyCorners( vecCorner, vecNW, vecNE, vecSE, vecSW );
- areaNew->Build( vecNW, vecNE, vecSE, vecSW );
-
- // add it to the nav area list
- TheNavAreas.AddToTail( areaNew );
- AddNavArea( areaNew );
-
- areaNew->SetAttributes( area->GetAttributes() );
-
- // reciprocally connect between this area and new area
- area->ConnectTo( areaNew, dirAlongOtherEdge );
- areaNew->ConnectTo( area, OppositeDirection( dirAlongOtherEdge ) );
-
- // reciprocally connect between other area and new area
- areaOther->ConnectTo( areaNew, dirAlongOurEdge );
- areaNew->ConnectTo( areaOther, OppositeDirection( dirAlongOurEdge ) );
- }
- }
- }
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Fix any areas where one nav area overhangs another and the two nav areas are connected. Subdivide the lower
-* nav area such that the upper nav area doesn't overhang any area it's connected to.
-*/
-void CNavMesh::SplitAreasUnderOverhangs( void )
-{
- // restart the whole process whenever this gets set to true
- bool bRestartProcessing = false;
-
- do
- {
- bRestartProcessing = false;
-
- // iterate all nav areas
- for ( int it = 0; it < TheNavAreas.Count() && !bRestartProcessing; it++ )
- {
- CNavArea *area = TheNavAreas[ it ];
- Extent areaExtent;
- area->GetExtent( &areaExtent );
-
- // iterate all directions
- for ( int dir = NORTH; dir < NUM_DIRECTIONS && !bRestartProcessing; dir++ )
- {
- // iterate all connections in that direction
- const NavConnectVector *pConnections = area->GetAdjacentAreas( (NavDirType) dir );
- for ( int iConnection = 0; iConnection < pConnections->Count() && !bRestartProcessing; iConnection++ )
- {
- CNavArea *otherArea = (*pConnections)[iConnection].area;
- Extent otherAreaExtent;
- otherArea->GetExtent( &otherAreaExtent );
-
- // see if the area we are connected to overlaps our X/Y extents
- if ( area->IsOverlapping( otherArea ) )
- {
- // if the upper area isn't at least crouch height above the lower area, this is some weird minor
- // overlap, disregard it
- const float flMinSeparation = HumanCrouchHeight;
- if ( !( areaExtent.lo.z > otherAreaExtent.hi.z + flMinSeparation ) &&
- !( otherAreaExtent.lo.z > areaExtent.hi.z + flMinSeparation ) )
- continue;
-
- // figure out which area is above and which is below
- CNavArea *areaBelow = area, *areaAbove = otherArea;
- NavDirType dirFromAboveToBelow = OppositeDirection( (NavDirType) dir );
- if ( otherAreaExtent.lo.z < areaExtent.lo.z )
- {
- areaBelow = otherArea;
- areaAbove = area;
- dirFromAboveToBelow = OppositeDirection( dirFromAboveToBelow );
- }
- NavDirType dirFromBelowToAbove = OppositeDirection( dirFromAboveToBelow );
-
- // Msg( "area %d overhangs area %d and is connected\n", areaAbove->GetID(), areaBelow->GetID() );
-
- Extent extentBelow, extentAbove;
- areaBelow->GetExtent( &extentBelow );
- areaAbove->GetExtent( &extentAbove );
-
- float splitCoord; // absolute world coordinate along which we will split lower nav area (X or Y, depending on axis we split on)
- float splitLen; // length of the segment of lower nav area that is in shadow of the upper nav area
- float splitEdgeSize; // current length of the edge of nav area that is getting split
- bool bSplitAlongX = false;
-
- // determine along what edge we are splitting and make some key measurements
- if ( ( dirFromAboveToBelow == EAST ) || ( dirFromAboveToBelow == WEST ) )
- {
- splitEdgeSize = extentBelow.hi.x - extentBelow.lo.x;
- if ( extentAbove.hi.x < extentBelow.hi.x )
- {
- splitCoord = extentAbove.hi.x;
- splitLen = splitCoord - extentBelow.lo.x;
- }
- else
- {
- splitCoord = extentAbove.lo.x;
- splitLen = extentBelow.hi.x - splitCoord;
- }
- }
- else
- {
- splitEdgeSize = extentBelow.hi.y - extentBelow.lo.y;
- bSplitAlongX = true;
- if ( extentAbove.hi.y < extentBelow.hi.y )
- {
- splitCoord = extentAbove.hi.y;
- splitLen = splitCoord - extentBelow.lo.y;
- }
- else
- {
- splitCoord = extentAbove.lo.y;
- splitLen = extentBelow.hi.y - splitCoord;
- }
- }
- Assert( splitLen >= 0 );
- Assert( splitEdgeSize > 0 );
-
- // if we split the lower nav area right where it's in shadow of the upper nav area, will it create a really tiny strip?
- if ( splitLen < GenerationStepSize )
- {
- // if the "in shadow" part of the lower nav area is really small or the lower nav area is really small to begin with,
- // don't split it, we're better off as is
- if ( ( splitLen < GenerationStepSize*0.3 ) || ( splitEdgeSize <= GenerationStepSize * 2 ) )
- continue;
-
- // Move our split point so we don't create a really tiny strip on the lower nav area. Move the split point away from
- // the upper nav area so the "in shadow" area expands to be GenerationStepSize. The checks above ensure we have room to do this.
- float splitDelta = GenerationStepSize - splitLen;
- splitCoord += splitDelta * ( ( ( dirFromAboveToBelow == NORTH ) || ( dirFromAboveToBelow == WEST ) ) ? -1 : 1 );
- }
-
- // remove any connections between the two areas (so they don't get inherited by the new areas when we split the lower area),
- // but remember what the connections were.
- bool bConnectionFromBelow = false, bConnectionFromAbove = false;
- if ( areaBelow->IsConnected( areaAbove, dirFromBelowToAbove ) )
- {
- bConnectionFromBelow = true;
- areaBelow->Disconnect( areaAbove );
- }
- if ( areaAbove->IsConnected( areaBelow, dirFromAboveToBelow ) )
- {
- bConnectionFromAbove = true;
- areaAbove->Disconnect( areaBelow );
- }
-
- CNavArea *pNewAlpha = NULL,*pNewBeta = NULL;
-// int idBelow = areaBelow->GetID();
-// AddToSelectedSet( areaBelow );
- // split the lower nav area
- if ( areaBelow->SplitEdit( bSplitAlongX, splitCoord, &pNewAlpha, &pNewBeta ) )
- {
-// Msg( "Split area %d into %d and %d\n", idBelow, pNewAlpha->GetID(), pNewBeta->GetID() );
-
- // determine which of the two new lower areas is the one *not* in shadow of the upper nav area. This is the one we want to
- // reconnect to
- CNavArea *pNewNonoverlappedArea = ( ( dirFromAboveToBelow == NORTH ) || ( dirFromAboveToBelow == WEST ) ) ? pNewAlpha : pNewBeta;
-
- // restore the previous connections from the upper nav area to the new lower nav area that is not in shadow of the upper
- if ( bConnectionFromAbove )
- {
- areaAbove->ConnectTo( pNewNonoverlappedArea, dirFromAboveToBelow );
- }
- if ( bConnectionFromBelow )
- {
- areaBelow->ConnectTo( pNewNonoverlappedArea, OppositeDirection( dirFromAboveToBelow ) );
- }
-
- // Now we need to just start the whole process over. We've just perturbed the list we're iterating on (removed a nav area, added two
- // new ones, when we did the split), and it's possible we may have to subdivide a lower nav area twice if the upper nav area
- // overhangs a corner of the lower area. We just start all over again each time we do a split until no more overhangs occur.
- bRestartProcessing = true;
- }
- else
- {
-// Msg( "Failed to split area %d\n", idBelow );
- }
- }
- }
- }
- }
- }
- while ( bRestartProcessing );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-bool TestForValidCrouchArea( CNavNode *node )
-{
- // must make sure we don't have a bogus crouch area. check up to JumpCrouchHeight above
- // the node for a HumanCrouchHeight space.
-
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_EVERYTHING );
- trace_t tr;
- Vector start( *node->GetPosition() );
- Vector end( *node->GetPosition() );
- end.z += JumpCrouchHeight;
-
- Vector mins( 0, 0, 0 );
- Vector maxs( GenerationStepSize, GenerationStepSize, HumanCrouchHeight );
-
- UTIL_TraceHull(
- start,
- end,
- mins,
- maxs,
- TheNavMesh->GetGenerationTraceMask(),
- &filter,
- &tr );
-
- return ( !tr.allsolid );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Make sure that if other* are similar, test is also close. Used in TestForValidJumpArea.
- */
-bool IsHeightDifferenceValid( float test, float other1, float other2, float other3 )
-{
- // Make sure the other nodes are level.
- const float CloseDelta = StepHeight / 2;
- if ( fabs( other1 - other2 ) > CloseDelta )
- return true;
-
- if ( fabs( other1 - other3 ) > CloseDelta )
- return true;
-
- if ( fabs( other2 - other3 ) > CloseDelta )
- return true;
-
- // Now make sure the test node is near the others. If it is more than StepHeight away,
- // it'll form a distorted jump area.
- const float MaxDelta = StepHeight;
- if ( fabs( test - other1 ) > MaxDelta )
- return false;
-
- if ( fabs( test - other2 ) > MaxDelta )
- return false;
-
- if ( fabs( test - other3 ) > MaxDelta )
- return false;
-
- return true;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Check that a 1x1 area with 'node' at the northwest corner has a valid shape - if 3 corners
- * are flat, and the 4th is significantly higher or lower, it would form a jump area that bots
- * can't navigate over well.
- */
-bool TestForValidJumpArea( CNavNode *node )
-{
- return true;
-
- CNavNode *east = node->GetConnectedNode( EAST );
- CNavNode *south = node->GetConnectedNode( SOUTH );
- if ( !east || !south )
- return false;
-
- CNavNode *southEast = east->GetConnectedNode( SOUTH );
- if ( !southEast )
- return false;
-
- if ( !IsHeightDifferenceValid(
- node->GetPosition()->z,
- south->GetPosition()->z,
- southEast->GetPosition()->z,
- east->GetPosition()->z ) )
- return false;
-
- if ( !IsHeightDifferenceValid(
- south->GetPosition()->z,
- node->GetPosition()->z,
- southEast->GetPosition()->z,
- east->GetPosition()->z ) )
- return false;
-
- if ( !IsHeightDifferenceValid(
- southEast->GetPosition()->z,
- south->GetPosition()->z,
- node->GetPosition()->z,
- east->GetPosition()->z ) )
- return false;
-
- if ( !IsHeightDifferenceValid(
- east->GetPosition()->z,
- south->GetPosition()->z,
- southEast->GetPosition()->z,
- node->GetPosition()->z ) )
- return false;
-
- return true;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-class TestOverlapping
-{
- Vector m_nw;
- Vector m_ne;
- Vector m_sw;
- Vector m_se;
-public:
- TestOverlapping( const Vector &nw, const Vector &ne, const Vector &sw, const Vector &se ) :
- m_nw( nw ), m_ne( ne ), m_sw( sw ), m_se( se )
- {
- }
-
- // This approximates CNavArea::GetZ, so we can pretend our four corners delineate a nav area
- float GetZ( const Vector &pos ) const
- {
- float dx = m_se.x - m_nw.x;
- float dy = m_se.y - m_nw.y;
-
- // guard against division by zero due to degenerate areas
- if (dx == 0.0f || dy == 0.0f)
- return m_ne.z;
-
- float u = (pos.x - m_nw.x) / dx;
- float v = (pos.y - m_nw.y) / dy;
-
- // clamp Z values to (x,y) volume
- if (u < 0.0f)
- u = 0.0f;
- else if (u > 1.0f)
- u = 1.0f;
-
- if (v < 0.0f)
- v = 0.0f;
- else if (v > 1.0f)
- v = 1.0f;
-
- float northZ = m_nw.z + u * (m_ne.z - m_nw.z);
- float southZ = m_sw.z + u * (m_se.z - m_sw.z);
-
- return northZ + v * (southZ - northZ);
- }
-
- bool OverlapsExistingArea( void )
- {
- CNavArea *overlappingArea = NULL;
- CNavLadder *overlappingLadder = NULL;
-
- Vector nw = m_nw;
- Vector se = m_se;
- Vector start = nw;
- start.x += GenerationStepSize/2;
- start.y += GenerationStepSize/2;
-
- while ( start.x < se.x )
- {
- start.y = nw.y + GenerationStepSize/2;
- while ( start.y < se.y )
- {
- start.z = GetZ( start );
- Vector end = start;
- start.z -= StepHeight;
- end.z += HalfHumanHeight;
-
- if ( TheNavMesh->FindNavAreaOrLadderAlongRay( start, end, &overlappingArea, &overlappingLadder, NULL ) )
- {
- if ( overlappingArea )
- {
- return true;
- }
- }
-
- start.y += GenerationStepSize;
- }
- start.x += GenerationStepSize;
- }
- return false;
- }
-};
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Check if an rectangular area of the given size can be
- * made starting from the given node as the NW corner.
- * Only consider fully connected nodes for this check.
- * All of the nodes within the test area must have the same attributes.
- * All of the nodes must be approximately co-planar w.r.t the NW node's normal, with the
- * exception of 1x1 areas which can be any angle.
- */
-bool CNavMesh::TestArea( CNavNode *node, int width, int height )
-{
- Vector normal = *node->GetNormal();
- float d = -DotProduct( normal, *node->GetPosition() );
-
- bool nodeCrouch = node->m_crouch[ SOUTH_EAST ];
-
- // The area's interior will be the south-east side of this north-west node.
- // If that interior space is blocked, there's no space to build an area.
- if ( node->m_isBlocked[ SOUTH_EAST ] )
- {
- return false;
- }
-
- int nodeAttributes = node->GetAttributes() & ~NAV_MESH_CROUCH;
-
- const float offPlaneTolerance = 5.0f;
-
- CNavNode *vertNode, *horizNode;
-
- vertNode = node;
- int x,y;
- for( y=0; y<height; y++ )
- {
- horizNode = vertNode;
-
- for( x=0; x<width; x++ )
- {
- //
- // Compute the crouch attributes for the test node, taking into account only the side(s) of the node
- // that are in the area
-
- // NOTE: The nodes on the south and east borders of an area aren't contained in the area. This means that
- // crouch attributes and blocked state need to be checked to the south and east of the southEdge and eastEdge nodes.
-
- bool horizNodeCrouch = false;
- bool westEdge = (x == 0);
- bool eastEdge = (x == width - 1);
- bool northEdge = (y == 0);
- bool southEdge = (y == height - 1);
-
- // Check corners first
- if ( northEdge && westEdge )
- {
- // The area's interior will be the south-east side of this north-west node.
- // If that interior space is blocked, there's no space to build an area.
- horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ];
- if ( horizNode->m_isBlocked[ SOUTH_EAST ] )
- {
- return false;
- }
- }
- else if ( northEdge && eastEdge )
- {
- // interior space of the area extends one more cell to the east past the easternmost nodes.
- // This means we need to check to the southeast as well as the southwest.
- horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ SOUTH_WEST ];
- if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ SOUTH_WEST ] )
- {
- return false;
- }
- }
- else if ( southEdge && westEdge )
- {
- // The interior space of the area extends one more cell to the south past the southernmost nodes.
- // This means we need to check to the southeast as well as the southwest.
- horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ NORTH_EAST ];
- if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ NORTH_EAST ] )
- {
- return false;
- }
- }
- else if ( southEdge && eastEdge )
- {
- // This node is completely in the interior of the area, so we need to check in all directions.
- horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
- if ( horizNode->IsBlockedInAnyDirection() )
- {
- return false;
- }
- }
- // check sides next
- else if ( northEdge )
- {
- horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ SOUTH_WEST ];
- if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ SOUTH_WEST ] )
- {
- return false;
- }
- }
- else if ( southEdge )
- {
- // This node is completely in the interior of the area, so we need to check in all directions.
- horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
- if ( horizNode->IsBlockedInAnyDirection() )
- {
- return false;
- }
- }
- else if ( eastEdge )
- {
- // This node is completely in the interior of the area, so we need to check in all directions.
- horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
- if ( horizNode->IsBlockedInAnyDirection() )
- {
- return false;
- }
- }
- else if ( westEdge )
- {
- horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ NORTH_EAST ];
- if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ NORTH_EAST ] )
- {
- return false;
- }
- }
- // finally, we have a center node
- else
- {
- // This node is completely in the interior of the area, so we need to check in all directions.
- horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
- if ( horizNode->IsBlockedInAnyDirection() )
- {
- return false;
- }
- }
-
- // all nodes must be crouch/non-crouch
- if ( nodeCrouch != horizNodeCrouch )
- return false;
-
- // all nodes must have the same non-crouch attributes
- int horizNodeAttributes = horizNode->GetAttributes() & ~NAV_MESH_CROUCH;
- if (horizNodeAttributes != nodeAttributes)
- return false;
-
- if (horizNode->IsCovered())
- return false;
-
- if (!horizNode->IsClosedCell())
- return false;
-
- if ( !CheckObstacles( horizNode, width, height, x, y ) )
- return false;
-
- horizNode = horizNode->GetConnectedNode( EAST );
- if (horizNode == NULL)
- return false;
-
- // nodes must lie on/near the plane
- if (width > 1 || height > 1)
- {
- float dist = (float)fabs( DotProduct( *horizNode->GetPosition(), normal ) + d );
- if (dist > offPlaneTolerance)
- return false;
- }
- }
-
- // Check the final (x=width) node, the above only checks thru x=width-1
- if ( !CheckObstacles( horizNode, width, height, x, y ) )
- return false;
-
- vertNode = vertNode->GetConnectedNode( SOUTH );
- if (vertNode == NULL)
- return false;
-
- // nodes must lie on/near the plane
- if (width > 1 || height > 1)
- {
- float dist = (float)fabs( DotProduct( *vertNode->GetPosition(), normal ) + d );
- if (dist > offPlaneTolerance)
- return false;
- }
- }
-
- // check planarity of southern edge
- if (width > 1 || height > 1)
- {
- horizNode = vertNode;
-
- for( x=0; x<width; x++ )
- {
- if ( !CheckObstacles( horizNode, width, height, x, y ) )
- return false;
-
- horizNode = horizNode->GetConnectedNode( EAST );
- if (horizNode == NULL)
- return false;
-
- // nodes must lie on/near the plane
- float dist = (float)fabs( DotProduct( *horizNode->GetPosition(), normal ) + d );
- if (dist > offPlaneTolerance)
- return false;
- }
-
- // Check the final (x=width) node, the above only checks thru x=width-1
- if ( !CheckObstacles( horizNode, width, height, x, y ) )
- return false;
- }
-
- vertNode = node;
- for( y=0; y<height; ++y )
- {
- horizNode = vertNode;
-
- for( int x=0; x<width; ++x )
- {
- // look for odd jump areas (3 points on the ground, 1 point floating much higher or lower)
- if ( !TestForValidJumpArea( horizNode ) )
- {
- return false;
- }
-
- // Now that we've done the quick checks, test for a valid crouch area.
- // This finds pillars etc in the middle of 4 nodes, that weren't found initially.
- if ( nodeCrouch && !TestForValidCrouchArea( horizNode ) )
- {
- return false;
- }
-
- horizNode = horizNode->GetConnectedNode( EAST );
- }
-
- vertNode = vertNode->GetConnectedNode( SOUTH );
- }
-
- if ( m_generationMode == GENERATE_INCREMENTAL )
- {
- // Incremental generation needs to check that it's not overlapping existing areas...
- const Vector *nw = node->GetPosition();
-
- vertNode = node;
- for( int y=0; y<height; ++y )
- {
- vertNode = vertNode->GetConnectedNode( SOUTH );
- }
- const Vector *sw = vertNode->GetPosition();
-
- horizNode = node;
- for( int x=0; x<width; ++x )
- {
- horizNode = horizNode->GetConnectedNode( EAST );
- }
- const Vector *ne = horizNode->GetPosition();
-
- vertNode = horizNode;
- for( int y=0; y<height; ++y )
- {
- vertNode = vertNode->GetConnectedNode( SOUTH );
- }
- const Vector *se = vertNode->GetPosition();
-
- TestOverlapping test( *nw, *ne, *sw, *se );
- if ( test.OverlapsExistingArea() )
- return false;
- }
-
- return true;
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Checks if a node has an untraversable obstacle in any direction to a neighbor.
-* width and height are size of nav area this node would be a part of, x and y are node's position
-* within that grid
-*/
-bool CNavMesh::CheckObstacles( CNavNode *node, int width, int height, int x, int y )
-{
- // any area bigger than 1x1 can't have obstacles in any connection between nodes
- if ( width > 1 || height > 1 )
- {
- if ( ( x > 0 ) && ( node->m_obstacleHeight[WEST] > MaxTraversableHeight ) )
- return false;
-
- if ( ( y > 0 ) && ( node->m_obstacleHeight[NORTH] > MaxTraversableHeight ) )
- return false;
-
- if ( ( x < width-1 ) && ( node->m_obstacleHeight[EAST] > MaxTraversableHeight ) )
- return false;
-
- if ( ( y < height-1 ) && ( node->m_obstacleHeight[SOUTH] > MaxTraversableHeight ) )
- return false;
- }
-
- // 1x1 area can have obstacles, that area will get fixed up later
- return true;
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Create a nav area, and mark all nodes it overlaps as "covered"
- * NOTE: Nodes on the east and south edges are not included.
- * Returns number of nodes covered by this area, or -1 for error;
- */
-int CNavMesh::BuildArea( CNavNode *node, int width, int height )
-{
- CNavNode *nwNode = node;
- CNavNode *neNode = NULL;
- CNavNode *swNode = NULL;
- CNavNode *seNode = NULL;
-
- CNavNode *vertNode = node;
- CNavNode *horizNode;
-
- int coveredNodes = 0;
-
- for( int y=0; y<height; y++ )
- {
- horizNode = vertNode;
-
- for( int x=0; x<width; x++ )
- {
- horizNode->Cover();
- ++coveredNodes;
-
- horizNode = horizNode->GetConnectedNode( EAST );
- }
-
- if (y == 0)
- neNode = horizNode;
-
- vertNode = vertNode->GetConnectedNode( SOUTH );
- }
-
- swNode = vertNode;
-
- horizNode = vertNode;
- for( int x=0; x<width; x++ )
- {
- horizNode = horizNode->GetConnectedNode( EAST );
- }
- seNode = horizNode;
-
- if (!nwNode || !neNode || !seNode || !swNode)
- {
- Error( "BuildArea - NULL node.\n" );
- return -1;
- }
-
- CNavArea *area = CreateArea();
- if (area == NULL)
- {
- Error( "BuildArea: Out of memory.\n" );
- return -1;
- }
-
- area->Build( nwNode, neNode, seNode, swNode );
-
- TheNavAreas.AddToTail( area );
- // since all internal nodes have the same attributes, set this area's attributes
-
- area->SetAttributes( node->GetAttributes() );
-
- // If any of the corners have an obstacle in the direction of another corner, then there's an internal obstruction of this nav node.
- // Mark it as not mergable so it doesn't become a part of anything else and we will fix it up later.
- if ( nwNode->m_obstacleHeight[SOUTH] > MaxTraversableHeight || nwNode->m_obstacleHeight[EAST] > MaxTraversableHeight ||
- neNode->m_obstacleHeight[WEST] > MaxTraversableHeight || neNode->m_obstacleHeight[SOUTH] > MaxTraversableHeight ||
- seNode->m_obstacleHeight[NORTH] > MaxTraversableHeight || seNode->m_obstacleHeight[WEST] > MaxTraversableHeight ||
- swNode->m_obstacleHeight[EAST] > MaxTraversableHeight || swNode->m_obstacleHeight[NORTH] > MaxTraversableHeight )
- {
- Assert( width == 1 ); // We should only ever try to build a 1x1 area out of any two nodes that have an obstruction between them
- Assert( height == 1 );
-
- area->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE );
- }
-
- // Check that the node was crouch in the right direction
- bool nodeCrouch = node->m_crouch[ SOUTH_EAST ];
- if ( (area->GetAttributes() & NAV_MESH_CROUCH) && !nodeCrouch )
- {
- area->SetAttributes( area->GetAttributes() & ~NAV_MESH_CROUCH );
- }
-
- return coveredNodes;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * This function uses the CNavNodes that have been sampled from the map to
- * generate CNavAreas - rectangular areas of "walkable" space. These areas
- * are connected to each other, proving information on know how to move from
- * area to area.
- *
- * This is a "greedy" algorithm that attempts to cover the walkable area
- * with the fewest, largest, rectangles.
- */
-void CNavMesh::CreateNavAreasFromNodes( void )
-{
- // haven't yet seen a map use larger than 30...
- int tryWidth = nav_area_max_size.GetInt();
- int tryHeight = tryWidth;
- int uncoveredNodes = CNavNode::GetListLength();
-
- while( uncoveredNodes > 0 )
- {
- for( CNavNode *node = CNavNode::GetFirst(); node; node = node->GetNext() )
- {
- if (node->IsCovered())
- continue;
-
- if (TestArea( node, tryWidth, tryHeight ))
- {
- int covered = BuildArea( node, tryWidth, tryHeight );
- if (covered < 0)
- {
- Error( "Generate: Error - Data corrupt.\n" );
- return;
- }
-
- uncoveredNodes -= covered;
- }
- }
-
- if (tryWidth >= tryHeight)
- --tryWidth;
- else
- --tryHeight;
-
- if (tryWidth <= 0 || tryHeight <= 0)
- break;
- }
-
- if ( !TheNavAreas.Count() )
- {
- // If we somehow have no areas, don't try to create an impossibly-large grid
- AllocateGrid( 0, 0, 0, 0 );
- return;
- }
-
- Extent extent;
- extent.lo.x = 9999999999.9f;
- extent.lo.y = 9999999999.9f;
- extent.hi.x = -9999999999.9f;
- extent.hi.y = -9999999999.9f;
-
- // compute total extent
- FOR_EACH_VEC( TheNavAreas, it )
- {
- CNavArea *area = TheNavAreas[ it ];
- Extent areaExtent;
- area->GetExtent( &areaExtent );
-
- if (areaExtent.lo.x < extent.lo.x)
- extent.lo.x = areaExtent.lo.x;
- if (areaExtent.lo.y < extent.lo.y)
- extent.lo.y = areaExtent.lo.y;
- if (areaExtent.hi.x > extent.hi.x)
- extent.hi.x = areaExtent.hi.x;
- if (areaExtent.hi.y > extent.hi.y)
- extent.hi.y = areaExtent.hi.y;
- }
-
- // add the areas to the grid
- AllocateGrid( extent.lo.x, extent.hi.x, extent.lo.y, extent.hi.y );
-
- FOR_EACH_VEC( TheNavAreas, git )
- {
- AddNavArea( TheNavAreas[ git ] );
- }
-
-
- ConnectGeneratedAreas();
- MarkPlayerClipAreas();
- MarkJumpAreas(); // mark jump areas before we merge generated areas, so we don't merge jump and non-jump areas
- MergeGeneratedAreas();
- SplitAreasUnderOverhangs();
- SquareUpAreas();
- MarkStairAreas();
- StichAndRemoveJumpAreas();
- HandleObstacleTopAreas();
- FixUpGeneratedAreas();
-
- /// @TODO: incremental generation doesn't create ladders yet
- if ( m_generationMode != GENERATE_INCREMENTAL )
- {
- for ( int i=0; i<m_ladders.Count(); ++i )
- {
- CNavLadder *ladder = m_ladders[i];
- ladder->ConnectGeneratedLadder( 0.0f );
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-// adds walkable positions for any/all positions a mod specifies
-void CNavMesh::AddWalkableSeeds( void )
-{
- CBaseEntity *spawn = gEntList.FindEntityByClassname( NULL, GetPlayerSpawnName() );
-
- if (spawn )
- {
- // snap it to the sampling grid
- Vector pos = spawn->GetAbsOrigin();
- pos.x = TheNavMesh->SnapToGrid( pos.x );
- pos.y = TheNavMesh->SnapToGrid( pos.y );
-
- Vector normal;
- if ( FindGroundForNode( &pos, &normal ) )
- {
- AddWalkableSeed( pos, normal );
- }
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Initiate the generation process
- */
-void CNavMesh::BeginGeneration( bool incremental )
-{
- IGameEvent *event = gameeventmanager->CreateEvent( "nav_generate" );
- if ( event )
- {
- gameeventmanager->FireEvent( event );
- }
-
-#ifdef TERROR
- engine->ServerCommand( "director_stop\nnb_delete_all\n" );
- if ( !incremental && !engine->IsDedicatedServer() )
- {
- CBasePlayer *host = UTIL_GetListenServerHost();
- if ( host )
- {
- host->ChangeTeam( TEAM_SPECTATOR );
- }
- }
-#else
- engine->ServerCommand( "bot_kick\n" );
-#endif
-
- // Right now, incrementally-generated areas won't connect to existing areas automatically.
- // Since this means hand-editing will be necessary, don't do a full analyze.
- if ( incremental )
- {
- nav_quicksave.SetValue( 1 );
- }
-
- m_generationState = SAMPLE_WALKABLE_SPACE;
- m_sampleTick = 0;
- m_generationMode = (incremental) ? GENERATE_INCREMENTAL : GENERATE_FULL;
- lastMsgTime = 0.0f;
-
- // clear any previous mesh
- DestroyNavigationMesh( incremental );
-
- SetNavPlace( UNDEFINED_PLACE );
-
- // build internal representations of ladders, which are used to find new walkable areas
- if ( !incremental ) ///< @incremental update doesn't build ladders to avoid overlapping existing ones
- {
- BuildLadders();
- }
-
- // start sampling from a spawn point
- if ( !incremental )
- {
- AddWalkableSeeds();
- }
-
- // the system will see this NULL and select the next walkable seed
- m_currentNode = NULL;
-
- // if there are no seed points, we can't generate
- if (m_walkableSeeds.Count() == 0)
- {
- m_generationMode = GENERATE_NONE;
- Msg( "No valid walkable seed positions. Cannot generate Navigation Mesh.\n" );
- return;
- }
-
- // initialize seed list index
- m_seedIdx = 0;
-
- Msg( "Generating Navigation Mesh...\n" );
- m_generationStartTime = Plat_FloatTime();
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Re-analyze an existing Mesh. Determine Hiding Spots, Encounter Spots, etc.
- */
-void CNavMesh::BeginAnalysis( bool quitWhenFinished )
-{
-#ifdef TERROR
- if ( !engine->IsDedicatedServer() )
- {
- CBasePlayer *host = UTIL_GetListenServerHost();
- if ( host )
- {
- host->ChangeTeam( TEAM_SPECTATOR );
- engine->ServerCommand( "director_no_death_check 1\ndirector_stop\nnb_delete_all\n" );
-
- ConVarRef mat_fullbright( "mat_fullbright" );
- ConVarRef mat_hdr_level( "mat_hdr_level" );
-
- if( mat_fullbright.GetBool() )
- {
- Warning( "Setting mat_fullbright 0\n" );
- mat_fullbright.SetValue( 0 );
- }
-
- if ( mat_hdr_level.GetInt() < 2 )
- {
- Warning( "Enabling HDR and reloading materials\n" );
- mat_hdr_level.SetValue( 2 );
- engine->ClientCommand( host->edict(), "mat_reloadallmaterials\n" );
- }
-
- // Running a threaded server breaks our lighting calculations
- ConVarRef host_thread_mode( "host_thread_mode" );
- m_hostThreadModeRestoreValue = host_thread_mode.GetInt();
- host_thread_mode.SetValue( 0 );
- ConVarRef mat_queue_mode( "mat_queue_mode" );
- mat_queue_mode.SetValue( 0 );
- }
- }
-#endif
-
- // Remove and re-add elements in TheNavAreas, to ensure indices are useful for progress feedback
- NavAreaVector tmpSet;
- {
- FOR_EACH_VEC( TheNavAreas, it )
- {
- tmpSet.AddToTail( TheNavAreas[it] );
- }
- }
- TheNavAreas.RemoveAll();
- {
- FOR_EACH_VEC( tmpSet, it )
- {
- TheNavAreas.AddToTail( tmpSet[it] );
- }
- }
-
- DestroyHidingSpots();
- m_generationState = FIND_HIDING_SPOTS;
- m_generationIndex = 0;
- m_generationMode = GENERATE_ANALYSIS_ONLY;
- m_bQuitWhenFinished = quitWhenFinished;
- lastMsgTime = 0.0f;
- m_generationStartTime = Plat_FloatTime();
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-void ShowViewPortPanelToAll( const char * name, bool bShow, KeyValues *data )
-{
- CRecipientFilter filter;
- filter.AddAllPlayers();
- filter.MakeReliable();
-
- int count = 0;
- KeyValues *subkey = NULL;
-
- if ( data )
- {
- subkey = data->GetFirstSubKey();
- while ( subkey )
- {
- count++; subkey = subkey->GetNextKey();
- }
-
- subkey = data->GetFirstSubKey(); // reset
- }
-
- UserMessageBegin( filter, "VGUIMenu" );
- WRITE_STRING( name ); // menu name
- WRITE_BYTE( bShow?1:0 );
- WRITE_BYTE( count );
-
- // write additional data (be careful not more than 192 bytes!)
- while ( subkey )
- {
- WRITE_STRING( subkey->GetName() );
- WRITE_STRING( subkey->GetString() );
- subkey = subkey->GetNextKey();
- }
- MessageEnd();
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-static void AnalysisProgress( const char *msg, int ticks, int current, bool showPercent = true )
-{
- const float MsgInterval = 10.0f;
- float now = Plat_FloatTime();
- if ( now > lastMsgTime + MsgInterval )
- {
- if ( showPercent && ticks )
- {
- Msg( "%s %.0f%%\n", msg, current*100.0f/ticks );
- }
- else
- {
- Msg( "%s\n", msg );
- }
-
- lastMsgTime = now;
- }
-
- KeyValues *data = new KeyValues("data");
- data->SetString( "msg", msg );
- data->SetInt( "total", ticks );
- data->SetInt( "current", current );
-
- ShowViewPortPanelToAll( PANEL_NAV_PROGRESS, true, data );
-
- data->deleteThis();
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-static void HideAnalysisProgress( void )
-{
- KeyValues *data = new KeyValues("data");
- ShowViewPortPanelToAll( PANEL_NAV_PROGRESS, false, data );
- data->deleteThis();
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Process the auto-generation for 'maxTime' seconds. return false if generation is complete.
- */
-bool CNavMesh::UpdateGeneration( float maxTime )
-{
- double startTime = Plat_FloatTime();
- static unsigned int s_movedPlayerToArea = 0; // Last area we moved a player to for lighting calcs
- static CountdownTimer s_playerSettleTimer; // Settle time after moving the player for lighting calcs
- static CUtlVector<CNavArea *> s_unlitAreas;
- static CUtlVector<CNavArea *> s_unlitSeedAreas;
-
- static ConVarRef host_thread_mode( "host_thread_mode" );
-
- switch( m_generationState )
- {
- //---------------------------------------------------------------------------
- case SAMPLE_WALKABLE_SPACE:
- {
- AnalysisProgress( "Sampling walkable space...", 100, m_sampleTick / 10, false );
- m_sampleTick = ( m_sampleTick + 1 ) % 1000;
-
- while ( SampleStep() )
- {
- if ( Plat_FloatTime() - startTime > maxTime )
- {
- return true;
- }
- }
-
- // sampling is complete, now build nav areas
- m_generationState = CREATE_AREAS_FROM_SAMPLES;
-
- return true;
- }
-
- //---------------------------------------------------------------------------
- case CREATE_AREAS_FROM_SAMPLES:
- {
- Msg( "Creating navigation areas from sampled data...\n" );
-
- // Select all pre-existing areas
- if ( m_generationMode == GENERATE_INCREMENTAL )
- {
- ClearSelectedSet();
- FOR_EACH_VEC( TheNavAreas, nit )
- {
- CNavArea *area = TheNavAreas[nit];
- AddToSelectedSet( area );
- }
- }
-
- // Create new areas
- CreateNavAreasFromNodes();
-
- // And toggle the selection, so we end up with the new areas
- if ( m_generationMode == GENERATE_INCREMENTAL )
- {
- CommandNavToggleSelectedSet();
- }
-
- DestroyHidingSpots();
-
- // Remove and re-add elements in TheNavAreas, to ensure indices are useful for progress feedback
- NavAreaVector tmpSet;
- {
- FOR_EACH_VEC( TheNavAreas, it )
- {
- tmpSet.AddToTail( TheNavAreas[it] );
- }
- }
- TheNavAreas.RemoveAll();
- {
- FOR_EACH_VEC( tmpSet, it )
- {
- TheNavAreas.AddToTail( tmpSet[it] );
- }
- }
-
- m_generationState = FIND_HIDING_SPOTS;
- m_generationIndex = 0;
- return true;
- }
-
- //---------------------------------------------------------------------------
- case FIND_HIDING_SPOTS:
- {
- while( m_generationIndex < TheNavAreas.Count() )
- {
- CNavArea *area = TheNavAreas[ m_generationIndex ];
- ++m_generationIndex;
-
- area->ComputeHidingSpots();
-
- // don't go over our time allotment
- if( Plat_FloatTime() - startTime > maxTime )
- {
- AnalysisProgress( "Finding hiding spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
- return true;
- }
- }
-
- Msg( "Finding hiding spots...DONE\n" );
-
- m_generationState = FIND_ENCOUNTER_SPOTS;
- m_generationIndex = 0;
- return true;
- }
-
- //---------------------------------------------------------------------------
- case FIND_ENCOUNTER_SPOTS:
- {
- while( m_generationIndex < TheNavAreas.Count() )
- {
- CNavArea *area = TheNavAreas[ m_generationIndex ];
- ++m_generationIndex;
-
- area->ComputeSpotEncounters();
-
- // don't go over our time allotment
- if( Plat_FloatTime() - startTime > maxTime )
- {
- AnalysisProgress( "Finding encounter spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
- return true;
- }
- }
-
- Msg( "Finding encounter spots...DONE\n" );
-
- m_generationState = FIND_SNIPER_SPOTS;
- m_generationIndex = 0;
- return true;
- }
-
- //---------------------------------------------------------------------------
- case FIND_SNIPER_SPOTS:
- {
- while( m_generationIndex < TheNavAreas.Count() )
- {
- CNavArea *area = TheNavAreas[ m_generationIndex ];
- ++m_generationIndex;
-
- area->ComputeSniperSpots();
-
- // don't go over our time allotment
- if( Plat_FloatTime() - startTime > maxTime )
- {
- AnalysisProgress( "Finding sniper spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
- return true;
- }
- }
-
- Msg( "Finding sniper spots...DONE\n" );
-
- m_generationState = COMPUTE_MESH_VISIBILITY;
- m_generationIndex = 0;
- BeginVisibilityComputations();
- Msg( "Computing mesh visibility...\n" );
-
- return true;
- }
-
- //---------------------------------------------------------------------------
- case COMPUTE_MESH_VISIBILITY:
- {
- while( m_generationIndex < TheNavAreas.Count() )
- {
- CNavArea *area = TheNavAreas[ m_generationIndex ];
- ++m_generationIndex;
-
- area->ComputeVisibilityToMesh();
-
- // don't go over our time allotment
- if ( Plat_FloatTime() - startTime > maxTime )
- {
- AnalysisProgress( "Computing mesh visibility...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
- return true;
- }
- }
-
- Msg( "Optimizing mesh visibility...\n" );
-
- EndVisibilityComputations();
-
- Msg( "Computing mesh visibility...DONE\n" );
-
- m_generationState = FIND_EARLIEST_OCCUPY_TIMES;
- m_generationIndex = 0;
- return true;
- }
-
- //---------------------------------------------------------------------------
- case FIND_EARLIEST_OCCUPY_TIMES:
- {
- while( m_generationIndex < TheNavAreas.Count() )
- {
- CNavArea *area = TheNavAreas[ m_generationIndex ];
- ++m_generationIndex;
-
- area->ComputeEarliestOccupyTimes();
-
- // don't go over our time allotment
- if( Plat_FloatTime() - startTime > maxTime )
- {
- AnalysisProgress( "Finding earliest occupy times...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
- return true;
- }
- }
-
- Msg( "Finding earliest occupy times...DONE\n" );
-
-#ifdef NAV_ANALYZE_LIGHT_INTENSITY
- bool shouldSkipLightComputation = ( m_generationMode == GENERATE_INCREMENTAL || engine->IsDedicatedServer() );
-#else
- bool shouldSkipLightComputation = true;
-#endif
-
- if ( shouldSkipLightComputation )
- {
- m_generationState = CUSTOM; // no light intensity calcs for incremental generation or dedicated servers
- }
- else
- {
- m_generationState = FIND_LIGHT_INTENSITY;
- s_playerSettleTimer.Invalidate();
- CNavArea::MakeNewMarker();
- s_unlitAreas.RemoveAll();
- FOR_EACH_VEC( TheNavAreas, nit )
- {
- s_unlitAreas.AddToTail( TheNavAreas[nit] );
- s_unlitSeedAreas.AddToTail( TheNavAreas[nit] );
- }
- }
-
- m_generationIndex = 0;
- return true;
- }
-
- //---------------------------------------------------------------------------
- case FIND_LIGHT_INTENSITY:
- {
- host_thread_mode.SetValue( 0 ); // need non-threaded server for light calcs
-
- CBasePlayer *host = UTIL_GetListenServerHost();
-
- if ( !s_unlitAreas.Count() || !host )
- {
- Msg( "Finding light intensity...DONE\n" );
-
- m_generationState = CUSTOM;
- m_generationIndex = 0;
- return true;
- }
-
- if ( !s_playerSettleTimer.IsElapsed() )
- return true; // wait for eyePos to settle
-
- // Now try to compute lighting for remaining areas
- int sit = 0;
- while( sit < s_unlitAreas.Count() )
- {
- CNavArea *area = s_unlitAreas[sit];
- if ( area->ComputeLighting() )
- {
- s_unlitSeedAreas.FindAndRemove( area );
- s_unlitAreas.Remove( sit );
-
- continue;
- }
- else
- {
- ++sit;
- }
- }
-
- if ( s_unlitAreas.Count() )
- {
- if ( s_unlitSeedAreas.Count() )
- {
- CNavArea *moveArea = s_unlitSeedAreas[0];
- s_unlitSeedAreas.FastRemove( 0 );
-
- //Msg( "Moving to new area %d to compute lighting for %d/%d areas\n", moveArea->GetID(), s_unlitAreas.Count(), TheNavAreas.Count() );
-
- Vector eyePos = moveArea->GetCenter();
- float height;
- if ( GetGroundHeight( eyePos, &height ) )
- {
- eyePos.z = height + HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings
- }
- else
- {
- eyePos.z += HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings
- }
- host->SetAbsOrigin( eyePos );
- AnalysisProgress( "Finding light intensity...", 100, 100 * (TheNavAreas.Count() - s_unlitAreas.Count()) / TheNavAreas.Count() );
- s_movedPlayerToArea = moveArea->GetID();
- s_playerSettleTimer.Start( 0.1f );
- return true;
- }
- else
- {
- Msg( "Finding light intensity...DONE (%d unlit areas)\n", s_unlitAreas.Count() );
- if ( s_unlitAreas.Count() )
- {
- Warning( "To see unlit areas:\n" );
- for ( int sit=0; sit<s_unlitAreas.Count(); ++sit )
- {
- CNavArea *area = s_unlitAreas[ sit ];
- Warning( "nav_unmark; nav_mark %d; nav_warp_to_mark;\n", area->GetID() );
- }
- }
-
- m_generationState = CUSTOM;
- m_generationIndex = 0;
- }
- }
-
- Msg( "Finding light intensity...DONE\n" );
-
- m_generationState = CUSTOM;
- m_generationIndex = 0;
- return true;
- }
-
- //---------------------------------------------------------------------------
- case CUSTOM:
- {
- if ( m_generationIndex == 0 )
- {
- BeginCustomAnalysis( m_generationMode == GENERATE_INCREMENTAL );
- Msg( "Start custom...\n ");
- }
- while( m_generationIndex < TheNavAreas.Count() )
- {
- CNavArea *area = TheNavAreas[ m_generationIndex ];
- ++m_generationIndex;
-
- area->CustomAnalysis( m_generationMode == GENERATE_INCREMENTAL );
-
- // don't go over our time allotment
- if( Plat_FloatTime() - startTime > maxTime )
- {
- AnalysisProgress( "Custom game-specific analysis...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
- return true;
- }
- }
-
- Msg( "Post custom...\n ");
- PostCustomAnalysis();
-
- EndCustomAnalysis();
- Msg( "Custom game-specific analysis...DONE\n" );
-
- m_generationState = SAVE_NAV_MESH;
- m_generationIndex = 0;
- ConVarRef mat_queue_mode( "mat_queue_mode" );
- mat_queue_mode.SetValue( -1 );
- host_thread_mode.SetValue( m_hostThreadModeRestoreValue ); // restore this
- return true;
- }
-
- //---------------------------------------------------------------------------
- case SAVE_NAV_MESH:
- {
- if ( m_generationMode == GENERATE_ANALYSIS_ONLY || m_generationMode == GENERATE_FULL )
- {
- m_isAnalyzed = true;
- }
-
- // generation complete!
- float generationTime = Plat_FloatTime() - m_generationStartTime;
- Msg( "Generation complete! %0.1f seconds elapsed.\n", generationTime );
- bool restart = m_generationMode != GENERATE_INCREMENTAL;
- m_generationMode = GENERATE_NONE;
- m_isLoaded = true;
- ClearWalkableSeeds();
-
- HideAnalysisProgress();
-
- // save the mesh
- if (Save())
- {
- Msg( "Navigation map '%s' saved.\n", GetFilename() );
- }
- else
- {
- const char *filename = GetFilename();
- Msg( "ERROR: Cannot save navigation map '%s'.\n", (filename) ? filename : "(null)" );
- }
-
- if ( m_bQuitWhenFinished )
- {
- engine->ServerCommand( "quit\n" );
- }
- else if ( restart )
- {
- engine->ChangeLevel( STRING( gpGlobals->mapname ), NULL );
- }
- else
- {
- FOR_EACH_VEC( TheNavAreas, it )
- {
- TheNavAreas[ it ]->ResetNodes();
- }
-
-#if !(DEBUG_NAV_NODES)
- // destroy navigation nodes created during map generation
- CNavNode *node, *next;
- for( node = CNavNode::m_list; node; node = next )
- {
- next = node->m_next;
- delete node;
- }
- CNavNode::m_list = NULL;
- CNavNode::m_listLength = 0;
- CNavNode::m_nextID = 1;
-#endif // !(DEBUG_NAV_NODES)
- }
-
- return false;
- }
- }
-
- return false;
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Define the name of player spawn entities
- */
-void CNavMesh::SetPlayerSpawnName( const char *name )
-{
- if (m_spawnName)
- {
- delete [] m_spawnName;
- }
-
- m_spawnName = new char [ strlen(name) + 1 ];
- strcpy( m_spawnName, name );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Return name of player spawn entity
- */
-const char *CNavMesh::GetPlayerSpawnName( void ) const
-{
- if (m_spawnName)
- return m_spawnName;
-
- // default value
- return "info_player_start";
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Add a nav node and connect it.
- * Node Z positions are ground level.
- */
-CNavNode *CNavMesh::AddNode( const Vector &destPos, const Vector &normal, NavDirType dir, CNavNode *source, bool isOnDisplacement,
- float obstacleHeight, float obstacleStartDist, float obstacleEndDist )
-{
- // check if a node exists at this location
- CNavNode *node = CNavNode::GetNode( destPos );
-
- // if no node exists, create one
- bool useNew = false;
- if (node == NULL)
- {
- node = new CNavNode( destPos, normal, source, isOnDisplacement );
- OnNodeAdded( node );
- useNew = true;
- }
-
- // connect source node to new node
- source->ConnectTo( node, dir, obstacleHeight, obstacleStartDist, obstacleEndDist );
-
- // optimization: if deltaZ changes very little, assume connection is commutative
- const float zTolerance = 50.0f;
- float deltaZ = source->GetPosition()->z - destPos.z;
- if (fabs( deltaZ ) < zTolerance)
- {
- if ( obstacleHeight > 0 )
- {
- obstacleHeight = MAX( obstacleHeight + deltaZ, 0 );
- Assert( obstacleHeight > 0 );
- }
- node->ConnectTo( source, OppositeDirection( dir ), obstacleHeight, GenerationStepSize - obstacleEndDist, GenerationStepSize - obstacleStartDist );
- node->MarkAsVisited( OppositeDirection( dir ) );
- }
-
- if (useNew)
- {
- // new node becomes current node
- m_currentNode = node;
- }
-
- node->CheckCrouch();
-
- // determine if there's a cliff nearby and set an attribute on this node
- for ( int i = 0; i < NUM_DIRECTIONS; i++ )
- {
- NavDirType dir = (NavDirType) i;
- if ( CheckCliff( node->GetPosition(), dir ) )
- {
- node->SetAttributes( node->GetAttributes() | NAV_MESH_CLIFF );
- break;
- }
- }
-
- return node;
-}
-
-//--------------------------------------------------------------------------------------------------------------
-inline CNavNode *LadderEndSearch( const Vector *pos, NavDirType mountDir )
-{
- Vector center = *pos;
- AddDirectionVector( &center, mountDir, HalfHumanWidth );
-
- //
- // Test the ladder dismount point first, then each cardinal direction one and two steps away
- //
- for( int d=(-1); d<2*NUM_DIRECTIONS; ++d )
- {
- Vector tryPos = center;
-
- if (d >= NUM_DIRECTIONS)
- AddDirectionVector( &tryPos, (NavDirType)(d - NUM_DIRECTIONS), 2.0f*GenerationStepSize );
- else if (d >= 0)
- AddDirectionVector( &tryPos, (NavDirType)d, GenerationStepSize );
-
- // step up a rung, to ensure adjacent floors are below us
- tryPos.z += GenerationStepSize;
-
- tryPos.x = TheNavMesh->SnapToGrid( tryPos.x );
- tryPos.y = TheNavMesh->SnapToGrid( tryPos.y );
-
- // adjust height to account for sloping areas
- Vector tryNormal;
- if (TheNavMesh->GetGroundHeight( tryPos, &tryPos.z, &tryNormal ) == false)
- continue;
-
- // make sure this point is not on the other side of a wall
- const float fudge = 4.0f;
- trace_t result;
- UTIL_TraceHull( center + Vector( 0, 0, fudge ), tryPos + Vector( 0, 0, fudge ), NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
- if (result.fraction != 1.0f || result.startsolid)
- continue;
-
- // if no node exists here, create one and continue the search
- if (CNavNode::GetNode( tryPos ) == NULL)
- {
- return new CNavNode( tryPos, tryNormal, NULL, false );
- }
- }
-
- return NULL;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-bool CNavMesh::FindGroundForNode( Vector *pos, Vector *normal )
-{
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_EVERYTHING );
- trace_t tr;
- Vector start( pos->x, pos->y, pos->z + VEC_DUCK_HULL_MAX.z - 0.1f );
- Vector end( *pos );
- end.z -= DeathDrop;
-
- UTIL_TraceHull(
- start,
- end,
- NavTraceMins,
- NavTraceMaxs,
- GetGenerationTraceMask(),
- &filter,
- &tr );
-
- *pos = tr.endpos;
- *normal = tr.plane.normal;
-
- return ( !tr.allsolid );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-void DrawTrace( const trace_t *trace )
-{
- /*
- if ( trace->fraction > 0.0f && !trace->startsolid )
- {
- NDebugOverlay::SweptBox( trace->startpos, trace->endpos, NavTraceMins, NavTraceMaxs, vec3_angle, 0, 255, 0, 45, 100 );
- }
- else
- {
- NDebugOverlay::SweptBox( trace->startpos, trace->endpos, NavTraceMins, NavTraceMaxs, vec3_angle, 255, 0, 0, 45, 100 );
- }
- */
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-bool StayOnFloor( trace_t *trace, float zLimit /* = DeathDrop */ )
-{
- Vector start( trace->endpos );
- Vector end( start );
- end.z -= zLimit;
-
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace );
- DrawTrace( trace );
-
- if ( trace->startsolid || trace->fraction >= 1.0f )
- {
- return false;
- }
-
- if ( trace->plane.normal.z < nav_slope_limit.GetFloat() )
- {
- return false;
- }
-
- return true;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-bool TraceAdjacentNode( int depth, const Vector& start, const Vector& end, trace_t *trace, float zLimit /* = DeathDrop */ )
-{
- const float MinDistance = 1.0f; // if we can't move at least this far, don't bother stepping up.
-
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace );
- DrawTrace( trace );
-
- // If we started in the ground for some reason, bail
- if ( trace->startsolid )
- return false;
-
- // If we made it, so try to find the floor
- if ( end.x == trace->endpos.x && end.y == trace->endpos.y )
- {
- return StayOnFloor( trace, zLimit );
- }
-
- // If we didn't make enough progress, bail
- if ( depth && start.AsVector2D().DistToSqr( trace->endpos.AsVector2D() ) < MinDistance * MinDistance )
- {
- return false;
- }
-
- // We made it more than MinDistance. If the slope is too steep, we can't go on.
- if ( !StayOnFloor( trace, zLimit ) )
- {
- return false;
- }
-
- // Try to go up as if we stepped up, forward, and down.
- Vector testStart( trace->endpos );
- Vector testEnd( testStart );
- testEnd.z += StepHeight;
- UTIL_TraceHull( testStart, testEnd, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace );
- DrawTrace( trace );
-
- Vector forwardTestStart = trace->endpos;
- Vector forwardTestEnd = end;
- forwardTestEnd.z = forwardTestStart.z;
- return TraceAdjacentNode( depth+1, forwardTestStart, forwardTestEnd, trace );
-}
-
-
-//--------------------------------------------------------------------------------------------------------
-static bool IsNodeOverlapped( const Vector& pos, const Vector& offset )
-{
- bool overlap = TheNavMesh->GetNavArea( pos + offset, HumanHeight ) != NULL;
- if ( !overlap )
- {
- Vector mins( -0.5f, -0.5f, -0.5f );
- Vector maxs( 0.5f, 0.5f, 0.5f );
-
- Vector start = pos;
- start.z += HalfHumanHeight;
- Vector end = start;
- end.x += offset.x * GenerationStepSize;
- end.y += offset.y * GenerationStepSize;
- trace_t trace;
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
- UTIL_TraceHull( start, end, mins, maxs, TheNavMesh->GetGenerationTraceMask(), &filter, &trace );
- if ( trace.startsolid || trace.allsolid )
- {
- return true;
- }
-
- if ( trace.fraction < 0.1f )
- {
- return true;
- }
-
- start = trace.endpos;
- end.z -= HalfHumanHeight * 2;
- UTIL_TraceHull( start, end, mins, maxs, TheNavMesh->GetGenerationTraceMask(), &filter, &trace );
- if ( trace.startsolid || trace.allsolid )
- {
- return true;
- }
-
- if ( trace.fraction == 1.0f )
- {
- return true;
- }
-
- if ( trace.plane.normal.z < 0.7f )
- {
- return true;
- }
- }
- return overlap;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Search the world and build a map of possible movements.
- * The algorithm begins at the bot's current location, and does a recursive search
- * outwards, tracking all valid steps and generating a directed graph of CNavNodes.
- *
- * Sample the map one "step" in a cardinal direction to learn the map.
- *
- * Returns true if sampling needs to continue, or false if done.
- */
-bool CNavMesh::SampleStep( void )
-{
- // take a step
- while( true )
- {
- if (m_currentNode == NULL)
- {
- // sampling is complete from current seed, try next one
- m_currentNode = GetNextWalkableSeedNode();
-
- if (m_currentNode == NULL)
- {
- if ( m_generationMode == GENERATE_INCREMENTAL || m_generationMode == GENERATE_SIMPLIFY )
- {
- return false;
- }
-
- // search is exhausted - continue search from ends of ladders
- for ( int i=0; i<m_ladders.Count(); ++i )
- {
- CNavLadder *ladder = m_ladders[i];
-
- // check ladder bottom
- if ((m_currentNode = LadderEndSearch( &ladder->m_bottom, ladder->GetDir() )) != 0)
- break;
-
- // check ladder top
- if ((m_currentNode = LadderEndSearch( &ladder->m_top, ladder->GetDir() )) != 0)
- break;
- }
-
- if (m_currentNode == NULL)
- {
- // all seeds exhausted, sampling complete
- return false;
- }
- }
- }
-
- //
- // Take a step from this node
- //
- for( int dir = NORTH; dir < NUM_DIRECTIONS; dir++ )
- {
- if (!m_currentNode->HasVisited( (NavDirType)dir ))
- {
- // have not searched in this direction yet
-
- // start at current node position
- Vector pos = *m_currentNode->GetPosition();
-
- // snap to grid
- int cx = SnapToGrid( pos.x );
- int cy = SnapToGrid( pos.y );
-
- // attempt to move to adjacent node
- switch( dir )
- {
- case NORTH: cy -= GenerationStepSize; break;
- case SOUTH: cy += GenerationStepSize; break;
- case EAST: cx += GenerationStepSize; break;
- case WEST: cx -= GenerationStepSize; break;
- }
-
- pos.x = cx;
- pos.y = cy;
-
- m_generationDir = (NavDirType)dir;
-
- // mark direction as visited
- m_currentNode->MarkAsVisited( m_generationDir );
-
- // sanity check to not generate across the world for incremental generation
- const float incrementalRange = nav_generate_incremental_range.GetFloat();
- if ( m_generationMode == GENERATE_INCREMENTAL && incrementalRange > 0 )
- {
- bool inRange = false;
- for ( int i=0; i<m_walkableSeeds.Count(); ++i )
- {
- const Vector &seedPos = m_walkableSeeds[i].pos;
- if ( (seedPos - pos).IsLengthLessThan( incrementalRange ) )
- {
- inRange = true;
- break;
- }
- }
-
- if ( !inRange )
- {
- return true;
- }
- }
-
- if ( m_generationMode == GENERATE_SIMPLIFY )
- {
- if ( !m_simplifyGenerationExtent.Contains( pos ) )
- {
- return true;
- }
- }
-
- // test if we can move to new position
- trace_t result;
- Vector from( *m_currentNode->GetPosition() );
- CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
- Vector to, toNormal;
- float obstacleHeight = 0, obstacleStartDist = 0, obstacleEndDist = GenerationStepSize;
- if ( TraceAdjacentNode( 0, from, pos, &result ) )
- {
- to = result.endpos;
- toNormal = result.plane.normal;
- }
- else
- {
- // test going up ClimbUpHeight
- bool success = false;
- for ( float height = StepHeight; height <= ClimbUpHeight; height += 1.0f )
- {
- trace_t tr;
- Vector start( from );
- Vector end( pos );
- start.z += height;
- end.z += height;
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr );
- if ( !tr.startsolid && tr.fraction == 1.0f )
- {
- if ( !StayOnFloor( &tr ) )
- {
- break;
- }
-
- to = tr.endpos;
- toNormal = tr.plane.normal;
-
- start = end = from;
- end.z += height;
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr );
- if ( tr.fraction < 1.0f )
- {
- break;
- }
-
- // keep track of far up we had to go to find a path to the next node
- obstacleHeight = height;
- success = true;
- break;
- }
- else
- {
- // Could not trace from node to node at this height, something is in the way.
- // Trace in the other direction to see if we hit something
- Vector vecToObstacleStart = tr.endpos - start;
- Assert( vecToObstacleStart.LengthSqr() <= Square( GenerationStepSize ) );
- if ( vecToObstacleStart.LengthSqr() <= Square( GenerationStepSize ) )
- {
- UTIL_TraceHull( end, start, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr );
- if ( !tr.startsolid && tr.fraction < 1.0 )
- {
- // We hit something going the other direction. There is some obstacle between the two nodes.
- Vector vecToObstacleEnd = tr.endpos - start;
- Assert( vecToObstacleEnd.LengthSqr() <= Square( GenerationStepSize ) );
- if ( vecToObstacleEnd.LengthSqr() <= Square( GenerationStepSize ) )
- {
- // Remember the distances to start and end of the obstacle (with respect to the "from" node).
- // Keep track of the last distances to obstacle as we keep increasing the height we do a trace for.
- // If we do eventually clear the obstacle, these values will be the start and end distance to the
- // very tip of the obstacle.
- obstacleStartDist = vecToObstacleStart.Length();
- obstacleEndDist = vecToObstacleEnd.Length();
- if ( obstacleEndDist == 0 )
- {
- obstacleEndDist = GenerationStepSize;
- }
- }
- }
- }
- }
- }
-
- if ( !success )
- {
- return true;
- }
- }
-
- // Don't generate nodes if we spill off the end of the world onto skybox
- if ( result.surface.flags & ( SURF_SKY|SURF_SKY2D ) )
- {
- return true;
- }
-
- // If we're incrementally generating, don't overlap existing nav areas.
- Vector testPos( to );
- bool overlapSE = IsNodeOverlapped( testPos, Vector( 1, 1, HalfHumanHeight ) );
- bool overlapSW = IsNodeOverlapped( testPos, Vector( -1, 1, HalfHumanHeight ) );
- bool overlapNE = IsNodeOverlapped( testPos, Vector( 1, -1, HalfHumanHeight ) );
- bool overlapNW = IsNodeOverlapped( testPos, Vector( -1, -1, HalfHumanHeight ) );
- if ( overlapSE && overlapSW && overlapNE && overlapNW && m_generationMode != GENERATE_SIMPLIFY )
- {
- return true;
- }
-
- int nTolerance = nav_generate_incremental_tolerance.GetInt();
- if ( nTolerance > 0 && m_generationMode == GENERATE_INCREMENTAL )
- {
- bool bValid = false;
- int zPos = to.z;
- for ( int i=0; i<m_walkableSeeds.Count(); ++i )
- {
- const Vector &seedPos = m_walkableSeeds[i].pos;
- int zMin = seedPos.z - nTolerance;
- int zMax = seedPos.z + nTolerance;
-
- if ( zPos >= zMin && zPos <= zMax )
- {
- bValid = true;
- break;
- }
- }
-
- if ( !bValid )
- return true;
- }
-
-
- bool isOnDisplacement = result.IsDispSurface();
-
- if ( nav_displacement_test.GetInt() > 0 )
- {
- // Test for nodes under displacement surfaces.
- // This happens during development, and is a pain because the space underneath a displacement
- // is not 'solid'.
- Vector start = to + Vector( 0, 0, 0 );
- Vector end = start + Vector( 0, 0, nav_displacement_test.GetInt() );
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &result );
-
- if ( result.fraction > 0 )
- {
- end = start;
- start = result.endpos;
- UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &result );
- if ( result.fraction < 1 )
- {
- // if we made it down to within StepHeight, maybe we're on a static prop
- if ( result.endpos.z > to.z + StepHeight )
- {
- return true;
- }
- }
- }
- }
-
- float deltaZ = to.z - m_currentNode->GetPosition()->z;
- // If there's an obstacle in the way and it's traversable, or the obstacle is not higher than the destination node itself minus a small epsilon
- // (meaning the obstacle was just the height change to get to the destination node, no extra obstacle between the two), clear obstacle height
- // and distances
- if ( ( obstacleHeight < MaxTraversableHeight ) || ( deltaZ > ( obstacleHeight - 2.0f ) ) )
- {
- obstacleHeight = 0;
- obstacleStartDist = 0;
- obstacleEndDist = GenerationStepSize;
- }
-
- // we can move here
- // create a new navigation node, and update current node pointer
- AddNode( to, toNormal, m_generationDir, m_currentNode, isOnDisplacement, obstacleHeight, obstacleStartDist, obstacleEndDist );
-
- return true;
- }
- }
-
- // all directions have been searched from this node - pop back to its parent and continue
- m_currentNode = m_currentNode->GetParent();
- }
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Add given walkable position to list of seed positions for map sampling
- */
-void CNavMesh::AddWalkableSeed( const Vector &pos, const Vector &normal )
-{
- WalkableSeedSpot seed;
-
- seed.pos.x = RoundToUnits( pos.x, GenerationStepSize );
- seed.pos.y = RoundToUnits( pos.y, GenerationStepSize );
- seed.pos.z = pos.z;
- seed.normal = normal;
-
- m_walkableSeeds.AddToTail( seed );
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Return the next walkable seed as a node
- */
-CNavNode *CNavMesh::GetNextWalkableSeedNode( void )
-{
- if ( m_seedIdx >= m_walkableSeeds.Count() )
- return NULL;
-
- WalkableSeedSpot spot = m_walkableSeeds[ m_seedIdx ];
- ++m_seedIdx;
-
- // check if a node exists at this location
- CNavNode *node = CNavNode::GetNode( spot.pos );
- if ( node )
- return NULL;
-
- return new CNavNode( spot.pos, spot.normal, NULL, false );
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Check LOS, ignoring any entities that we can walk through
- */
-bool IsWalkableTraceLineClear( const Vector &from, const Vector &to, unsigned int flags )
-{
- trace_t result;
- CBaseEntity *ignore = NULL;
- Vector useFrom = from;
-
- CTraceFilterWalkableEntities traceFilter( NULL, COLLISION_GROUP_NONE, flags );
-
- result.fraction = 0.0f;
-
- const int maxTries = 50;
- for( int t=0; t<maxTries; ++t )
- {
- UTIL_TraceLine( useFrom, to, MASK_NPCSOLID, &traceFilter, &result );
-
- // if we hit a walkable entity, try again
- if (result.fraction != 1.0f && IsEntityWalkable( result.m_pEnt, flags ))
- {
- ignore = result.m_pEnt;
-
- // start from just beyond where we hit to avoid infinite loops
- Vector dir = to - from;
- dir.NormalizeInPlace();
- useFrom = result.endpos + 5.0f * dir;
- }
- else
- {
- break;
- }
- }
-
- if (result.fraction == 1.0f)
- return true;
-
- return false;
-}
-
-
-//--------------------------------------------------------------------------------------------------------------
-class Subdivider
-{
-public:
- Subdivider( int depth )
- {
- m_depth = depth;
- }
-
- bool operator() ( CNavArea *area )
- {
- SubdivideX( area, true, true, m_depth );
-
- return true;
- }
-
- void SubdivideX( CNavArea *area, bool canDivideX, bool canDivideY, int depth )
- {
- if (!canDivideX || depth <= 0)
- return;
-
- float split = area->GetSizeX() / 2.0f;
-
- if (split < GenerationStepSize)
- {
- if (canDivideY)
- {
- SubdivideY( area, false, canDivideY, depth );
- }
- return;
- }
-
- split += area->GetCorner( NORTH_WEST ).x;
-
- split = TheNavMesh->SnapToGrid( split );
-
- CNavArea *alpha, *beta;
- if (area->SplitEdit( false, split, &alpha, &beta ))
- {
- SubdivideY( alpha, canDivideX, canDivideY, depth );
- SubdivideY( beta, canDivideX, canDivideY, depth );
- }
- }
-
-
- void SubdivideY( CNavArea *area, bool canDivideX, bool canDivideY, int depth )
- {
- if (!canDivideY)
- return;
-
- float split = area->GetSizeY() / 2.0f;
-
- if (split < GenerationStepSize)
- {
- if (canDivideX)
- {
- SubdivideX( area, canDivideX, false, depth-1 );
- }
- return;
- }
-
- split += area->GetCorner( NORTH_WEST ).y;
-
- split = TheNavMesh->SnapToGrid( split );
-
- CNavArea *alpha, *beta;
- if (area->SplitEdit( true, split, &alpha, &beta ))
- {
- SubdivideX( alpha, canDivideX, canDivideY, depth-1 );
- SubdivideX( beta, canDivideX, canDivideY, depth-1 );
- }
- }
-
- int m_depth;
-};
-
-
-//--------------------------------------------------------------------------------------------------------------
-/**
- * Subdivide each nav area in X and Y to create 4 new areas
- */
-void CNavMesh::CommandNavSubdivide( const CCommand &args )
-{
- int depth = 1;
-
- if (args.ArgC() == 2)
- {
- depth = atoi( args[1] );
- }
-
- Subdivider chop( depth );
- TheNavMesh->ForAllSelectedAreas( chop );
-}
-
-CON_COMMAND_F( nav_subdivide, "Subdivides all selected areas.", FCVAR_GAMEDLL | FCVAR_CHEAT )
-{
- if ( !UTIL_IsCommandIssuedByServerAdmin() )
- return;
-
- TheNavMesh->CommandNavSubdivide( args );
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Debugging code to verify that all nav area connections are internally consistent
-*/
-void CNavMesh::ValidateNavAreaConnections( void )
-{
- // iterate all nav areas
- NavConnect connect;
-
- for ( int it = 0; it < TheNavAreas.Count(); it++ )
- {
- CNavArea *area = TheNavAreas[ it ];
-
- for ( NavDirType dir = NORTH; dir < NUM_DIRECTIONS; dir = (NavDirType) ( ( (int) dir ) +1 ) )
- {
- const NavConnectVector *pOutgoing = area->GetAdjacentAreas( dir );
- const NavConnectVector *pIncoming = area->GetIncomingConnections( dir );
-
- for ( int iConnect = 0; iConnect < pOutgoing->Count(); iConnect++ )
- {
- // make sure no area is on both the connection and incoming list
- CNavArea *areaOther = (*pOutgoing)[iConnect].area;
- connect.area = areaOther;
- if ( pIncoming->Find( connect ) != pIncoming->InvalidIndex() )
- {
- Msg( "Area %d has area %d on both 2-way and incoming list, should only be on one\n", area->GetID(), areaOther->GetID() );
- Assert( false );
- }
-
- // make sure there are no duplicate connections on the list
- for ( int iConnectCheck = iConnect+1; iConnectCheck < pOutgoing->Count(); iConnectCheck++ )
- {
- CNavArea *areaCheck = (*pOutgoing)[iConnectCheck].area;
- if ( areaOther == areaCheck )
- {
- Msg( "Area %d has multiple outgoing connections to area %d in direction %d\n", area->GetID(), areaOther->GetID(), dir );
- Assert( false );
- }
- }
-
- const NavConnectVector *pOutgoingOther = areaOther->GetAdjacentAreas( OppositeDirection( dir ) );
- const NavConnectVector *pIncomingOther = areaOther->GetIncomingConnections( OppositeDirection( dir ) );
-
- // if we have a one-way outgoing connection, make sure we are on the other area's incoming list
- connect.area = area;
- bool bIsTwoWay = pOutgoingOther->Find( connect ) != pOutgoingOther->InvalidIndex();
- if ( !bIsTwoWay )
- {
- connect.area = area;
- bool bOnOthersIncomingList = pIncomingOther->Find( connect ) != pIncomingOther->InvalidIndex();
- if ( !bOnOthersIncomingList )
- {
- Msg( "Area %d has one-way connect to area %d but does not appear on the latter's incoming list\n", area->GetID(), areaOther->GetID() );
- }
- }
- }
-
- for ( int iConnect = 0; iConnect < pIncoming->Count(); iConnect++ )
- {
- CNavArea *areaOther = (*pIncoming)[iConnect].area;
-
- // make sure there are not duplicate areas on the incoming list
- for ( int iConnectCheck = iConnect+1; iConnectCheck < pIncoming->Count(); iConnectCheck++ )
- {
- CNavArea *areaCheck = (*pIncoming)[iConnectCheck].area;
- if ( areaOther == areaCheck )
- {
- Msg( "Area %d has multiple incoming connections to area %d in direction %d\n", area->GetID(), areaOther->GetID(), dir );
- Assert( false );
- }
- }
-
- const NavConnectVector *pOutgoingOther = areaOther->GetAdjacentAreas( OppositeDirection( dir ) );
- connect.area = area;
- bool bOnOthersOutgoingList = pOutgoingOther->Find( connect ) != pOutgoingOther->InvalidIndex();
- if ( !bOnOthersOutgoingList )
- {
- Msg( "Area %d has incoming connection from area %d but does not appear on latter's outgoing connection list\n", area->GetID(), areaOther->GetID() );
- Assert( false );
- }
- }
- }
- }
-}
-
-//--------------------------------------------------------------------------------------------------------------
-/**
-* Temp way to mark cliff areas after generation without regen'ing. Any area that is adjacent to a cliff
-* gets marked as a cliff. This will leave some big areas marked as cliff just because one edge is adjacent to
-* a cliff so it's not great. The code that does this at generation time is better because it ensures that
-* areas next to cliffs don't get merged with no-cliff areas.
-*/
-void CNavMesh::PostProcessCliffAreas()
-{
- for ( int it = 0; it < TheNavAreas.Count(); it++ )
- {
- CNavArea *area = TheNavAreas[ it ];
- if ( area->GetAttributes() & NAV_MESH_CLIFF )
- continue;
-
- for ( int i = 0; i < NUM_DIRECTIONS; i++ )
- {
- bool bHasCliff = false;
- NavDirType dir = (NavDirType) i;
- NavCornerType corner[2];
-
- // look at either corner along this edge
- corner[0] = (NavCornerType) i;
- corner[1] = (NavCornerType) ( ( i+ 1 ) % NUM_CORNERS );
-
- for ( int j = 0; j < 2; j++ )
- {
- Vector cornerPos = area->GetCorner( corner[j] );
- if ( CheckCliff( &cornerPos, dir ) )
- {
- bHasCliff = true;
- break;
- }
- }
-
- if ( bHasCliff )
- {
- area->SetAttributes( area->GetAttributes() | NAV_MESH_CLIFF );
- break;
- }
- }
- }
-}
-
-CON_COMMAND_F( nav_gen_cliffs_approx, "Mark cliff areas, post-processing approximation", FCVAR_CHEAT )
-{
- TheNavMesh->PostProcessCliffAreas();
-}
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+// $NoKeywords: $
+//
+//=============================================================================//
+// nav_generate.cpp
+// Auto-generate a Navigation Mesh by sampling the current map
+// Author: Michael S. Booth ([email protected]), 2003
+
+#include "cbase.h"
+#include "util_shared.h"
+#include "nav_mesh.h"
+#include "nav_node.h"
+#include "nav_pathfind.h"
+#include "viewport_panel_names.h"
+//#include "terror/TerrorShared.h"
+#include "fmtstr.h"
+
+#ifdef TERROR
+#include "func_simpleladder.h"
+#endif
+
+// NOTE: This has to be the last file included!
+#include "tier0/memdbgon.h"
+
+
+enum { MAX_BLOCKED_AREAS = 256 };
+static unsigned int blockedID[ MAX_BLOCKED_AREAS ];
+static int blockedIDCount = 0;
+static float lastMsgTime = 0.0f;
+
+bool TraceAdjacentNode( int depth, const Vector& start, const Vector& end, trace_t *trace, float zLimit = DeathDrop );
+bool StayOnFloor( trace_t *trace, float zLimit = DeathDrop );
+
+ConVar nav_slope_limit( "nav_slope_limit", "0.7", FCVAR_CHEAT, "The ground unit normal's Z component must be greater than this for nav areas to be generated." );
+ConVar nav_slope_tolerance( "nav_slope_tolerance", "0.1", FCVAR_CHEAT, "The ground unit normal's Z component must be this close to the nav area's Z component to be generated." );
+ConVar nav_displacement_test( "nav_displacement_test", "10000", FCVAR_CHEAT, "Checks for nodes embedded in displacements (useful for in-development maps)" );
+ConVar nav_generate_fencetops( "nav_generate_fencetops", "1", FCVAR_CHEAT, "Autogenerate nav areas on fence and obstacle tops" );
+ConVar nav_generate_fixup_jump_areas( "nav_generate_fixup_jump_areas", "1", FCVAR_CHEAT, "Convert obsolete jump areas into 2-way connections" );
+ConVar nav_generate_incremental_range( "nav_generate_incremental_range", "2000", FCVAR_CHEAT );
+ConVar nav_generate_incremental_tolerance( "nav_generate_incremental_tolerance", "0", FCVAR_CHEAT, "Z tolerance for adding new nav areas." );
+ConVar nav_area_max_size( "nav_area_max_size", "50", FCVAR_CHEAT, "Max area size created in nav generation" );
+
+// Common bounding box for traces
+Vector NavTraceMins( -0.45, -0.45, 0 );
+Vector NavTraceMaxs( 0.45, 0.45, HumanCrouchHeight );
+bool FindGroundForNode( Vector *pos, Vector *normal ); // find a ground Z for pos that is clear for NavTraceMins -> NavTraceMaxs
+
+const float MaxTraversableHeight = StepHeight; // max internal obstacle height that can occur between nav nodes and safely disregarded
+const float MinObstacleAreaWidth = 10.0f; // min width of a nav area we will generate on top of an obstacle
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Shortest path cost, paying attention to "blocked" areas
+ */
+class ApproachAreaCost
+{
+public:
+ float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator )
+ {
+ // check if this area is "blocked"
+ for( int i=0; i<blockedIDCount; ++i )
+ {
+ if (area->GetID() == blockedID[i])
+ {
+ return -1.0f;
+ }
+ }
+
+ if (fromArea == NULL)
+ {
+ // first area in path, no cost
+ return 0.0f;
+ }
+ else
+ {
+ // compute distance traveled along path so far
+ float dist;
+
+ if (ladder)
+ {
+ dist = ladder->m_length;
+ }
+ else
+ {
+ dist = (area->GetCenter() - fromArea->GetCenter()).Length();
+ }
+
+ float cost = dist + fromArea->GetCostSoFar();
+
+ return cost;
+ }
+ }
+};
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Start at given position and find first area in given direction
+ */
+inline CNavArea *findFirstAreaInDirection( const Vector *start, NavDirType dir, float range, float beneathLimit, CBaseEntity *traceIgnore = NULL, Vector *closePos = NULL )
+{
+ CNavArea *area = NULL;
+
+ Vector pos = *start;
+
+ int end = (int)((range / GenerationStepSize) + 0.5f);
+
+ for( int i=1; i<=end; i++ )
+ {
+ AddDirectionVector( &pos, dir, GenerationStepSize );
+
+ // make sure we dont look thru the wall
+ trace_t result;
+
+ UTIL_TraceHull( *start, pos, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), traceIgnore, COLLISION_GROUP_NONE, &result );
+
+ if (result.fraction < 1.0f)
+ break;
+
+ area = TheNavMesh->GetNavArea( pos, beneathLimit );
+ if (area)
+ {
+ if (closePos)
+ {
+ closePos->x = pos.x;
+ closePos->y = pos.y;
+ closePos->z = area->GetZ( pos.x, pos.y );
+ }
+
+ break;
+ }
+ }
+
+ return area;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * For each ladder in the map, create a navigation representation of it.
+ */
+void CNavMesh::BuildLadders( void )
+{
+ // remove any left-over ladders
+ DestroyLadders();
+
+#ifdef TERROR
+ CFuncSimpleLadder *ladder = NULL;
+ while( (ladder = dynamic_cast< CFuncSimpleLadder * >(gEntList.FindEntityByClassname( ladder, "func_simpleladder" ))) != NULL )
+ {
+ Vector mins, maxs;
+ ladder->CollisionProp()->WorldSpaceSurroundingBounds( &mins, &maxs );
+ CreateLadder( mins, maxs, 0.0f );
+ }
+#endif
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Create a navigation representation of a ladder.
+ */
+void CNavMesh::CreateLadder( const Vector& absMin, const Vector& absMax, float maxHeightAboveTopArea )
+{
+ CNavLadder *ladder = new CNavLadder;
+
+ // compute top & bottom of ladder
+ ladder->m_top.x = (absMin.x + absMax.x) / 2.0f;
+ ladder->m_top.y = (absMin.y + absMax.y) / 2.0f;
+ ladder->m_top.z = absMax.z;
+
+ ladder->m_bottom.x = ladder->m_top.x;
+ ladder->m_bottom.y = ladder->m_top.y;
+ ladder->m_bottom.z = absMin.z;
+
+ // determine facing - assumes "normal" runged ladder
+ float xSize = absMax.x - absMin.x;
+ float ySize = absMax.y - absMin.y;
+ trace_t result;
+ if (xSize > ySize)
+ {
+ // ladder is facing north or south - determine which way
+ // "pull in" traceline from bottom and top in case ladder abuts floor and/or ceiling
+ Vector from = ladder->m_bottom + Vector( 0.0f, GenerationStepSize, GenerationStepSize/2 );
+ Vector to = ladder->m_top + Vector( 0.0f, GenerationStepSize, -GenerationStepSize/2 );
+
+ UTIL_TraceLine( from, to, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+
+ if (result.fraction != 1.0f || result.startsolid)
+ ladder->SetDir( NORTH );
+ else
+ ladder->SetDir( SOUTH );
+
+ ladder->m_width = xSize;
+ }
+ else
+ {
+ // ladder is facing east or west - determine which way
+ Vector from = ladder->m_bottom + Vector( GenerationStepSize, 0.0f, GenerationStepSize/2 );
+ Vector to = ladder->m_top + Vector( GenerationStepSize, 0.0f, -GenerationStepSize/2 );
+
+ UTIL_TraceLine( from, to, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+
+ if (result.fraction != 1.0f || result.startsolid)
+ ladder->SetDir( WEST );
+ else
+ ladder->SetDir( EAST );
+
+ ladder->m_width = ySize;
+ }
+
+ // adjust top and bottom of ladder to make sure they are reachable
+ // (cs_office has a crate right in front of the base of a ladder)
+ Vector along = ladder->m_top - ladder->m_bottom;
+ float length = along.NormalizeInPlace();
+ Vector on, out;
+ const float minLadderClearance = 32.0f;
+
+ // adjust bottom to bypass blockages
+ const float inc = 10.0f;
+ float t;
+ for( t = 0.0f; t <= length; t += inc )
+ {
+ on = ladder->m_bottom + t * along;
+
+ out = on + ladder->GetNormal() * minLadderClearance;
+
+ UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+
+ if (result.fraction == 1.0f && !result.startsolid)
+ {
+ // found viable ladder bottom
+ ladder->m_bottom = on;
+ break;
+ }
+ }
+
+ // adjust top to bypass blockages
+ for( t = 0.0f; t <= length; t += inc )
+ {
+ on = ladder->m_top - t * along;
+
+ out = on + ladder->GetNormal() * minLadderClearance;
+
+ UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+
+ if (result.fraction == 1.0f && !result.startsolid)
+ {
+ // found viable ladder top
+ ladder->m_top = on;
+ break;
+ }
+ }
+
+ ladder->m_length = (ladder->m_top - ladder->m_bottom).Length();
+
+ ladder->SetDir( ladder->GetDir() ); // now that we've adjusted the top and bottom, re-check the normal
+
+ ladder->m_bottomArea = NULL;
+ ladder->m_topForwardArea = NULL;
+ ladder->m_topLeftArea = NULL;
+ ladder->m_topRightArea = NULL;
+ ladder->m_topBehindArea = NULL;
+ ladder->ConnectGeneratedLadder( maxHeightAboveTopArea );
+
+ // add ladder to global list
+ m_ladders.AddToTail( ladder );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Create a navigation representation of a ladder.
+ */
+void CNavMesh::CreateLadder( const Vector &top, const Vector &bottom, float width, const Vector2D &ladderDir, float maxHeightAboveTopArea )
+{
+ CNavLadder *ladder = new CNavLadder;
+
+ ladder->m_top = top;
+ ladder->m_bottom = bottom;
+ ladder->m_width = width;
+ if ( fabs( ladderDir.x ) > fabs( ladderDir.y ) )
+ {
+ if ( ladderDir.x > 0.0f )
+ {
+ ladder->SetDir( EAST );
+ }
+ else
+ {
+ ladder->SetDir( WEST );
+ }
+ }
+ else
+ {
+ if ( ladderDir.y > 0.0f )
+ {
+ ladder->SetDir( SOUTH );
+ }
+ else
+ {
+ ladder->SetDir( NORTH );
+ }
+ }
+
+ // adjust top and bottom of ladder to make sure they are reachable
+ // (cs_office has a crate right in front of the base of a ladder)
+ Vector along = ladder->m_top - ladder->m_bottom;
+ float length = along.NormalizeInPlace();
+ Vector on, out;
+ const float minLadderClearance = 32.0f;
+
+ // adjust bottom to bypass blockages
+ const float inc = 10.0f;
+ float t;
+ trace_t result;
+ for( t = 0.0f; t <= length; t += inc )
+ {
+ on = ladder->m_bottom + t * along;
+
+ out = on + ladder->GetNormal() * minLadderClearance;
+
+ UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+
+ if (result.fraction == 1.0f && !result.startsolid)
+ {
+ // found viable ladder bottom
+ ladder->m_bottom = on;
+ break;
+ }
+ }
+
+ // adjust top to bypass blockages
+ for( t = 0.0f; t <= length; t += inc )
+ {
+ on = ladder->m_top - t * along;
+
+ out = on + ladder->GetNormal() * minLadderClearance;
+
+ UTIL_TraceLine( on, out, GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+
+ if (result.fraction == 1.0f && !result.startsolid)
+ {
+ // found viable ladder top
+ ladder->m_top = on;
+ break;
+ }
+ }
+
+ ladder->m_length = (ladder->m_top - ladder->m_bottom).Length();
+
+ ladder->SetDir( ladder->GetDir() ); // now that we've adjusted the top and bottom, re-check the normal
+
+ ladder->m_bottomArea = NULL;
+ ladder->m_topForwardArea = NULL;
+ ladder->m_topLeftArea = NULL;
+ ladder->m_topRightArea = NULL;
+ ladder->m_topBehindArea = NULL;
+ ladder->ConnectGeneratedLadder( maxHeightAboveTopArea );
+
+ // add ladder to global list
+ m_ladders.AddToTail( ladder );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+void CNavLadder::ConnectGeneratedLadder( float maxHeightAboveTopArea )
+{
+ const float nearLadderRange = 75.0f; // 50
+
+ //
+ // Find naviagtion area at bottom of ladder
+ //
+
+ // get approximate postion of player on ladder
+ Vector center = m_bottom + Vector( 0, 0, GenerationStepSize );
+ AddDirectionVector( &center, m_dir, HalfHumanWidth );
+
+ m_bottomArea = TheNavMesh->GetNearestNavArea( center, true );
+ if (!m_bottomArea)
+ {
+ DevMsg( "ERROR: Unconnected ladder bottom at ( %g, %g, %g )\n", m_bottom.x, m_bottom.y, m_bottom.z );
+ }
+ else
+ {
+ // store reference to ladder in the area
+ m_bottomArea->AddLadderUp( this );
+ }
+
+ //
+ // Find adjacent navigation areas at the top of the ladder
+ //
+
+ // get approximate postion of player on ladder
+ center = m_top + Vector( 0, 0, GenerationStepSize );
+ AddDirectionVector( &center, m_dir, HalfHumanWidth );
+
+ float beneathLimit = MIN( 120.0f, m_top.z - m_bottom.z + HalfHumanWidth );
+
+ // find "ahead" area
+ m_topForwardArea = findFirstAreaInDirection( &center, OppositeDirection( m_dir ), nearLadderRange, beneathLimit, NULL );
+ if (m_topForwardArea == m_bottomArea)
+ m_topForwardArea = NULL;
+
+ // find "left" area
+ m_topLeftArea = findFirstAreaInDirection( &center, DirectionLeft( m_dir ), nearLadderRange, beneathLimit, NULL );
+ if (m_topLeftArea == m_bottomArea)
+ m_topLeftArea = NULL;
+
+ // find "right" area
+ m_topRightArea = findFirstAreaInDirection( &center, DirectionRight( m_dir ), nearLadderRange, beneathLimit, NULL );
+ if (m_topRightArea == m_bottomArea)
+ m_topRightArea = NULL;
+
+ // find "behind" area - must look farther, since ladder is against the wall away from this area
+ m_topBehindArea = findFirstAreaInDirection( &center, m_dir, 2.0f*nearLadderRange, beneathLimit, NULL );
+ if (m_topBehindArea == m_bottomArea)
+ m_topBehindArea = NULL;
+
+ // can't include behind area, since it is not used when going up a ladder
+ if (!m_topForwardArea && !m_topLeftArea && !m_topRightArea)
+ DevMsg( "ERROR: Unconnected ladder top at ( %g, %g, %g )\n", m_top.x, m_top.y, m_top.z );
+
+ // store reference to ladder in the area(s)
+ if (m_topForwardArea)
+ m_topForwardArea->AddLadderDown( this );
+
+ if (m_topLeftArea)
+ m_topLeftArea->AddLadderDown( this );
+
+ if (m_topRightArea)
+ m_topRightArea->AddLadderDown( this );
+
+ if (m_topBehindArea)
+ {
+ m_topBehindArea->AddLadderDown( this );
+ Disconnect( m_topBehindArea );
+ }
+
+ // adjust top of ladder to highest connected area
+ float topZ = m_bottom.z + 5.0f;
+ bool topAdjusted = false;
+ CNavArea *topAreaList[4];
+ topAreaList[0] = m_topForwardArea;
+ topAreaList[1] = m_topLeftArea;
+ topAreaList[2] = m_topRightArea;
+ topAreaList[3] = m_topBehindArea;
+
+ for( int a=0; a<4; ++a )
+ {
+ CNavArea *topArea = topAreaList[a];
+ if (topArea == NULL)
+ continue;
+
+ Vector close;
+ topArea->GetClosestPointOnArea( m_top, &close );
+ if (topZ < close.z)
+ {
+ topZ = close.z;
+ topAdjusted = true;
+ }
+ }
+
+ if (topAdjusted)
+ {
+ if ( maxHeightAboveTopArea > 0.0f )
+ {
+ m_top.z = MIN( topZ + maxHeightAboveTopArea, m_top.z );
+ }
+ else
+ {
+ m_top.z = topZ; // not manually specifying a top, so snap exactly
+ }
+ }
+
+ //
+ // Determine whether this ladder is "dangling" or not
+ // "Dangling" ladders are too high to go up
+ //
+ if (m_bottomArea)
+ {
+ Vector bottomSpot;
+ m_bottomArea->GetClosestPointOnArea( m_bottom, &bottomSpot );
+ if (m_bottom.z - bottomSpot.z > HumanHeight)
+ {
+ m_bottomArea->Disconnect( this );
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+class JumpConnector
+{
+public:
+ bool operator()( CNavArea *jumpArea )
+ {
+ if ( !(jumpArea->GetAttributes() & NAV_MESH_JUMP) )
+ {
+ return true;
+ }
+
+ for ( int i=0; i<NUM_DIRECTIONS; ++i )
+ {
+ NavDirType incomingDir = (NavDirType)i;
+ NavDirType outgoingDir = OppositeDirection( incomingDir );
+
+ const NavConnectVector *incoming = jumpArea->GetIncomingConnections( incomingDir );
+ const NavConnectVector *from = jumpArea->GetAdjacentAreas( incomingDir );
+ const NavConnectVector *dest = jumpArea->GetAdjacentAreas( outgoingDir );
+
+ TryToConnect( jumpArea, incoming, dest, outgoingDir );
+ TryToConnect( jumpArea, from, dest, outgoingDir );
+ }
+
+ return true;
+ }
+
+private:
+ struct Connection
+ {
+ CNavArea *source;
+ CNavArea *dest;
+ NavDirType direction;
+ };
+
+ void TryToConnect( CNavArea *jumpArea, const NavConnectVector *source, const NavConnectVector *dest, NavDirType outgoingDir )
+ {
+ FOR_EACH_VEC( (*source), sourceIt )
+ {
+ CNavArea *sourceArea = const_cast< CNavArea * >( (*source)[ sourceIt ].area );
+ if ( !sourceArea->IsConnected( jumpArea, outgoingDir ) )
+ {
+ continue;
+ }
+
+ if ( sourceArea->HasAttributes( NAV_MESH_JUMP ) )
+ {
+ NavDirType incomingDir = OppositeDirection( outgoingDir );
+ const NavConnectVector *in1 = sourceArea->GetIncomingConnections( incomingDir );
+ const NavConnectVector *in2 = sourceArea->GetAdjacentAreas( incomingDir );
+
+ TryToConnect( jumpArea, in1, dest, outgoingDir );
+ TryToConnect( jumpArea, in2, dest, outgoingDir );
+
+ continue;
+ }
+
+ TryToConnect( jumpArea, sourceArea, dest, outgoingDir );
+ }
+ }
+
+ void TryToConnect( CNavArea *jumpArea, CNavArea *sourceArea, const NavConnectVector *dest, NavDirType outgoingDir )
+ {
+ FOR_EACH_VEC( (*dest), destIt )
+ {
+ CNavArea *destArea = const_cast< CNavArea * >( (*dest)[ destIt ].area );
+ if ( destArea->HasAttributes( NAV_MESH_JUMP ) )
+ {
+ // Don't connect areas across 2 jump areas. This means we'll have some missing links due to sampling errors.
+ // This is preferable to generating incorrect links across multiple jump areas, which is far more common.
+ continue;
+ }
+
+ Vector center;
+ float halfWidth;
+ sourceArea->ComputePortal( destArea, outgoingDir, &center, &halfWidth );
+
+ // Don't create corner-to-corner connections
+ if ( halfWidth <= 0.0f )
+ {
+ continue;
+ }
+
+ Vector dir( vec3_origin );
+ AddDirectionVector( &dir, outgoingDir, 5.0f );
+
+ if ( halfWidth > 0.0f )
+ {
+ Vector sourcePos, destPos;
+ sourceArea->GetClosestPointOnArea( center, &sourcePos );
+ destArea->GetClosestPointOnArea( center, &destPos );
+
+ // No jumping up from stairs.
+ if ( sourceArea->HasAttributes( NAV_MESH_STAIRS ) && sourcePos.z + StepHeight < destPos.z )
+ {
+ continue;
+ }
+
+ if ( (sourcePos-destPos).AsVector2D().IsLengthLessThan( GenerationStepSize * 3 ) )
+ {
+ sourceArea->ConnectTo( destArea, outgoingDir );
+// DevMsg( "Connected %d->%d via %d (len %f)\n",
+// sourceArea->GetID(), destArea->GetID(), jumpArea->GetID(), sourcePos.DistTo( destPos ) );
+ }
+ }
+ }
+ }
+};
+
+//--------------------------------------------------------------------------------------------------------------
+void CNavMesh::MarkPlayerClipAreas( void )
+{
+#ifdef TERROR
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ TerrorNavArea *area = static_cast< TerrorNavArea * >(TheNavAreas[it]);
+
+ // Trace upward a bit from our center point just colliding wtih PLAYERCLIP to see if we're in one, if we are, mark us as accordingly.
+ trace_t trace;
+ Vector start = area->GetCenter() + Vector(0.0f, 0.0f, 16.0f );
+ Vector end = area->GetCenter() + Vector(0.0f, 0.0f, 32.0f );
+ UTIL_TraceHull( start, end, Vector(0,0,0), Vector(0,0,0), CONTENTS_PLAYERCLIP, NULL, &trace);
+
+ if ( trace.fraction < 1.0 )
+ {
+ area->SetAttributes( area->GetAttributes() | TerrorNavArea::NAV_PLAYERCLIP );
+ }
+ }
+#endif
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Mark all areas that require a jump to get through them.
+ * This currently relies on jump areas having extreme slope.
+ */
+void CNavMesh::MarkJumpAreas( void )
+{
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ if ( !area->HasNodes() )
+ continue;
+
+ Vector normal, otherNormal;
+ area->ComputeNormal( &normal );
+ area->ComputeNormal( &otherNormal, true );
+
+ float lowestNormalZ = MIN( normal.z, otherNormal.z );
+ if (lowestNormalZ < nav_slope_limit.GetFloat())
+ {
+ // The area is a jump area, and we don't merge jump areas together
+ area->SetAttributes( area->GetAttributes() | NAV_MESH_JUMP | NAV_MESH_NO_MERGE );
+ }
+ else if ( lowestNormalZ < nav_slope_limit.GetFloat() + nav_slope_tolerance.GetFloat() )
+ {
+ Vector testPos = area->GetCenter();
+ testPos.z += HalfHumanHeight;
+ Vector groundNormal;
+ float dummy;
+ if ( GetSimpleGroundHeight( testPos, &dummy, &groundNormal ) )
+ {
+ // If the ground normal is divergent from the area's normal, mark it as a jump area - it's not
+ // really representative of the ground.
+ float deltaNormalZ = fabs( groundNormal.z - lowestNormalZ );
+ if ( deltaNormalZ > nav_slope_tolerance.GetFloat() )
+ {
+ // The area is a jump area, and we don't merge jump areas together
+ area->SetAttributes( area->GetAttributes() | NAV_MESH_JUMP | NAV_MESH_NO_MERGE );
+ }
+ }
+ }
+ }
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Remove all areas marked as jump areas and connect the areas connecting to them
+*
+*/
+void CNavMesh::StichAndRemoveJumpAreas( void )
+{
+ // Now, go through and remove jump areas, connecting areas to make up for it
+ JumpConnector connector;
+ ForAllAreas( connector );
+ RemoveJumpAreas();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Adjusts obstacle start and end distances such that obstacle width (end-start) is not less than MinObstacleAreaWidth,
+* and end distance is not greater than maxAllowedDist
+*/
+void AdjustObstacleDistances( float *pObstacleStartDist, float *pObstacleEndDist, float maxAllowedDist )
+{
+ float obstacleWidth = *pObstacleEndDist - *pObstacleStartDist;
+ // is the obstacle width too narrow?
+ if ( obstacleWidth < MinObstacleAreaWidth )
+ {
+ float halfDelta = ( MinObstacleAreaWidth - obstacleWidth ) /2;
+ // move start so it's half of min width from center, but no less than zero
+ *pObstacleStartDist = MAX( *pObstacleStartDist - halfDelta, 0 );
+ // move end so it's min width from start
+ *pObstacleEndDist = *pObstacleStartDist + MinObstacleAreaWidth;
+
+ // if this pushes the end past max allowed distance, pull start and end back so that end is within allowed distance
+ if ( *pObstacleEndDist > maxAllowedDist )
+ {
+ float delta = *pObstacleEndDist - maxAllowedDist;
+ *pObstacleStartDist -= delta;
+ *pObstacleEndDist -= delta;
+ }
+ }
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Makes sure tall, slim obstacles like fencetops, railings and narrow walls have nav areas placed on top of them
+* to allow climbing & traversal
+*/
+void CNavMesh::HandleObstacleTopAreas( void )
+{
+ if ( !nav_generate_fencetops.GetBool() )
+ return;
+
+ // For any 1x1 area that is internally blocked by an obstacle, raise it on top of the obstacle and size to fit.
+ RaiseAreasWithInternalObstacles();
+
+ // Create new areas as required
+ CreateObstacleTopAreas();
+
+ // It's possible for obstacle top areas to wind up overlapping one another, fix any such cases
+ RemoveOverlappingObstacleTopAreas();
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* For any nav area that has internal obstacles between its corners of greater than traversable height,
+* raise that nav area to sit at the top of the obstacle, and shrink it to fit the obstacle. Such nav
+* areas are already restricted to be 1x1 so this will only be performed on areas that are already small.
+*/
+void CNavMesh::RaiseAreasWithInternalObstacles()
+{
+ // obstacle areas next to stairs are bad - delete them
+ CUtlVector< CNavArea * > areasToDelete;
+
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+
+ // any nav area with internal obstacles will be 1x1 (width and height = GenerationStepSize), so
+ // only need to consider areas of that size
+ if ( ( area->GetSizeX() != GenerationStepSize ) || (area->GetSizeY() != GenerationStepSize ) )
+ continue;
+
+ float obstacleZ[2] = { -FLT_MAX, -FLT_MAX };
+ float obstacleZMax = -FLT_MAX;
+ NavDirType obstacleDir = NORTH;
+ float obstacleStartDist = GenerationStepSize;
+ float obstacleEndDist = 0;
+
+ bool isStairNeighbor = false;
+
+ // Look at all 4 directions and determine if there are obstacles in that direction. Find the direction with the highest obstacle, if any.
+ for ( int i = 0; i < NUM_DIRECTIONS; i++ )
+ {
+ NavDirType dir = (NavDirType) i;
+
+ // For this direction, look at the left and right edges of the nav area relative to this direction and determined if they are both blocked
+ // by obstacles. We only consider this area obstructed if both edges are blocked (e.g. fence runs all the way through it).
+
+ NavCornerType corner[2];
+ int iEdgesBlocked = 0;
+ corner[0] = (NavCornerType) ( ( i + 3 ) % NUM_CORNERS ); // lower left-hand corner relative to current direction
+ corner[1] = (NavCornerType) ( ( i + 2 ) % NUM_CORNERS ); // lower right-hand corner relative to current direction
+ float obstacleZThisDir[2] = { -FLT_MAX, -FLT_MAX }; // absolute Z pos of obstacle for left and right edge in this direction
+ float obstacleStartDistThisDir = GenerationStepSize; // closest obstacle start distance in this direction
+ float obstacleEndDistThisDir = 0; // farthest obstacle end distance in this direction
+
+ // consider left and right edges of nav area relative to current direction
+ for ( int iEdge = 0; iEdge < 2; iEdge++ )
+ {
+ NavCornerType cornerType = corner[iEdge];
+ CNavNode *nodeFrom = area->m_node[cornerType];
+ if ( nodeFrom )
+ {
+ // is there an obstacle going from corner to corner along this edge?
+ float obstacleHeight = nodeFrom->m_obstacleHeight[dir];
+ if ( obstacleHeight > MaxTraversableHeight )
+ {
+ // yes, this edge is blocked
+ iEdgesBlocked++;
+ // keep track of obstacle height and start and end distance for this edge
+ float obstacleZ = nodeFrom->GetPosition()->z + obstacleHeight;
+ if ( obstacleZ > obstacleZThisDir[iEdge] )
+ {
+ obstacleZThisDir[iEdge] = obstacleZ;
+ }
+ obstacleStartDistThisDir = MIN( nodeFrom->m_obstacleStartDist[dir], obstacleStartDistThisDir );
+ obstacleEndDistThisDir = MAX( nodeFrom->m_obstacleEndDist[dir], obstacleEndDistThisDir );
+ }
+ }
+ }
+
+ int BlockedEdgeCutoff = 2;
+ const NavConnectVector *connections = area->GetAdjacentAreas( dir );
+ if ( connections )
+ {
+ for ( int conIndex=0; conIndex<connections->Count(); ++conIndex )
+ {
+ const CNavArea *connectedArea = connections->Element( conIndex ).area;
+ if ( connectedArea && connectedArea->HasAttributes( NAV_MESH_STAIRS ) )
+ {
+ isStairNeighbor = true;
+ BlockedEdgeCutoff = 1; // one blocked edge is already too much when we're next to a stair
+ break;
+ }
+ }
+ }
+
+ // are both edged blocked in this direction, and is the obstacle height in this direction the tallest we've seen?
+ if ( (iEdgesBlocked >= BlockedEdgeCutoff ) && ( MAX( obstacleZThisDir[0], obstacleZThisDir[1] ) ) > obstacleZMax )
+ {
+ // this is the tallest obstacle we've encountered so far, remember its details
+ obstacleZ[0] = obstacleZThisDir[0];
+ obstacleZ[1] = obstacleZThisDir[1];
+ obstacleZMax = MAX( obstacleZ[0], obstacleZ[1] );
+ obstacleDir = dir;
+ obstacleStartDist = obstacleStartDistThisDir;
+ obstacleEndDist = obstacleStartDistThisDir;
+ }
+ }
+
+ if ( isStairNeighbor && obstacleZMax > -FLT_MAX )
+ {
+ areasToDelete.AddToTail( area );
+ continue;
+ }
+
+ // if we found an obstacle, raise this nav areas and size it to fit
+ if ( obstacleZMax > -FLT_MAX )
+ {
+ // enforce minimum obstacle width so we don't shrink to become a teensy nav area
+ AdjustObstacleDistances( &obstacleStartDist, &obstacleEndDist, GenerationStepSize );
+ Assert( obstacleEndDist - obstacleStartDist >= MinObstacleAreaWidth );
+
+ // get current corner coords
+ Vector corner[4];
+ for ( int i = NORTH_WEST; i < NUM_CORNERS; i++ )
+ {
+ corner[i] = area->GetCorner( (NavCornerType) i );
+ }
+
+ // adjust our size to fit the obstacle
+ switch ( obstacleDir )
+ {
+ case NORTH:
+ corner[NORTH_WEST].y = corner[SOUTH_WEST].y - obstacleEndDist;
+ corner[NORTH_EAST].y = corner[SOUTH_EAST].y - obstacleEndDist;
+ corner[SOUTH_WEST].y -= obstacleStartDist;
+ corner[SOUTH_EAST].y -= obstacleStartDist;
+ break;
+ case SOUTH:
+ corner[SOUTH_WEST].y = corner[NORTH_WEST].y + obstacleEndDist;
+ corner[SOUTH_EAST].y = corner[NORTH_EAST].y + obstacleEndDist;
+ corner[NORTH_WEST].y += obstacleStartDist;
+ corner[NORTH_EAST].y += obstacleStartDist;
+ ::V_swap( obstacleZ[0], obstacleZ[1] ); // swap left and right Z heights for obstacle so we can run common code below
+ break;
+ case EAST:
+ corner[NORTH_EAST].x = corner[NORTH_WEST].x + obstacleEndDist;
+ corner[SOUTH_EAST].x = corner[SOUTH_WEST].x + obstacleEndDist;
+ corner[NORTH_WEST].x += obstacleStartDist;
+ corner[SOUTH_WEST].x += obstacleStartDist;
+ case WEST:
+ corner[NORTH_WEST].x = corner[NORTH_EAST].x - obstacleEndDist;
+ corner[SOUTH_WEST].x = corner[SOUTH_EAST].x - obstacleEndDist;
+ corner[NORTH_EAST].x -= obstacleStartDist;
+ corner[SOUTH_EAST].x -= obstacleStartDist;
+ ::V_swap( obstacleZ[0], obstacleZ[1] ); // swap left and right Z heights for obstacle so we can run common code below
+ break;
+ }
+
+ // adjust Z positions to be z pos of obstacle top
+ corner[NORTH_WEST].z = obstacleZ[0];
+ corner[NORTH_EAST].z = obstacleZ[1];
+ corner[SOUTH_EAST].z = obstacleZ[1];
+ corner[SOUTH_WEST].z = obstacleZ[0];
+
+ // move the area
+ RemoveNavArea( area );
+ area->Build( corner[NORTH_WEST], corner[NORTH_EAST], corner[SOUTH_EAST], corner[SOUTH_WEST] );
+ Assert( !area->IsDegenerate() );
+ AddNavArea( area );
+
+ // remove side-to-side connections if there are any so AI does try to do things like run along fencetops
+ area->RemoveOrthogonalConnections( obstacleDir );
+ area->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE | NAV_MESH_OBSTACLE_TOP );
+ area->SetAttributes( area->GetAttributes() & ( ~NAV_MESH_JUMP ) );
+ // clear out the nodes associated with this area's corners -- corners don't match the node positions any more
+ for ( int i = 0; i < NUM_CORNERS; i++ )
+ {
+ area->m_node[i] = NULL;
+ }
+ }
+ }
+
+ for ( int i=0; i<areasToDelete.Count(); ++i )
+ {
+ TheNavAreas.FindAndRemove( areasToDelete[i] );
+ DestroyArea( areasToDelete[i] );
+ }
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* For any two nav areas that have an obstacle between them such as a fence, railing or small wall, creates a new
+* nav area on top of the obstacle and connects it between the areas
+*/
+void CNavMesh::CreateObstacleTopAreas()
+{
+ // enumerate all areas
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+
+ // if this is a jump node (which will ultimately get removed) or is an obstacle top, ignore it
+ if ( area->GetAttributes() & ( NAV_MESH_JUMP | NAV_MESH_OBSTACLE_TOP ) )
+ return;
+
+ // Look in all directions
+ for ( int i = NORTH; i < NUM_DIRECTIONS; i++ )
+ {
+ NavDirType dir = (NavDirType) i;
+
+ // Look at all adjacent areas in this direction
+ int iConnections = area->GetAdjacentCount( dir );
+ for ( int j = 0; j < iConnections; j++ )
+ {
+ CNavArea *areaOther = area->GetAdjacentArea( dir, j );
+ // if this is a jump node (which will ultimately get removed) or is an obstacle top, ignore it
+ if ( areaOther->GetAttributes() & ( NAV_MESH_JUMP | NAV_MESH_OBSTACLE_TOP ) )
+ continue;
+
+ // create an obstacle top if there is a one-node separation between the areas and there is an intra-node obstacle within that separation
+ if ( !CreateObstacleTopAreaIfNecessary( area, areaOther, dir, false ) )
+ {
+ // if not, create an obstacle top if there is a two-node separation between the areas and the intermediate node is significantly
+ // higher than the two areas, which means there's some geometry there that causes the middle node to be higher
+ CreateObstacleTopAreaIfNecessary( area, areaOther, dir, true );
+ }
+ }
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Creates a new nav area if an obstacle exists between the two nav areas. If bMultiNode is false, this checks
+* if there's a one-node separation between the areas, and if so if there is an obstacle detected between the nodes.
+* If bMultiNode is true, checks if there is a two-node separation between the areas, and if so if the middle node is
+* higher than the two areas, suggesting an obstacle in the middle.
+*/
+bool CNavMesh::CreateObstacleTopAreaIfNecessary( CNavArea *area, CNavArea *areaOther, NavDirType dir, bool bMultiNode )
+{
+ float obstacleHeightMin = FLT_MAX;
+ float obstacleHeightMax = 0;
+ float obstacleHeightStart = 0;
+ float obstacleHeightEnd = 0;
+ float obstacleDistMin = GenerationStepSize;
+ float obstacleDistMax = 0;
+
+ Vector center;
+ float halfPortalWidth;
+
+ area->ComputePortal( areaOther, dir, &center, &halfPortalWidth );
+
+ if ( halfPortalWidth > 0 )
+ {
+ // get the corners to left and right of direction toward other area
+ NavCornerType cornerStart = (NavCornerType) dir;
+ NavCornerType cornerEnd = (NavCornerType) ( ( dir + 1 ) % NUM_CORNERS );
+ CNavNode *node = area->m_node[cornerStart];
+ CNavNode *nodeEnd = area->m_node[cornerEnd];
+ NavDirType dirEdge = (NavDirType) ( ( dir + 1 ) % NUM_DIRECTIONS );
+ obstacleHeightMin = FLT_MAX;
+ float zStart = 0, zEnd = 0;
+ // along the edge of this area that faces the other area, look at every node that's in the portal between the two
+ while ( node )
+ {
+ Vector vecToPortalCenter = *node->GetPosition() - center;
+ vecToPortalCenter.z = 0;
+ if ( vecToPortalCenter.IsLengthLessThan( halfPortalWidth + 1.0f ) )
+ {
+ // this node is in the portal
+
+ float obstacleHeight = 0;
+ float obstacleDistStartCur = node->m_obstacleStartDist[dir];
+ float obstacleDistEndCur = node->m_obstacleEndDist[dir];
+
+ if ( !bMultiNode )
+ {
+ // use the inter-node obstacle height from this node toward the next area
+ obstacleHeight = node->m_obstacleHeight[dir];
+ }
+ else
+ {
+ if ( !areaOther->Contains( *node->GetPosition() ) )
+ {
+ // step one node toward the other area
+ CNavNode *nodeTowardOtherArea = node->GetConnectedNode( dir );
+ if ( nodeTowardOtherArea )
+ {
+ // see if that step took us upward a significant amount
+ float deltaZ = nodeTowardOtherArea->GetPosition()->z - node->GetPosition()->z;
+ if ( deltaZ > MaxTraversableHeight )
+ {
+ // see if we've arrived in the other area
+ bool bInOtherArea = false;
+ if ( areaOther->Contains( *nodeTowardOtherArea->GetPosition() ) )
+ {
+ float z = areaOther->GetZ( nodeTowardOtherArea->GetPosition()->x, nodeTowardOtherArea->GetPosition()->y );
+ float deltaZ = fabs( nodeTowardOtherArea->GetPosition()->z - z );
+ if ( deltaZ < 2.0f )
+ {
+ bInOtherArea = true;
+ }
+ }
+
+ // if we have not arrived in the other area yet, take one more step in the same direction
+ if ( !bInOtherArea )
+ {
+ CNavNode *nodeTowardOtherArea2 = nodeTowardOtherArea->GetConnectedNode( dir );
+ if ( nodeTowardOtherArea2 && areaOther->Contains( *nodeTowardOtherArea2->GetPosition() ) )
+ {
+ float areaDeltaZ = node->GetPosition()->z - nodeTowardOtherArea2->GetPosition()->z;
+ if ( fabs( areaDeltaZ ) <= MaxTraversableHeight )
+ {
+ // if we arrived in the other area, the obstacle height to get here was the peak deltaZ of the node above to get here
+ obstacleHeight = deltaZ;
+ // make a nav area MinObstacleAreaWidth wide centered on the peak node, which is GenerationStepSize away from where we started
+ obstacleDistStartCur = GenerationStepSize - (MinObstacleAreaWidth / 2);
+ obstacleDistEndCur = GenerationStepSize + (MinObstacleAreaWidth / 2);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ obstacleHeightMin = MIN( obstacleHeight, obstacleHeightMin );
+ obstacleHeightMax = MAX( obstacleHeight, obstacleHeightMax );
+ obstacleDistMin = MIN( obstacleDistStartCur, obstacleDistMin );
+ obstacleDistMax = MAX( obstacleDistEndCur, obstacleDistMax );
+
+ if ( obstacleHeightStart == 0 )
+ {
+ // keep track of the obstacle height and node z pos at the start of the edge
+ obstacleHeightStart = obstacleHeight;
+ zStart = node->GetPosition()->z;
+ }
+ // keep track of the obstacle height and node z pos at the end of the edge
+ obstacleHeightEnd = obstacleHeight;
+ zEnd = node->GetPosition()->z;
+
+ }
+ if ( node == nodeEnd )
+ break;
+
+ node = node->GetConnectedNode( dirEdge );
+ }
+
+
+
+
+ // if we had some obstacle height from EVERY node along the portal, then getting from this area to the other requires scaling an obstacle,
+ // need to generate a nav area on top of it
+ if ( ( obstacleHeightMax > MaxTraversableHeight ) && ( obstacleHeightMin > MaxTraversableHeight ) )
+ {
+ // If the maximum obstacle height was greater than both the height at start and end of the edge, then the obstacle is highest somewhere
+ // in the middle. Use that as the height of both ends.
+ if ( ( obstacleHeightMax > obstacleHeightStart ) && ( obstacleHeightMax > obstacleHeightEnd ) )
+ {
+ obstacleHeightStart = obstacleHeightMax;
+ obstacleHeightEnd = obstacleHeightMax;
+ }
+
+ // for south and west, swap "start" and "end" values of edges so we can use common code below
+ if ( dir == SOUTH || dir == WEST )
+ {
+ ::V_swap( obstacleHeightStart, obstacleHeightEnd );
+ ::V_swap( zStart, zEnd );
+ }
+
+ // Enforce min area width for new area
+ AdjustObstacleDistances( &obstacleDistMin, &obstacleDistMax, bMultiNode ? GenerationStepSize * 2 : GenerationStepSize );
+ Assert( obstacleDistMin < obstacleDistMax );
+ Assert( obstacleDistMax - obstacleDistMin >= MinObstacleAreaWidth );
+ float newAreaWidth = obstacleDistMax - obstacleDistMin;
+ Assert( newAreaWidth > 0 );
+
+ // Calculate new area coordinates
+ AddDirectionVector( &center, dir, obstacleDistMin + (newAreaWidth/2) );
+
+ Vector cornerNW, cornerNE, cornerSE, cornerSW;
+ switch ( dir )
+ {
+ case NORTH:
+ case SOUTH:
+ cornerNW.Init( center.x - halfPortalWidth, center.y - (newAreaWidth/2), zStart + obstacleHeightStart );
+ cornerNE.Init( center.x + halfPortalWidth, center.y - (newAreaWidth/2), zEnd + obstacleHeightEnd );
+ cornerSE.Init( center.x + halfPortalWidth, center.y + (newAreaWidth/2), zEnd + obstacleHeightEnd );
+ cornerSW.Init( center.x - halfPortalWidth, center.y + (newAreaWidth/2), zStart + obstacleHeightStart );
+ break;
+ case EAST:
+ case WEST:
+ cornerNW.Init( center.x - (newAreaWidth/2), center.y - halfPortalWidth, zStart + obstacleHeightStart );
+ cornerNE.Init( center.x + (newAreaWidth/2), center.y - halfPortalWidth, zEnd + obstacleHeightEnd );
+ cornerSE.Init( center.x + (newAreaWidth/2), center.y + halfPortalWidth, zEnd + obstacleHeightEnd );
+ cornerSW.Init( center.x - (newAreaWidth/2), center.y + halfPortalWidth, zStart + obstacleHeightStart );
+ break;
+ }
+
+ CNavArea *areaNew = CreateArea();
+ areaNew->Build( cornerNW, cornerNE, cornerSE, cornerSW );
+
+ // add it to the nav area list
+ TheNavAreas.AddToTail( areaNew );
+ AddNavArea( areaNew );
+
+ Assert( !areaNew->IsDegenerate() );
+
+ Msg( "Created new fencetop area %d(%x) between %d(%x) and %d(%x)\n", areaNew->GetID(), areaNew->GetDebugID(), area->GetID(), area->GetDebugID(), areaOther->GetID(), areaOther->GetDebugID() );
+
+ areaNew->SetAttributes( area->GetAttributes() );
+ areaNew->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE | NAV_MESH_OBSTACLE_TOP );
+
+ area->Disconnect( areaOther );
+ area->ConnectTo( areaNew, dir );
+
+ areaNew->ConnectTo( area, OppositeDirection( dir ) );
+ areaNew->ConnectTo( areaOther, dir );
+ if ( areaOther->IsConnected( area, OppositeDirection( dir ) ) )
+ {
+ areaOther->Disconnect( area );
+ areaOther->ConnectTo( areaNew, OppositeDirection( dir ) );
+ }
+// AddToSelectedSet( areaNew );
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Remove any obstacle top areas which overlap.
+*/
+void CNavMesh::RemoveOverlappingObstacleTopAreas()
+{
+ // What we really want is the union of all obstacle top areas that get generated. That would be hard to compute exactly,
+ // so instead we'll just remove any that overlap. The obstacle top areas don't have to be exact, we just need enough of
+ // them so there is generally a path to get over any obstacle.
+
+ // make a list of just the obstacle top areas to reduce the N of the N squared operation we're about to do
+ CUtlVector<CNavArea *> vecObstacleTopAreas;
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ if ( area->GetAttributes() & NAV_MESH_OBSTACLE_TOP )
+ {
+ vecObstacleTopAreas.AddToTail( area );
+ }
+ }
+
+ // look at every pair of obstacle top areas
+ CUtlVector<CNavArea *> vecAreasToRemove;
+ FOR_EACH_VEC( vecObstacleTopAreas, it )
+ {
+ CNavArea *area = vecObstacleTopAreas[it];
+
+ Vector normal, otherNormal;
+ area->ComputeNormal( &normal );
+ area->ComputeNormal( &otherNormal, true );
+
+ // Remove any obstacle areas that are steep enough to be jump areas
+ float lowestNormalZ = MIN( normal.z, otherNormal.z );
+ if ( lowestNormalZ < nav_slope_limit.GetFloat() )
+ {
+ vecAreasToRemove.AddToTail( area );
+ }
+
+ for ( int it2 = it+1; it2 < vecObstacleTopAreas.Count(); it2++ )
+ {
+ CNavArea *areaOther = vecObstacleTopAreas[it2];
+ if ( area->IsOverlapping( areaOther ) )
+ {
+ if ( area->Contains( areaOther ) )
+ {
+ // if one entirely contains the other, mark the other for removal
+ vecAreasToRemove.AddToTail( areaOther );
+ }
+ else if ( areaOther->Contains( area ) )
+ {
+ // if one entirely contains the other, mark the other for removal
+ vecAreasToRemove.AddToTail( area );
+ }
+ else
+ {
+ // if they overlap without one being a superset of the other, just remove the smaller area
+ CNavArea *areaToRemove = ( area->GetSizeX() * area->GetSizeY() > areaOther->GetSizeX() * areaOther->GetSizeY() ? areaOther : area );
+ vecAreasToRemove.AddToTail( areaToRemove );
+ }
+ }
+ }
+ }
+
+ // now go delete all the areas we want to remove
+ while ( vecAreasToRemove.Count() > 0 )
+ {
+ CNavArea *areaToDelete = vecAreasToRemove[0];
+ RemoveFromSelectedSet( areaToDelete );
+ TheNavMesh->OnEditDestroyNotify( areaToDelete );
+ TheNavAreas.FindAndRemove( areaToDelete );
+ TheNavMesh->DestroyArea( areaToDelete );
+
+ // remove duplicates so we don't double-delete
+ while ( vecAreasToRemove.FindAndRemove( areaToDelete ) );
+ }
+
+}
+
+static void CommandNavCheckStairs( void )
+{
+ TheNavMesh->MarkStairAreas();
+}
+static ConCommand nav_check_stairs( "nav_check_stairs", CommandNavCheckStairs, "Update the nav mesh STAIRS attribute" );
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Mark all areas that are on stairs.
+ */
+void CNavMesh::MarkStairAreas( void )
+{
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ area->TestStairs();
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+enum StairTestType
+{
+ STAIRS_NO,
+ STAIRS_YES,
+ STAIRS_MAYBE,
+};
+
+
+//--------------------------------------------------------------------------------------------------------
+// Test if a line across a nav area could be part of a stairway
+StairTestType IsStairs( const Vector &start, const Vector &end, StairTestType ret )
+{
+ if ( ret == STAIRS_NO )
+ return ret;
+
+ const float inc = 5.0f;
+
+ // the minimum height change each step to be a step and not a slope
+ const float minStepZ = inc * tan( acos( nav_slope_limit.GetFloat() ) );
+ const float MinStairNormal = 0.97f; // we don't care about ramps, just actual flat steps
+
+ float t;
+ Vector pos, normal;
+ float height, priorHeight;
+
+ // walk the line, checking for step height discontinuities
+ float length = start.AsVector2D().DistTo( end.AsVector2D() );
+
+ trace_t trace;
+ CTraceFilterNoNPCsOrPlayer filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT );
+ Vector hullMins( -inc/2, -inc/2, 0 );
+ Vector hullMaxs( inc/2, inc/2, 0 );
+ hullMaxs.z = 1; // don't care about vertical clearance
+
+ if ( fabs( start.x - end.x ) > fabs( start.y - end.y ) )
+ {
+ hullMins.x = -8;
+ hullMaxs.x = 8;
+ }
+ else
+ {
+ hullMins.y = -8;
+ hullMaxs.y = 8;
+ }
+
+ Vector traceOffset( 0, 0, VEC_DUCK_HULL_MAX.z );
+
+ // total height change must exceed a single step to be stairs
+ if ( abs( start.z - end.z ) > StepHeight )
+ {
+ // initialize the height delta
+ UTIL_TraceHull( start + traceOffset, start - traceOffset, hullMins, hullMaxs, MASK_NPCSOLID, &filter, &trace );
+ if ( trace.startsolid || trace.IsDispSurface() )
+ {
+ return STAIRS_NO;
+ }
+ priorHeight = trace.endpos.z;
+
+ // Save a copy for debug overlays
+ Vector prevGround = start;
+ prevGround.z = priorHeight;
+
+ float traceIncrement = inc / length;
+ for( t = 0.0f; t <= 1.0f; t += traceIncrement )
+ {
+ pos = start + t * ( end - start );
+
+ UTIL_TraceHull( pos + traceOffset, pos - traceOffset, hullMins, hullMaxs, MASK_NPCSOLID, &filter, &trace );
+ if ( trace.startsolid || trace.IsDispSurface() )
+ {
+ return STAIRS_NO;
+ }
+ height = trace.endpos.z;
+ normal = trace.plane.normal;
+
+ // Save a copy for debug overlays
+ Vector ground( pos );
+ ground.z = height;
+ //NDebugOverlay::Cross3D( ground, 3, 0, 0, 255, true, 100.0f );
+ //NDebugOverlay::Box( ground, hullMins, hullMaxs, 0, 0, 255, 0.0f, 100.0f );
+
+ if ( t == 0.0f && fabs( height - start.z ) > StepHeight )
+ {
+ // Discontinuity at start
+ return STAIRS_NO;
+ }
+
+ if ( t == 1.0f && fabs( height - end.z ) > StepHeight )
+ {
+ // Discontinuity at end
+ return STAIRS_NO;
+ }
+
+ if ( normal.z < MinStairNormal )
+ {
+ // too steep here
+ return STAIRS_NO;
+ }
+
+
+ float deltaZ = abs( height - priorHeight );
+
+ if ( deltaZ >= minStepZ && deltaZ <= StepHeight )
+ {
+ // found a step
+ ret = STAIRS_YES;
+ }
+ else if ( deltaZ > StepHeight )
+ {
+ // too steep here
+ //NDebugOverlay::Cross3D( ground, 5, 255, 0, 0, true, 10.0f );
+ //NDebugOverlay::Cross3D( prevGround, 5, 0, 255, 0, true, 10.0f );
+ return STAIRS_NO;
+ }
+
+ // Save a copy for debug overlays
+ prevGround = pos;
+ prevGround.z = height;
+
+ priorHeight = height;
+ }
+ }
+
+ return ret;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Test an area for being on stairs
+ * NOTE: This assumes a globally constant "step height",
+ * and walkable surface normal, which really should be locomotor-specific.
+ */
+bool CNavArea::TestStairs( void )
+{
+ // clear STAIRS attribute
+ SetAttributes( GetAttributes() & ~NAV_MESH_STAIRS );
+
+ if ( GetSizeX() <= GenerationStepSize && GetSizeY() <= GenerationStepSize )
+ {
+ // Don't bother with stairs on small areas
+ return false;
+ }
+
+ const float MatchingNormalDot = 0.95f;
+ Vector firstNormal, secondNormal;
+ ComputeNormal( &firstNormal );
+ ComputeNormal( &secondNormal, true );
+ if ( firstNormal.Dot( secondNormal ) < MatchingNormalDot )
+ {
+ // area corners aren't coplanar - no stairs
+ return false;
+ }
+
+ // test center and edges north-to-south, and east-to-west
+ StairTestType ret = STAIRS_MAYBE;
+ Vector from, to;
+
+ const float inset = 5.0f; // inset to keep the tests completely inside the nav area
+
+ from = GetCorner( NORTH_WEST ) + Vector( inset, inset, 0 );
+ to = GetCorner( NORTH_EAST ) + Vector( -inset, inset, 0 );
+ ret = IsStairs( from, to, ret );
+
+ from = GetCorner( SOUTH_WEST ) + Vector( inset, -inset, 0 );
+ to = GetCorner( SOUTH_EAST ) + Vector( -inset, -inset, 0 );
+ ret = IsStairs( from, to, ret );
+
+ from = GetCorner( NORTH_WEST ) + Vector( inset, inset, 0 );
+ to = GetCorner( SOUTH_WEST ) + Vector( inset, -inset, 0 );
+ ret = IsStairs( from, to, ret );
+
+ from = GetCorner( NORTH_EAST ) + Vector( -inset, inset, 0 );
+ to = GetCorner( SOUTH_EAST ) + Vector( -inset, -inset, 0 );
+ ret = IsStairs( from, to, ret );
+
+ from = ( GetCorner( NORTH_WEST ) + GetCorner( NORTH_EAST ) ) / 2.0f + Vector( 0, inset, 0 );
+ to = ( GetCorner( SOUTH_WEST ) + GetCorner( SOUTH_EAST ) ) / 2.0f + Vector( 0, -inset, 0 );
+ ret = IsStairs( from, to, ret );
+
+ from = ( GetCorner( NORTH_EAST ) + GetCorner( SOUTH_EAST ) ) / 2.0f + Vector( -inset, 0, 0 );
+ to = ( GetCorner( NORTH_WEST ) + GetCorner( SOUTH_WEST ) ) / 2.0f + Vector( inset, 0, 0 );
+ ret = IsStairs( from, to, ret );
+
+ if ( ret == STAIRS_YES )
+ {
+ SetAttributes( NAV_MESH_STAIRS );
+ return true;
+ }
+
+ return false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+CON_COMMAND_F( nav_test_stairs, "Test the selected set for being on stairs", FCVAR_CHEAT )
+{
+ int count = 0;
+
+ const NavAreaVector &selectedSet = TheNavMesh->GetSelectedSet();
+ for ( int i=0; i<selectedSet.Count(); ++i )
+ {
+ CNavArea *area = selectedSet[i];
+ if ( area->TestStairs() )
+ {
+ ++count;
+ }
+ }
+
+ Msg( "Marked %d areas as stairs\n", count );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Jump areas aren't used by the NextBot. Delete them, connecting adjacent areas.
+ */
+void CNavMesh::RemoveJumpAreas( void )
+{
+ if ( !nav_generate_fixup_jump_areas.GetBool() )
+ {
+ return;
+ }
+
+ CUtlVector< CNavArea * > unusedAreas;
+
+ int i;
+ for ( i=0; i<TheNavAreas.Count(); ++i )
+ {
+ CNavArea *testArea = TheNavAreas[i];
+ if ( !(testArea->GetAttributes() & NAV_MESH_JUMP) )
+ {
+ continue;
+ }
+
+ unusedAreas.AddToTail( testArea );
+ }
+
+ for ( i=0; i<unusedAreas.Count(); ++i )
+ {
+ CNavArea *areaToDelete = unusedAreas[i];
+ TheNavMesh->OnEditDestroyNotify( areaToDelete );
+ TheNavAreas.FindAndRemove( areaToDelete );
+ TheNavMesh->DestroyArea( areaToDelete );
+ }
+
+ StripNavigationAreas();
+
+ SetMarkedArea( NULL ); // unmark the mark area
+ m_markedCorner = NUM_CORNERS; // clear the corner selection
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+void CNavMesh::CommandNavRemoveJumpAreas( void )
+{
+ JumpConnector connector;
+ ForAllAreas( connector );
+
+ int before = TheNavAreas.Count();
+ RemoveJumpAreas();
+ int after = TheNavAreas.Count();
+
+ Msg( "Removed %d jump areas\n", before - after );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Recursively chop area in half along X until child areas are roughly square
+ */
+static void splitX( CNavArea *area )
+{
+ if (area->IsRoughlySquare())
+ return;
+
+ float split = area->GetSizeX();
+ split /= 2.0f;
+ split += area->GetCorner( NORTH_WEST ).x;
+
+ split = TheNavMesh->SnapToGrid( split );
+
+ const float epsilon = 0.1f;
+ if (fabs(split - area->GetCorner( NORTH_WEST ).x) < epsilon ||
+ fabs(split - area->GetCorner( SOUTH_EAST ).x) < epsilon)
+ {
+ // too small to subdivide
+ return;
+ }
+
+ CNavArea *alpha, *beta;
+ if (area->SplitEdit( false, split, &alpha, &beta ))
+ {
+ // split each new area until square
+ splitX( alpha );
+ splitX( beta );
+ }
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Recursively chop area in half along Y until child areas are roughly square
+ */
+static void splitY( CNavArea *area )
+{
+ if (area->IsRoughlySquare())
+ return;
+
+ float split = area->GetSizeY();
+ split /= 2.0f;
+ split += area->GetCorner( NORTH_WEST ).y;
+
+ split = TheNavMesh->SnapToGrid( split );
+
+ const float epsilon = 0.1f;
+ if (fabs(split - area->GetCorner( NORTH_WEST ).y) < epsilon ||
+ fabs(split - area->GetCorner( SOUTH_EAST ).y) < epsilon)
+ {
+ // too small to subdivide
+ return;
+ }
+
+ CNavArea *alpha, *beta;
+ if (area->SplitEdit( true, split, &alpha, &beta ))
+ {
+ // split each new area until square
+ splitY( alpha );
+ splitY( beta );
+ }
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Split any long, thin, areas into roughly square chunks.
+ */
+void CNavMesh::SquareUpAreas( void )
+{
+ int it = 0;
+
+ while( it < TheNavAreas.Count() )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+
+ // move the iterator in case the current area is split and deleted
+ ++it;
+
+ if (area->HasNodes() && !area->IsRoughlySquare())
+ {
+ // chop this area into square pieces
+ if (area->GetSizeX() > area->GetSizeY())
+ splitX( area );
+ else
+ splitY( area );
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+static bool testStitchConnection( CNavArea *source, CNavArea *target, const Vector &sourcePos, const Vector &targetPos )
+{
+ trace_t result;
+ Vector from( sourcePos );
+ Vector pos( targetPos );
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
+ Vector to, toNormal;
+ bool success = false;
+ if ( TraceAdjacentNode( 0, from, pos, &result ) )
+ {
+ to = result.endpos;
+ toNormal = result.plane.normal;
+ success = true;
+ }
+ else
+ {
+ // test going up ClimbUpHeight
+ bool success = false;
+ for ( float height = StepHeight; height <= ClimbUpHeight; height += 1.0f )
+ {
+ trace_t tr;
+ Vector start( from );
+ Vector end( pos );
+ start.z += height;
+ end.z += height;
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, &tr );
+ if ( !tr.startsolid && tr.fraction == 1.0f )
+ {
+ if ( !StayOnFloor( &tr ) )
+ {
+ break;
+ }
+
+ to = tr.endpos;
+ toNormal = tr.plane.normal;
+
+ start = end = from;
+ end.z += height;
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, &tr );
+ if ( tr.fraction < 1.0f )
+ {
+ break;
+ }
+
+ success = true;
+ break;
+ }
+ }
+ }
+
+ return success;
+}
+
+
+
+
+//--------------------------------------------------------------------------------------------------------
+class IncrementallyGeneratedAreas
+{
+public:
+ bool operator()( CNavArea *area )
+ {
+ return area->HasNodes();
+ }
+};
+
+
+//--------------------------------------------------------------------------------------------------------
+/**
+ * Incremental generation fixup for where edges lap up against the existing nav mesh:
+ * we have nodes, but the surrounding areas don't. So, we trace outward, to see if we
+ * can walk/fall to an adjacent area. This handles dropping down into existing areas etc.
+ * TODO: test pre-existing areas for drop-downs into the newly-generated areas.
+ */
+void CNavMesh::StitchGeneratedAreas( void )
+{
+ if ( m_generationMode == GENERATE_INCREMENTAL )
+ {
+ IncrementallyGeneratedAreas incrementalAreas;
+ StitchMesh( incrementalAreas );
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+class AreaSet
+{
+public:
+ AreaSet( CUtlVector< CNavArea * > *areas )
+ {
+ m_areas = areas;
+ }
+
+ bool operator()( CNavArea *area )
+ {
+ return ( m_areas->HasElement( area ) );
+ }
+
+private:
+ CUtlVector< CNavArea * > *m_areas;
+};
+
+
+//--------------------------------------------------------------------------------------------------------
+/**
+ * Stitches an arbitrary set of areas (newly-merged, for example) into the existing mesh
+ */
+void CNavMesh::StitchAreaSet( CUtlVector< CNavArea * > *areas )
+{
+ AreaSet areaSet( areas );
+ StitchMesh( areaSet );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Determine if we can "jump down" from given point
+ */
+inline bool testJumpDown( const Vector *fromPos, const Vector *toPos )
+{
+ float dz = fromPos->z - toPos->z;
+
+ // drop can't be too far, or too short (or nonexistant)
+ if (dz <= JumpCrouchHeight || dz >= DeathDrop)
+ return false;
+
+ //
+ // Check LOS out and down
+ //
+ // +-----+
+ // | |
+ // F |
+ // |
+ // T
+ //
+
+ Vector from, to;
+ float up;
+ trace_t result;
+
+ // Try to go up and out, up to ClimbUpHeight, to get over obstacles
+ for ( up=1.0f; up<=ClimbUpHeight; up += 1.0f )
+ {
+ from = *fromPos;
+ to.Init( fromPos->x, fromPos->y, fromPos->z + up );
+
+ UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+ if (result.fraction <= 0.0f || result.startsolid)
+ continue;
+
+ from.Init( fromPos->x, fromPos->y, result.endpos.z - 0.5f );
+ to.Init( toPos->x, toPos->y, from.z );
+
+ UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+ if (result.fraction != 1.0f || result.startsolid)
+ continue;
+
+ // Success!
+ break;
+ }
+
+ if ( up > ClimbUpHeight )
+ return false;
+
+ // We've made it up and out, so see if we can drop down
+ from = to;
+ to.z = toPos->z + 2.0f;
+ UTIL_TraceHull( from, to, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+ if (result.fraction <= 0.0f || result.startsolid)
+ return false;
+
+ // Allow a little fudge so we can drop down onto stairs
+ if ( result.endpos.z > to.z + StepHeight )
+ return false;
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+inline CNavArea *findJumpDownArea( const Vector *fromPos, NavDirType dir )
+{
+ Vector start( fromPos->x, fromPos->y, fromPos->z + HalfHumanHeight );
+ AddDirectionVector( &start, dir, GenerationStepSize/2.0f );
+
+ Vector toPos;
+ CNavArea *downArea = findFirstAreaInDirection( &start, dir, 4.0f * GenerationStepSize, DeathDrop, NULL, &toPos );
+
+ if (downArea && testJumpDown( fromPos, &toPos ))
+ return downArea;
+
+ return NULL;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+template < typename Functor >
+void CNavMesh::StitchAreaIntoMesh( CNavArea *area, NavDirType dir, Functor &func )
+{
+ Vector corner1, corner2;
+ switch ( dir )
+ {
+ case NORTH:
+ corner1 = area->GetCorner( NORTH_WEST );
+ corner2 = area->GetCorner( NORTH_EAST );
+ break;
+ case SOUTH:
+ corner1 = area->GetCorner( SOUTH_WEST );
+ corner2 = area->GetCorner( SOUTH_EAST );
+ break;
+ case EAST:
+ corner1 = area->GetCorner( NORTH_EAST );
+ corner2 = area->GetCorner( SOUTH_EAST );
+ break;
+ case WEST:
+ corner1 = area->GetCorner( NORTH_WEST );
+ corner2 = area->GetCorner( SOUTH_WEST );
+ break;
+ }
+
+ Vector edgeDir = corner2 - corner1;
+ edgeDir.z = 0.0f;
+
+ float edgeLength = edgeDir.NormalizeInPlace();
+
+ for ( float n=0; n<edgeLength - 1.0f; n += GenerationStepSize )
+ {
+ Vector sourcePos = corner1 + edgeDir * ( n + 0.5f );
+ sourcePos.z += HalfHumanHeight;
+
+ Vector targetPos = sourcePos;
+ switch ( dir )
+ {
+ case NORTH: targetPos.y -= GenerationStepSize * 0.5f; break;
+ case SOUTH: targetPos.y += GenerationStepSize * 0.5f; break;
+ case EAST: targetPos.x += GenerationStepSize * 0.5f; break;
+ case WEST: targetPos.x -= GenerationStepSize * 0.5f; break;
+ }
+
+ CNavArea *targetArea = TheNavMesh->GetNavArea( targetPos );
+ if ( targetArea && !func( targetArea ) )
+ {
+ targetPos.z = targetArea->GetZ( targetPos.x, targetPos.y ) + HalfHumanHeight;
+
+ // outgoing connection
+ if ( testStitchConnection( area, targetArea, sourcePos, targetPos ) )
+ {
+ area->ConnectTo( targetArea, dir );
+ }
+
+ // incoming connection
+ if ( testStitchConnection( targetArea, area, targetPos, sourcePos ) )
+ {
+ targetArea->ConnectTo( area, OppositeDirection( dir ) );
+ }
+ }
+ else
+ {
+ sourcePos.z -= HalfHumanHeight;
+ sourcePos.z += 1;
+ CNavArea *downArea = findJumpDownArea( &sourcePos, dir );
+ if ( downArea && downArea != area && !func( downArea ) )
+ {
+ area->ConnectTo( downArea, dir );
+ }
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Checks to see if there is a cliff - a drop of at least CliffHeight - in specified direction.
+*/
+inline bool CheckCliff( const Vector *fromPos, NavDirType dir, bool bExhaustive = true )
+{
+ // cliffs are half-baked, not used by any existing AI, and create poorly behaved nav areas (ie: long, thin, strips) (MSB 8/7/09)
+ return false;
+
+
+ Vector toPos( fromPos->x, fromPos->y, fromPos->z );
+ AddDirectionVector( &toPos, dir, GenerationStepSize );
+
+ trace_t trace;
+ // trace a step in specified direction and see where we'd find up
+ if ( TraceAdjacentNode( 0, *fromPos, toPos, &trace, DeathDrop * 10 ) && !trace.allsolid && !trace.startsolid )
+ {
+ float deltaZ = fromPos->z - trace.endpos.z;
+ // would we fall off a cliff?
+ if ( deltaZ > CliffHeight )
+ return true;
+
+ // if not, special case for south and east. South and east edges are not considered part of a nav area, so
+ // we look ahead two steps for south and east. This ensures that the n-1th row and column of nav nodes
+ // on the south and east sides of a nav area reflect any cliffs on the nth row and column.
+
+ // if we're looking to south or east, and the first node we found was approximately flat, and this is the top-level
+ // call, recurse one level to check one more step in this direction
+ if ( ( dir == SOUTH || dir == EAST ) && ( fabs( deltaZ ) < StepHeight ) && bExhaustive )
+ {
+ return CheckCliff( &trace.endpos, dir, false );
+ }
+ }
+ return false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Define connections between adjacent generated areas
+ */
+void CNavMesh::ConnectGeneratedAreas( void )
+{
+ Msg( "Connecting navigation areas...\n" );
+
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+
+ // scan along edge nodes, stepping one node over into the next area
+ // for now, only use bi-directional connections
+
+ // north edge
+ CNavNode *node;
+ for( node = area->m_node[ NORTH_WEST ]; node != area->m_node[ NORTH_EAST ]; node = node->GetConnectedNode( EAST ) )
+ {
+ CNavNode *adj = node->GetConnectedNode( NORTH );
+
+ if (adj && adj->GetArea() && adj->GetConnectedNode( SOUTH ) == node )
+ {
+ area->ConnectTo( adj->GetArea(), NORTH );
+ }
+ else
+ {
+ CNavArea *downArea = findJumpDownArea( node->GetPosition(), NORTH );
+ if (downArea && downArea != area)
+ area->ConnectTo( downArea, NORTH );
+ }
+ }
+
+ // west edge
+ for( node = area->m_node[ NORTH_WEST ]; node != area->m_node[ SOUTH_WEST ]; node = node->GetConnectedNode( SOUTH ) )
+ {
+ CNavNode *adj = node->GetConnectedNode( WEST );
+
+ if (adj && adj->GetArea() && adj->GetConnectedNode( EAST ) == node )
+ {
+ area->ConnectTo( adj->GetArea(), WEST );
+ }
+ else
+ {
+ CNavArea *downArea = findJumpDownArea( node->GetPosition(), WEST );
+ if (downArea && downArea != area)
+ area->ConnectTo( downArea, WEST );
+ }
+ }
+
+ // south edge - this edge's nodes are actually part of adjacent areas
+ // move one node north, and scan west to east
+ /// @todo This allows one-node-wide areas - do we want this?
+ node = area->m_node[ SOUTH_WEST ];
+ if ( node ) // pre-existing areas in incremental generates won't have nodes
+ {
+ node = node->GetConnectedNode( NORTH );
+ }
+ if (node)
+ {
+ CNavNode *end = area->m_node[ SOUTH_EAST ]->GetConnectedNode( NORTH );
+ /// @todo Figure out why cs_backalley gets a NULL node in here...
+ for( ; node && node != end; node = node->GetConnectedNode( EAST ) )
+ {
+ CNavNode *adj = node->GetConnectedNode( SOUTH );
+
+ if (adj && adj->GetArea() && adj->GetConnectedNode( NORTH ) == node )
+ {
+ area->ConnectTo( adj->GetArea(), SOUTH );
+ }
+ else
+ {
+ CNavArea *downArea = findJumpDownArea( node->GetPosition(), SOUTH );
+ if (downArea && downArea != area)
+ area->ConnectTo( downArea, SOUTH );
+ }
+ }
+ }
+
+ // south edge part 2 - scan the actual south edge. If the node is not part of an adjacent area, then it
+ // really belongs to us. This will happen if our area runs right up against a ledge.
+ for( node = area->m_node[ SOUTH_WEST ]; node != area->m_node[ SOUTH_EAST ]; node = node->GetConnectedNode( EAST ) )
+ {
+ if ( node->GetArea() )
+ continue; // some other area owns this node, pay no attention to it
+
+ CNavNode *adj = node->GetConnectedNode( SOUTH );
+
+ if ( node->IsBlockedInAnyDirection() || (adj && adj->IsBlockedInAnyDirection()) )
+ continue; // The space around this node is blocked, so don't connect across it
+
+ // Don't directly connect to adj's area, since it's already 1 cell removed from our area.
+ // There was no area in between, presumably for good reason. Only look for jump down links.
+ if ( !adj || !adj->GetArea() )
+ {
+ CNavArea *downArea = findJumpDownArea( node->GetPosition(), SOUTH );
+ if (downArea && downArea != area)
+ area->ConnectTo( downArea, SOUTH );
+ }
+ }
+
+ // east edge - this edge's nodes are actually part of adjacent areas
+ node = area->m_node[ NORTH_EAST ];
+ if ( node ) // pre-existing areas in incremental generates won't have nodes
+ {
+ node = node->GetConnectedNode( WEST );
+ }
+ if (node)
+ {
+ CNavNode *end = area->m_node[ SOUTH_EAST ]->GetConnectedNode( WEST );
+ for( ; node && node != end; node = node->GetConnectedNode( SOUTH ) )
+ {
+ CNavNode *adj = node->GetConnectedNode( EAST );
+
+ if (adj && adj->GetArea() && adj->GetConnectedNode( WEST ) == node )
+ {
+ area->ConnectTo( adj->GetArea(), EAST );
+ }
+ else
+ {
+ CNavArea *downArea = findJumpDownArea( node->GetPosition(), EAST );
+ if (downArea && downArea != area)
+ area->ConnectTo( downArea, EAST );
+ }
+ }
+ }
+
+ // east edge part 2 - scan the actual east edge. If the node is not part of an adjacent area, then it
+ // really belongs to us. This will happen if our area runs right up against a ledge.
+ for( node = area->m_node[ NORTH_EAST ]; node != area->m_node[ SOUTH_EAST ]; node = node->GetConnectedNode( SOUTH ) )
+ {
+ if ( node->GetArea() )
+ continue; // some other area owns this node, pay no attention to it
+
+ CNavNode *adj = node->GetConnectedNode( EAST );
+
+ if ( node->IsBlockedInAnyDirection() || (adj && adj->IsBlockedInAnyDirection()) )
+ continue; // The space around this node is blocked, so don't connect across it
+
+ // Don't directly connect to adj's area, since it's already 1 cell removed from our area.
+ // There was no area in between, presumably for good reason. Only look for jump down links.
+ if ( !adj || !adj->GetArea() )
+ {
+ CNavArea *downArea = findJumpDownArea( node->GetPosition(), EAST );
+ if (downArea && downArea != area)
+ area->ConnectTo( downArea, EAST );
+ }
+ }
+ }
+
+ StitchGeneratedAreas();
+}
+
+//--------------------------------------------------------------------------------------------------------------
+bool CNavArea::IsAbleToMergeWith( CNavArea *other ) const
+{
+ if ( !HasNodes() || ( GetAttributes() & NAV_MESH_NO_MERGE ) )
+ return false;
+
+ if ( !other->HasNodes() || ( other->GetAttributes() & NAV_MESH_NO_MERGE ) )
+ return false;
+
+ return true;
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Merge areas together to make larger ones (must remain rectangular - convex).
+ * Areas can only be merged if their attributes match.
+ */
+void CNavMesh::MergeGeneratedAreas( void )
+{
+ Msg( "Merging navigation areas...\n" );
+
+ bool merged;
+
+ do
+ {
+ merged = false;
+
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ if ( !area->HasNodes() || ( area->GetAttributes() & NAV_MESH_NO_MERGE ) )
+ continue;
+
+ // north edge
+ FOR_EACH_VEC( area->m_connect[ NORTH ], nit )
+ {
+ CNavArea *adjArea = area->m_connect[ NORTH ][ nit ].area;
+ if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
+ continue;
+
+ if ( area->GetSizeY() + adjArea->GetSizeY() > GenerationStepSize * nav_area_max_size.GetInt() )
+ continue;
+
+ if (area->m_node[ NORTH_WEST ] == adjArea->m_node[ SOUTH_WEST ] &&
+ area->m_node[ NORTH_EAST ] == adjArea->m_node[ SOUTH_EAST ] &&
+ area->GetAttributes() == adjArea->GetAttributes() &&
+ area->IsCoplanar( adjArea ))
+ {
+ // merge vertical
+ area->m_node[ NORTH_WEST ] = adjArea->m_node[ NORTH_WEST ];
+ area->m_node[ NORTH_EAST ] = adjArea->m_node[ NORTH_EAST ];
+
+ merged = true;
+ //CONSOLE_ECHO( " Merged (north) areas #%d and #%d\n", area->m_id, adjArea->m_id );
+
+ area->FinishMerge( adjArea );
+
+ // restart scan - iterator is invalidated
+ break;
+ }
+ }
+
+ if (merged)
+ break;
+
+ // south edge
+ FOR_EACH_VEC( area->m_connect[ SOUTH ], sit )
+ {
+ CNavArea *adjArea = area->m_connect[ SOUTH ][ sit ].area;
+ if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
+ continue;
+
+ if ( area->GetSizeY() + adjArea->GetSizeY() > GenerationStepSize * nav_area_max_size.GetInt() )
+ continue;
+
+ if (adjArea->m_node[ NORTH_WEST ] == area->m_node[ SOUTH_WEST ] &&
+ adjArea->m_node[ NORTH_EAST ] == area->m_node[ SOUTH_EAST ] &&
+ area->GetAttributes() == adjArea->GetAttributes() &&
+ area->IsCoplanar( adjArea ))
+ {
+ // merge vertical
+ area->m_node[ SOUTH_WEST ] = adjArea->m_node[ SOUTH_WEST ];
+ area->m_node[ SOUTH_EAST ] = adjArea->m_node[ SOUTH_EAST ];
+
+ merged = true;
+ //CONSOLE_ECHO( " Merged (south) areas #%d and #%d\n", area->m_id, adjArea->m_id );
+
+ area->FinishMerge( adjArea );
+
+ // restart scan - iterator is invalidated
+ break;
+ }
+
+ }
+
+ if (merged)
+ break;
+
+
+ // west edge
+ FOR_EACH_VEC( area->m_connect[ WEST ], wit )
+ {
+ CNavArea *adjArea = area->m_connect[ WEST ][ wit ].area;
+ if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
+ continue;
+
+ if ( area->GetSizeX() + adjArea->GetSizeX() > GenerationStepSize * nav_area_max_size.GetInt() )
+ continue;
+
+ if (area->m_node[ NORTH_WEST ] == adjArea->m_node[ NORTH_EAST ] &&
+ area->m_node[ SOUTH_WEST ] == adjArea->m_node[ SOUTH_EAST ] &&
+ area->GetAttributes() == adjArea->GetAttributes() &&
+ area->IsCoplanar( adjArea ))
+ {
+ // merge horizontal
+ area->m_node[ NORTH_WEST ] = adjArea->m_node[ NORTH_WEST ];
+ area->m_node[ SOUTH_WEST ] = adjArea->m_node[ SOUTH_WEST ];
+
+ merged = true;
+ //CONSOLE_ECHO( " Merged (west) areas #%d and #%d\n", area->m_id, adjArea->m_id );
+
+ area->FinishMerge( adjArea );
+
+ // restart scan - iterator is invalidated
+ break;
+ }
+
+ }
+
+ if (merged)
+ break;
+
+ // east edge
+ FOR_EACH_VEC( area->m_connect[ EAST ], eit )
+ {
+ CNavArea *adjArea = area->m_connect[ EAST ][ eit ].area;
+ if ( !area->IsAbleToMergeWith( adjArea ) ) // pre-existing areas in incremental generates won't have nodes
+ continue;
+
+ if ( area->GetSizeX() + adjArea->GetSizeX() > GenerationStepSize * nav_area_max_size.GetInt() )
+ continue;
+
+ if (adjArea->m_node[ NORTH_WEST ] == area->m_node[ NORTH_EAST ] &&
+ adjArea->m_node[ SOUTH_WEST ] == area->m_node[ SOUTH_EAST ] &&
+ area->GetAttributes() == adjArea->GetAttributes() &&
+ area->IsCoplanar( adjArea ))
+ {
+ // merge horizontal
+ area->m_node[ NORTH_EAST ] = adjArea->m_node[ NORTH_EAST ];
+ area->m_node[ SOUTH_EAST ] = adjArea->m_node[ SOUTH_EAST ];
+
+ merged = true;
+ //CONSOLE_ECHO( " Merged (east) areas #%d and #%d\n", area->m_id, adjArea->m_id );
+
+ area->FinishMerge( adjArea );
+
+ // restart scan - iterator is invalidated
+ break;
+ }
+ }
+
+ if (merged)
+ break;
+ }
+ }
+ while( merged );
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Given arbitrary corners of a compass grid-aligned rectangle, classify them by compass direction.
+* Input: vec[4]: arbitrary corners
+* Output: vecNW, vecNE, vecSE, vecSW: filled in with which corner is in which compass direction
+*/
+void ClassifyCorners( Vector vec[4], Vector &vecNW, Vector &vecNE, Vector &vecSE, Vector &vecSW )
+{
+ vecNW = vecNE = vecSE = vecSW = vec[0];
+
+ for ( int i = 0; i < 4; i++ )
+ {
+ if ( ( vec[i].x <= vecNW.x ) && ( vec[i].y <= vecNW.y ) )
+ {
+ vecNW = vec[i];
+ }
+ if ( ( vec[i].x >= vecNE.x ) && ( vec[i].y <= vecNE.y ) )
+ {
+ vecNE = vec[i];
+ }
+ if ( ( vec[i].x >= vecSE.x ) && ( vec[i].y >= vecSE.y ) )
+ {
+ vecSE = vec[i];
+ }
+ if ( ( vec[i].x <= vecSW.x ) && ( vec[i].y >= vecSW.y ) )
+ {
+ vecSW = vec[i];
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Perform miscellaneous fixups to generated mesh
+*/
+void CNavMesh::FixUpGeneratedAreas( void )
+{
+ FixCornerOnCornerAreas();
+ FixConnections();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+void CNavMesh::FixConnections( void )
+{
+ // Test the steep sides of stairs for any outgoing links that cross nodes that were partially obstructed.
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ if ( !area->HasAttributes( NAV_MESH_STAIRS ) )
+ continue;
+
+ if ( !area->HasNodes() )
+ continue;
+
+ for ( int dir=0; dir<NUM_DIRECTIONS; ++dir )
+ {
+ NavCornerType cornerType[2];
+ GetCornerTypesInDirection( (NavDirType)dir, &cornerType[0], &cornerType[1] );
+
+ // Flat edges of stairs need to connect. It's the slopes we don't want to climb over things for.
+ float cornerDeltaZ = fabs( area->GetCorner( cornerType[0] ).z - area->GetCorner( cornerType[1] ).z );
+ if ( cornerDeltaZ < StepHeight )
+ continue;
+
+ const NavConnectVector *connectedAreas = area->GetAdjacentAreas( (NavDirType)dir );
+ CUtlVector< CNavArea * > areasToDisconnect;
+ for ( int i=0; i<connectedAreas->Count(); ++i )
+ {
+ CNavArea *adjArea = connectedAreas->Element(i).area;
+ if ( !adjArea->HasNodes() )
+ continue;
+
+ Vector pos, adjPos;
+ float width;
+ area->ComputePortal( adjArea, (NavDirType)dir, &pos, &width );
+ adjArea->GetClosestPointOnArea( pos, &adjPos );
+
+ CNavNode *node = area->FindClosestNode( pos, (NavDirType)dir );
+ CNavNode *adjNode = adjArea->FindClosestNode( adjPos, OppositeDirection( (NavDirType)dir ) );
+ pos = *node->GetPosition();
+ adjPos = *adjNode->GetPosition();
+
+ if ( !node || !adjNode )
+ continue;
+
+ NavCornerType adjCornerType[2];
+ GetCornerTypesInDirection( OppositeDirection((NavDirType)dir), &adjCornerType[0], &adjCornerType[1] );
+
+ // From the stair's perspective, we can't go up more than step height to reach the adjacent area.
+ // Also, if the adjacent area has to jump up higher than StepHeight above the stair area to reach the stairs,
+ // there's an obstruction close to the adjacent area that could prevent walking from the stairs down.
+ if ( node->GetGroundHeightAboveNode( cornerType[0] ) > StepHeight )
+ {
+ areasToDisconnect.AddToTail( adjArea );
+ }
+ else if ( node->GetGroundHeightAboveNode( cornerType[1] ) > StepHeight )
+ {
+ areasToDisconnect.AddToTail( adjArea );
+ }
+ else if ( adjPos.z + adjNode->GetGroundHeightAboveNode( adjCornerType[0] ) > pos.z + StepHeight )
+ {
+ areasToDisconnect.AddToTail( adjArea );
+ }
+ else if ( adjPos.z + adjNode->GetGroundHeightAboveNode( adjCornerType[1] ) > pos.z + StepHeight )
+ {
+ areasToDisconnect.AddToTail( adjArea );
+ }
+ }
+
+ for ( int i=0; i<areasToDisconnect.Count(); ++i )
+ {
+ area->Disconnect( areasToDisconnect[i] );
+ }
+ }
+ }
+
+ // Test to prevent A->C if A->B->C. This can happen in doorways and dropdowns from rooftops.
+ // @TODO: find the root cause of A->C links.
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ CUtlVector< CNavArea * > areasToDisconnect;
+ for ( int dir=0; dir<NUM_DIRECTIONS; ++dir )
+ {
+ const NavConnectVector *connectedAreas = area->GetAdjacentAreas( (NavDirType)dir );
+ for ( int i=0; i<connectedAreas->Count(); ++i )
+ {
+ CNavArea *adjArea = connectedAreas->Element(i).area;
+ const NavConnectVector *adjConnectedAreas = adjArea->GetAdjacentAreas( (NavDirType)dir );
+ for ( int j=0; j<adjConnectedAreas->Count(); ++j )
+ {
+ CNavArea *farArea = adjConnectedAreas->Element(j).area;
+
+ if ( area->IsConnected( farArea, (NavDirType)dir ) )
+ {
+ areasToDisconnect.AddToTail( farArea );
+ }
+ }
+ }
+ }
+
+ for ( int i=0; i<areasToDisconnect.Count(); ++i )
+ {
+ area->Disconnect( areasToDisconnect[i] );
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Fix any spots where we there are nav nodes touching only corner-on-corner but we intend bots to be able to traverse
+*/
+void CNavMesh::FixCornerOnCornerAreas( void )
+{
+ const float MaxDrop = StepHeight; // don't make corner on corner areas that are too steep
+
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+
+ // determine if we have any corners where the only nav area we touch is diagonally corner-to-corner.
+ // if there are, generate additional small (0.5 x 0.5 grid size) nav areas in the corners between
+ // them if map geometry allows and make connections in cardinal compass directions to create a path
+ // between the two areas.
+
+//
+// XXXXXXXXX XXXXXXXXX
+// X X X X
+// X other X ****X other X
+// X X *newX X
+// XXXXXXXXXXXXXXXXX => XXXXXXXXXXXXXXXXX
+// X X X Xnew*
+// X area X X area X****
+// X X X X
+// XXXXXXXXX XXXXXXXXX
+//
+
+ // check each corner
+ for ( int iCorner = NORTH_WEST; iCorner < NUM_CORNERS; iCorner++ )
+ {
+ // get cardinal direction to right and left of this corner
+ NavDirType dirToRight = (NavDirType) iCorner;
+ NavDirType dirToLeft = (NavDirType) ( ( iCorner+3 ) % NUM_DIRECTIONS );
+
+ // if we have any connections on cardinal compass directions on edge on either side of corner we're OK, skip this nav area
+ if ( area->GetAdjacentCount( dirToLeft ) > 0 || area->GetAdjacentCount( dirToRight ) > 0 ||
+ area->GetIncomingConnections( dirToLeft )->Count() > 0 || area->GetIncomingConnections( dirToRight )->Count() > 0 )
+ continue;
+
+ Vector cornerPos = area->GetCorner( (NavCornerType) iCorner );
+ NavDirType dirToRightTwice = DirectionRight( dirToRight );
+ NavDirType dirToLeftTwice = DirectionLeft( dirToLeft );
+ NavDirType dirsAlongOtherEdge[2] = { dirToLeft, dirToRight };
+ NavDirType dirsAlongOurEdge[2] = { dirToLeftTwice, dirToRightTwice };
+
+ // consider 2 potential new nav areas, to left and right of the corner we're considering
+ for ( int iDir = 0; iDir < ARRAYSIZE( dirsAlongOtherEdge ); iDir++ )
+ {
+ NavDirType dirAlongOtherEdge = dirsAlongOtherEdge[iDir];
+ NavDirType dirAlongOurEdge = dirsAlongOurEdge[iDir];
+
+ // look at the point 0.5 grid units along edge of other nav area
+ Vector vecDeltaOtherEdge;
+ DirectionToVector2D( dirAlongOtherEdge, (Vector2D *) &vecDeltaOtherEdge );
+ vecDeltaOtherEdge.z = 0;
+ vecDeltaOtherEdge *= GenerationStepSize * 0.5;
+ Vector vecOtherEdgePos = cornerPos + vecDeltaOtherEdge;
+
+ // see if there is a nav area at that location
+ CNavArea *areaOther = GetNavArea( vecOtherEdgePos );
+ Assert( areaOther != area );
+ if ( !areaOther )
+ continue; // no other area in that location, we're not touching on corner
+
+ // see if we can move from our corner in that direction
+ trace_t result;
+ if ( !TraceAdjacentNode( 0, cornerPos, vecOtherEdgePos, &result, MaxDrop ) )
+ continue; // something is blocking movement, don't create additional nodes to aid movement
+
+ // get the corner of the other nav area that might touch our corner
+ int iCornerOther = ( ( iCorner + 2 ) % NUM_CORNERS );
+ Vector cornerPosOther = areaOther->GetCorner( (NavCornerType) iCornerOther );
+
+ if ( cornerPos != cornerPosOther )
+ continue; // that nav area does not touch us on corner
+
+ // we are touching corner-to-corner with the other nav area and don't have connections in cardinal directions around
+ // the corner that touches, this is a candidate to generate new small helper nav areas.
+
+ // calculate the corners of the 0.5 x 0.5 nav area we would consider building between us and the other nav area whose corner we touch
+ Vector vecDeltaOurEdge;
+ DirectionToVector2D( dirAlongOurEdge, (Vector2D *) &vecDeltaOurEdge );
+ vecDeltaOurEdge.z = 0;
+ vecDeltaOurEdge *= GenerationStepSize * 0.5;
+ Vector vecOurEdgePos = cornerPos + vecDeltaOurEdge;
+ Vector vecCorner[4];
+ vecCorner[0] = cornerPos + vecDeltaOtherEdge + vecDeltaOurEdge; // far corner of new nav area
+ vecCorner[1] = cornerPos + vecDeltaOtherEdge; // intersection of far edge of new nav area with other nav area we touch
+ vecCorner[2] = cornerPos; // common corner of this nav area, nav area we touch, and new nav area
+ vecCorner[3] = cornerPos + vecDeltaOurEdge; // intersection of far edge of new nav area with this nav area
+
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
+ if ( !TraceAdjacentNode( 0, vecCorner[1], vecCorner[0], &result, MaxDrop ) || // can we move from edge of other area to far corner of new node
+ !TraceAdjacentNode( 0, vecCorner[3], vecCorner[0], &result, MaxDrop ) ) // can we move from edge of this area to far corner of new node
+ continue; // new node would not fit
+
+ // as sanity check, make sure there's not already a nav area there, shouldn't be
+ CNavArea *areaTest = GetNavArea( vecCorner[0] );
+ Assert ( !areaTest );
+ if ( areaTest )
+ continue;
+
+ vecCorner[0] = result.endpos;
+
+ // create a new nav area
+ CNavArea *areaNew = CreateArea();
+
+ // arrange the corners of the new nav area by compass direction
+ Vector vecNW, vecNE, vecSE, vecSW;
+ ClassifyCorners( vecCorner, vecNW, vecNE, vecSE, vecSW );
+ areaNew->Build( vecNW, vecNE, vecSE, vecSW );
+
+ // add it to the nav area list
+ TheNavAreas.AddToTail( areaNew );
+ AddNavArea( areaNew );
+
+ areaNew->SetAttributes( area->GetAttributes() );
+
+ // reciprocally connect between this area and new area
+ area->ConnectTo( areaNew, dirAlongOtherEdge );
+ areaNew->ConnectTo( area, OppositeDirection( dirAlongOtherEdge ) );
+
+ // reciprocally connect between other area and new area
+ areaOther->ConnectTo( areaNew, dirAlongOurEdge );
+ areaNew->ConnectTo( areaOther, OppositeDirection( dirAlongOurEdge ) );
+ }
+ }
+ }
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Fix any areas where one nav area overhangs another and the two nav areas are connected. Subdivide the lower
+* nav area such that the upper nav area doesn't overhang any area it's connected to.
+*/
+void CNavMesh::SplitAreasUnderOverhangs( void )
+{
+ // restart the whole process whenever this gets set to true
+ bool bRestartProcessing = false;
+
+ do
+ {
+ bRestartProcessing = false;
+
+ // iterate all nav areas
+ for ( int it = 0; it < TheNavAreas.Count() && !bRestartProcessing; it++ )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ Extent areaExtent;
+ area->GetExtent( &areaExtent );
+
+ // iterate all directions
+ for ( int dir = NORTH; dir < NUM_DIRECTIONS && !bRestartProcessing; dir++ )
+ {
+ // iterate all connections in that direction
+ const NavConnectVector *pConnections = area->GetAdjacentAreas( (NavDirType) dir );
+ for ( int iConnection = 0; iConnection < pConnections->Count() && !bRestartProcessing; iConnection++ )
+ {
+ CNavArea *otherArea = (*pConnections)[iConnection].area;
+ Extent otherAreaExtent;
+ otherArea->GetExtent( &otherAreaExtent );
+
+ // see if the area we are connected to overlaps our X/Y extents
+ if ( area->IsOverlapping( otherArea ) )
+ {
+ // if the upper area isn't at least crouch height above the lower area, this is some weird minor
+ // overlap, disregard it
+ const float flMinSeparation = HumanCrouchHeight;
+ if ( !( areaExtent.lo.z > otherAreaExtent.hi.z + flMinSeparation ) &&
+ !( otherAreaExtent.lo.z > areaExtent.hi.z + flMinSeparation ) )
+ continue;
+
+ // figure out which area is above and which is below
+ CNavArea *areaBelow = area, *areaAbove = otherArea;
+ NavDirType dirFromAboveToBelow = OppositeDirection( (NavDirType) dir );
+ if ( otherAreaExtent.lo.z < areaExtent.lo.z )
+ {
+ areaBelow = otherArea;
+ areaAbove = area;
+ dirFromAboveToBelow = OppositeDirection( dirFromAboveToBelow );
+ }
+ NavDirType dirFromBelowToAbove = OppositeDirection( dirFromAboveToBelow );
+
+ // Msg( "area %d overhangs area %d and is connected\n", areaAbove->GetID(), areaBelow->GetID() );
+
+ Extent extentBelow, extentAbove;
+ areaBelow->GetExtent( &extentBelow );
+ areaAbove->GetExtent( &extentAbove );
+
+ float splitCoord; // absolute world coordinate along which we will split lower nav area (X or Y, depending on axis we split on)
+ float splitLen; // length of the segment of lower nav area that is in shadow of the upper nav area
+ float splitEdgeSize; // current length of the edge of nav area that is getting split
+ bool bSplitAlongX = false;
+
+ // determine along what edge we are splitting and make some key measurements
+ if ( ( dirFromAboveToBelow == EAST ) || ( dirFromAboveToBelow == WEST ) )
+ {
+ splitEdgeSize = extentBelow.hi.x - extentBelow.lo.x;
+ if ( extentAbove.hi.x < extentBelow.hi.x )
+ {
+ splitCoord = extentAbove.hi.x;
+ splitLen = splitCoord - extentBelow.lo.x;
+ }
+ else
+ {
+ splitCoord = extentAbove.lo.x;
+ splitLen = extentBelow.hi.x - splitCoord;
+ }
+ }
+ else
+ {
+ splitEdgeSize = extentBelow.hi.y - extentBelow.lo.y;
+ bSplitAlongX = true;
+ if ( extentAbove.hi.y < extentBelow.hi.y )
+ {
+ splitCoord = extentAbove.hi.y;
+ splitLen = splitCoord - extentBelow.lo.y;
+ }
+ else
+ {
+ splitCoord = extentAbove.lo.y;
+ splitLen = extentBelow.hi.y - splitCoord;
+ }
+ }
+ Assert( splitLen >= 0 );
+ Assert( splitEdgeSize > 0 );
+
+ // if we split the lower nav area right where it's in shadow of the upper nav area, will it create a really tiny strip?
+ if ( splitLen < GenerationStepSize )
+ {
+ // if the "in shadow" part of the lower nav area is really small or the lower nav area is really small to begin with,
+ // don't split it, we're better off as is
+ if ( ( splitLen < GenerationStepSize*0.3 ) || ( splitEdgeSize <= GenerationStepSize * 2 ) )
+ continue;
+
+ // Move our split point so we don't create a really tiny strip on the lower nav area. Move the split point away from
+ // the upper nav area so the "in shadow" area expands to be GenerationStepSize. The checks above ensure we have room to do this.
+ float splitDelta = GenerationStepSize - splitLen;
+ splitCoord += splitDelta * ( ( ( dirFromAboveToBelow == NORTH ) || ( dirFromAboveToBelow == WEST ) ) ? -1 : 1 );
+ }
+
+ // remove any connections between the two areas (so they don't get inherited by the new areas when we split the lower area),
+ // but remember what the connections were.
+ bool bConnectionFromBelow = false, bConnectionFromAbove = false;
+ if ( areaBelow->IsConnected( areaAbove, dirFromBelowToAbove ) )
+ {
+ bConnectionFromBelow = true;
+ areaBelow->Disconnect( areaAbove );
+ }
+ if ( areaAbove->IsConnected( areaBelow, dirFromAboveToBelow ) )
+ {
+ bConnectionFromAbove = true;
+ areaAbove->Disconnect( areaBelow );
+ }
+
+ CNavArea *pNewAlpha = NULL,*pNewBeta = NULL;
+// int idBelow = areaBelow->GetID();
+// AddToSelectedSet( areaBelow );
+ // split the lower nav area
+ if ( areaBelow->SplitEdit( bSplitAlongX, splitCoord, &pNewAlpha, &pNewBeta ) )
+ {
+// Msg( "Split area %d into %d and %d\n", idBelow, pNewAlpha->GetID(), pNewBeta->GetID() );
+
+ // determine which of the two new lower areas is the one *not* in shadow of the upper nav area. This is the one we want to
+ // reconnect to
+ CNavArea *pNewNonoverlappedArea = ( ( dirFromAboveToBelow == NORTH ) || ( dirFromAboveToBelow == WEST ) ) ? pNewAlpha : pNewBeta;
+
+ // restore the previous connections from the upper nav area to the new lower nav area that is not in shadow of the upper
+ if ( bConnectionFromAbove )
+ {
+ areaAbove->ConnectTo( pNewNonoverlappedArea, dirFromAboveToBelow );
+ }
+ if ( bConnectionFromBelow )
+ {
+ areaBelow->ConnectTo( pNewNonoverlappedArea, OppositeDirection( dirFromAboveToBelow ) );
+ }
+
+ // Now we need to just start the whole process over. We've just perturbed the list we're iterating on (removed a nav area, added two
+ // new ones, when we did the split), and it's possible we may have to subdivide a lower nav area twice if the upper nav area
+ // overhangs a corner of the lower area. We just start all over again each time we do a split until no more overhangs occur.
+ bRestartProcessing = true;
+ }
+ else
+ {
+// Msg( "Failed to split area %d\n", idBelow );
+ }
+ }
+ }
+ }
+ }
+ }
+ while ( bRestartProcessing );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+bool TestForValidCrouchArea( CNavNode *node )
+{
+ // must make sure we don't have a bogus crouch area. check up to JumpCrouchHeight above
+ // the node for a HumanCrouchHeight space.
+
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_EVERYTHING );
+ trace_t tr;
+ Vector start( *node->GetPosition() );
+ Vector end( *node->GetPosition() );
+ end.z += JumpCrouchHeight;
+
+ Vector mins( 0, 0, 0 );
+ Vector maxs( GenerationStepSize, GenerationStepSize, HumanCrouchHeight );
+
+ UTIL_TraceHull(
+ start,
+ end,
+ mins,
+ maxs,
+ TheNavMesh->GetGenerationTraceMask(),
+ &filter,
+ &tr );
+
+ return ( !tr.allsolid );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Make sure that if other* are similar, test is also close. Used in TestForValidJumpArea.
+ */
+bool IsHeightDifferenceValid( float test, float other1, float other2, float other3 )
+{
+ // Make sure the other nodes are level.
+ const float CloseDelta = StepHeight / 2;
+ if ( fabs( other1 - other2 ) > CloseDelta )
+ return true;
+
+ if ( fabs( other1 - other3 ) > CloseDelta )
+ return true;
+
+ if ( fabs( other2 - other3 ) > CloseDelta )
+ return true;
+
+ // Now make sure the test node is near the others. If it is more than StepHeight away,
+ // it'll form a distorted jump area.
+ const float MaxDelta = StepHeight;
+ if ( fabs( test - other1 ) > MaxDelta )
+ return false;
+
+ if ( fabs( test - other2 ) > MaxDelta )
+ return false;
+
+ if ( fabs( test - other3 ) > MaxDelta )
+ return false;
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Check that a 1x1 area with 'node' at the northwest corner has a valid shape - if 3 corners
+ * are flat, and the 4th is significantly higher or lower, it would form a jump area that bots
+ * can't navigate over well.
+ */
+bool TestForValidJumpArea( CNavNode *node )
+{
+ return true;
+
+ CNavNode *east = node->GetConnectedNode( EAST );
+ CNavNode *south = node->GetConnectedNode( SOUTH );
+ if ( !east || !south )
+ return false;
+
+ CNavNode *southEast = east->GetConnectedNode( SOUTH );
+ if ( !southEast )
+ return false;
+
+ if ( !IsHeightDifferenceValid(
+ node->GetPosition()->z,
+ south->GetPosition()->z,
+ southEast->GetPosition()->z,
+ east->GetPosition()->z ) )
+ return false;
+
+ if ( !IsHeightDifferenceValid(
+ south->GetPosition()->z,
+ node->GetPosition()->z,
+ southEast->GetPosition()->z,
+ east->GetPosition()->z ) )
+ return false;
+
+ if ( !IsHeightDifferenceValid(
+ southEast->GetPosition()->z,
+ south->GetPosition()->z,
+ node->GetPosition()->z,
+ east->GetPosition()->z ) )
+ return false;
+
+ if ( !IsHeightDifferenceValid(
+ east->GetPosition()->z,
+ south->GetPosition()->z,
+ southEast->GetPosition()->z,
+ node->GetPosition()->z ) )
+ return false;
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+class TestOverlapping
+{
+ Vector m_nw;
+ Vector m_ne;
+ Vector m_sw;
+ Vector m_se;
+public:
+ TestOverlapping( const Vector &nw, const Vector &ne, const Vector &sw, const Vector &se ) :
+ m_nw( nw ), m_ne( ne ), m_sw( sw ), m_se( se )
+ {
+ }
+
+ // This approximates CNavArea::GetZ, so we can pretend our four corners delineate a nav area
+ float GetZ( const Vector &pos ) const
+ {
+ float dx = m_se.x - m_nw.x;
+ float dy = m_se.y - m_nw.y;
+
+ // guard against division by zero due to degenerate areas
+ if (dx == 0.0f || dy == 0.0f)
+ return m_ne.z;
+
+ float u = (pos.x - m_nw.x) / dx;
+ float v = (pos.y - m_nw.y) / dy;
+
+ // clamp Z values to (x,y) volume
+ if (u < 0.0f)
+ u = 0.0f;
+ else if (u > 1.0f)
+ u = 1.0f;
+
+ if (v < 0.0f)
+ v = 0.0f;
+ else if (v > 1.0f)
+ v = 1.0f;
+
+ float northZ = m_nw.z + u * (m_ne.z - m_nw.z);
+ float southZ = m_sw.z + u * (m_se.z - m_sw.z);
+
+ return northZ + v * (southZ - northZ);
+ }
+
+ bool OverlapsExistingArea( void )
+ {
+ CNavArea *overlappingArea = NULL;
+ CNavLadder *overlappingLadder = NULL;
+
+ Vector nw = m_nw;
+ Vector se = m_se;
+ Vector start = nw;
+ start.x += GenerationStepSize/2;
+ start.y += GenerationStepSize/2;
+
+ while ( start.x < se.x )
+ {
+ start.y = nw.y + GenerationStepSize/2;
+ while ( start.y < se.y )
+ {
+ start.z = GetZ( start );
+ Vector end = start;
+ start.z -= StepHeight;
+ end.z += HalfHumanHeight;
+
+ if ( TheNavMesh->FindNavAreaOrLadderAlongRay( start, end, &overlappingArea, &overlappingLadder, NULL ) )
+ {
+ if ( overlappingArea )
+ {
+ return true;
+ }
+ }
+
+ start.y += GenerationStepSize;
+ }
+ start.x += GenerationStepSize;
+ }
+ return false;
+ }
+};
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Check if an rectangular area of the given size can be
+ * made starting from the given node as the NW corner.
+ * Only consider fully connected nodes for this check.
+ * All of the nodes within the test area must have the same attributes.
+ * All of the nodes must be approximately co-planar w.r.t the NW node's normal, with the
+ * exception of 1x1 areas which can be any angle.
+ */
+bool CNavMesh::TestArea( CNavNode *node, int width, int height )
+{
+ Vector normal = *node->GetNormal();
+ float d = -DotProduct( normal, *node->GetPosition() );
+
+ bool nodeCrouch = node->m_crouch[ SOUTH_EAST ];
+
+ // The area's interior will be the south-east side of this north-west node.
+ // If that interior space is blocked, there's no space to build an area.
+ if ( node->m_isBlocked[ SOUTH_EAST ] )
+ {
+ return false;
+ }
+
+ int nodeAttributes = node->GetAttributes() & ~NAV_MESH_CROUCH;
+
+ const float offPlaneTolerance = 5.0f;
+
+ CNavNode *vertNode, *horizNode;
+
+ vertNode = node;
+ int x,y;
+ for( y=0; y<height; y++ )
+ {
+ horizNode = vertNode;
+
+ for( x=0; x<width; x++ )
+ {
+ //
+ // Compute the crouch attributes for the test node, taking into account only the side(s) of the node
+ // that are in the area
+
+ // NOTE: The nodes on the south and east borders of an area aren't contained in the area. This means that
+ // crouch attributes and blocked state need to be checked to the south and east of the southEdge and eastEdge nodes.
+
+ bool horizNodeCrouch = false;
+ bool westEdge = (x == 0);
+ bool eastEdge = (x == width - 1);
+ bool northEdge = (y == 0);
+ bool southEdge = (y == height - 1);
+
+ // Check corners first
+ if ( northEdge && westEdge )
+ {
+ // The area's interior will be the south-east side of this north-west node.
+ // If that interior space is blocked, there's no space to build an area.
+ horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ];
+ if ( horizNode->m_isBlocked[ SOUTH_EAST ] )
+ {
+ return false;
+ }
+ }
+ else if ( northEdge && eastEdge )
+ {
+ // interior space of the area extends one more cell to the east past the easternmost nodes.
+ // This means we need to check to the southeast as well as the southwest.
+ horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ SOUTH_WEST ];
+ if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ SOUTH_WEST ] )
+ {
+ return false;
+ }
+ }
+ else if ( southEdge && westEdge )
+ {
+ // The interior space of the area extends one more cell to the south past the southernmost nodes.
+ // This means we need to check to the southeast as well as the southwest.
+ horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ NORTH_EAST ];
+ if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ NORTH_EAST ] )
+ {
+ return false;
+ }
+ }
+ else if ( southEdge && eastEdge )
+ {
+ // This node is completely in the interior of the area, so we need to check in all directions.
+ horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
+ if ( horizNode->IsBlockedInAnyDirection() )
+ {
+ return false;
+ }
+ }
+ // check sides next
+ else if ( northEdge )
+ {
+ horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ SOUTH_WEST ];
+ if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ SOUTH_WEST ] )
+ {
+ return false;
+ }
+ }
+ else if ( southEdge )
+ {
+ // This node is completely in the interior of the area, so we need to check in all directions.
+ horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
+ if ( horizNode->IsBlockedInAnyDirection() )
+ {
+ return false;
+ }
+ }
+ else if ( eastEdge )
+ {
+ // This node is completely in the interior of the area, so we need to check in all directions.
+ horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
+ if ( horizNode->IsBlockedInAnyDirection() )
+ {
+ return false;
+ }
+ }
+ else if ( westEdge )
+ {
+ horizNodeCrouch = horizNode->m_crouch[ SOUTH_EAST ] || horizNode->m_crouch[ NORTH_EAST ];
+ if ( horizNode->m_isBlocked[ SOUTH_EAST ] || horizNode->m_isBlocked[ NORTH_EAST ] )
+ {
+ return false;
+ }
+ }
+ // finally, we have a center node
+ else
+ {
+ // This node is completely in the interior of the area, so we need to check in all directions.
+ horizNodeCrouch = (horizNode->GetAttributes() & NAV_MESH_CROUCH) != 0;
+ if ( horizNode->IsBlockedInAnyDirection() )
+ {
+ return false;
+ }
+ }
+
+ // all nodes must be crouch/non-crouch
+ if ( nodeCrouch != horizNodeCrouch )
+ return false;
+
+ // all nodes must have the same non-crouch attributes
+ int horizNodeAttributes = horizNode->GetAttributes() & ~NAV_MESH_CROUCH;
+ if (horizNodeAttributes != nodeAttributes)
+ return false;
+
+ if (horizNode->IsCovered())
+ return false;
+
+ if (!horizNode->IsClosedCell())
+ return false;
+
+ if ( !CheckObstacles( horizNode, width, height, x, y ) )
+ return false;
+
+ horizNode = horizNode->GetConnectedNode( EAST );
+ if (horizNode == NULL)
+ return false;
+
+ // nodes must lie on/near the plane
+ if (width > 1 || height > 1)
+ {
+ float dist = (float)fabs( DotProduct( *horizNode->GetPosition(), normal ) + d );
+ if (dist > offPlaneTolerance)
+ return false;
+ }
+ }
+
+ // Check the final (x=width) node, the above only checks thru x=width-1
+ if ( !CheckObstacles( horizNode, width, height, x, y ) )
+ return false;
+
+ vertNode = vertNode->GetConnectedNode( SOUTH );
+ if (vertNode == NULL)
+ return false;
+
+ // nodes must lie on/near the plane
+ if (width > 1 || height > 1)
+ {
+ float dist = (float)fabs( DotProduct( *vertNode->GetPosition(), normal ) + d );
+ if (dist > offPlaneTolerance)
+ return false;
+ }
+ }
+
+ // check planarity of southern edge
+ if (width > 1 || height > 1)
+ {
+ horizNode = vertNode;
+
+ for( x=0; x<width; x++ )
+ {
+ if ( !CheckObstacles( horizNode, width, height, x, y ) )
+ return false;
+
+ horizNode = horizNode->GetConnectedNode( EAST );
+ if (horizNode == NULL)
+ return false;
+
+ // nodes must lie on/near the plane
+ float dist = (float)fabs( DotProduct( *horizNode->GetPosition(), normal ) + d );
+ if (dist > offPlaneTolerance)
+ return false;
+ }
+
+ // Check the final (x=width) node, the above only checks thru x=width-1
+ if ( !CheckObstacles( horizNode, width, height, x, y ) )
+ return false;
+ }
+
+ vertNode = node;
+ for( y=0; y<height; ++y )
+ {
+ horizNode = vertNode;
+
+ for( int x=0; x<width; ++x )
+ {
+ // look for odd jump areas (3 points on the ground, 1 point floating much higher or lower)
+ if ( !TestForValidJumpArea( horizNode ) )
+ {
+ return false;
+ }
+
+ // Now that we've done the quick checks, test for a valid crouch area.
+ // This finds pillars etc in the middle of 4 nodes, that weren't found initially.
+ if ( nodeCrouch && !TestForValidCrouchArea( horizNode ) )
+ {
+ return false;
+ }
+
+ horizNode = horizNode->GetConnectedNode( EAST );
+ }
+
+ vertNode = vertNode->GetConnectedNode( SOUTH );
+ }
+
+ if ( m_generationMode == GENERATE_INCREMENTAL )
+ {
+ // Incremental generation needs to check that it's not overlapping existing areas...
+ const Vector *nw = node->GetPosition();
+
+ vertNode = node;
+ for( int y=0; y<height; ++y )
+ {
+ vertNode = vertNode->GetConnectedNode( SOUTH );
+ }
+ const Vector *sw = vertNode->GetPosition();
+
+ horizNode = node;
+ for( int x=0; x<width; ++x )
+ {
+ horizNode = horizNode->GetConnectedNode( EAST );
+ }
+ const Vector *ne = horizNode->GetPosition();
+
+ vertNode = horizNode;
+ for( int y=0; y<height; ++y )
+ {
+ vertNode = vertNode->GetConnectedNode( SOUTH );
+ }
+ const Vector *se = vertNode->GetPosition();
+
+ TestOverlapping test( *nw, *ne, *sw, *se );
+ if ( test.OverlapsExistingArea() )
+ return false;
+ }
+
+ return true;
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Checks if a node has an untraversable obstacle in any direction to a neighbor.
+* width and height are size of nav area this node would be a part of, x and y are node's position
+* within that grid
+*/
+bool CNavMesh::CheckObstacles( CNavNode *node, int width, int height, int x, int y )
+{
+ // any area bigger than 1x1 can't have obstacles in any connection between nodes
+ if ( width > 1 || height > 1 )
+ {
+ if ( ( x > 0 ) && ( node->m_obstacleHeight[WEST] > MaxTraversableHeight ) )
+ return false;
+
+ if ( ( y > 0 ) && ( node->m_obstacleHeight[NORTH] > MaxTraversableHeight ) )
+ return false;
+
+ if ( ( x < width-1 ) && ( node->m_obstacleHeight[EAST] > MaxTraversableHeight ) )
+ return false;
+
+ if ( ( y < height-1 ) && ( node->m_obstacleHeight[SOUTH] > MaxTraversableHeight ) )
+ return false;
+ }
+
+ // 1x1 area can have obstacles, that area will get fixed up later
+ return true;
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Create a nav area, and mark all nodes it overlaps as "covered"
+ * NOTE: Nodes on the east and south edges are not included.
+ * Returns number of nodes covered by this area, or -1 for error;
+ */
+int CNavMesh::BuildArea( CNavNode *node, int width, int height )
+{
+ CNavNode *nwNode = node;
+ CNavNode *neNode = NULL;
+ CNavNode *swNode = NULL;
+ CNavNode *seNode = NULL;
+
+ CNavNode *vertNode = node;
+ CNavNode *horizNode;
+
+ int coveredNodes = 0;
+
+ for( int y=0; y<height; y++ )
+ {
+ horizNode = vertNode;
+
+ for( int x=0; x<width; x++ )
+ {
+ horizNode->Cover();
+ ++coveredNodes;
+
+ horizNode = horizNode->GetConnectedNode( EAST );
+ }
+
+ if (y == 0)
+ neNode = horizNode;
+
+ vertNode = vertNode->GetConnectedNode( SOUTH );
+ }
+
+ swNode = vertNode;
+
+ horizNode = vertNode;
+ for( int x=0; x<width; x++ )
+ {
+ horizNode = horizNode->GetConnectedNode( EAST );
+ }
+ seNode = horizNode;
+
+ if (!nwNode || !neNode || !seNode || !swNode)
+ {
+ Error( "BuildArea - NULL node.\n" );
+ return -1;
+ }
+
+ CNavArea *area = CreateArea();
+ if (area == NULL)
+ {
+ Error( "BuildArea: Out of memory.\n" );
+ return -1;
+ }
+
+ area->Build( nwNode, neNode, seNode, swNode );
+
+ TheNavAreas.AddToTail( area );
+ // since all internal nodes have the same attributes, set this area's attributes
+
+ area->SetAttributes( node->GetAttributes() );
+
+ // If any of the corners have an obstacle in the direction of another corner, then there's an internal obstruction of this nav node.
+ // Mark it as not mergable so it doesn't become a part of anything else and we will fix it up later.
+ if ( nwNode->m_obstacleHeight[SOUTH] > MaxTraversableHeight || nwNode->m_obstacleHeight[EAST] > MaxTraversableHeight ||
+ neNode->m_obstacleHeight[WEST] > MaxTraversableHeight || neNode->m_obstacleHeight[SOUTH] > MaxTraversableHeight ||
+ seNode->m_obstacleHeight[NORTH] > MaxTraversableHeight || seNode->m_obstacleHeight[WEST] > MaxTraversableHeight ||
+ swNode->m_obstacleHeight[EAST] > MaxTraversableHeight || swNode->m_obstacleHeight[NORTH] > MaxTraversableHeight )
+ {
+ Assert( width == 1 ); // We should only ever try to build a 1x1 area out of any two nodes that have an obstruction between them
+ Assert( height == 1 );
+
+ area->SetAttributes( area->GetAttributes() | NAV_MESH_NO_MERGE );
+ }
+
+ // Check that the node was crouch in the right direction
+ bool nodeCrouch = node->m_crouch[ SOUTH_EAST ];
+ if ( (area->GetAttributes() & NAV_MESH_CROUCH) && !nodeCrouch )
+ {
+ area->SetAttributes( area->GetAttributes() & ~NAV_MESH_CROUCH );
+ }
+
+ return coveredNodes;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * This function uses the CNavNodes that have been sampled from the map to
+ * generate CNavAreas - rectangular areas of "walkable" space. These areas
+ * are connected to each other, proving information on know how to move from
+ * area to area.
+ *
+ * This is a "greedy" algorithm that attempts to cover the walkable area
+ * with the fewest, largest, rectangles.
+ */
+void CNavMesh::CreateNavAreasFromNodes( void )
+{
+ // haven't yet seen a map use larger than 30...
+ int tryWidth = nav_area_max_size.GetInt();
+ int tryHeight = tryWidth;
+ int uncoveredNodes = CNavNode::GetListLength();
+
+ while( uncoveredNodes > 0 )
+ {
+ for( CNavNode *node = CNavNode::GetFirst(); node; node = node->GetNext() )
+ {
+ if (node->IsCovered())
+ continue;
+
+ if (TestArea( node, tryWidth, tryHeight ))
+ {
+ int covered = BuildArea( node, tryWidth, tryHeight );
+ if (covered < 0)
+ {
+ Error( "Generate: Error - Data corrupt.\n" );
+ return;
+ }
+
+ uncoveredNodes -= covered;
+ }
+ }
+
+ if (tryWidth >= tryHeight)
+ --tryWidth;
+ else
+ --tryHeight;
+
+ if (tryWidth <= 0 || tryHeight <= 0)
+ break;
+ }
+
+ if ( !TheNavAreas.Count() )
+ {
+ // If we somehow have no areas, don't try to create an impossibly-large grid
+ AllocateGrid( 0, 0, 0, 0 );
+ return;
+ }
+
+ Extent extent;
+ extent.lo.x = 9999999999.9f;
+ extent.lo.y = 9999999999.9f;
+ extent.hi.x = -9999999999.9f;
+ extent.hi.y = -9999999999.9f;
+
+ // compute total extent
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ Extent areaExtent;
+ area->GetExtent( &areaExtent );
+
+ if (areaExtent.lo.x < extent.lo.x)
+ extent.lo.x = areaExtent.lo.x;
+ if (areaExtent.lo.y < extent.lo.y)
+ extent.lo.y = areaExtent.lo.y;
+ if (areaExtent.hi.x > extent.hi.x)
+ extent.hi.x = areaExtent.hi.x;
+ if (areaExtent.hi.y > extent.hi.y)
+ extent.hi.y = areaExtent.hi.y;
+ }
+
+ // add the areas to the grid
+ AllocateGrid( extent.lo.x, extent.hi.x, extent.lo.y, extent.hi.y );
+
+ FOR_EACH_VEC( TheNavAreas, git )
+ {
+ AddNavArea( TheNavAreas[ git ] );
+ }
+
+
+ ConnectGeneratedAreas();
+ MarkPlayerClipAreas();
+ MarkJumpAreas(); // mark jump areas before we merge generated areas, so we don't merge jump and non-jump areas
+ MergeGeneratedAreas();
+ SplitAreasUnderOverhangs();
+ SquareUpAreas();
+ MarkStairAreas();
+ StichAndRemoveJumpAreas();
+ HandleObstacleTopAreas();
+ FixUpGeneratedAreas();
+
+ /// @TODO: incremental generation doesn't create ladders yet
+ if ( m_generationMode != GENERATE_INCREMENTAL )
+ {
+ for ( int i=0; i<m_ladders.Count(); ++i )
+ {
+ CNavLadder *ladder = m_ladders[i];
+ ladder->ConnectGeneratedLadder( 0.0f );
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+// adds walkable positions for any/all positions a mod specifies
+void CNavMesh::AddWalkableSeeds( void )
+{
+ CBaseEntity *spawn = gEntList.FindEntityByClassname( NULL, GetPlayerSpawnName() );
+
+ if (spawn )
+ {
+ // snap it to the sampling grid
+ Vector pos = spawn->GetAbsOrigin();
+ pos.x = TheNavMesh->SnapToGrid( pos.x );
+ pos.y = TheNavMesh->SnapToGrid( pos.y );
+
+ Vector normal;
+ if ( FindGroundForNode( &pos, &normal ) )
+ {
+ AddWalkableSeed( pos, normal );
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Initiate the generation process
+ */
+void CNavMesh::BeginGeneration( bool incremental )
+{
+ IGameEvent *event = gameeventmanager->CreateEvent( "nav_generate" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+
+#ifdef TERROR
+ engine->ServerCommand( "director_stop\nnb_delete_all\n" );
+ if ( !incremental && !engine->IsDedicatedServer() )
+ {
+ CBasePlayer *host = UTIL_GetListenServerHost();
+ if ( host )
+ {
+ host->ChangeTeam( TEAM_SPECTATOR );
+ }
+ }
+#else
+ engine->ServerCommand( "bot_kick\n" );
+#endif
+
+ // Right now, incrementally-generated areas won't connect to existing areas automatically.
+ // Since this means hand-editing will be necessary, don't do a full analyze.
+ if ( incremental )
+ {
+ nav_quicksave.SetValue( 1 );
+ }
+
+ m_generationState = SAMPLE_WALKABLE_SPACE;
+ m_sampleTick = 0;
+ m_generationMode = (incremental) ? GENERATE_INCREMENTAL : GENERATE_FULL;
+ lastMsgTime = 0.0f;
+
+ // clear any previous mesh
+ DestroyNavigationMesh( incremental );
+
+ SetNavPlace( UNDEFINED_PLACE );
+
+ // build internal representations of ladders, which are used to find new walkable areas
+ if ( !incremental ) ///< @incremental update doesn't build ladders to avoid overlapping existing ones
+ {
+ BuildLadders();
+ }
+
+ // start sampling from a spawn point
+ if ( !incremental )
+ {
+ AddWalkableSeeds();
+ }
+
+ // the system will see this NULL and select the next walkable seed
+ m_currentNode = NULL;
+
+ // if there are no seed points, we can't generate
+ if (m_walkableSeeds.Count() == 0)
+ {
+ m_generationMode = GENERATE_NONE;
+ Msg( "No valid walkable seed positions. Cannot generate Navigation Mesh.\n" );
+ return;
+ }
+
+ // initialize seed list index
+ m_seedIdx = 0;
+
+ Msg( "Generating Navigation Mesh...\n" );
+ m_generationStartTime = Plat_FloatTime();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Re-analyze an existing Mesh. Determine Hiding Spots, Encounter Spots, etc.
+ */
+void CNavMesh::BeginAnalysis( bool quitWhenFinished )
+{
+#ifdef TERROR
+ if ( !engine->IsDedicatedServer() )
+ {
+ CBasePlayer *host = UTIL_GetListenServerHost();
+ if ( host )
+ {
+ host->ChangeTeam( TEAM_SPECTATOR );
+ engine->ServerCommand( "director_no_death_check 1\ndirector_stop\nnb_delete_all\n" );
+
+ ConVarRef mat_fullbright( "mat_fullbright" );
+ ConVarRef mat_hdr_level( "mat_hdr_level" );
+
+ if( mat_fullbright.GetBool() )
+ {
+ Warning( "Setting mat_fullbright 0\n" );
+ mat_fullbright.SetValue( 0 );
+ }
+
+ if ( mat_hdr_level.GetInt() < 2 )
+ {
+ Warning( "Enabling HDR and reloading materials\n" );
+ mat_hdr_level.SetValue( 2 );
+ engine->ClientCommand( host->edict(), "mat_reloadallmaterials\n" );
+ }
+
+ // Running a threaded server breaks our lighting calculations
+ ConVarRef host_thread_mode( "host_thread_mode" );
+ m_hostThreadModeRestoreValue = host_thread_mode.GetInt();
+ host_thread_mode.SetValue( 0 );
+ ConVarRef mat_queue_mode( "mat_queue_mode" );
+ mat_queue_mode.SetValue( 0 );
+ }
+ }
+#endif
+
+ // Remove and re-add elements in TheNavAreas, to ensure indices are useful for progress feedback
+ NavAreaVector tmpSet;
+ {
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ tmpSet.AddToTail( TheNavAreas[it] );
+ }
+ }
+ TheNavAreas.RemoveAll();
+ {
+ FOR_EACH_VEC( tmpSet, it )
+ {
+ TheNavAreas.AddToTail( tmpSet[it] );
+ }
+ }
+
+ DestroyHidingSpots();
+ m_generationState = FIND_HIDING_SPOTS;
+ m_generationIndex = 0;
+ m_generationMode = GENERATE_ANALYSIS_ONLY;
+ m_bQuitWhenFinished = quitWhenFinished;
+ lastMsgTime = 0.0f;
+ m_generationStartTime = Plat_FloatTime();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+void ShowViewPortPanelToAll( const char * name, bool bShow, KeyValues *data )
+{
+ CRecipientFilter filter;
+ filter.AddAllPlayers();
+ filter.MakeReliable();
+
+ int count = 0;
+ KeyValues *subkey = NULL;
+
+ if ( data )
+ {
+ subkey = data->GetFirstSubKey();
+ while ( subkey )
+ {
+ count++; subkey = subkey->GetNextKey();
+ }
+
+ subkey = data->GetFirstSubKey(); // reset
+ }
+
+ UserMessageBegin( filter, "VGUIMenu" );
+ WRITE_STRING( name ); // menu name
+ WRITE_BYTE( bShow?1:0 );
+ WRITE_BYTE( count );
+
+ // write additional data (be careful not more than 192 bytes!)
+ while ( subkey )
+ {
+ WRITE_STRING( subkey->GetName() );
+ WRITE_STRING( subkey->GetString() );
+ subkey = subkey->GetNextKey();
+ }
+ MessageEnd();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+static void AnalysisProgress( const char *msg, int ticks, int current, bool showPercent = true )
+{
+ const float MsgInterval = 10.0f;
+ float now = Plat_FloatTime();
+ if ( now > lastMsgTime + MsgInterval )
+ {
+ if ( showPercent && ticks )
+ {
+ Msg( "%s %.0f%%\n", msg, current*100.0f/ticks );
+ }
+ else
+ {
+ Msg( "%s\n", msg );
+ }
+
+ lastMsgTime = now;
+ }
+
+ KeyValues *data = new KeyValues("data");
+ data->SetString( "msg", msg );
+ data->SetInt( "total", ticks );
+ data->SetInt( "current", current );
+
+ ShowViewPortPanelToAll( PANEL_NAV_PROGRESS, true, data );
+
+ data->deleteThis();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+static void HideAnalysisProgress( void )
+{
+ KeyValues *data = new KeyValues("data");
+ ShowViewPortPanelToAll( PANEL_NAV_PROGRESS, false, data );
+ data->deleteThis();
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Process the auto-generation for 'maxTime' seconds. return false if generation is complete.
+ */
+bool CNavMesh::UpdateGeneration( float maxTime )
+{
+ double startTime = Plat_FloatTime();
+ static unsigned int s_movedPlayerToArea = 0; // Last area we moved a player to for lighting calcs
+ static CountdownTimer s_playerSettleTimer; // Settle time after moving the player for lighting calcs
+ static CUtlVector<CNavArea *> s_unlitAreas;
+ static CUtlVector<CNavArea *> s_unlitSeedAreas;
+
+ static ConVarRef host_thread_mode( "host_thread_mode" );
+
+ switch( m_generationState )
+ {
+ //---------------------------------------------------------------------------
+ case SAMPLE_WALKABLE_SPACE:
+ {
+ AnalysisProgress( "Sampling walkable space...", 100, m_sampleTick / 10, false );
+ m_sampleTick = ( m_sampleTick + 1 ) % 1000;
+
+ while ( SampleStep() )
+ {
+ if ( Plat_FloatTime() - startTime > maxTime )
+ {
+ return true;
+ }
+ }
+
+ // sampling is complete, now build nav areas
+ m_generationState = CREATE_AREAS_FROM_SAMPLES;
+
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case CREATE_AREAS_FROM_SAMPLES:
+ {
+ Msg( "Creating navigation areas from sampled data...\n" );
+
+ // Select all pre-existing areas
+ if ( m_generationMode == GENERATE_INCREMENTAL )
+ {
+ ClearSelectedSet();
+ FOR_EACH_VEC( TheNavAreas, nit )
+ {
+ CNavArea *area = TheNavAreas[nit];
+ AddToSelectedSet( area );
+ }
+ }
+
+ // Create new areas
+ CreateNavAreasFromNodes();
+
+ // And toggle the selection, so we end up with the new areas
+ if ( m_generationMode == GENERATE_INCREMENTAL )
+ {
+ CommandNavToggleSelectedSet();
+ }
+
+ DestroyHidingSpots();
+
+ // Remove and re-add elements in TheNavAreas, to ensure indices are useful for progress feedback
+ NavAreaVector tmpSet;
+ {
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ tmpSet.AddToTail( TheNavAreas[it] );
+ }
+ }
+ TheNavAreas.RemoveAll();
+ {
+ FOR_EACH_VEC( tmpSet, it )
+ {
+ TheNavAreas.AddToTail( tmpSet[it] );
+ }
+ }
+
+ m_generationState = FIND_HIDING_SPOTS;
+ m_generationIndex = 0;
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case FIND_HIDING_SPOTS:
+ {
+ while( m_generationIndex < TheNavAreas.Count() )
+ {
+ CNavArea *area = TheNavAreas[ m_generationIndex ];
+ ++m_generationIndex;
+
+ area->ComputeHidingSpots();
+
+ // don't go over our time allotment
+ if( Plat_FloatTime() - startTime > maxTime )
+ {
+ AnalysisProgress( "Finding hiding spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
+ return true;
+ }
+ }
+
+ Msg( "Finding hiding spots...DONE\n" );
+
+ m_generationState = FIND_ENCOUNTER_SPOTS;
+ m_generationIndex = 0;
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case FIND_ENCOUNTER_SPOTS:
+ {
+ while( m_generationIndex < TheNavAreas.Count() )
+ {
+ CNavArea *area = TheNavAreas[ m_generationIndex ];
+ ++m_generationIndex;
+
+ area->ComputeSpotEncounters();
+
+ // don't go over our time allotment
+ if( Plat_FloatTime() - startTime > maxTime )
+ {
+ AnalysisProgress( "Finding encounter spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
+ return true;
+ }
+ }
+
+ Msg( "Finding encounter spots...DONE\n" );
+
+ m_generationState = FIND_SNIPER_SPOTS;
+ m_generationIndex = 0;
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case FIND_SNIPER_SPOTS:
+ {
+ while( m_generationIndex < TheNavAreas.Count() )
+ {
+ CNavArea *area = TheNavAreas[ m_generationIndex ];
+ ++m_generationIndex;
+
+ area->ComputeSniperSpots();
+
+ // don't go over our time allotment
+ if( Plat_FloatTime() - startTime > maxTime )
+ {
+ AnalysisProgress( "Finding sniper spots...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
+ return true;
+ }
+ }
+
+ Msg( "Finding sniper spots...DONE\n" );
+
+ m_generationState = COMPUTE_MESH_VISIBILITY;
+ m_generationIndex = 0;
+ BeginVisibilityComputations();
+ Msg( "Computing mesh visibility...\n" );
+
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case COMPUTE_MESH_VISIBILITY:
+ {
+ while( m_generationIndex < TheNavAreas.Count() )
+ {
+ CNavArea *area = TheNavAreas[ m_generationIndex ];
+ ++m_generationIndex;
+
+ area->ComputeVisibilityToMesh();
+
+ // don't go over our time allotment
+ if ( Plat_FloatTime() - startTime > maxTime )
+ {
+ AnalysisProgress( "Computing mesh visibility...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
+ return true;
+ }
+ }
+
+ Msg( "Optimizing mesh visibility...\n" );
+
+ EndVisibilityComputations();
+
+ Msg( "Computing mesh visibility...DONE\n" );
+
+ m_generationState = FIND_EARLIEST_OCCUPY_TIMES;
+ m_generationIndex = 0;
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case FIND_EARLIEST_OCCUPY_TIMES:
+ {
+ while( m_generationIndex < TheNavAreas.Count() )
+ {
+ CNavArea *area = TheNavAreas[ m_generationIndex ];
+ ++m_generationIndex;
+
+ area->ComputeEarliestOccupyTimes();
+
+ // don't go over our time allotment
+ if( Plat_FloatTime() - startTime > maxTime )
+ {
+ AnalysisProgress( "Finding earliest occupy times...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
+ return true;
+ }
+ }
+
+ Msg( "Finding earliest occupy times...DONE\n" );
+
+#ifdef NAV_ANALYZE_LIGHT_INTENSITY
+ bool shouldSkipLightComputation = ( m_generationMode == GENERATE_INCREMENTAL || engine->IsDedicatedServer() );
+#else
+ bool shouldSkipLightComputation = true;
+#endif
+
+ if ( shouldSkipLightComputation )
+ {
+ m_generationState = CUSTOM; // no light intensity calcs for incremental generation or dedicated servers
+ }
+ else
+ {
+ m_generationState = FIND_LIGHT_INTENSITY;
+ s_playerSettleTimer.Invalidate();
+ CNavArea::MakeNewMarker();
+ s_unlitAreas.RemoveAll();
+ FOR_EACH_VEC( TheNavAreas, nit )
+ {
+ s_unlitAreas.AddToTail( TheNavAreas[nit] );
+ s_unlitSeedAreas.AddToTail( TheNavAreas[nit] );
+ }
+ }
+
+ m_generationIndex = 0;
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case FIND_LIGHT_INTENSITY:
+ {
+ host_thread_mode.SetValue( 0 ); // need non-threaded server for light calcs
+
+ CBasePlayer *host = UTIL_GetListenServerHost();
+
+ if ( !s_unlitAreas.Count() || !host )
+ {
+ Msg( "Finding light intensity...DONE\n" );
+
+ m_generationState = CUSTOM;
+ m_generationIndex = 0;
+ return true;
+ }
+
+ if ( !s_playerSettleTimer.IsElapsed() )
+ return true; // wait for eyePos to settle
+
+ // Now try to compute lighting for remaining areas
+ int sit = 0;
+ while( sit < s_unlitAreas.Count() )
+ {
+ CNavArea *area = s_unlitAreas[sit];
+ if ( area->ComputeLighting() )
+ {
+ s_unlitSeedAreas.FindAndRemove( area );
+ s_unlitAreas.Remove( sit );
+
+ continue;
+ }
+ else
+ {
+ ++sit;
+ }
+ }
+
+ if ( s_unlitAreas.Count() )
+ {
+ if ( s_unlitSeedAreas.Count() )
+ {
+ CNavArea *moveArea = s_unlitSeedAreas[0];
+ s_unlitSeedAreas.FastRemove( 0 );
+
+ //Msg( "Moving to new area %d to compute lighting for %d/%d areas\n", moveArea->GetID(), s_unlitAreas.Count(), TheNavAreas.Count() );
+
+ Vector eyePos = moveArea->GetCenter();
+ float height;
+ if ( GetGroundHeight( eyePos, &height ) )
+ {
+ eyePos.z = height + HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings
+ }
+ else
+ {
+ eyePos.z += HalfHumanHeight - StepHeight; // players light from their centers, and we light from slightly below that, to allow for low ceilings
+ }
+ host->SetAbsOrigin( eyePos );
+ AnalysisProgress( "Finding light intensity...", 100, 100 * (TheNavAreas.Count() - s_unlitAreas.Count()) / TheNavAreas.Count() );
+ s_movedPlayerToArea = moveArea->GetID();
+ s_playerSettleTimer.Start( 0.1f );
+ return true;
+ }
+ else
+ {
+ Msg( "Finding light intensity...DONE (%d unlit areas)\n", s_unlitAreas.Count() );
+ if ( s_unlitAreas.Count() )
+ {
+ Warning( "To see unlit areas:\n" );
+ for ( int sit=0; sit<s_unlitAreas.Count(); ++sit )
+ {
+ CNavArea *area = s_unlitAreas[ sit ];
+ Warning( "nav_unmark; nav_mark %d; nav_warp_to_mark;\n", area->GetID() );
+ }
+ }
+
+ m_generationState = CUSTOM;
+ m_generationIndex = 0;
+ }
+ }
+
+ Msg( "Finding light intensity...DONE\n" );
+
+ m_generationState = CUSTOM;
+ m_generationIndex = 0;
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case CUSTOM:
+ {
+ if ( m_generationIndex == 0 )
+ {
+ BeginCustomAnalysis( m_generationMode == GENERATE_INCREMENTAL );
+ Msg( "Start custom...\n ");
+ }
+ while( m_generationIndex < TheNavAreas.Count() )
+ {
+ CNavArea *area = TheNavAreas[ m_generationIndex ];
+ ++m_generationIndex;
+
+ area->CustomAnalysis( m_generationMode == GENERATE_INCREMENTAL );
+
+ // don't go over our time allotment
+ if( Plat_FloatTime() - startTime > maxTime )
+ {
+ AnalysisProgress( "Custom game-specific analysis...", 100, 100 * m_generationIndex / TheNavAreas.Count() );
+ return true;
+ }
+ }
+
+ Msg( "Post custom...\n ");
+ PostCustomAnalysis();
+
+ EndCustomAnalysis();
+ Msg( "Custom game-specific analysis...DONE\n" );
+
+ m_generationState = SAVE_NAV_MESH;
+ m_generationIndex = 0;
+ ConVarRef mat_queue_mode( "mat_queue_mode" );
+ mat_queue_mode.SetValue( -1 );
+ host_thread_mode.SetValue( m_hostThreadModeRestoreValue ); // restore this
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ case SAVE_NAV_MESH:
+ {
+ if ( m_generationMode == GENERATE_ANALYSIS_ONLY || m_generationMode == GENERATE_FULL )
+ {
+ m_isAnalyzed = true;
+ }
+
+ // generation complete!
+ float generationTime = Plat_FloatTime() - m_generationStartTime;
+ Msg( "Generation complete! %0.1f seconds elapsed.\n", generationTime );
+ bool restart = m_generationMode != GENERATE_INCREMENTAL;
+ m_generationMode = GENERATE_NONE;
+ m_isLoaded = true;
+ ClearWalkableSeeds();
+
+ HideAnalysisProgress();
+
+ // save the mesh
+ if (Save())
+ {
+ Msg( "Navigation map '%s' saved.\n", GetFilename() );
+ }
+ else
+ {
+ const char *filename = GetFilename();
+ Msg( "ERROR: Cannot save navigation map '%s'.\n", (filename) ? filename : "(null)" );
+ }
+
+ if ( m_bQuitWhenFinished )
+ {
+ engine->ServerCommand( "quit\n" );
+ }
+ else if ( restart )
+ {
+ engine->ChangeLevel( STRING( gpGlobals->mapname ), NULL );
+ }
+ else
+ {
+ FOR_EACH_VEC( TheNavAreas, it )
+ {
+ TheNavAreas[ it ]->ResetNodes();
+ }
+
+#if !(DEBUG_NAV_NODES)
+ // destroy navigation nodes created during map generation
+ CNavNode *node, *next;
+ for( node = CNavNode::m_list; node; node = next )
+ {
+ next = node->m_next;
+ delete node;
+ }
+ CNavNode::m_list = NULL;
+ CNavNode::m_listLength = 0;
+ CNavNode::m_nextID = 1;
+#endif // !(DEBUG_NAV_NODES)
+ }
+
+ return false;
+ }
+ }
+
+ return false;
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Define the name of player spawn entities
+ */
+void CNavMesh::SetPlayerSpawnName( const char *name )
+{
+ if (m_spawnName)
+ {
+ delete [] m_spawnName;
+ }
+
+ m_spawnName = new char [ strlen(name) + 1 ];
+ strcpy( m_spawnName, name );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Return name of player spawn entity
+ */
+const char *CNavMesh::GetPlayerSpawnName( void ) const
+{
+ if (m_spawnName)
+ return m_spawnName;
+
+ // default value
+ return "info_player_start";
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Add a nav node and connect it.
+ * Node Z positions are ground level.
+ */
+CNavNode *CNavMesh::AddNode( const Vector &destPos, const Vector &normal, NavDirType dir, CNavNode *source, bool isOnDisplacement,
+ float obstacleHeight, float obstacleStartDist, float obstacleEndDist )
+{
+ // check if a node exists at this location
+ CNavNode *node = CNavNode::GetNode( destPos );
+
+ // if no node exists, create one
+ bool useNew = false;
+ if (node == NULL)
+ {
+ node = new CNavNode( destPos, normal, source, isOnDisplacement );
+ OnNodeAdded( node );
+ useNew = true;
+ }
+
+ // connect source node to new node
+ source->ConnectTo( node, dir, obstacleHeight, obstacleStartDist, obstacleEndDist );
+
+ // optimization: if deltaZ changes very little, assume connection is commutative
+ const float zTolerance = 50.0f;
+ float deltaZ = source->GetPosition()->z - destPos.z;
+ if (fabs( deltaZ ) < zTolerance)
+ {
+ if ( obstacleHeight > 0 )
+ {
+ obstacleHeight = MAX( obstacleHeight + deltaZ, 0 );
+ Assert( obstacleHeight > 0 );
+ }
+ node->ConnectTo( source, OppositeDirection( dir ), obstacleHeight, GenerationStepSize - obstacleEndDist, GenerationStepSize - obstacleStartDist );
+ node->MarkAsVisited( OppositeDirection( dir ) );
+ }
+
+ if (useNew)
+ {
+ // new node becomes current node
+ m_currentNode = node;
+ }
+
+ node->CheckCrouch();
+
+ // determine if there's a cliff nearby and set an attribute on this node
+ for ( int i = 0; i < NUM_DIRECTIONS; i++ )
+ {
+ NavDirType dir = (NavDirType) i;
+ if ( CheckCliff( node->GetPosition(), dir ) )
+ {
+ node->SetAttributes( node->GetAttributes() | NAV_MESH_CLIFF );
+ break;
+ }
+ }
+
+ return node;
+}
+
+//--------------------------------------------------------------------------------------------------------------
+inline CNavNode *LadderEndSearch( const Vector *pos, NavDirType mountDir )
+{
+ Vector center = *pos;
+ AddDirectionVector( &center, mountDir, HalfHumanWidth );
+
+ //
+ // Test the ladder dismount point first, then each cardinal direction one and two steps away
+ //
+ for( int d=(-1); d<2*NUM_DIRECTIONS; ++d )
+ {
+ Vector tryPos = center;
+
+ if (d >= NUM_DIRECTIONS)
+ AddDirectionVector( &tryPos, (NavDirType)(d - NUM_DIRECTIONS), 2.0f*GenerationStepSize );
+ else if (d >= 0)
+ AddDirectionVector( &tryPos, (NavDirType)d, GenerationStepSize );
+
+ // step up a rung, to ensure adjacent floors are below us
+ tryPos.z += GenerationStepSize;
+
+ tryPos.x = TheNavMesh->SnapToGrid( tryPos.x );
+ tryPos.y = TheNavMesh->SnapToGrid( tryPos.y );
+
+ // adjust height to account for sloping areas
+ Vector tryNormal;
+ if (TheNavMesh->GetGroundHeight( tryPos, &tryPos.z, &tryNormal ) == false)
+ continue;
+
+ // make sure this point is not on the other side of a wall
+ const float fudge = 4.0f;
+ trace_t result;
+ UTIL_TraceHull( center + Vector( 0, 0, fudge ), tryPos + Vector( 0, 0, fudge ), NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), NULL, COLLISION_GROUP_NONE, &result );
+ if (result.fraction != 1.0f || result.startsolid)
+ continue;
+
+ // if no node exists here, create one and continue the search
+ if (CNavNode::GetNode( tryPos ) == NULL)
+ {
+ return new CNavNode( tryPos, tryNormal, NULL, false );
+ }
+ }
+
+ return NULL;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+bool CNavMesh::FindGroundForNode( Vector *pos, Vector *normal )
+{
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_PLAYER_MOVEMENT, WALK_THRU_EVERYTHING );
+ trace_t tr;
+ Vector start( pos->x, pos->y, pos->z + VEC_DUCK_HULL_MAX.z - 0.1f );
+ Vector end( *pos );
+ end.z -= DeathDrop;
+
+ UTIL_TraceHull(
+ start,
+ end,
+ NavTraceMins,
+ NavTraceMaxs,
+ GetGenerationTraceMask(),
+ &filter,
+ &tr );
+
+ *pos = tr.endpos;
+ *normal = tr.plane.normal;
+
+ return ( !tr.allsolid );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+void DrawTrace( const trace_t *trace )
+{
+ /*
+ if ( trace->fraction > 0.0f && !trace->startsolid )
+ {
+ NDebugOverlay::SweptBox( trace->startpos, trace->endpos, NavTraceMins, NavTraceMaxs, vec3_angle, 0, 255, 0, 45, 100 );
+ }
+ else
+ {
+ NDebugOverlay::SweptBox( trace->startpos, trace->endpos, NavTraceMins, NavTraceMaxs, vec3_angle, 255, 0, 0, 45, 100 );
+ }
+ */
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+bool StayOnFloor( trace_t *trace, float zLimit /* = DeathDrop */ )
+{
+ Vector start( trace->endpos );
+ Vector end( start );
+ end.z -= zLimit;
+
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace );
+ DrawTrace( trace );
+
+ if ( trace->startsolid || trace->fraction >= 1.0f )
+ {
+ return false;
+ }
+
+ if ( trace->plane.normal.z < nav_slope_limit.GetFloat() )
+ {
+ return false;
+ }
+
+ return true;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+bool TraceAdjacentNode( int depth, const Vector& start, const Vector& end, trace_t *trace, float zLimit /* = DeathDrop */ )
+{
+ const float MinDistance = 1.0f; // if we can't move at least this far, don't bother stepping up.
+
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace );
+ DrawTrace( trace );
+
+ // If we started in the ground for some reason, bail
+ if ( trace->startsolid )
+ return false;
+
+ // If we made it, so try to find the floor
+ if ( end.x == trace->endpos.x && end.y == trace->endpos.y )
+ {
+ return StayOnFloor( trace, zLimit );
+ }
+
+ // If we didn't make enough progress, bail
+ if ( depth && start.AsVector2D().DistToSqr( trace->endpos.AsVector2D() ) < MinDistance * MinDistance )
+ {
+ return false;
+ }
+
+ // We made it more than MinDistance. If the slope is too steep, we can't go on.
+ if ( !StayOnFloor( trace, zLimit ) )
+ {
+ return false;
+ }
+
+ // Try to go up as if we stepped up, forward, and down.
+ Vector testStart( trace->endpos );
+ Vector testEnd( testStart );
+ testEnd.z += StepHeight;
+ UTIL_TraceHull( testStart, testEnd, NavTraceMins, NavTraceMaxs, TheNavMesh->GetGenerationTraceMask(), &filter, trace );
+ DrawTrace( trace );
+
+ Vector forwardTestStart = trace->endpos;
+ Vector forwardTestEnd = end;
+ forwardTestEnd.z = forwardTestStart.z;
+ return TraceAdjacentNode( depth+1, forwardTestStart, forwardTestEnd, trace );
+}
+
+
+//--------------------------------------------------------------------------------------------------------
+static bool IsNodeOverlapped( const Vector& pos, const Vector& offset )
+{
+ bool overlap = TheNavMesh->GetNavArea( pos + offset, HumanHeight ) != NULL;
+ if ( !overlap )
+ {
+ Vector mins( -0.5f, -0.5f, -0.5f );
+ Vector maxs( 0.5f, 0.5f, 0.5f );
+
+ Vector start = pos;
+ start.z += HalfHumanHeight;
+ Vector end = start;
+ end.x += offset.x * GenerationStepSize;
+ end.y += offset.y * GenerationStepSize;
+ trace_t trace;
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
+ UTIL_TraceHull( start, end, mins, maxs, TheNavMesh->GetGenerationTraceMask(), &filter, &trace );
+ if ( trace.startsolid || trace.allsolid )
+ {
+ return true;
+ }
+
+ if ( trace.fraction < 0.1f )
+ {
+ return true;
+ }
+
+ start = trace.endpos;
+ end.z -= HalfHumanHeight * 2;
+ UTIL_TraceHull( start, end, mins, maxs, TheNavMesh->GetGenerationTraceMask(), &filter, &trace );
+ if ( trace.startsolid || trace.allsolid )
+ {
+ return true;
+ }
+
+ if ( trace.fraction == 1.0f )
+ {
+ return true;
+ }
+
+ if ( trace.plane.normal.z < 0.7f )
+ {
+ return true;
+ }
+ }
+ return overlap;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Search the world and build a map of possible movements.
+ * The algorithm begins at the bot's current location, and does a recursive search
+ * outwards, tracking all valid steps and generating a directed graph of CNavNodes.
+ *
+ * Sample the map one "step" in a cardinal direction to learn the map.
+ *
+ * Returns true if sampling needs to continue, or false if done.
+ */
+bool CNavMesh::SampleStep( void )
+{
+ // take a step
+ while( true )
+ {
+ if (m_currentNode == NULL)
+ {
+ // sampling is complete from current seed, try next one
+ m_currentNode = GetNextWalkableSeedNode();
+
+ if (m_currentNode == NULL)
+ {
+ if ( m_generationMode == GENERATE_INCREMENTAL || m_generationMode == GENERATE_SIMPLIFY )
+ {
+ return false;
+ }
+
+ // search is exhausted - continue search from ends of ladders
+ for ( int i=0; i<m_ladders.Count(); ++i )
+ {
+ CNavLadder *ladder = m_ladders[i];
+
+ // check ladder bottom
+ if ((m_currentNode = LadderEndSearch( &ladder->m_bottom, ladder->GetDir() )) != 0)
+ break;
+
+ // check ladder top
+ if ((m_currentNode = LadderEndSearch( &ladder->m_top, ladder->GetDir() )) != 0)
+ break;
+ }
+
+ if (m_currentNode == NULL)
+ {
+ // all seeds exhausted, sampling complete
+ return false;
+ }
+ }
+ }
+
+ //
+ // Take a step from this node
+ //
+ for( int dir = NORTH; dir < NUM_DIRECTIONS; dir++ )
+ {
+ if (!m_currentNode->HasVisited( (NavDirType)dir ))
+ {
+ // have not searched in this direction yet
+
+ // start at current node position
+ Vector pos = *m_currentNode->GetPosition();
+
+ // snap to grid
+ int cx = SnapToGrid( pos.x );
+ int cy = SnapToGrid( pos.y );
+
+ // attempt to move to adjacent node
+ switch( dir )
+ {
+ case NORTH: cy -= GenerationStepSize; break;
+ case SOUTH: cy += GenerationStepSize; break;
+ case EAST: cx += GenerationStepSize; break;
+ case WEST: cx -= GenerationStepSize; break;
+ }
+
+ pos.x = cx;
+ pos.y = cy;
+
+ m_generationDir = (NavDirType)dir;
+
+ // mark direction as visited
+ m_currentNode->MarkAsVisited( m_generationDir );
+
+ // sanity check to not generate across the world for incremental generation
+ const float incrementalRange = nav_generate_incremental_range.GetFloat();
+ if ( m_generationMode == GENERATE_INCREMENTAL && incrementalRange > 0 )
+ {
+ bool inRange = false;
+ for ( int i=0; i<m_walkableSeeds.Count(); ++i )
+ {
+ const Vector &seedPos = m_walkableSeeds[i].pos;
+ if ( (seedPos - pos).IsLengthLessThan( incrementalRange ) )
+ {
+ inRange = true;
+ break;
+ }
+ }
+
+ if ( !inRange )
+ {
+ return true;
+ }
+ }
+
+ if ( m_generationMode == GENERATE_SIMPLIFY )
+ {
+ if ( !m_simplifyGenerationExtent.Contains( pos ) )
+ {
+ return true;
+ }
+ }
+
+ // test if we can move to new position
+ trace_t result;
+ Vector from( *m_currentNode->GetPosition() );
+ CTraceFilterWalkableEntities filter( NULL, COLLISION_GROUP_NONE, WALK_THRU_EVERYTHING );
+ Vector to, toNormal;
+ float obstacleHeight = 0, obstacleStartDist = 0, obstacleEndDist = GenerationStepSize;
+ if ( TraceAdjacentNode( 0, from, pos, &result ) )
+ {
+ to = result.endpos;
+ toNormal = result.plane.normal;
+ }
+ else
+ {
+ // test going up ClimbUpHeight
+ bool success = false;
+ for ( float height = StepHeight; height <= ClimbUpHeight; height += 1.0f )
+ {
+ trace_t tr;
+ Vector start( from );
+ Vector end( pos );
+ start.z += height;
+ end.z += height;
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr );
+ if ( !tr.startsolid && tr.fraction == 1.0f )
+ {
+ if ( !StayOnFloor( &tr ) )
+ {
+ break;
+ }
+
+ to = tr.endpos;
+ toNormal = tr.plane.normal;
+
+ start = end = from;
+ end.z += height;
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr );
+ if ( tr.fraction < 1.0f )
+ {
+ break;
+ }
+
+ // keep track of far up we had to go to find a path to the next node
+ obstacleHeight = height;
+ success = true;
+ break;
+ }
+ else
+ {
+ // Could not trace from node to node at this height, something is in the way.
+ // Trace in the other direction to see if we hit something
+ Vector vecToObstacleStart = tr.endpos - start;
+ Assert( vecToObstacleStart.LengthSqr() <= Square( GenerationStepSize ) );
+ if ( vecToObstacleStart.LengthSqr() <= Square( GenerationStepSize ) )
+ {
+ UTIL_TraceHull( end, start, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &tr );
+ if ( !tr.startsolid && tr.fraction < 1.0 )
+ {
+ // We hit something going the other direction. There is some obstacle between the two nodes.
+ Vector vecToObstacleEnd = tr.endpos - start;
+ Assert( vecToObstacleEnd.LengthSqr() <= Square( GenerationStepSize ) );
+ if ( vecToObstacleEnd.LengthSqr() <= Square( GenerationStepSize ) )
+ {
+ // Remember the distances to start and end of the obstacle (with respect to the "from" node).
+ // Keep track of the last distances to obstacle as we keep increasing the height we do a trace for.
+ // If we do eventually clear the obstacle, these values will be the start and end distance to the
+ // very tip of the obstacle.
+ obstacleStartDist = vecToObstacleStart.Length();
+ obstacleEndDist = vecToObstacleEnd.Length();
+ if ( obstacleEndDist == 0 )
+ {
+ obstacleEndDist = GenerationStepSize;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ( !success )
+ {
+ return true;
+ }
+ }
+
+ // Don't generate nodes if we spill off the end of the world onto skybox
+ if ( result.surface.flags & ( SURF_SKY|SURF_SKY2D ) )
+ {
+ return true;
+ }
+
+ // If we're incrementally generating, don't overlap existing nav areas.
+ Vector testPos( to );
+ bool overlapSE = IsNodeOverlapped( testPos, Vector( 1, 1, HalfHumanHeight ) );
+ bool overlapSW = IsNodeOverlapped( testPos, Vector( -1, 1, HalfHumanHeight ) );
+ bool overlapNE = IsNodeOverlapped( testPos, Vector( 1, -1, HalfHumanHeight ) );
+ bool overlapNW = IsNodeOverlapped( testPos, Vector( -1, -1, HalfHumanHeight ) );
+ if ( overlapSE && overlapSW && overlapNE && overlapNW && m_generationMode != GENERATE_SIMPLIFY )
+ {
+ return true;
+ }
+
+ int nTolerance = nav_generate_incremental_tolerance.GetInt();
+ if ( nTolerance > 0 && m_generationMode == GENERATE_INCREMENTAL )
+ {
+ bool bValid = false;
+ int zPos = to.z;
+ for ( int i=0; i<m_walkableSeeds.Count(); ++i )
+ {
+ const Vector &seedPos = m_walkableSeeds[i].pos;
+ int zMin = seedPos.z - nTolerance;
+ int zMax = seedPos.z + nTolerance;
+
+ if ( zPos >= zMin && zPos <= zMax )
+ {
+ bValid = true;
+ break;
+ }
+ }
+
+ if ( !bValid )
+ return true;
+ }
+
+
+ bool isOnDisplacement = result.IsDispSurface();
+
+ if ( nav_displacement_test.GetInt() > 0 )
+ {
+ // Test for nodes under displacement surfaces.
+ // This happens during development, and is a pain because the space underneath a displacement
+ // is not 'solid'.
+ Vector start = to + Vector( 0, 0, 0 );
+ Vector end = start + Vector( 0, 0, nav_displacement_test.GetInt() );
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &result );
+
+ if ( result.fraction > 0 )
+ {
+ end = start;
+ start = result.endpos;
+ UTIL_TraceHull( start, end, NavTraceMins, NavTraceMaxs, GetGenerationTraceMask(), &filter, &result );
+ if ( result.fraction < 1 )
+ {
+ // if we made it down to within StepHeight, maybe we're on a static prop
+ if ( result.endpos.z > to.z + StepHeight )
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ float deltaZ = to.z - m_currentNode->GetPosition()->z;
+ // If there's an obstacle in the way and it's traversable, or the obstacle is not higher than the destination node itself minus a small epsilon
+ // (meaning the obstacle was just the height change to get to the destination node, no extra obstacle between the two), clear obstacle height
+ // and distances
+ if ( ( obstacleHeight < MaxTraversableHeight ) || ( deltaZ > ( obstacleHeight - 2.0f ) ) )
+ {
+ obstacleHeight = 0;
+ obstacleStartDist = 0;
+ obstacleEndDist = GenerationStepSize;
+ }
+
+ // we can move here
+ // create a new navigation node, and update current node pointer
+ AddNode( to, toNormal, m_generationDir, m_currentNode, isOnDisplacement, obstacleHeight, obstacleStartDist, obstacleEndDist );
+
+ return true;
+ }
+ }
+
+ // all directions have been searched from this node - pop back to its parent and continue
+ m_currentNode = m_currentNode->GetParent();
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Add given walkable position to list of seed positions for map sampling
+ */
+void CNavMesh::AddWalkableSeed( const Vector &pos, const Vector &normal )
+{
+ WalkableSeedSpot seed;
+
+ seed.pos.x = RoundToUnits( pos.x, GenerationStepSize );
+ seed.pos.y = RoundToUnits( pos.y, GenerationStepSize );
+ seed.pos.z = pos.z;
+ seed.normal = normal;
+
+ m_walkableSeeds.AddToTail( seed );
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Return the next walkable seed as a node
+ */
+CNavNode *CNavMesh::GetNextWalkableSeedNode( void )
+{
+ if ( m_seedIdx >= m_walkableSeeds.Count() )
+ return NULL;
+
+ WalkableSeedSpot spot = m_walkableSeeds[ m_seedIdx ];
+ ++m_seedIdx;
+
+ // check if a node exists at this location
+ CNavNode *node = CNavNode::GetNode( spot.pos );
+ if ( node )
+ return NULL;
+
+ return new CNavNode( spot.pos, spot.normal, NULL, false );
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Check LOS, ignoring any entities that we can walk through
+ */
+bool IsWalkableTraceLineClear( const Vector &from, const Vector &to, unsigned int flags )
+{
+ trace_t result;
+ CBaseEntity *ignore = NULL;
+ Vector useFrom = from;
+
+ CTraceFilterWalkableEntities traceFilter( NULL, COLLISION_GROUP_NONE, flags );
+
+ result.fraction = 0.0f;
+
+ const int maxTries = 50;
+ for( int t=0; t<maxTries; ++t )
+ {
+ UTIL_TraceLine( useFrom, to, MASK_NPCSOLID, &traceFilter, &result );
+
+ // if we hit a walkable entity, try again
+ if (result.fraction != 1.0f && IsEntityWalkable( result.m_pEnt, flags ))
+ {
+ ignore = result.m_pEnt;
+
+ // start from just beyond where we hit to avoid infinite loops
+ Vector dir = to - from;
+ dir.NormalizeInPlace();
+ useFrom = result.endpos + 5.0f * dir;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (result.fraction == 1.0f)
+ return true;
+
+ return false;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+class Subdivider
+{
+public:
+ Subdivider( int depth )
+ {
+ m_depth = depth;
+ }
+
+ bool operator() ( CNavArea *area )
+ {
+ SubdivideX( area, true, true, m_depth );
+
+ return true;
+ }
+
+ void SubdivideX( CNavArea *area, bool canDivideX, bool canDivideY, int depth )
+ {
+ if (!canDivideX || depth <= 0)
+ return;
+
+ float split = area->GetSizeX() / 2.0f;
+
+ if (split < GenerationStepSize)
+ {
+ if (canDivideY)
+ {
+ SubdivideY( area, false, canDivideY, depth );
+ }
+ return;
+ }
+
+ split += area->GetCorner( NORTH_WEST ).x;
+
+ split = TheNavMesh->SnapToGrid( split );
+
+ CNavArea *alpha, *beta;
+ if (area->SplitEdit( false, split, &alpha, &beta ))
+ {
+ SubdivideY( alpha, canDivideX, canDivideY, depth );
+ SubdivideY( beta, canDivideX, canDivideY, depth );
+ }
+ }
+
+
+ void SubdivideY( CNavArea *area, bool canDivideX, bool canDivideY, int depth )
+ {
+ if (!canDivideY)
+ return;
+
+ float split = area->GetSizeY() / 2.0f;
+
+ if (split < GenerationStepSize)
+ {
+ if (canDivideX)
+ {
+ SubdivideX( area, canDivideX, false, depth-1 );
+ }
+ return;
+ }
+
+ split += area->GetCorner( NORTH_WEST ).y;
+
+ split = TheNavMesh->SnapToGrid( split );
+
+ CNavArea *alpha, *beta;
+ if (area->SplitEdit( true, split, &alpha, &beta ))
+ {
+ SubdivideX( alpha, canDivideX, canDivideY, depth-1 );
+ SubdivideX( beta, canDivideX, canDivideY, depth-1 );
+ }
+ }
+
+ int m_depth;
+};
+
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+ * Subdivide each nav area in X and Y to create 4 new areas
+ */
+void CNavMesh::CommandNavSubdivide( const CCommand &args )
+{
+ int depth = 1;
+
+ if (args.ArgC() == 2)
+ {
+ depth = atoi( args[1] );
+ }
+
+ Subdivider chop( depth );
+ TheNavMesh->ForAllSelectedAreas( chop );
+}
+
+CON_COMMAND_F( nav_subdivide, "Subdivides all selected areas.", FCVAR_GAMEDLL | FCVAR_CHEAT )
+{
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ TheNavMesh->CommandNavSubdivide( args );
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Debugging code to verify that all nav area connections are internally consistent
+*/
+void CNavMesh::ValidateNavAreaConnections( void )
+{
+ // iterate all nav areas
+ NavConnect connect;
+
+ for ( int it = 0; it < TheNavAreas.Count(); it++ )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+
+ for ( NavDirType dir = NORTH; dir < NUM_DIRECTIONS; dir = (NavDirType) ( ( (int) dir ) +1 ) )
+ {
+ const NavConnectVector *pOutgoing = area->GetAdjacentAreas( dir );
+ const NavConnectVector *pIncoming = area->GetIncomingConnections( dir );
+
+ for ( int iConnect = 0; iConnect < pOutgoing->Count(); iConnect++ )
+ {
+ // make sure no area is on both the connection and incoming list
+ CNavArea *areaOther = (*pOutgoing)[iConnect].area;
+ connect.area = areaOther;
+ if ( pIncoming->Find( connect ) != pIncoming->InvalidIndex() )
+ {
+ Msg( "Area %d has area %d on both 2-way and incoming list, should only be on one\n", area->GetID(), areaOther->GetID() );
+ Assert( false );
+ }
+
+ // make sure there are no duplicate connections on the list
+ for ( int iConnectCheck = iConnect+1; iConnectCheck < pOutgoing->Count(); iConnectCheck++ )
+ {
+ CNavArea *areaCheck = (*pOutgoing)[iConnectCheck].area;
+ if ( areaOther == areaCheck )
+ {
+ Msg( "Area %d has multiple outgoing connections to area %d in direction %d\n", area->GetID(), areaOther->GetID(), dir );
+ Assert( false );
+ }
+ }
+
+ const NavConnectVector *pOutgoingOther = areaOther->GetAdjacentAreas( OppositeDirection( dir ) );
+ const NavConnectVector *pIncomingOther = areaOther->GetIncomingConnections( OppositeDirection( dir ) );
+
+ // if we have a one-way outgoing connection, make sure we are on the other area's incoming list
+ connect.area = area;
+ bool bIsTwoWay = pOutgoingOther->Find( connect ) != pOutgoingOther->InvalidIndex();
+ if ( !bIsTwoWay )
+ {
+ connect.area = area;
+ bool bOnOthersIncomingList = pIncomingOther->Find( connect ) != pIncomingOther->InvalidIndex();
+ if ( !bOnOthersIncomingList )
+ {
+ Msg( "Area %d has one-way connect to area %d but does not appear on the latter's incoming list\n", area->GetID(), areaOther->GetID() );
+ }
+ }
+ }
+
+ for ( int iConnect = 0; iConnect < pIncoming->Count(); iConnect++ )
+ {
+ CNavArea *areaOther = (*pIncoming)[iConnect].area;
+
+ // make sure there are not duplicate areas on the incoming list
+ for ( int iConnectCheck = iConnect+1; iConnectCheck < pIncoming->Count(); iConnectCheck++ )
+ {
+ CNavArea *areaCheck = (*pIncoming)[iConnectCheck].area;
+ if ( areaOther == areaCheck )
+ {
+ Msg( "Area %d has multiple incoming connections to area %d in direction %d\n", area->GetID(), areaOther->GetID(), dir );
+ Assert( false );
+ }
+ }
+
+ const NavConnectVector *pOutgoingOther = areaOther->GetAdjacentAreas( OppositeDirection( dir ) );
+ connect.area = area;
+ bool bOnOthersOutgoingList = pOutgoingOther->Find( connect ) != pOutgoingOther->InvalidIndex();
+ if ( !bOnOthersOutgoingList )
+ {
+ Msg( "Area %d has incoming connection from area %d but does not appear on latter's outgoing connection list\n", area->GetID(), areaOther->GetID() );
+ Assert( false );
+ }
+ }
+ }
+ }
+}
+
+//--------------------------------------------------------------------------------------------------------------
+/**
+* Temp way to mark cliff areas after generation without regen'ing. Any area that is adjacent to a cliff
+* gets marked as a cliff. This will leave some big areas marked as cliff just because one edge is adjacent to
+* a cliff so it's not great. The code that does this at generation time is better because it ensures that
+* areas next to cliffs don't get merged with no-cliff areas.
+*/
+void CNavMesh::PostProcessCliffAreas()
+{
+ for ( int it = 0; it < TheNavAreas.Count(); it++ )
+ {
+ CNavArea *area = TheNavAreas[ it ];
+ if ( area->GetAttributes() & NAV_MESH_CLIFF )
+ continue;
+
+ for ( int i = 0; i < NUM_DIRECTIONS; i++ )
+ {
+ bool bHasCliff = false;
+ NavDirType dir = (NavDirType) i;
+ NavCornerType corner[2];
+
+ // look at either corner along this edge
+ corner[0] = (NavCornerType) i;
+ corner[1] = (NavCornerType) ( ( i+ 1 ) % NUM_CORNERS );
+
+ for ( int j = 0; j < 2; j++ )
+ {
+ Vector cornerPos = area->GetCorner( corner[j] );
+ if ( CheckCliff( &cornerPos, dir ) )
+ {
+ bHasCliff = true;
+ break;
+ }
+ }
+
+ if ( bHasCliff )
+ {
+ area->SetAttributes( area->GetAttributes() | NAV_MESH_CLIFF );
+ break;
+ }
+ }
+ }
+}
+
+CON_COMMAND_F( nav_gen_cliffs_approx, "Mark cliff areas, post-processing approximation", FCVAR_CHEAT )
+{
+ TheNavMesh->PostProcessCliffAreas();
+}