// nav_area.cpp // AI Navigation areas // Author: Michael S. Booth (mike@turtlerockstudios.com), January 2003 #pragma warning( disable : 4530 ) // STL uses exceptions, but we are not compiling with them - ignore warning #pragma warning( disable : 4786 ) // long STL names get truncated in browse info. #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #define _write write #define _close close #define MAX_OSPATH PATH_MAX #endif #include "extdll.h" #include "util.h" #include "cbase.h" #include "player.h" #include "gamerules.h" #include "bot_util.h" /// @todo Abstract hostages and cs-bots out of here #include "cs_bot.h" #include "cs_bot_manager.h" #include "hostage.h" #include "nav.h" #include "nav_node.h" #include "nav_area.h" #include "pm_shared.h" // for OBS_ROAMING extern void HintMessageToAllPlayers( const char *message ); unsigned int CNavArea::m_nextID = 1; NavAreaList TheNavAreaList; NavLadderList TheNavLadderList; unsigned int CNavArea::m_masterMarker = 1; CNavArea *CNavArea::m_openList = NULL; bool CNavArea::m_isReset = false; static float lastDrawTimestamp = 0.0f; //-------------------------------------------------------------------------------------------------------------- /** * This list contains all "good-sized" areas used to compute "approach points" */ static NavAreaList goodSizedAreaList; static void buildGoodSizedList( void ) { const float minSize = 200.0f; // 150 NavAreaList::iterator iter; for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; // skip the small areas const Extent *extent = area->GetExtent(); if (extent->SizeX() < minSize || extent->SizeY() < minSize) continue; goodSizedAreaList.push_back( area ); } } //-------------------------------------------------------------------------------------------------------------- HidingSpotList TheHidingSpotList; unsigned int HidingSpot::m_nextID = 1; unsigned int HidingSpot::m_masterMarker = 0; void DestroyHidingSpots( void ) { // remove all hiding spot references from the nav areas for( NavAreaList::iterator areaIter = TheNavAreaList.begin(); areaIter != TheNavAreaList.end(); ++areaIter ) { CNavArea *area = *areaIter; area->m_hidingSpotList.clear(); } HidingSpot::m_nextID = 0; // free all the HidingSpots for( HidingSpotList::iterator iter = TheHidingSpotList.begin(); iter != TheHidingSpotList.end(); ++iter ) delete *iter; TheHidingSpotList.clear(); } /** * For use when loading from a file */ HidingSpot::HidingSpot( void ) { m_pos = Vector( 0, 0, 0 ); m_id = 0; m_flags = 0; TheHidingSpotList.push_back( this ); } /** * For use when generating - assigns unique ID */ HidingSpot::HidingSpot( const Vector *pos, unsigned char flags ) { m_pos = *pos; m_id = m_nextID++; m_flags = flags; TheHidingSpotList.push_back( this ); } void HidingSpot::Save( int fd, unsigned int version ) const { _write( fd, &m_id, sizeof(unsigned int) ); _write( fd, &m_pos, 3 * sizeof(float) ); _write( fd, &m_flags, sizeof(unsigned char) ); } void HidingSpot::Load( SteamFile *file, unsigned int version ) { file->Read( &m_id, sizeof(unsigned int) ); file->Read( &m_pos, 3 * sizeof(float) ); file->Read( &m_flags, sizeof(unsigned char) ); // update next ID to avoid ID collisions by later spots if (m_id >= m_nextID) m_nextID = m_id+1; } /** * Given a HidingSpot ID, return the associated HidingSpot */ HidingSpot *GetHidingSpotByID( unsigned int id ) { for( HidingSpotList::iterator iter = TheHidingSpotList.begin(); iter != TheHidingSpotList.end(); ++iter ) { HidingSpot *spot = *iter; if (spot->GetID() == id) return spot; } return NULL; } //-------------------------------------------------------------------------------------------------------------- /** * To keep constructors consistent */ void CNavArea::Initialize( void ) { m_marker = 0; m_parent = NULL; m_parentHow = GO_NORTH; m_attributeFlags = 0; m_place = 0; for ( int i=0; ix < otherCorner->x) { m_extent.lo.x = corner->x; m_extent.hi.x = otherCorner->x; } else { m_extent.hi.x = corner->x; m_extent.lo.x = otherCorner->x; } if (corner->y < otherCorner->y) { m_extent.lo.y = corner->y; m_extent.hi.y = otherCorner->y; } else { m_extent.hi.y = corner->y; m_extent.lo.y = otherCorner->y; } m_extent.lo.z = corner->z; m_extent.hi.z = corner->z; m_center.x = (m_extent.lo.x + m_extent.hi.x)/2.0f; m_center.y = (m_extent.lo.y + m_extent.hi.y)/2.0f; m_center.z = (m_extent.lo.z + m_extent.hi.z)/2.0f; m_neZ = corner->z; m_swZ = otherCorner->z; } //-------------------------------------------------------------------------------------------------------------- /** * */ CNavArea::CNavArea( const Vector *nwCorner, const Vector *neCorner, const Vector *seCorner, const Vector *swCorner ) { Initialize(); m_extent.lo = *nwCorner; m_extent.hi = *seCorner; m_center.x = (m_extent.lo.x + m_extent.hi.x)/2.0f; m_center.y = (m_extent.lo.y + m_extent.hi.y)/2.0f; m_center.z = (m_extent.lo.z + m_extent.hi.z)/2.0f; m_neZ = neCorner->z; m_swZ = swCorner->z; } //-------------------------------------------------------------------------------------------------------------- /** * Constructor used during generation phase. */ CNavArea::CNavArea( CNavNode *nwNode, CNavNode *neNode, CNavNode *seNode, CNavNode *swNode ) { Initialize(); m_extent.lo = *nwNode->GetPosition(); m_extent.hi = *seNode->GetPosition(); m_center.x = (m_extent.lo.x + m_extent.hi.x)/2.0f; m_center.y = (m_extent.lo.y + m_extent.hi.y)/2.0f; m_center.z = (m_extent.lo.z + m_extent.hi.z)/2.0f; m_neZ = neNode->GetPosition()->z; m_swZ = swNode->GetPosition()->z; m_node[ NORTH_WEST ] = nwNode; m_node[ NORTH_EAST ] = neNode; m_node[ SOUTH_EAST ] = seNode; m_node[ SOUTH_WEST ] = swNode; // mark internal nodes as part of this area AssignNodes( this ); } //-------------------------------------------------------------------------------------------------------------- /** * Destructor */ CNavArea::~CNavArea() { // if we are resetting the system, don't bother cleaning up - all areas are being destroyed if (m_isReset) return; // tell the other areas we are going away NavAreaList::iterator iter; for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; if (area == this) continue; area->OnDestroyNotify( this ); } // unhook from ladders for( int i=0; iOnDestroyNotify( this ); } } // remove the area from the grid TheNavAreaGrid.RemoveNavArea( this ); } //-------------------------------------------------------------------------------------------------------------- /** * This is invoked when an area is going away. * Remove any references we have to it. */ void CNavArea::OnDestroyNotify( CNavArea *dead ) { NavConnect con; con.area = dead; for( int d=0; dm_id, dirName[ dir ] ); } //-------------------------------------------------------------------------------------------------------------- /** * Disconnect this area from given area */ void CNavArea::Disconnect( CNavArea *area ) { NavConnect connect; connect.area = area; for( int dir = 0; dirGetPosition(); m_extent.hi = *m_node[ SOUTH_EAST ]->GetPosition(); m_center.x = (m_extent.lo.x + m_extent.hi.x)/2.0f; m_center.y = (m_extent.lo.y + m_extent.hi.y)/2.0f; m_center.z = (m_extent.lo.z + m_extent.hi.z)/2.0f; m_neZ = m_node[ NORTH_EAST ]->GetPosition()->z; m_swZ = m_node[ SOUTH_WEST ]->GetPosition()->z; // reassign the adjacent area's internal nodes to the final area adjArea->AssignNodes( this ); // merge adjacency links - we gain all the connections that adjArea had MergeAdjacentConnections( adjArea ); // remove subsumed adjacent area TheNavAreaList.remove( adjArea ); delete adjArea; } //-------------------------------------------------------------------------------------------------------------- /** * For merging with "adjArea" - pick up all of "adjArea"s connections */ void CNavArea::MergeAdjacentConnections( CNavArea *adjArea ) { // merge adjacency links - we gain all the connections that adjArea had NavConnectList::iterator iter; int dir; for( dir = 0; dirm_connect[ dir ].begin(); iter != adjArea->m_connect[ dir ].end(); ++iter ) { NavConnect connect = *iter; if (connect.area != adjArea && connect.area != this) ConnectTo( connect.area, (NavDirType)dir ); } } // remove any references from this area to the adjacent area, since it is now part of us for( dir = 0; dirm_connect[ dir ].begin(); iter != area->m_connect[ dir ].end(); ++iter ) { NavConnect connect = *iter; if (connect.area == adjArea) { connected = true; break; } } if (connected) { // remove all references to adjArea NavConnect connect; connect.area = adjArea; area->m_connect[dir].remove( connect ); // remove all references to the new area connect.area = this; area->m_connect[dir].remove( connect ); // add a single connection to the new area connect.area = this; area->m_connect[dir].push_back( connect ); } } } } //-------------------------------------------------------------------------------------------------------------- /** * Assign internal nodes to the given area * NOTE: "internal" nodes do not include the east or south border nodes */ void CNavArea::AssignNodes( CNavArea *area ) { CNavNode *horizLast = m_node[ NORTH_EAST ]; for( CNavNode *vertNode = m_node[ NORTH_WEST ]; vertNode != m_node[ SOUTH_WEST ]; vertNode = vertNode->GetConnectedNode( SOUTH ) ) { for( CNavNode *horizNode = vertNode; horizNode != horizLast; horizNode = horizNode->GetConnectedNode( EAST ) ) { horizNode->AssignArea( area ); } horizLast = horizLast->GetConnectedNode( SOUTH ); } } //-------------------------------------------------------------------------------------------------------------- /** * Split this area into two areas at the given edge. * Preserve all adjacency connections. * NOTE: This does not update node connections, only areas. */ bool CNavArea::SplitEdit( bool splitAlongX, float splitEdge, CNavArea **outAlpha, CNavArea **outBeta ) { CNavArea *alpha = NULL; CNavArea *beta = NULL; if (splitAlongX) { // +-----+->X // | A | // +-----+ // | B | // +-----+ // | // Y // don't do split if at edge of area if (splitEdge <= m_extent.lo.y + 1.0f) return false; if (splitEdge >= m_extent.hi.y - 1.0f) return false; alpha = new CNavArea; alpha->m_extent.lo = m_extent.lo; alpha->m_extent.hi.x = m_extent.hi.x; alpha->m_extent.hi.y = splitEdge; alpha->m_extent.hi.z = GetZ( &alpha->m_extent.hi ); beta = new CNavArea; beta->m_extent.lo.x = m_extent.lo.x; beta->m_extent.lo.y = splitEdge; beta->m_extent.lo.z = GetZ( &beta->m_extent.lo ); beta->m_extent.hi = m_extent.hi; alpha->ConnectTo( beta, SOUTH ); beta->ConnectTo( alpha, NORTH ); FinishSplitEdit( alpha, SOUTH ); FinishSplitEdit( beta, NORTH ); } else { // +--+--+->X // | | | // | A|B | // | | | // +--+--+ // | // Y // don't do split if at edge of area if (splitEdge <= m_extent.lo.x + 1.0f) return false; if (splitEdge >= m_extent.hi.x - 1.0f) return false; alpha = new CNavArea; alpha->m_extent.lo = m_extent.lo; alpha->m_extent.hi.x = splitEdge; alpha->m_extent.hi.y = m_extent.hi.y; alpha->m_extent.hi.z = GetZ( &alpha->m_extent.hi ); beta = new CNavArea; beta->m_extent.lo.x = splitEdge; beta->m_extent.lo.y = m_extent.lo.y; beta->m_extent.lo.z = GetZ( &beta->m_extent.lo ); beta->m_extent.hi = m_extent.hi; alpha->ConnectTo( beta, EAST ); beta->ConnectTo( alpha, WEST ); FinishSplitEdit( alpha, EAST ); FinishSplitEdit( beta, WEST ); } // new areas inherit attributes from original area alpha->SetAttributes( GetAttributes() ); beta->SetAttributes( GetAttributes() ); // new areas inherit place from original area alpha->SetPlace( GetPlace() ); beta->SetPlace( GetPlace() ); // return new areas if (outAlpha) *outAlpha = alpha; if (outBeta) *outBeta = beta; // remove original area TheNavAreaList.remove( this ); delete this; return true; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if given area is connected in given direction * if dir == NUM_DIRECTIONS, check all directions (direction is unknown) * @todo Formalize "asymmetric" flag on connections */ bool CNavArea::IsConnected( const CNavArea *area, NavDirType dir ) const { // we are connected to ourself if (area == this) return true; NavConnectList::const_iterator iter; if (dir == NUM_DIRECTIONS) { // search all directions for( int d=0; dm_topBehindArea == area || ladder->m_topForwardArea == area || ladder->m_topLeftArea == area || ladder->m_topRightArea == area) return true; } for( liter = m_ladder[ LADDER_DOWN ].begin(); liter != m_ladder[ LADDER_DOWN ].end(); ++liter ) { CNavLadder *ladder = *liter; if (ladder->m_bottomArea == area) return true; } } else { // check specific direction for( iter = m_connect[ dir ].begin(); iter != m_connect[ dir ].end(); ++iter ) { if (area == (*iter).area) return true; } } return false; } //-------------------------------------------------------------------------------------------------------------- /** * Compute change in height from this area to given area * @todo This is approximate for now */ float CNavArea::ComputeHeightChange( const CNavArea *area ) { float ourZ = GetZ( GetCenter() ); float areaZ = area->GetZ( area->GetCenter() ); return areaZ - ourZ; } //-------------------------------------------------------------------------------------------------------------- /** * Given the portion of the original area, update its internal data * The "ignoreEdge" direction defines the side of the original area that the new area does not include */ void CNavArea::FinishSplitEdit( CNavArea *newArea, NavDirType ignoreEdge ) { newArea->m_center.x = (newArea->m_extent.lo.x + newArea->m_extent.hi.x)/2.0f; newArea->m_center.y = (newArea->m_extent.lo.y + newArea->m_extent.hi.y)/2.0f; newArea->m_center.z = (newArea->m_extent.lo.z + newArea->m_extent.hi.z)/2.0f; newArea->m_neZ = GetZ( newArea->m_extent.hi.x, newArea->m_extent.lo.y ); newArea->m_swZ = GetZ( newArea->m_extent.lo.x, newArea->m_extent.hi.y ); // connect to adjacent areas for( int d=0; dIsOverlappingX( adj )) { newArea->ConnectTo( adj, (NavDirType)d ); // add reciprocal connection if needed if (adj->IsConnected( this, OppositeDirection( (NavDirType)d ))) adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); } break; case EAST: case WEST: if (newArea->IsOverlappingY( adj )) { newArea->ConnectTo( adj, (NavDirType)d ); // add reciprocal connection if needed if (adj->IsConnected( this, OppositeDirection( (NavDirType)d ))) adj->ConnectTo( newArea, OppositeDirection( (NavDirType)d ) ); } break; } } } TheNavAreaList.push_back( newArea ); TheNavAreaGrid.AddNavArea( newArea ); } //-------------------------------------------------------------------------------------------------------------- /** * Create a new area between this area and given area */ bool CNavArea::SpliceEdit( CNavArea *other ) { CNavArea *newArea = NULL; Vector nw, ne, se, sw; if (m_extent.lo.x > other->m_extent.hi.x) { // 'this' is east of 'other' float top = max( m_extent.lo.y, other->m_extent.lo.y ); float bottom = min( m_extent.hi.y, other->m_extent.hi.y ); nw.x = other->m_extent.hi.x; nw.y = top; nw.z = other->GetZ( &nw ); se.x = m_extent.lo.x; se.y = bottom; se.z = GetZ( &se ); ne.x = se.x; ne.y = nw.y; ne.z = GetZ( &ne ); sw.x = nw.x; sw.y = se.y; sw.z = other->GetZ( &sw ); newArea = new CNavArea( &nw, &ne, &se, &sw ); this->ConnectTo( newArea, WEST ); newArea->ConnectTo( this, EAST ); other->ConnectTo( newArea, EAST ); newArea->ConnectTo( other, WEST ); } else if (m_extent.hi.x < other->m_extent.lo.x) { // 'this' is west of 'other' float top = max( m_extent.lo.y, other->m_extent.lo.y ); float bottom = min( m_extent.hi.y, other->m_extent.hi.y ); nw.x = m_extent.hi.x; nw.y = top; nw.z = GetZ( &nw ); se.x = other->m_extent.lo.x; se.y = bottom; se.z = other->GetZ( &se ); ne.x = se.x; ne.y = nw.y; ne.z = other->GetZ( &ne ); sw.x = nw.x; sw.y = se.y; sw.z = GetZ( &sw ); newArea = new CNavArea( &nw, &ne, &se, &sw ); this->ConnectTo( newArea, EAST ); newArea->ConnectTo( this, WEST ); other->ConnectTo( newArea, WEST ); newArea->ConnectTo( other, EAST ); } else // 'this' overlaps in X { if (m_extent.lo.y > other->m_extent.hi.y) { // 'this' is south of 'other' float left = max( m_extent.lo.x, other->m_extent.lo.x ); float right = min( m_extent.hi.x, other->m_extent.hi.x ); nw.x = left; nw.y = other->m_extent.hi.y; nw.z = other->GetZ( &nw ); se.x = right; se.y = m_extent.lo.y; se.z = GetZ( &se ); ne.x = se.x; ne.y = nw.y; ne.z = other->GetZ( &ne ); sw.x = nw.x; sw.y = se.y; sw.z = GetZ( &sw ); newArea = new CNavArea( &nw, &ne, &se, &sw ); this->ConnectTo( newArea, NORTH ); newArea->ConnectTo( this, SOUTH ); other->ConnectTo( newArea, SOUTH ); newArea->ConnectTo( other, NORTH ); } else if (m_extent.hi.y < other->m_extent.lo.y) { // 'this' is north of 'other' float left = max( m_extent.lo.x, other->m_extent.lo.x ); float right = min( m_extent.hi.x, other->m_extent.hi.x ); nw.x = left; nw.y = m_extent.hi.y; nw.z = GetZ( &nw ); se.x = right; se.y = other->m_extent.lo.y; se.z = other->GetZ( &se ); ne.x = se.x; ne.y = nw.y; ne.z = GetZ( &ne ); sw.x = nw.x; sw.y = se.y; sw.z = other->GetZ( &sw ); newArea = new CNavArea( &nw, &ne, &se, &sw ); this->ConnectTo( newArea, SOUTH ); newArea->ConnectTo( this, NORTH ); other->ConnectTo( newArea, NORTH ); newArea->ConnectTo( other, SOUTH ); } else { // areas overlap return false; } } // if both areas have the same place, the new area inherits it if (GetPlace() == other->GetPlace()) { newArea->SetPlace( GetPlace() ); } else if (GetPlace() == UNDEFINED_PLACE) { newArea->SetPlace( other->GetPlace() ); } else if (other->GetPlace() == UNDEFINED_PLACE) { newArea->SetPlace( GetPlace() ); } else { // both have valid, but different places - pick on at random if (RANDOM_LONG( 0, 100 ) < 50) newArea->SetPlace( GetPlace() ); else newArea->SetPlace( other->GetPlace() ); } TheNavAreaList.push_back( newArea ); TheNavAreaGrid.AddNavArea( newArea ); return true; } //-------------------------------------------------------------------------------------------------------------- /** * Merge this area and given adjacent area */ bool CNavArea::MergeEdit( CNavArea *adj ) { // can only merge if attributes of both areas match // check that these areas can be merged const float tolerance = 1.0f; bool merge = false; if (ABS( m_extent.lo.x - adj->m_extent.lo.x ) < tolerance && ABS( m_extent.hi.x - adj->m_extent.hi.x ) < tolerance) merge = true; if (ABS( m_extent.lo.y - adj->m_extent.lo.y ) < tolerance && ABS( m_extent.hi.y - adj->m_extent.hi.y ) < tolerance) merge = true; if (merge == false) return false; Extent origExtent = m_extent; // update extent if (m_extent.lo.x > adj->m_extent.lo.x || m_extent.lo.y > adj->m_extent.lo.y) m_extent.lo = adj->m_extent.lo; if (m_extent.hi.x < adj->m_extent.hi.x || m_extent.hi.y < adj->m_extent.hi.y) m_extent.hi = adj->m_extent.hi; m_center.x = (m_extent.lo.x + m_extent.hi.x)/2.0f; m_center.y = (m_extent.lo.y + m_extent.hi.y)/2.0f; m_center.z = (m_extent.lo.z + m_extent.hi.z)/2.0f; if (m_extent.hi.x > origExtent.hi.x || m_extent.lo.y < origExtent.lo.y) m_neZ = adj->GetZ( m_extent.hi.x, m_extent.lo.y ); else m_neZ = GetZ( m_extent.hi.x, m_extent.lo.y ); if (m_extent.lo.x < origExtent.lo.x || m_extent.hi.y > origExtent.hi.y) m_swZ = adj->GetZ( m_extent.lo.x, m_extent.hi.y ); else m_swZ = GetZ( m_extent.lo.x, m_extent.hi.y ); // merge adjacency links - we gain all the connections that adjArea had MergeAdjacentConnections( adj ); // remove subsumed adjacent area TheNavAreaList.remove( adj ); delete adj; return true; } //-------------------------------------------------------------------------------------------------------------- void ApproachAreaAnalysisPrep( void ) { // collect "good-sized" areas for computing approach areas buildGoodSizedList(); } //-------------------------------------------------------------------------------------------------------------- void CleanupApproachAreaAnalysisPrep( void ) { goodSizedAreaList.clear(); } //-------------------------------------------------------------------------------------------------------------- /** * Destroy ladder representations */ void DestroyLadders( void ) { while( !TheNavLadderList.empty() ) { CNavLadder *ladder = TheNavLadderList.front(); TheNavLadderList.pop_front(); delete ladder; } } //-------------------------------------------------------------------------------------------------------------- /** * Free navigation map data. */ void DestroyNavigationMap( void ) { CNavArea::m_isReset = true; // remove each element of the list and delete them while( !TheNavAreaList.empty() ) { CNavArea *area = TheNavAreaList.front(); TheNavAreaList.pop_front(); delete area; } CNavArea::m_isReset = false; // destroy ladder representations DestroyLadders(); // destroy all hiding spots DestroyHidingSpots(); // destroy navigation nodes created during map learning CNavNode *node, *next; for( node = CNavNode::m_list; node; node = next ) { next = node->m_next; delete node; } CNavNode::m_list = NULL; // reset the grid TheNavAreaGrid.Reset(); } //-------------------------------------------------------------------------------------------------------------- /** * Strip the "analyzed" data out of all navigation areas */ void StripNavigationAreas( void ) { NavAreaList::iterator iter; for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; area->Strip(); } } //-------------------------------------------------------------------------------------------------------------- /** * Remove "analyzed" data from nav area */ void CNavArea::Strip( void ) { m_approachCount = 0; m_spotEncounterList.clear(); // memory leak } //-------------------------------------------------------------------------------------------------------------- /** * 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 TraceResult result; if (traceIgnore) UTIL_TraceLine( *start, pos, ignore_monsters, ENT( traceIgnore->pev ), &result ); else UTIL_TraceLine( *start, pos, ignore_monsters, NULL, &result ); if (result.flFraction != 1.0f) break; area = TheNavAreaGrid.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; } //-------------------------------------------------------------------------------------------------------------- /** * 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( fromPos->x, fromPos->y, fromPos->z + HumanHeight ); Vector to( toPos->x, toPos->y, from.z ); TraceResult result; UTIL_TraceLine( from, to, ignore_monsters, NULL, &result ); if (result.flFraction != 1.0f || result.fStartSolid) return false; from = to; to.z = toPos->z + 2.0f; UTIL_TraceLine( from, to, ignore_monsters, NULL, &result ); if (result.flFraction != 1.0f || result.fStartSolid) 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; } //-------------------------------------------------------------------------------------------------------------- /** * Define connections between adjacent generated areas */ void ConnectGeneratedAreas( void ) { CONSOLE_ECHO( " Connecting navigation areas...\n" ); for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; // 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 ]; 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 ); } } } // east edge - this edge's nodes are actually part of adjacent areas node = area->m_node[ NORTH_EAST ]; 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 ); } } } } } //-------------------------------------------------------------------------------------------------------------- /** * Merge areas together to make larger ones (must remain rectangular - convex). * Areas can only be merged if their attributes match. */ void MergeGeneratedAreas( void ) { CONSOLE_ECHO( " Merging navigation areas...\n" ); bool merged; do { merged = false; for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; // north edge NavConnectList::iterator citer; for( citer = area->m_connect[ NORTH ].begin(); citer != area->m_connect[ NORTH ].end(); ++citer ) { CNavArea *adjArea = (*citer).area; 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( citer = area->m_connect[ SOUTH ].begin(); citer != area->m_connect[ SOUTH ].end(); ++citer ) { CNavArea *adjArea = (*citer).area; 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( citer = area->m_connect[ WEST ].begin(); citer != area->m_connect[ WEST ].end(); ++citer ) { CNavArea *adjArea = (*citer).area; 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( citer = area->m_connect[ EAST ].begin(); citer != area->m_connect[ EAST ].end(); ++citer ) { CNavArea *adjArea = (*citer).area; 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 ); } //-------------------------------------------------------------------------------------------------------------- /** * Return true if area is more or less square. * This is used when merging to prevent long, thin, areas being created. */ inline bool IsAreaRoughlySquare( const CNavArea *area ) { float aspect = area->GetSizeX() / area->GetSizeY(); const float maxAspect = 3.01; const float minAspect = 1.0f / maxAspect; if (aspect < minAspect || aspect > maxAspect) return false; return true; } //-------------------------------------------------------------------------------------------------------------- /** * Recursively chop area in half along X until child areas are roughly square */ void SplitX( CNavArea *area ) { if (IsAreaRoughlySquare( area )) return; float split = area->GetSizeX(); split /= 2.0f; split += area->GetExtent()->lo.x; SnapToGrid( &split ); const float epsilon = 0.1f; if (abs(split - area->GetExtent()->lo.x) < epsilon || abs(split - area->GetExtent()->hi.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 */ void SplitY( CNavArea *area ) { if (IsAreaRoughlySquare( area )) return; float split = area->GetSizeY(); split /= 2.0f; split += area->GetExtent()->lo.y; SnapToGrid( &split ); const float epsilon = 0.1f; if (abs(split - area->GetExtent()->lo.y) < epsilon || abs(split - area->GetExtent()->hi.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 SquareUpAreas( void ) { NavAreaList::iterator iter = TheNavAreaList.begin(); while( iter != TheNavAreaList.end() ) { CNavArea *area = *iter; ++iter; if (!IsAreaRoughlySquare( area )) { // chop this area into square pieces if (area->GetSizeX() > area->GetSizeY()) SplitX( area ); else SplitY( area ); } } } //-------------------------------------------------------------------------------------------------------------- /** * 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 TestArea( CNavNode *node, int width, int height ) { Vector normal = *node->GetNormal(); float d = -DotProduct( normal, *node->GetPosition() ); const float offPlaneTolerance = 5.0f; CNavNode *vertNode, *horizNode; vertNode = node; for( int y=0; yGetAttributes() != node->GetAttributes()) return false; if (horizNode->IsCovered()) return false; if (!horizNode->IsClosedCell()) 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 = abs(DotProduct( *horizNode->GetPosition(), normal ) + d); if (dist > offPlaneTolerance) 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 = abs(DotProduct( *vertNode->GetPosition(), normal ) + d); if (dist > offPlaneTolerance) return false; } } // check planarity of southern edge if (width > 1 || height > 1) { horizNode = vertNode; for( int x=0; xGetConnectedNode( EAST ); if (horizNode == NULL) return false; // nodes must lie on/near the plane float dist = abs(DotProduct( *horizNode->GetPosition(), normal ) + d); if (dist > offPlaneTolerance) return false; } } 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 BuildArea( CNavNode *node, int width, int height ) { //CONSOLE_ECHO( "BuildArea( #%d, %d, %d )\n", node->GetID(), width, 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; yCover(); ++coveredNodes; horizNode = horizNode->GetConnectedNode( EAST ); } if (y == 0) neNode = horizNode; vertNode = vertNode->GetConnectedNode( SOUTH ); } swNode = vertNode; horizNode = vertNode; for( int x=0; xGetConnectedNode( EAST ); } seNode = horizNode; if (!nwNode || !neNode || !seNode || !swNode) { CONSOLE_ECHO( "ERROR: BuildArea - NULL node.\n" ); return -1; } CNavArea *area = new CNavArea( nwNode, neNode, seNode, swNode ); TheNavAreaList.push_back( area ); // since all internal nodes have the same attributes, set this area's attributes area->SetAttributes( node->GetAttributes() ); // fprintf( fp, "f %d %d %d %d\n", nwNode->m_id, neNode->m_id, seNode->m_id, swNode->m_id ); return coveredNodes; } //-------------------------------------------------------------------------------------------------------------- /** * For each ladder in the map, create a navigation representation of it. */ void BuildLadders( void ) { // remove any left-over ladders DestroyLadders(); TraceResult result; CBaseEntity *entity = UTIL_FindEntityByClassname( NULL, "func_ladder" ); while( entity && !FNullEnt( entity->edict() ) ) { CNavLadder *ladder = new CNavLadder; // compute top & bottom of ladder ladder->m_top.x = (entity->pev->absmin.x + entity->pev->absmax.x) / 2.0f; ladder->m_top.y = (entity->pev->absmin.y + entity->pev->absmax.y) / 2.0f; ladder->m_top.z = entity->pev->absmax.z; ladder->m_bottom.x = ladder->m_top.x; ladder->m_bottom.y = ladder->m_top.y; ladder->m_bottom.z = entity->pev->absmin.z; // determine facing - assumes "normal" runged ladder float xSize = entity->pev->absmax.x - entity->pev->absmin.x; float ySize = entity->pev->absmax.y - entity->pev->absmin.y; 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 ); Vector to = ladder->m_top + Vector( 0.0f, GenerationStepSize, -GenerationStepSize ); UTIL_TraceLine( from, to, ignore_monsters, ENT( entity->pev ), &result ); if (result.flFraction != 1.0f || result.fStartSolid) ladder->m_dir = NORTH; else ladder->m_dir = SOUTH; } else { // ladder is facing east or west - determine which way Vector from = ladder->m_bottom + Vector( GenerationStepSize, 0.0f, GenerationStepSize ); Vector to = ladder->m_top + Vector( GenerationStepSize, 0.0f, -GenerationStepSize ); UTIL_TraceLine( from, to, ignore_monsters, ENT( entity->pev ), &result ); if (result.flFraction != 1.0f || result.fStartSolid) ladder->m_dir = WEST; else ladder->m_dir = EAST; } // 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; AddDirectionVector( &out, ladder->m_dir, minLadderClearance ); UTIL_TraceLine( on, out, ignore_monsters, ENT( entity->pev ), &result ); if (result.flFraction == 1.0f && !result.fStartSolid) { // 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; AddDirectionVector( &out, ladder->m_dir, minLadderClearance ); UTIL_TraceLine( on, out, ignore_monsters, ENT( entity->pev ), &result ); if (result.flFraction == 1.0f && !result.fStartSolid) { // found viable ladder top ladder->m_top = on; break; } } ladder->m_length = (ladder->m_top - ladder->m_bottom).Length(); DirectionToVector2D( ladder->m_dir, &ladder->m_dirVector ); ladder->m_entity = entity; const float nearLadderRange = 75.0f; // 50 // // Find naviagtion area at bottom of ladder // // get approximate postion of player on ladder Vector center = ladder->m_bottom + Vector( 0, 0, GenerationStepSize ); AddDirectionVector( ¢er, ladder->m_dir, HalfHumanWidth ); ladder->m_bottomArea = TheNavAreaGrid.GetNearestNavArea( ¢er, true ); if (!ladder->m_bottomArea) { ALERT( at_console, "ERROR: Unconnected ladder bottom at ( %g, %g, %g )\n", ladder->m_bottom.x, ladder->m_bottom.y, ladder->m_bottom.z ); } else { // store reference to ladder in the area ladder->m_bottomArea->AddLadderUp( ladder ); } // // Find adjacent navigation areas at the top of the ladder // // get approximate postion of player on ladder center = ladder->m_top + Vector( 0, 0, GenerationStepSize ); AddDirectionVector( ¢er, ladder->m_dir, HalfHumanWidth ); // find "ahead" area ladder->m_topForwardArea = FindFirstAreaInDirection( ¢er, OppositeDirection( ladder->m_dir ), nearLadderRange, 120.0f, entity ); if (ladder->m_topForwardArea == ladder->m_bottomArea) ladder->m_topForwardArea = NULL; // find "left" area ladder->m_topLeftArea = FindFirstAreaInDirection( ¢er, DirectionLeft( ladder->m_dir ), nearLadderRange, 120.0f, entity ); if (ladder->m_topLeftArea == ladder->m_bottomArea) ladder->m_topLeftArea = NULL; // find "right" area ladder->m_topRightArea = FindFirstAreaInDirection( ¢er, DirectionRight( ladder->m_dir ), nearLadderRange, 120.0f, entity ); if (ladder->m_topRightArea == ladder->m_bottomArea) ladder->m_topRightArea = NULL; // find "behind" area - must look farther, since ladder is against the wall away from this area ladder->m_topBehindArea = FindFirstAreaInDirection( ¢er, ladder->m_dir, 2.0f*nearLadderRange, 120.0f, entity ); if (ladder->m_topBehindArea == ladder->m_bottomArea) ladder->m_topBehindArea = NULL; // can't include behind area, since it is not used when going up a ladder if (!ladder->m_topForwardArea && !ladder->m_topLeftArea && !ladder->m_topRightArea) ALERT( at_console, "ERROR: Unconnected ladder top at ( %g, %g, %g )\n", ladder->m_top.x, ladder->m_top.y, ladder->m_top.z ); // store reference to ladder in the area(s) if (ladder->m_topForwardArea) ladder->m_topForwardArea->AddLadderDown( ladder ); if (ladder->m_topLeftArea) ladder->m_topLeftArea->AddLadderDown( ladder ); if (ladder->m_topRightArea) ladder->m_topRightArea->AddLadderDown( ladder ); if (ladder->m_topBehindArea) ladder->m_topBehindArea->AddLadderDown( ladder ); // adjust top of ladder to highest connected area float topZ = -99999.9f; bool topAdjusted = false; CNavArea *topAreaList[4]; topAreaList[0] = ladder->m_topForwardArea; topAreaList[1] = ladder->m_topLeftArea; topAreaList[2] = ladder->m_topRightArea; topAreaList[3] = ladder->m_topBehindArea; for( int a=0; a<4; ++a ) { CNavArea *topArea = topAreaList[a]; if (topArea == NULL) continue; Vector close; topArea->GetClosestPointOnArea( &ladder->m_top, &close ); if (topZ < close.z) { topZ = close.z; topAdjusted = true; } } if (topAdjusted) ladder->m_top.z = topZ; // // Determine whether this ladder is "dangling" or not // "Dangling" ladders are too high to go up // ladder->m_isDangling = false; if (ladder->m_bottomArea) { Vector bottomSpot; ladder->m_bottomArea->GetClosestPointOnArea( &ladder->m_bottom, &bottomSpot ); if (ladder->m_bottom.z - bottomSpot.z > HumanHeight) ladder->m_isDangling = true; } // add ladder to global list TheNavLadderList.push_back( ladder ); entity = UTIL_FindEntityByClassname( entity, "func_ladder" ); } } //-------------------------------------------------------------------------------------------------------------- /** * Mark all areas that require a jump to get through them. * This currently relies on jump areas having extreme slope. */ void MarkJumpAreas( void ) { for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; Vector u, v; // compute our unit surface normal u.x = area->m_extent.hi.x - area->m_extent.lo.x; u.y = 0.0f; u.z = area->m_neZ - area->m_extent.lo.z; v.x = 0.0f; v.y = area->m_extent.hi.y - area->m_extent.lo.y; v.z = area->m_swZ - area->m_extent.lo.z; Vector normal = CrossProduct( u, v ); normal.NormalizeInPlace(); if (normal.z < MaxUnitZSlope) area->SetAttributes( area->GetAttributes() | NAV_JUMP ); } } //-------------------------------------------------------------------------------------------------------------- /** * 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, allowing the AI to 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 GenerateNavigationAreaMesh( void ) { // haven't yet seen a map use larger than 30... int tryWidth = 50; int tryHeight = 50; 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) { CONSOLE_ECHO( "GenerateNavigationAreaMesh: Error - Data corrupt.\n" ); return; } uncoveredNodes -= covered; } } if (tryWidth >= tryHeight) --tryWidth; else --tryHeight; if (tryWidth <= 0 || tryHeight <= 0) break; } 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 NavAreaList::iterator iter; for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; const Extent *areaExtent = area->GetExtent(); 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 TheNavAreaGrid.Initialize( extent.lo.x, extent.hi.x, extent.lo.y, extent.hi.y ); for( iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) TheNavAreaGrid.AddNavArea( *iter ); ConnectGeneratedAreas(); MergeGeneratedAreas(); SquareUpAreas(); MarkJumpAreas(); } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'pos' is within 2D extents of area. */ bool CNavArea::IsOverlapping( const Vector *pos ) const { if (pos->x >= m_extent.lo.x && pos->x <= m_extent.hi.x && pos->y >= m_extent.lo.y && pos->y <= m_extent.hi.y) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'area' overlaps our 2D extents */ bool CNavArea::IsOverlapping( const CNavArea *area ) const { if (area->m_extent.lo.x < m_extent.hi.x && area->m_extent.hi.x > m_extent.lo.x && area->m_extent.lo.y < m_extent.hi.y && area->m_extent.hi.y > m_extent.lo.y) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'area' overlaps our X extent */ bool CNavArea::IsOverlappingX( const CNavArea *area ) const { if (area->m_extent.lo.x < m_extent.hi.x && area->m_extent.hi.x > m_extent.lo.x) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if 'area' overlaps our Y extent */ bool CNavArea::IsOverlappingY( const CNavArea *area ) const { if (area->m_extent.lo.y < m_extent.hi.y && area->m_extent.hi.y > m_extent.lo.y) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if given point is on or above this area, but no others */ bool CNavArea::Contains( const Vector *pos ) const { // check 2D overlap if (!IsOverlapping( pos )) return false; // the point overlaps us, check that it is above us, but not above any areas that overlap us float ourZ = GetZ( pos ); // if we are above this point, fail if (ourZ > pos->z) return false; for( NavAreaList::const_iterator iter = m_overlapList.begin(); iter != m_overlapList.end(); ++iter ) { const CNavArea *area = *iter; // skip self if (area == this) continue; // check 2D overlap if (!area->IsOverlapping( pos )) continue; float theirZ = area->GetZ( pos ); if (theirZ > pos->z) { // they are above the point continue; } if (theirZ > ourZ) { // we are below an area that is closer underneath the point return false; } } return true; } //-------------------------------------------------------------------------------------------------------------- /** * Return true if this area and given area are approximately co-planar */ bool CNavArea::IsCoplanar( const CNavArea *area ) const { Vector u, v; // compute our unit surface normal u.x = m_extent.hi.x - m_extent.lo.x; u.y = 0.0f; u.z = m_neZ - m_extent.lo.z; v.x = 0.0f; v.y = m_extent.hi.y - m_extent.lo.y; v.z = m_swZ - m_extent.lo.z; Vector normal = CrossProduct( u, v ); normal.NormalizeInPlace(); // compute their unit surface normal u.x = area->m_extent.hi.x - area->m_extent.lo.x; u.y = 0.0f; u.z = area->m_neZ - area->m_extent.lo.z; v.x = 0.0f; v.y = area->m_extent.hi.y - area->m_extent.lo.y; v.z = area->m_swZ - area->m_extent.lo.z; Vector otherNormal = CrossProduct( u, v ); otherNormal.NormalizeInPlace(); // can only merge areas that are nearly planar, to ensure areas do not differ from underlying geometry much const float tolerance = 0.99f; // 0.7071f; // 0.9 if (DotProduct( normal, otherNormal ) > tolerance) return true; return false; } //-------------------------------------------------------------------------------------------------------------- /** * Return Z of area at (x,y) of 'pos' * Trilinear interpolation of Z values at quad edges. * NOTE: pos->z is not used. */ float CNavArea::GetZ( const Vector *pos ) const { float dx = m_extent.hi.x - m_extent.lo.x; float dy = m_extent.hi.y - m_extent.lo.y; // guard against division by zero due to degenerate areas if (dx == 0.0f || dy == 0.0f) return m_neZ; float u = (pos->x - m_extent.lo.x) / dx; float v = (pos->y - m_extent.lo.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_extent.lo.z + u * (m_neZ - m_extent.lo.z); float southZ = m_swZ + u * (m_extent.hi.z - m_swZ); return northZ + v * (southZ - northZ); } float CNavArea::GetZ( float x, float y ) const { Vector pos( x, y, 0.0f ); return GetZ( &pos ); } //-------------------------------------------------------------------------------------------------------------- /** * Return closest point to 'pos' on 'area'. * Returned point is in 'close'. */ void CNavArea::GetClosestPointOnArea( const Vector *pos, Vector *close ) const { const Extent *extent = GetExtent(); if (pos->x < extent->lo.x) { if (pos->y < extent->lo.y) { // position is north-west of area *close = extent->lo; } else if (pos->y > extent->hi.y) { // position is south-west of area close->x = extent->lo.x; close->y = extent->hi.y; } else { // position is west of area close->x = extent->lo.x; close->y = pos->y; } } else if (pos->x > extent->hi.x) { if (pos->y < extent->lo.y) { // position is north-east of area close->x = extent->hi.x; close->y = extent->lo.y; } else if (pos->y > extent->hi.y) { // position is south-east of area *close = extent->hi; } else { // position is east of area close->x = extent->hi.x; close->y = pos->y; } } else if (pos->y < extent->lo.y) { // position is north of area close->x = pos->x; close->y = extent->lo.y; } else if (pos->y > extent->hi.y) { // position is south of area close->x = pos->x; close->y = extent->hi.y; } else { // position is inside of area - it is the 'closest point' to itself *close = *pos; } close->z = GetZ( close ); } //-------------------------------------------------------------------------------------------------------------- /** * Return shortest distance squared between point and this area */ float CNavArea::GetDistanceSquaredToPoint( const Vector *pos ) const { const Extent *extent = GetExtent(); if (pos->x < extent->lo.x) { if (pos->y < extent->lo.y) { // position is north-west of area return (extent->lo - *pos).LengthSquared(); } else if (pos->y > extent->hi.y) { // position is south-west of area Vector d; d.x = extent->lo.x - pos->x; d.y = extent->hi.y - pos->y; d.z = m_swZ - pos->z; return d.LengthSquared(); } else { // position is west of area float d = extent->lo.x - pos->x; return d * d; } } else if (pos->x > extent->hi.x) { if (pos->y < extent->lo.y) { // position is north-east of area Vector d; d.x = extent->hi.x - pos->x; d.y = extent->lo.y - pos->y; d.z = m_neZ - pos->z; return d.LengthSquared(); } else if (pos->y > extent->hi.y) { // position is south-east of area return (extent->hi - *pos).LengthSquared(); } else { // position is east of area float d = pos->z - extent->hi.x; return d * d; } } else if (pos->y < extent->lo.y) { // position is north of area float d = extent->lo.y - pos->y; return d * d; } else if (pos->y > extent->hi.y) { // position is south of area float d = pos->y - extent->hi.y; return d * d; } else { // position is inside of 2D extent of area - find delta Z float z = GetZ( pos ); float d = z - pos->z; return d * d; } } //-------------------------------------------------------------------------------------------------------------- CNavArea *CNavArea::GetRandomAdjacentArea( NavDirType dir ) const { int count = m_connect[ dir ].size(); int which = RANDOM_LONG( 0, count-1 ); int i = 0; NavConnectList::const_iterator iter; for( iter = m_connect[ dir ].begin(); iter != m_connect[ dir ].end(); ++iter ) { if (i == which) return (*iter).area; i++; } return NULL; } //-------------------------------------------------------------------------------------------------------------- /** * Compute "portal" between to adjacent areas. * Return center of portal opening, and half-width defining sides of portal from center. * NOTE: center->z is unset. */ void CNavArea::ComputePortal( const CNavArea *to, NavDirType dir, Vector *center, float *halfWidth ) const { if (dir == NORTH || dir == SOUTH) { if (dir == NORTH) center->y = m_extent.lo.y; else center->y = m_extent.hi.y; float left = max( m_extent.lo.x, to->m_extent.lo.x ); float right = min( m_extent.hi.x, to->m_extent.hi.x ); // clamp to our extent in case areas are disjoint if (left < m_extent.lo.x) left = m_extent.lo.x; else if (left > m_extent.hi.x) left = m_extent.hi.x; if (right < m_extent.lo.x) right = m_extent.lo.x; else if (right > m_extent.hi.x) right = m_extent.hi.x; center->x = (left + right)/2.0f; *halfWidth = (right - left)/2.0f; } else // EAST or WEST { if (dir == WEST) center->x = m_extent.lo.x; else center->x = m_extent.hi.x; float top = max( m_extent.lo.y, to->m_extent.lo.y ); float bottom = min( m_extent.hi.y, to->m_extent.hi.y ); // clamp to our extent in case areas are disjoint if (top < m_extent.lo.y) top = m_extent.lo.y; else if (top > m_extent.hi.y) top = m_extent.hi.y; if (bottom < m_extent.lo.y) bottom = m_extent.lo.y; else if (bottom > m_extent.hi.y) bottom = m_extent.hi.y; center->y = (top + bottom)/2.0f; *halfWidth = (bottom - top)/2.0f; } } //-------------------------------------------------------------------------------------------------------------- /** * Compute closest point within the "portal" between to adjacent areas. */ void CNavArea::ComputeClosestPointInPortal( const CNavArea *to, NavDirType dir, const Vector *fromPos, Vector *closePos ) const { const float margin = GenerationStepSize/2.0f; if (dir == NORTH || dir == SOUTH) { if (dir == NORTH) closePos->y = m_extent.lo.y; else closePos->y = m_extent.hi.y; float left = max( m_extent.lo.x, to->m_extent.lo.x ); float right = min( m_extent.hi.x, to->m_extent.hi.x ); // clamp to our extent in case areas are disjoint if (left < m_extent.lo.x) left = m_extent.lo.x; else if (left > m_extent.hi.x) left = m_extent.hi.x; if (right < m_extent.lo.x) right = m_extent.lo.x; else if (right > m_extent.hi.x) right = m_extent.hi.x; // keep margin if against edge const float leftMargin = (to->IsEdge( WEST )) ? (left + margin) : left; const float rightMargin = (to->IsEdge( EAST )) ? (right - margin) : right; // limit x to within portal if (fromPos->x < leftMargin) closePos->x = leftMargin; else if (fromPos->x > rightMargin) closePos->x = rightMargin; else closePos->x = fromPos->x; } else // EAST or WEST { if (dir == WEST) closePos->x = m_extent.lo.x; else closePos->x = m_extent.hi.x; float top = max( m_extent.lo.y, to->m_extent.lo.y ); float bottom = min( m_extent.hi.y, to->m_extent.hi.y ); // clamp to our extent in case areas are disjoint if (top < m_extent.lo.y) top = m_extent.lo.y; else if (top > m_extent.hi.y) top = m_extent.hi.y; if (bottom < m_extent.lo.y) bottom = m_extent.lo.y; else if (bottom > m_extent.hi.y) bottom = m_extent.hi.y; // keep margin if against edge const float topMargin = (to->IsEdge( NORTH )) ? (top + margin) : top; const float bottomMargin = (to->IsEdge( SOUTH )) ? (bottom - margin) : bottom; // limit y to within portal if (fromPos->y < topMargin) closePos->y = topMargin; else if (fromPos->y > bottomMargin) closePos->y = bottomMargin; else closePos->y = fromPos->y; } } //-------------------------------------------------------------------------------------------------------------- /** * Return true if there are no bi-directional links on the given side */ bool CNavArea::IsEdge( NavDirType dir ) const { for( NavConnectList::const_iterator it = m_connect[ dir ].begin(); it != m_connect[ dir ].end(); ++it ) { const NavConnect connect = *it; if (connect.area->IsConnected( this, OppositeDirection( dir ) )) return false; } return true; } //-------------------------------------------------------------------------------------------------------------- /** * Return direction from this area to the given point */ NavDirType CNavArea::ComputeDirection( Vector *point ) const { if (point->x >= m_extent.lo.x && point->x <= m_extent.hi.x) { if (point->y < m_extent.lo.y) return NORTH; else if (point->y > m_extent.hi.y) return SOUTH; } else if (point->y >= m_extent.lo.y && point->y <= m_extent.hi.y) { if (point->x < m_extent.lo.x) return WEST; else if (point->x > m_extent.hi.x) return EAST; } // find closest direction Vector to = *point - m_center; if (abs(to.x) > abs(to.y)) { if (to.x > 0.0f) return EAST; return WEST; } else { if (to.y > 0.0f) return SOUTH; return NORTH; } return NUM_DIRECTIONS; } //-------------------------------------------------------------------------------------------------------------- /** * Draw area for debugging */ void CNavArea::Draw( byte red, byte green, byte blue, int duration ) { Vector nw, ne, sw, se; nw = m_extent.lo; se = m_extent.hi; ne.x = se.x; ne.y = nw.y; ne.z = m_neZ; sw.x = nw.x; sw.y = se.y; sw.z = m_swZ; nw.z += cv_bot_nav_zdraw.value; ne.z += cv_bot_nav_zdraw.value; sw.z += cv_bot_nav_zdraw.value; se.z += cv_bot_nav_zdraw.value; float border = 2.0f; nw.x += border; nw.y += border; ne.x -= border; ne.y += border; sw.x += border; sw.y -= border; se.x -= border; se.y -= border; UTIL_DrawBeamPoints( nw, ne, duration, red, green, blue ); UTIL_DrawBeamPoints( ne, se, duration, red, green, blue ); UTIL_DrawBeamPoints( se, sw, duration, red, green, blue ); UTIL_DrawBeamPoints( sw, nw, duration, red, green, blue ); if (GetAttributes() & NAV_CROUCH) UTIL_DrawBeamPoints( nw, se, duration, red, green, blue ); if (GetAttributes() & NAV_JUMP) { UTIL_DrawBeamPoints( nw, se, duration, red, green, blue ); UTIL_DrawBeamPoints( ne, sw, duration, red, green, blue ); } if (GetAttributes() & NAV_PRECISE) { float size = 8.0f; Vector up( m_center.x, m_center.y - size, m_center.z + cv_bot_nav_zdraw.value ); Vector down( m_center.x, m_center.y + size, m_center.z + cv_bot_nav_zdraw.value ); UTIL_DrawBeamPoints( up, down, duration, red, green, blue ); Vector left( m_center.x - size, m_center.y, m_center.z + cv_bot_nav_zdraw.value ); Vector right( m_center.x + size, m_center.y, m_center.z + cv_bot_nav_zdraw.value ); UTIL_DrawBeamPoints( left, right, duration, red, green, blue ); } if (GetAttributes() & NAV_NO_JUMP) { float size = 8.0f; Vector up( m_center.x, m_center.y - size, m_center.z + cv_bot_nav_zdraw.value ); Vector down( m_center.x, m_center.y + size, m_center.z + cv_bot_nav_zdraw.value ); Vector left( m_center.x - size, m_center.y, m_center.z + cv_bot_nav_zdraw.value ); Vector right( m_center.x + size, m_center.y, m_center.z + cv_bot_nav_zdraw.value ); UTIL_DrawBeamPoints( up, right, duration, red, green, blue ); UTIL_DrawBeamPoints( right, down, duration, red, green, blue ); UTIL_DrawBeamPoints( down, left, duration, red, green, blue ); UTIL_DrawBeamPoints( left, up, duration, red, green, blue ); } } //-------------------------------------------------------------------------------------------------------------- /** * Draw selected corner for debugging */ void CNavArea::DrawMarkedCorner( NavCornerType corner, byte red, byte green, byte blue, int duration ) { Vector nw, ne, sw, se; nw = m_extent.lo; se = m_extent.hi; ne.x = se.x; ne.y = nw.y; ne.z = m_neZ; sw.x = nw.x; sw.y = se.y; sw.z = m_swZ; nw.z += cv_bot_nav_zdraw.value; ne.z += cv_bot_nav_zdraw.value; sw.z += cv_bot_nav_zdraw.value; se.z += cv_bot_nav_zdraw.value; float border = 2.0f; nw.x += border; nw.y += border; ne.x -= border; ne.y += border; sw.x += border; sw.y -= border; se.x -= border; se.y -= border; switch( corner ) { case NORTH_WEST: UTIL_DrawBeamPoints( nw + Vector( 0, 0, 10 ), nw, duration, red, green, blue ); break; case NORTH_EAST: UTIL_DrawBeamPoints( ne + Vector( 0, 0, 10 ), ne, duration, red, green, blue ); break; case SOUTH_EAST: UTIL_DrawBeamPoints( se + Vector( 0, 0, 10 ), se, duration, red, green, blue ); break; case SOUTH_WEST: UTIL_DrawBeamPoints( sw + Vector( 0, 0, 10 ), sw, duration, red, green, blue ); break; } } //-------------------------------------------------------------------------------------------------------------- /** * Add to open list in decreasing value order */ void CNavArea::AddToOpenList( void ) { // mark as being on open list for quick check m_openMarker = m_masterMarker; // if list is empty, add and return if (m_openList == NULL) { m_openList = this; this->m_prevOpen = NULL; this->m_nextOpen = NULL; return; } // insert self in ascending cost order CNavArea *area, *last = NULL; for( area = m_openList; area; area = area->m_nextOpen ) { if (this->GetTotalCost() < area->GetTotalCost()) break; last = area; } if (area) { // insert before this area this->m_prevOpen = area->m_prevOpen; if (this->m_prevOpen) this->m_prevOpen->m_nextOpen = this; else m_openList = this; this->m_nextOpen = area; area->m_prevOpen = this; } else { // append to end of list last->m_nextOpen = this; this->m_prevOpen = last; this->m_nextOpen = NULL; } } //-------------------------------------------------------------------------------------------------------------- /** * A smaller value has been found, update this area on the open list * @todo "bubbling" does unnecessary work, since the order of all other nodes will be unchanged - only this node is altered */ void CNavArea::UpdateOnOpenList( void ) { // since value can only decrease, bubble this area up from current spot while( m_prevOpen && this->GetTotalCost() < m_prevOpen->GetTotalCost() ) { // swap position with predecessor CNavArea *other = m_prevOpen; CNavArea *before = other->m_prevOpen; CNavArea *after = this->m_nextOpen; this->m_nextOpen = other; this->m_prevOpen = before; other->m_prevOpen = this; other->m_nextOpen = after; if (before) before->m_nextOpen = this; else m_openList = this; if (after) after->m_prevOpen = other; } } //-------------------------------------------------------------------------------------------------------------- void CNavArea::RemoveFromOpenList( void ) { if (m_prevOpen) m_prevOpen->m_nextOpen = m_nextOpen; else m_openList = m_nextOpen; if (m_nextOpen) m_nextOpen->m_prevOpen = m_prevOpen; // zero is an invalid marker m_openMarker = 0; } //-------------------------------------------------------------------------------------------------------------- /** * Clears the open and closed lists for a new search */ void CNavArea::ClearSearchLists( void ) { // effectively clears all open list pointers and closed flags CNavArea::MakeNewMarker(); m_openList = NULL; } //-------------------------------------------------------------------------------------------------------------- /** * Return the coordinates of the area's corner. * NOTE: Do not retain the returned pointer - it is temporary. */ const Vector *CNavArea::GetCorner( NavCornerType corner ) const { static Vector pos; switch( corner ) { case NORTH_WEST: return &m_extent.lo; case NORTH_EAST: pos.x = m_extent.hi.x; pos.y = m_extent.lo.y; pos.z = m_neZ; return &pos; case SOUTH_WEST: pos.x = m_extent.lo.x; pos.y = m_extent.hi.y; pos.z = m_swZ; return &pos; case SOUTH_EAST: return &m_extent.hi; } return NULL; } //-------------------------------------------------------------------------------------------------------------- /** * Returns true if an existing hiding spot is too close to given position */ bool CNavArea::IsHidingSpotCollision( const Vector *pos ) const { const float collisionRange = 30.0f; for( HidingSpotList::const_iterator iter = m_hidingSpotList.begin(); iter != m_hidingSpotList.end(); ++iter ) { const HidingSpot *spot = *iter; if ((*spot->GetPosition() - *pos).IsLengthLessThan( collisionRange )) return true; } return false; } //-------------------------------------------------------------------------------------------------------------- bool IsHidingSpotInCover( const Vector *spot ) { int coverCount = 0; TraceResult result; Vector from = *spot; from.z += HalfHumanHeight; Vector to; // if we are crouched underneath something, that counts as good cover to = from + Vector( 0, 0, 20.0f ); UTIL_TraceLine( from, to, ignore_monsters, NULL, &result ); if (result.flFraction != 1.0f) return true; const float coverRange = 100.0f; const float inc = M_PI / 8.0f; for( float angle = 0.0f; angle < 2.0f * M_PI; angle += inc ) { to = from + Vector( coverRange * cos(angle), coverRange * sin(angle), HalfHumanHeight ); UTIL_TraceLine( from, to, ignore_monsters, NULL, &result ); // if traceline hit something, it hit "cover" if (result.flFraction != 1.0f) ++coverCount; } // if more than half of the circle has no cover, the spot is not "in cover" const int halfCover = 8; if (coverCount < halfCover) return false; return true; } //-------------------------------------------------------------------------------------------------------------- /** * Analyze local area neighborhood to find "hiding spots" for this area */ void CNavArea::ComputeHidingSpots( void ) { struct { float lo, hi; } extent; // "jump areas" cannot have hiding spots if (GetAttributes() & NAV_JUMP) return; int cornerCount[NUM_CORNERS]; for( int i=0; iIsConnected( this, OppositeDirection( static_cast( d ) ) ) == false) continue; // ignore jump areas if (connect.area->GetAttributes() & NAV_JUMP) continue; if (isHoriz) { if (connect.area->m_extent.lo.x < extent.lo) extent.lo = connect.area->m_extent.lo.x; if (connect.area->m_extent.hi.x > extent.hi) extent.hi = connect.area->m_extent.hi.x; } else { if (connect.area->m_extent.lo.y < extent.lo) extent.lo = connect.area->m_extent.lo.y; if (connect.area->m_extent.hi.y > extent.hi) extent.hi = connect.area->m_extent.hi.y; } } switch( d ) { case NORTH: if (extent.lo - m_extent.lo.x >= cornerSize) ++cornerCount[ NORTH_WEST ]; if (m_extent.hi.x - extent.hi >= cornerSize) ++cornerCount[ NORTH_EAST ]; break; case SOUTH: if (extent.lo - m_extent.lo.x >= cornerSize) ++cornerCount[ SOUTH_WEST ]; if (m_extent.hi.x - extent.hi >= cornerSize) ++cornerCount[ SOUTH_EAST ]; break; case EAST: if (extent.lo - m_extent.lo.y >= cornerSize) ++cornerCount[ NORTH_EAST ]; if (m_extent.hi.y - extent.hi >= cornerSize) ++cornerCount[ SOUTH_EAST ]; break; case WEST: if (extent.lo - m_extent.lo.y >= cornerSize) ++cornerCount[ NORTH_WEST ]; if (m_extent.hi.y - extent.hi >= cornerSize) ++cornerCount[ SOUTH_WEST ]; break; } } // if a corner count is 2, then it really is a corner (walls on both sides) float offset = 12.5f; if (cornerCount[ NORTH_WEST ] == 2) { Vector pos = *GetCorner( NORTH_WEST ) + Vector( offset, offset, 0.0f ); m_hidingSpotList.push_back( new HidingSpot( &pos, (IsHidingSpotInCover( &pos )) ? HidingSpot::IN_COVER : 0 ) ); } if (cornerCount[ NORTH_EAST ] == 2) { Vector pos = *GetCorner( NORTH_EAST ) + Vector( -offset, offset, 0.0f ); if (!IsHidingSpotCollision( &pos )) m_hidingSpotList.push_back( new HidingSpot( &pos, (IsHidingSpotInCover( &pos )) ? HidingSpot::IN_COVER : 0 ) ); } if (cornerCount[ SOUTH_WEST ] == 2) { Vector pos = *GetCorner( SOUTH_WEST ) + Vector( offset, -offset, 0.0f ); if (!IsHidingSpotCollision( &pos )) m_hidingSpotList.push_back( new HidingSpot( &pos, (IsHidingSpotInCover( &pos )) ? HidingSpot::IN_COVER : 0 ) ); } if (cornerCount[ SOUTH_EAST ] == 2) { Vector pos = *GetCorner( SOUTH_EAST ) + Vector( -offset, -offset, 0.0f ); if (!IsHidingSpotCollision( &pos )) m_hidingSpotList.push_back( new HidingSpot( &pos, (IsHidingSpotInCover( &pos )) ? HidingSpot::IN_COVER : 0 ) ); } } //-------------------------------------------------------------------------------------------------------------- /** * Determine how much walkable area we can see from the spot, and how far away we can see. */ void ClassifySniperSpot( HidingSpot *spot ) { Vector eye = *spot->GetPosition() + Vector( 0, 0, HalfHumanHeight ); // assume we are crouching Vector walkable; TraceResult result; Extent sniperExtent; float farthestRangeSq = 0.0f; const float minSniperRangeSq = 1000.0f * 1000.0f; bool found = false; for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; const Extent *extent = area->GetExtent(); // scan this area for( walkable.y = extent->lo.y + GenerationStepSize/2.0f; walkable.y < extent->hi.y; walkable.y += GenerationStepSize ) { for( walkable.x = extent->lo.x + GenerationStepSize/2.0f; walkable.x < extent->hi.x; walkable.x += GenerationStepSize ) { walkable.z = area->GetZ( &walkable ) + HalfHumanHeight; // check line of sight UTIL_TraceLine( eye, walkable, ignore_monsters, ignore_glass, NULL, &result ); if (result.flFraction == 1.0f && !result.fStartSolid) { // can see this spot // keep track of how far we can see float rangeSq = (eye - walkable).LengthSquared(); if (rangeSq > farthestRangeSq) { farthestRangeSq = rangeSq; if (rangeSq >= minSniperRangeSq) { // this is a sniper spot // determine how good of a sniper spot it is by keeping track of the snipable area if (found) { if (walkable.x < sniperExtent.lo.x) sniperExtent.lo.x = walkable.x; if (walkable.x > sniperExtent.hi.x) sniperExtent.hi.x = walkable.x; if (walkable.y < sniperExtent.lo.y) sniperExtent.lo.y = walkable.y; if (walkable.y > sniperExtent.hi.y) sniperExtent.hi.y = walkable.y; } else { sniperExtent.lo = walkable; sniperExtent.hi = walkable; found = true; } } } } } } } if (found) { // if we can see a large snipable area, it is an "ideal" spot float snipableArea = sniperExtent.Area(); const float minIdealSniperArea = 200.0f * 200.0f; const float longSniperRangeSq = 1500.0f * 1500.0f; if (snipableArea >= minIdealSniperArea || farthestRangeSq >= longSniperRangeSq) spot->SetFlags( HidingSpot::IDEAL_SNIPER_SPOT ); else spot->SetFlags( HidingSpot::GOOD_SNIPER_SPOT ); } } //-------------------------------------------------------------------------------------------------------------- /** * Analyze local area neighborhood to find "sniper spots" for this area */ void CNavArea::ComputeSniperSpots( void ) { if (cv_bot_quicksave.value > 0.0f) return; for( HidingSpotList::iterator iter = m_hidingSpotList.begin(); iter != m_hidingSpotList.end(); ++iter ) { HidingSpot *spot = *iter; ClassifySniperSpot( spot ); } } //-------------------------------------------------------------------------------------------------------------- /** * Given the areas we are moving between, return the spots we will encounter */ SpotEncounter *CNavArea::GetSpotEncounter( const CNavArea *from, const CNavArea *to ) { if (from && to) { SpotEncounter *e; for( SpotEncounterList::iterator iter = m_spotEncounterList.begin(); iter != m_spotEncounterList.end(); ++iter ) { e = &(*iter); if (e->from.area == from && e->to.area == to) return e; } } return NULL; } //-------------------------------------------------------------------------------------------------------------- /** * Add spot encounter data when moving from area to area */ void CNavArea::AddSpotEncounters( const CNavArea *from, NavDirType fromDir, const CNavArea *to, NavDirType toDir ) { SpotEncounter e; e.from.area = const_cast( from ); e.fromDir = fromDir; e.to.area = const_cast( to ); e.toDir = toDir; float halfWidth; ComputePortal( to, toDir, &e.path.to, &halfWidth ); ComputePortal( from, fromDir, &e.path.from, &halfWidth ); const float eyeHeight = HalfHumanHeight; e.path.from.z = from->GetZ( &e.path.from ) + eyeHeight; e.path.to.z = to->GetZ( &e.path.to ) + eyeHeight; // step along ray and track which spots can be seen Vector dir = e.path.to - e.path.from; float length = dir.NormalizeInPlace(); // create unique marker to flag used spots HidingSpot::ChangeMasterMarker(); const float stepSize = 25.0f; // 50 const float seeSpotRange = 2000.0f; // 3000 TraceResult result; Vector eye, delta; HidingSpot *spot; SpotOrder spotOrder; // step along path thru this area bool done = false; for( float along = 0.0f; !done; along += stepSize ) { // make sure we check the endpoint of the path segment if (along >= length) { along = length; done = true; } // move the eyepoint along the path segment eye = e.path.from + along * dir; // check each hiding spot for visibility for( HidingSpotList::iterator iter = TheHidingSpotList.begin(); iter != TheHidingSpotList.end(); ++iter ) { spot = *iter; // only look at spots with cover (others are out in the open and easily seen) if (!spot->HasGoodCover()) continue; if (spot->IsMarked()) continue; const Vector *spotPos = spot->GetPosition(); delta.x = spotPos->x - eye.x; delta.y = spotPos->y - eye.y; delta.z = (spotPos->z + eyeHeight) - eye.z; // check if in range if (delta.IsLengthGreaterThan( seeSpotRange )) continue; // check if we have LOS UTIL_TraceLine( eye, Vector( spotPos->x, spotPos->y, spotPos->z + HalfHumanHeight ), ignore_monsters, ignore_glass, NULL, &result ); if (result.flFraction != 1.0f) continue; // if spot is in front of us along our path, ignore it delta.NormalizeInPlace(); float dot = DotProduct( dir, delta ); if (dot < 0.7071f && dot > -0.7071f) { // we only want to keep spots that BECOME visible as we walk past them // therefore, skip ALL visible spots at the start of the path segment if (along > 0.0f) { // add spot to encounter spotOrder.spot = spot; spotOrder.t = along/length; e.spotList.push_back( spotOrder ); } } // mark spot as encountered spot->Mark(); } } // add encounter to list m_spotEncounterList.push_back( e ); } //-------------------------------------------------------------------------------------------------------------- /** * Compute "spot encounter" data. This is an ordered list of spots to look at * for each possible path thru a nav area. */ void CNavArea::ComputeSpotEncounters( void ) { m_spotEncounterList.clear(); if (cv_bot_quicksave.value > 0.0f) return; // for each adjacent area for( int fromDir=0; fromDirarea, (NavDirType)fromDir, toCon->area, (NavDirType)toDir ); } } } } } //-------------------------------------------------------------------------------------------------------------- /** * Decay the danger values */ void CNavArea::DecayDanger( void ) { // one kill == 1.0, which we will forget about in two minutes const float decayRate = 1.0f / 120.0f; for( int i=0; itime - m_dangerTimestamp[i]; float decayAmount = decayRate * deltaT; m_danger[i] -= decayAmount; if (m_danger[i] < 0.0f) m_danger[i] = 0.0f; // update timestamp m_dangerTimestamp[i] = gpGlobals->time; } } //-------------------------------------------------------------------------------------------------------------- /** * Increase the danger of this area for the given team */ void CNavArea::IncreaseDanger( int teamID, float amount ) { // before we add the new value, decay what's there DecayDanger(); m_danger[ teamID ] += amount; m_dangerTimestamp[ teamID ] = gpGlobals->time; } //-------------------------------------------------------------------------------------------------------------- /** * Return the danger of this area (decays over time) */ float CNavArea::GetDanger( int teamID ) { DecayDanger(); return m_danger[ teamID ]; } //-------------------------------------------------------------------------------------------------------------- /** * Increase the danger of nav areas containing and near the given position */ void IncreaseDangerNearby( int teamID, float amount, CNavArea *startArea, const Vector *pos, float maxRadius ) { if (startArea == NULL) return; CNavArea::MakeNewMarker(); CNavArea::ClearSearchLists(); startArea->AddToOpenList(); startArea->SetTotalCost( 0.0f ); startArea->Mark(); startArea->IncreaseDanger( teamID, amount ); while( !CNavArea::IsOpenListEmpty() ) { // get next area to check CNavArea *area = CNavArea::PopOpenList(); // area has no hiding spots, explore adjacent areas for( int dir=0; dirGetAdjacentCount( (NavDirType)dir ); for( int i=0; iGetAdjacentArea( (NavDirType)dir, i ); if (!adjArea->IsMarked()) { // compute distance from danger source float cost = (*adjArea->GetCenter() - *pos).Length(); if (cost <= maxRadius) { adjArea->AddToOpenList(); adjArea->SetTotalCost( cost ); adjArea->Mark(); adjArea->IncreaseDanger( teamID, amount * cost/maxRadius ); } } } } } } //-------------------------------------------------------------------------------------------------------------- /** * Show danger levels for debugging */ void DrawDanger( void ) { for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; Vector center = *area->GetCenter(); Vector top; center.z = area->GetZ( ¢er ); float danger = area->GetDanger( 0 ); if (danger > 0.1f) { top.x = center.x; top.y = center.y; top.z = center.z + 10.0f * danger; UTIL_DrawBeamPoints( center, top, 3.0f, 255, 0, 0 ); } danger = area->GetDanger( 1 ); if (danger > 0.1f) { top.x = center.x; top.y = center.y; top.z = center.z + 10.0f * danger; UTIL_DrawBeamPoints( center, top, 3.0f, 0, 0, 255 ); } } } //-------------------------------------------------------------------------------------------------------------- /** * If a player is at the given spot, return true */ bool IsSpotOccupied( CBaseEntity *me, const Vector *pos ) { const float closeRange = 75.0f; // 50 // is there a player in this spot float range; CBasePlayer *player = UTIL_GetClosestPlayer( pos, &range ); if (player != me) { if (player && range < closeRange) return true; } // is there is a hostage in this spot if (g_pHostages) { CHostage *hostage = g_pHostages->GetClosestHostage( *pos, &range ); if (hostage && hostage != me && range < closeRange) return true; } return false; } //-------------------------------------------------------------------------------------------------------------- class CollectHidingSpotsFunctor { public: CollectHidingSpotsFunctor( CBaseEntity *me, const Vector *origin, float range, unsigned char flags, Place place = UNDEFINED_PLACE, bool useCrouchAreas = true ) { m_me = me; m_count = 0; m_origin = origin; m_range = range; m_flags = flags; m_place = place; m_useCrouchAreas = useCrouchAreas; } enum { MAX_SPOTS = 256 }; bool operator() ( CNavArea *area ) { // if a place is specified, only consider hiding spots from areas in that place if (m_place != UNDEFINED_PLACE && area->GetPlace() != m_place) return true; // collect all the hiding spots in this area const HidingSpotList *list = area->GetHidingSpotList(); for( HidingSpotList::const_iterator iter = list->begin(); iter != list->end() && m_count < MAX_SPOTS; ++iter ) { const HidingSpot *spot = *iter; if (m_useCrouchAreas == false) { CNavArea *area = TheNavAreaGrid.GetNavArea( spot->GetPosition() ); if (area && (area->GetAttributes() & NAV_CROUCH)) continue; } // make sure hiding spot is in range if (m_range > 0.0f) if ((*spot->GetPosition() - *m_origin).IsLengthGreaterThan( m_range )) continue; // if a Player is using this hiding spot, don't consider it if (IsSpotOccupied( m_me, spot->GetPosition() )) { // player is in hiding spot /// @todo Check if player is moving or sitting still continue; } // only collect hiding spots with matching flags if (m_flags & spot->GetFlags()) { m_hidingSpot[ m_count++ ] = spot->GetPosition(); } } // if we've filled up, stop searching if (m_count == MAX_SPOTS) return false; return true; } /** * Remove the spot at index "i" */ void RemoveSpot( int i ) { if (m_count == 0) return; for( int j=i+1; jmaxClients; ++p ) { CBasePlayer *player = static_cast( UTIL_PlayerByIndex( p ) ); if (!IsEntityValid( player )) continue; if (player == ignore) continue; if (!player->IsAlive()) continue; if (ignoreTeam && player->m_iTeam == ignoreTeam) continue; // compute player's unit aiming vector UTIL_MakeVectors( player->pev->v_angle + player->pev->punchangle ); const float longRange = 5000.0f; Vector playerTarget = player->pev->origin + longRange * gpGlobals->v_forward; Vector result; if (IsIntersecting2D( start, finish, player->pev->origin, playerTarget, &result )) { // simple check to see if intersection lies in the Z range of the path float loZ, hiZ; if (start.z < finish.z) { loZ = start.z; hiZ = finish.z; } else { loZ = finish.z; hiZ = start.z; } if (result.z >= loZ && result.z <= hiZ + HumanHeight) return true; } } return false; } //-------------------------------------------------------------------------------------------------------------------- /** * Select a nearby retreat spot. * Don't pick a hiding spot that a Player is currently occupying. * If "avoidTeam" is nonzero, avoid getting close to members of that team. */ const Vector *FindNearbyRetreatSpot( CBaseEntity *me, const Vector *start, CNavArea *startArea, float maxRange, int avoidTeam, bool useCrouchAreas ) { if (startArea == NULL) return NULL; // collect hiding spots with decent "cover" CollectHidingSpotsFunctor collector( me, start, maxRange, HidingSpot::IN_COVER, UNDEFINED_PLACE, useCrouchAreas ); SearchSurroundingAreas( startArea, start, collector, maxRange ); if (collector.m_count == 0) return NULL; // find the closest unoccupied hiding spot that crosses the least lines of fire and has the best cover for( int i=0; imaxClients; ++i ) { CBasePlayer *player = static_cast( UTIL_PlayerByIndex( i ) ); if (player == ignore) continue; if (!IsEntityValid( player )) continue; if (!player->IsPlayer()) continue; if (!player->IsAlive()) continue; if (teamID == 0 || player->m_iTeam == teamID) if (Contains( &player->pev->origin )) ++count; } return count; } //-------------------------------------------------------------------------------------------------------------- static CNavArea *markedArea = NULL; static CNavArea *lastSelectedArea = NULL; static NavCornerType markedCorner = NUM_CORNERS; static bool isCreatingNavArea = false; ///< if true, we are manually creating a new nav area static bool isAnchored = false; static Vector anchor; static bool isPlaceMode = false; ///< if true, we are in place editing mode static bool isPlacePainting = false; ///< if true, we set an area's place by pointing at it static float editTimestamp = 0.0f; CNavArea *GetMarkedArea( void ) { return markedArea; } /** * Draw navigation areas and edit them */ void EditNavAreasReset( void ) { markedArea = NULL; lastSelectedArea = NULL; isCreatingNavArea = false; editTimestamp = 0.0f; isPlacePainting = false; lastDrawTimestamp = 0.0f; } void DrawHidingSpots( const CNavArea *area ) { const HidingSpotList *list = area->GetHidingSpotList(); for( HidingSpotList::const_iterator iter = list->begin(); iter != list->end(); ++iter ) { const HidingSpot *spot = *iter; int r, g, b; if (spot->IsIdealSniperSpot()) { r = 255; g = 0; b = 0; } else if (spot->IsGoodSniperSpot()) { r = 255; g = 0; b = 255; } else if (spot->HasGoodCover()) { r = 0; g = 255; b = 0; } else { r = 0; g = 0; b = 1; } UTIL_DrawBeamPoints( *spot->GetPosition(), *spot->GetPosition() + Vector( 0, 0, 50 ), 3, r, g, b ); } } //-------------------------------------------------------------------------------------------------------------- /** * Draw ourselves and adjacent areas */ void CNavArea::DrawConnectedAreas( void ) { CBasePlayer *player = UTIL_GetLocalPlayer(); if (player == NULL) return; CCSBotManager *ctrl = static_cast( TheBots ); const float maxRange = 500.0f; // draw self if (isPlaceMode) { if (GetPlace() == 0) Draw( 50, 0, 0, 3 ); else if (GetPlace() != ctrl->GetNavPlace()) Draw( 0, 0, 200, 3 ); else Draw( 0, 255, 0, 3 ); } else { Draw( 255, 255, 0, 3 ); DrawHidingSpots( this ); } // randomize order of directions to make sure all connected areas are // drawn, since we may have too many to render all at once int dirSet[ NUM_DIRECTIONS ]; int i; for( i=0; i= NUM_DIRECTIONS) nextI = 0; int tmp = dirSet[nextI]; dirSet[nextI] = dirSet[swapI]; dirSet[swapI] = tmp; } // draw connected areas for( i=0; iGetPlace() == 0) adj->Draw( 50, 0, 0, 3 ); else if (adj->GetPlace() != ctrl->GetNavPlace()) adj->Draw( 0, 0, 200, 3 ); else adj->Draw( 0, 255, 0, 3 ); } else { if (adj->IsDegenerate()) { static IntervalTimer blink; static bool blinkOn = false; if (blink.GetElapsedTime() > 1.0f) { blink.Reset(); blinkOn = !blinkOn; } if (blinkOn) adj->Draw( 255, 255, 255, 3 ); else adj->Draw( 255, 0, 255, 3 ); } else { adj->Draw( 255, 0, 0, 3 ); } DrawHidingSpots( adj ); Vector from, to; Vector hookPos; float halfWidth; float size = 5.0f; ComputePortal( adj, dir, &hookPos, &halfWidth ); switch( dir ) { case NORTH: from = hookPos + Vector( 0.0f, size, 0.0f ); to = hookPos + Vector( 0.0f, -size, 0.0f ); break; case SOUTH: from = hookPos + Vector( 0.0f, -size, 0.0f ); to = hookPos + Vector( 0.0f, size, 0.0f ); break; case EAST: from = hookPos + Vector( -size, 0.0f, 0.0f ); to = hookPos + Vector( +size, 0.0f, 0.0f ); break; case WEST: from = hookPos + Vector( size, 0.0f, 0.0f ); to = hookPos + Vector( -size, 0.0f, 0.0f ); break; } from.z = GetZ( &from ) + cv_bot_nav_zdraw.value; to.z = adj->GetZ( &to ) + cv_bot_nav_zdraw.value; Vector drawTo; adj->GetClosestPointOnArea( &to, &drawTo ); if (adj->IsConnected( this, OppositeDirection( dir ) ) ) UTIL_DrawBeamPoints( from, drawTo, 3, 0, 255, 255 ); else UTIL_DrawBeamPoints( from, drawTo, 3, 0, 0, 255 ); } } } } //-------------------------------------------------------------------------------------------------------------- /** * Raise/lower a corner */ void CNavArea::RaiseCorner( NavCornerType corner, int amount ) { if ( corner == NUM_CORNERS ) { m_extent.lo.z += amount; m_extent.hi.z += amount; m_neZ += amount; m_swZ += amount; } else { switch (corner) { case NORTH_WEST: m_extent.lo.z += amount; break; case NORTH_EAST: m_neZ += amount; break; case SOUTH_WEST: m_swZ += amount; break; case SOUTH_EAST: m_extent.hi.z += amount; break; } } m_center.x = (m_extent.lo.x + m_extent.hi.x)/2.0f; m_center.y = (m_extent.lo.y + m_extent.hi.y)/2.0f; m_center.z = (m_extent.lo.z + m_extent.hi.z)/2.0f; } /** * Flood fills all areas with current place */ class PlaceFloodFillFunctor { public: PlaceFloodFillFunctor( CNavArea *area ) { m_initialPlace = area->GetPlace(); } bool operator() ( CNavArea *area ) { CCSBotManager *ctrl = static_cast( TheBots ); if (area->GetPlace() != m_initialPlace) return false; area->SetPlace( ctrl->GetNavPlace() ); return true; } private: unsigned int m_initialPlace; }; //-------------------------------------------------------------------------------------------------------------- /** * Draw navigation areas and edit them */ void EditNavAreas( NavEditCmdType cmd ) { CCSBotManager *ctrl = static_cast( TheBots ); CBasePlayer *player = UTIL_GetLocalPlayer(); if (player == NULL) return; // don't draw too often on fast video cards or the areas may not appear (odd video effect) float drawTimestamp = gpGlobals->time; const float maxDrawRate = 0.05f; bool doDraw; if (drawTimestamp - lastDrawTimestamp < maxDrawRate) { doDraw = false; } else { doDraw = true; lastDrawTimestamp = drawTimestamp; } const float maxRange = 1000.0f; // 500 int beamTime = 1; if (doDraw) { // show ladder connections for( NavLadderList::iterator iter = TheNavLadderList.begin(); iter != TheNavLadderList.end(); ++iter ) { CNavLadder *ladder = *iter; float dx = player->pev->origin.x - ladder->m_bottom.x; float dy = player->pev->origin.y - ladder->m_bottom.y; if (dx*dx + dy*dy > maxRange*maxRange) continue; UTIL_DrawBeamPoints( ladder->m_top, ladder->m_bottom, beamTime, 255, 0, 255 ); Vector bottom = ladder->m_bottom; Vector top = ladder->m_top; AddDirectionVector( &top, ladder->m_dir, HalfHumanWidth ); AddDirectionVector( &bottom, ladder->m_dir, HalfHumanWidth ); UTIL_DrawBeamPoints( top, bottom, beamTime, 0, 0, 255 ); if (ladder->m_bottomArea) UTIL_DrawBeamPoints( bottom + Vector( 0, 0, GenerationStepSize ), *ladder->m_bottomArea->GetCenter(), beamTime, 0, 0, 255 ); if (ladder->m_topForwardArea) UTIL_DrawBeamPoints( top, *ladder->m_topForwardArea->GetCenter(), beamTime, 0, 0, 255 ); if (ladder->m_topLeftArea) UTIL_DrawBeamPoints( top, *ladder->m_topLeftArea->GetCenter(), beamTime, 0, 0, 255 ); if (ladder->m_topRightArea) UTIL_DrawBeamPoints( top, *ladder->m_topRightArea->GetCenter(), beamTime, 0, 0, 255 ); if (ladder->m_topBehindArea) UTIL_DrawBeamPoints( top, *ladder->m_topBehindArea->GetCenter(), beamTime, 0, 0, 255 ); } // draw approach points for marked area if (cv_bot_traceview.value == 3 && markedArea) { Vector ap; float halfWidth; for( int i=0; iGetApproachInfoCount(); ++i ) { const CNavArea::ApproachInfo *info = markedArea->GetApproachInfo( i ); // compute approach point if (info->hereToNextHow <= GO_WEST) { info->here.area->ComputePortal( info->next.area, (NavDirType)info->hereToNextHow, &ap, &halfWidth ); ap.z = info->next.area->GetZ( &ap ); } else { // use the area's center as an approach point ap = *info->here.area->GetCenter(); } UTIL_DrawBeamPoints( ap + Vector( 0, 0, 50 ), ap + Vector( 10, 0, 0 ), beamTime, 255, 100, 0 ); UTIL_DrawBeamPoints( ap + Vector( 0, 0, 50 ), ap + Vector( -10, 0, 0 ), beamTime, 255, 100, 0 ); UTIL_DrawBeamPoints( ap + Vector( 0, 0, 50 ), ap + Vector( 0, 10, 0 ), beamTime, 255, 100, 0 ); UTIL_DrawBeamPoints( ap + Vector( 0, 0, 50 ), ap + Vector( 0, -10, 0 ), beamTime, 255, 100, 0 ); } } } Vector dir; UTIL_MakeVectorsPrivate( player->pev->v_angle, dir, NULL, NULL ); Vector from = player->pev->origin + player->pev->view_ofs; // eye position Vector to = from + maxRange * dir; TraceResult result; UTIL_TraceLine( from, to, ignore_monsters, ignore_glass, ENT( player->pev ), &result ); if (result.flFraction != 1.0f) { // draw cursor Vector cursor = result.vecEndPos; float cursorSize = 10.0f; if (doDraw) { UTIL_DrawBeamPoints( cursor + Vector( 0, 0, cursorSize ), cursor, beamTime, 255, 255, 255 ); UTIL_DrawBeamPoints( cursor + Vector( cursorSize, 0, 0 ), cursor + Vector( -cursorSize, 0, 0 ), beamTime, 255, 255, 255 ); UTIL_DrawBeamPoints( cursor + Vector( 0, cursorSize, 0 ), cursor + Vector( 0, -cursorSize, 0 ), beamTime, 255, 255, 255 ); // show surface normal // UTIL_DrawBeamPoints( cursor + 50.0f * result.vecPlaneNormal, cursor, beamTime, 255, 0, 255 ); } if (isCreatingNavArea) { if (isAnchored) { // show drag rectangle if (doDraw) { float z = anchor.z + 2.0f; UTIL_DrawBeamPoints( Vector( cursor.x, cursor.y, z ), Vector( anchor.x, cursor.y, z ), beamTime, 0, 255, 255 ); UTIL_DrawBeamPoints( Vector( anchor.x, anchor.y, z ), Vector( anchor.x, cursor.y, z ), beamTime, 0, 255, 255 ); UTIL_DrawBeamPoints( Vector( anchor.x, anchor.y, z ), Vector( cursor.x, anchor.y, z ), beamTime, 0, 255, 255 ); UTIL_DrawBeamPoints( Vector( cursor.x, cursor.y, z ), Vector( cursor.x, anchor.y, z ), beamTime, 0, 255, 255 ); } } else { // anchor starting corner anchor = cursor; isAnchored = true; } } // find the area the player is pointing at CNavArea *area = TheNavAreaGrid.GetNearestNavArea( &result.vecEndPos ); if (area) { // if area changed, print its ID if (area != lastSelectedArea) { lastSelectedArea = area; char buffer[80]; char attrib[80]; char locName[80]; if (area->GetPlace()) { const char *name = TheBotPhrases->IDToName( area->GetPlace() ); if (name) strcpy( locName, name ); else strcpy( locName, "ERROR" ); } else { locName[0] = '\000'; } if (isPlaceMode) { attrib[0] = '\000'; } else { sprintf( attrib, "%s%s%s%s", (area->GetAttributes() & NAV_CROUCH) ? "CROUCH " : "", (area->GetAttributes() & NAV_JUMP) ? "JUMP " : "", (area->GetAttributes() & NAV_PRECISE) ? "PRECISE " : "", (area->GetAttributes() & NAV_NO_JUMP) ? "NO_JUMP " : "" ); } sprintf( buffer, "Area #%d %s %s\n", area->GetID(), locName, attrib ); UTIL_SayTextAll( buffer, player ); // do "place painting" if (isPlacePainting) { if (area->GetPlace() != ctrl->GetNavPlace()) { area->SetPlace( ctrl->GetNavPlace() ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/lightswitch2.wav", 1, ATTN_NORM, 0, 100 ); } } } if (isPlaceMode) { area->DrawConnectedAreas(); switch( cmd ) { case EDIT_TOGGLE_PLACE_MODE: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); isPlaceMode = false; return; case EDIT_TOGGLE_PLACE_PAINTING: { if (isPlacePainting) { isPlacePainting = false; EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/latchunlocked2.wav", 1, ATTN_NORM, 0, 100 ); } else { isPlacePainting = true; EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/lightswitch2.wav", 1, ATTN_NORM, 0, 100 ); // paint the initial area area->SetPlace( ctrl->GetNavPlace() ); } break; } case EDIT_PLACE_PICK: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); ctrl->SetNavPlace( area->GetPlace() ); break; case EDIT_PLACE_FLOODFILL: PlaceFloodFillFunctor pff( area ); SearchSurroundingAreas( area, area->GetCenter(), pff ); break; } } else // normal editing mode { // draw the "marked" area if (markedArea && doDraw) { markedArea->Draw( 0, 255, 255, beamTime ); if ( markedCorner != NUM_CORNERS ) markedArea->DrawMarkedCorner( markedCorner, 0, 0, 255, beamTime ); if (cv_bot_traceview.value == 11) { // draw areas connected to the marked area markedArea->DrawConnectedAreas(); } } // draw split line const Extent *extent = area->GetExtent(); float yaw = player->pev->v_angle.y; while( yaw > 360.0f ) yaw -= 360.0f; while( yaw < 0.0f ) yaw += 360.0f; float splitEdge; bool splitAlongX; if ((yaw < 45.0f || yaw > 315.0f) || (yaw > 135.0f && yaw < 225.0f)) { splitEdge = GenerationStepSize * (int)(result.vecEndPos.y/GenerationStepSize); from.x = extent->lo.x; from.y = splitEdge; from.z = area->GetZ( &from ) + cv_bot_nav_zdraw.value; to.x = extent->hi.x; to.y = splitEdge; to.z = area->GetZ( &to ) + cv_bot_nav_zdraw.value; splitAlongX = true; } else { splitEdge = GenerationStepSize * (int)(result.vecEndPos.x/GenerationStepSize); from.x = splitEdge; from.y = extent->lo.y; from.z = area->GetZ( &from ) + cv_bot_nav_zdraw.value; to.x = splitEdge; to.y = extent->hi.y; to.z = area->GetZ( &to ) + cv_bot_nav_zdraw.value; splitAlongX = false; } if (doDraw) UTIL_DrawBeamPoints( from, to, beamTime, 255, 255, 255 ); // draw the area we are pointing at and all connected areas if (doDraw && (cv_bot_traceview.value != 11 || markedArea == NULL)) area->DrawConnectedAreas(); // do area-dependant edit commands, if any switch( cmd ) { case EDIT_TOGGLE_PLACE_MODE: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); isPlaceMode = true; return; case EDIT_DELETE: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); TheNavAreaList.remove( area ); delete area; return; case EDIT_ATTRIB_CROUCH: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/bell1.wav", 1, ATTN_NORM, 0, 100 ); area->SetAttributes( area->GetAttributes() ^ NAV_CROUCH ); break; case EDIT_ATTRIB_JUMP: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/bell1.wav", 1, ATTN_NORM, 0, 100 ); area->SetAttributes( area->GetAttributes() ^ NAV_JUMP ); break; case EDIT_ATTRIB_PRECISE: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/bell1.wav", 1, ATTN_NORM, 0, 100 ); area->SetAttributes( area->GetAttributes() ^ NAV_PRECISE ); break; case EDIT_ATTRIB_NO_JUMP: EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/bell1.wav", 1, ATTN_NORM, 0, 100 ); area->SetAttributes( area->GetAttributes() ^ NAV_NO_JUMP ); break; case EDIT_SPLIT: if (area->SplitEdit( splitAlongX, splitEdge )) EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "weapons/knife_hitwall1.wav", 1, ATTN_NORM, 0, 100 ); else EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); break; case EDIT_MERGE: if (markedArea) { if (area->MergeEdit( markedArea )) EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); else EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } else { HintMessageToAllPlayers( "To merge, mark an area, highlight a second area, then invoke the merge command" ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; case EDIT_MARK: if (markedArea) { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); markedArea = NULL; } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip2.wav", 1, ATTN_NORM, 0, 100 ); markedArea = area; int connected = 0; connected += markedArea->GetAdjacentCount( NORTH ); connected += markedArea->GetAdjacentCount( SOUTH ); connected += markedArea->GetAdjacentCount( EAST ); connected += markedArea->GetAdjacentCount( WEST ); char buffer[80]; sprintf( buffer, "Marked Area is connected to %d other Areas\n", connected ); UTIL_SayTextAll( buffer, player ); } break; case EDIT_MARK_UNNAMED: if (markedArea) { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); markedArea = NULL; } else { markedArea = NULL; for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; if ( area->GetPlace() == 0 ) { markedArea = area; break; } } if ( !markedArea ) { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip2.wav", 1, ATTN_NORM, 0, 100 ); int connected = 0; connected += markedArea->GetAdjacentCount( NORTH ); connected += markedArea->GetAdjacentCount( SOUTH ); connected += markedArea->GetAdjacentCount( EAST ); connected += markedArea->GetAdjacentCount( WEST ); int totalUnnamedAreas = 0; for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; if ( area->GetPlace() == 0 ) { ++totalUnnamedAreas; } } char buffer[80]; sprintf( buffer, "Marked Area is connected to %d other Areas - there are %d total unnamed areas\n", connected, totalUnnamedAreas ); UTIL_SayTextAll( buffer, player ); } } break; case EDIT_WARP_TO_MARK: if (markedArea) { CBasePlayer *pLocalPlayer = UTIL_GetLocalPlayer(); if ( pLocalPlayer && pLocalPlayer->m_iTeam == SPECTATOR && pLocalPlayer->pev->iuser1 == OBS_ROAMING ) { Vector origin = *markedArea->GetCenter() + Vector( 0, 0, 0.75f * HumanHeight ); UTIL_SetOrigin( pLocalPlayer->pev, origin ); } } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; case EDIT_CONNECT: if (markedArea) { NavDirType dir = markedArea->ComputeDirection( &cursor ); if (dir == NUM_DIRECTIONS) { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } else { markedArea->ConnectTo( area, dir ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); } } else { HintMessageToAllPlayers( "To connect areas, mark an area, highlight a second area, then invoke the connect command. Make sure the cursor is directly north, south, east, or west of the marked area." ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; case EDIT_DISCONNECT: if (markedArea) { markedArea->Disconnect( area ); area->Disconnect( markedArea ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); } else { HintMessageToAllPlayers( "To disconnect areas, mark an area, highlight a second area, then invoke the disconnect command. This will remove all connections between the two areas." ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; case EDIT_SPLICE: if (markedArea) { if (area->SpliceEdit( markedArea )) EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); else EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } else { HintMessageToAllPlayers( "To splice, mark an area, highlight a second area, then invoke the splice command to create an area between them" ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; case EDIT_SELECT_CORNER: if (markedArea) { int corner = (markedCorner + 1) % (NUM_CORNERS + 1); markedCorner = (NavCornerType)corner; EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; case EDIT_RAISE_CORNER: if (markedArea) { markedArea->RaiseCorner( markedCorner, 1 ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; case EDIT_LOWER_CORNER: if (markedArea) { markedArea->RaiseCorner( markedCorner, -1 ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; } } } // do area-independant edit commands, if any switch( cmd ) { case EDIT_BEGIN_AREA: { if (isCreatingNavArea) { isCreatingNavArea = false; EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip2.wav", 1, ATTN_NORM, 0, 100 ); isCreatingNavArea = true; isAnchored = false; } break; } case EDIT_END_AREA: { if (isCreatingNavArea) { // create the new nav area CNavArea *newArea = new CNavArea( &anchor, &cursor ); TheNavAreaList.push_back( newArea ); TheNavAreaGrid.AddNavArea( newArea ); EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/blip1.wav", 1, ATTN_NORM, 0, 100 ); // if we have a marked area, inter-connect the two if (markedArea) { const Extent *extent = markedArea->GetExtent(); if (anchor.x > extent->hi.x && cursor.x > extent->hi.x) { markedArea->ConnectTo( newArea, EAST ); newArea->ConnectTo( markedArea, WEST ); } else if (anchor.x < extent->lo.x && cursor.x < extent->lo.x) { markedArea->ConnectTo( newArea, WEST ); newArea->ConnectTo( markedArea, EAST ); } else if (anchor.y > extent->hi.y && cursor.y > extent->hi.y) { markedArea->ConnectTo( newArea, SOUTH ); newArea->ConnectTo( markedArea, NORTH ); } else if (anchor.y < extent->lo.y && cursor.y < extent->lo.y) { markedArea->ConnectTo( newArea, NORTH ); newArea->ConnectTo( markedArea, SOUTH ); } // propogate marked area to new area markedArea = newArea; } isCreatingNavArea = false; } else { EMIT_SOUND_DYN( ENT(UTIL_GetLocalPlayer()->pev), CHAN_ITEM, "buttons/button11.wav", 1, ATTN_NORM, 0, 100 ); } break; } } } // if our last command was not mark (or no command), clear the mark area if (cmd != EDIT_MARK && cmd != EDIT_BEGIN_AREA && cmd != EDIT_END_AREA && cmd != EDIT_MARK_UNNAMED && cmd != EDIT_WARP_TO_MARK && cmd != EDIT_SELECT_CORNER && cmd != EDIT_RAISE_CORNER && cmd != EDIT_LOWER_CORNER && cmd != EDIT_NONE) markedArea = NULL; // if our last command was not affecting the corner, clear the corner selection if (cmd != EDIT_SELECT_CORNER && cmd != EDIT_RAISE_CORNER && cmd != EDIT_LOWER_CORNER && cmd != EDIT_NONE) markedCorner = NUM_CORNERS; if (isCreatingNavArea && cmd != EDIT_BEGIN_AREA && cmd != EDIT_END_AREA && cmd != EDIT_NONE) isCreatingNavArea = false; } //-------------------------------------------------------------------------------------------------------------- /** * Return the ground height below this point in "height". * Return false if position is invalid (outside of map, in a solid area, etc). */ bool GetGroundHeight( const Vector *pos, float *height, Vector *normal ) { Vector to; to.x = pos->x; to.y = pos->y; to.z = pos->z - 9999.9f; float offset; Vector from; TraceResult result; edict_t *ignore = NULL; float ground = 0.0f; const float maxOffset = 100.0f; const float inc = 10.0f; #define MAX_GROUND_LAYERS 16 struct GroundLayerInfo { float ground; Vector normal; } layer[ MAX_GROUND_LAYERS ]; int layerCount = 0; for( offset = 1.0f; offset < maxOffset; offset += inc ) { from = *pos + Vector( 0, 0, offset ); UTIL_TraceLine( from, to, ignore_monsters, dont_ignore_glass, ignore, &result ); // if the trace came down thru a door, ignore the door and try again // also ignore breakable floors if (result.pHit) { if (FClassnameIs( VARS( result.pHit ), "func_door" ) || FClassnameIs( VARS( result.pHit ), "func_door_rotating" ) || (FClassnameIs( VARS( result.pHit ), "func_breakable" ) && VARS( result.pHit )->takedamage == DAMAGE_YES)) { ignore = result.pHit; // keep incrementing to avoid infinite loop if more than one entity is along the traceline... /// @todo Deal with multiple ignore entities in a single TraceLine() //offset -= inc; continue; } } if (result.fStartSolid == false) { // if we didnt start inside a solid area, the trace hit a ground layer // if this is a new ground layer, add it to the set if (layerCount == 0 || result.vecEndPos.z > layer[ layerCount-1 ].ground) { layer[ layerCount ].ground = result.vecEndPos.z; layer[ layerCount ].normal = result.vecPlaneNormal; ++layerCount; if (layerCount == MAX_GROUND_LAYERS) break; } } } if (layerCount == 0) return false; // find the lowest layer that allows a player to stand or crouch upon it int i; for( i=0; i= HalfHumanHeight) break; } *height = layer[ i ].ground; if (normal) *normal = layer[ i ].normal; return true; } //-------------------------------------------------------------------------------------------------------------- /** * Return the "simple" ground height below this point in "height". * This function is much faster, but less tolerant. Make sure the give position is "well behaved". * Return false if position is invalid (outside of map, in a solid area, etc). */ bool GetSimpleGroundHeight( const Vector *pos, float *height, Vector *normal ) { Vector to; to.x = pos->x; to.y = pos->y; to.z = pos->z - 9999.9f; TraceResult result; UTIL_TraceLine( *pos, to, ignore_monsters, dont_ignore_glass, NULL, &result ); if (result.fStartSolid) return false; *height = result.vecEndPos.z; if (normal) *normal = result.vecPlaneNormal; return true; } //-------------------------------------------------------------------------------------------------------------- enum { MAX_BLOCKED_AREAS = 256 }; static unsigned int BlockedID[ MAX_BLOCKED_AREAS ]; static int BlockedIDCount = 0; /** * Shortest path cost, paying attention to "blocked" areas */ class ApproachAreaCost { public: float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder ) { // check if this area is "blocked" for( int i=0; iGetID() == BlockedID[i]) return -1.0f; if (fromArea == NULL) { // first area in path, no cost return 0.0f; } else { // compute distance travelled 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; } } }; /** * Can we see this area? * For now, if we can see any corner, we can see the area * @todo Need to check LOS to more than the corners for large and/or long areas */ inline bool IsAreaVisible( const Vector *pos, const CNavArea *area ) { Vector corner; TraceResult result; for( int c=0; cGetCorner( (NavCornerType)c ); corner.z += 0.75f * HumanHeight; UTIL_TraceLine( *pos, corner, ignore_monsters, NULL, &result ); if (result.flFraction == 1.0f) { // we can see this area return true; } } return false; } /** * Determine the set of "approach areas". * An approach area is an area representing a place where players * move into/out of our local neighborhood of areas. */ void CNavArea::ComputeApproachAreas( void ) { m_approachCount = 0; if (cv_bot_quicksave.value > 0.0f) return; // use the center of the nav area as the "view" point Vector eye = m_center; if (GetGroundHeight( &eye, &eye.z ) == false) return; // approximate eye position if (GetAttributes() & NAV_CROUCH) eye.z += 0.9f * HalfHumanHeight; else eye.z += 0.9f * HumanHeight; enum { MAX_PATH_LENGTH = 256 }; CNavArea *path[ MAX_PATH_LENGTH ]; // // In order to enumerate all of the approach areas, we need to // run the algorithm many times, once for each "far away" area // and keep the union of the approach area sets // NavAreaList::iterator iter; for( iter = goodSizedAreaList.begin(); iter != goodSizedAreaList.end(); ++iter ) { CNavArea *farArea = *iter; BlockedIDCount = 0; // if we can see 'farArea', try again - the whole point is to go "around the bend", so to speak if (IsAreaVisible( &eye, farArea )) continue; // make first path to far away area ApproachAreaCost cost; if (NavAreaBuildPath( this, farArea, NULL, cost ) == false) continue; // // Keep building paths to farArea and blocking them off until we // cant path there any more. // As areas are blocked off, all exits will be enumerated. // while( m_approachCount < MAX_APPROACH_AREAS ) { // find number of areas on path int count = 0; CNavArea *area; for( area = farArea; area; area = area->GetParent() ) ++count; if (count > MAX_PATH_LENGTH) count = MAX_PATH_LENGTH; // build path in correct order - from eye outwards int i = count; for( area = farArea; i && area; area = area->GetParent() ) path[ --i ] = area; // traverse path to find first area we cannot see (skip the first area) for( i=1; iGetID(); if (block == 0) break; // store new approach area if not already in set int a; for( a=0; a= 2) ? path[block-2] : NULL; m_approach[ m_approachCount ].here.area = path[block-1]; m_approach[ m_approachCount ].prevToHereHow = path[block-1]->GetParentHow(); m_approach[ m_approachCount ].next.area = path[block]; m_approach[ m_approachCount ].hereToNextHow = path[block]->GetParentHow(); ++m_approachCount; } // we are done with this path break; } // find another path to 'farArea' ApproachAreaCost cost; if (NavAreaBuildPath( this, farArea, NULL, cost ) == false) { // can't find a path to 'farArea' means all exits have been already tested and blocked break; } } } } //-------------------------------------------------------------------------------------------------------------- /** * The singleton for accessing the grid */ CNavAreaGrid TheNavAreaGrid; CNavAreaGrid::CNavAreaGrid( void ) : m_cellSize( 300.0f ) { m_grid = NULL; Reset(); } CNavAreaGrid::~CNavAreaGrid() { delete [] m_grid; m_grid = NULL; } /** * Clear the grid */ void CNavAreaGrid::Reset( void ) { if (m_grid) delete [] m_grid; m_grid = NULL; m_gridSizeX = 0; m_gridSizeY = 0; // clear the hash table for( int i=0; iGetExtent(); int loX = WorldToGridX( extent->lo.x ); int loY = WorldToGridY( extent->lo.y ); int hiX = WorldToGridX( extent->hi.x ); int hiY = WorldToGridY( extent->hi.y ); for( int y = loY; y <= hiY; ++y ) for( int x = loX; x <= hiX; ++x ) m_grid[ x + y*m_gridSizeX ].push_back( const_cast( area ) ); // add to hash table int key = ComputeHashKey( area->GetID() ); if (m_hashTable[key]) { // add to head of list in this slot area->m_prevHash = NULL; area->m_nextHash = m_hashTable[key]; m_hashTable[key]->m_prevHash = area; m_hashTable[key] = area; } else { // first entry in this slot m_hashTable[key] = area; area->m_nextHash = NULL; area->m_prevHash = NULL; } ++m_areaCount; } /** * Remove an area from the grid */ void CNavAreaGrid::RemoveNavArea( CNavArea *area ) { // add to grid const Extent *extent = area->GetExtent(); int loX = WorldToGridX( extent->lo.x ); int loY = WorldToGridY( extent->lo.y ); int hiX = WorldToGridX( extent->hi.x ); int hiY = WorldToGridY( extent->hi.y ); for( int y = loY; y <= hiY; ++y ) for( int x = loX; x <= hiX; ++x ) m_grid[ x + y*m_gridSizeX ].remove( area ); // remove from hash table int key = ComputeHashKey( area->GetID() ); if (area->m_prevHash) { area->m_prevHash->m_nextHash = area->m_nextHash; } else { // area was at start of list m_hashTable[key] = area->m_nextHash; if (m_hashTable[key]) m_hashTable[key]->m_prevHash = NULL; } if (area->m_nextHash) { area->m_nextHash->m_prevHash = area->m_prevHash; } --m_areaCount; } /** * Given a position, return the nav area that IsOverlapping and is *immediately* beneath it */ CNavArea *CNavAreaGrid::GetNavArea( const Vector *pos, float beneathLimit ) const { if (m_grid == NULL) return NULL; // get list in cell that contains position int x = WorldToGridX( pos->x ); int y = WorldToGridY( pos->y ); NavAreaList *list = &m_grid[ x + y*m_gridSizeX ]; // search cell list to find correct area CNavArea *use = NULL; float useZ = -99999999.9f; Vector testPos = *pos + Vector( 0, 0, 5 ); for( NavAreaList::iterator iter = list->begin(); iter != list->end(); ++iter ) { CNavArea *area = *iter; // check if position is within 2D boundaries of this area if (area->IsOverlapping( &testPos )) { // project position onto area to get Z float z = area->GetZ( &testPos ); // if area is above us, skip it if (z > testPos.z) continue; // if area is too far below us, skip it if (z < pos->z - beneathLimit) continue; // if area is higher than the one we have, use this instead if (z > useZ) { use = area; useZ = z; } } } return use; } //-------------------------------------------------------------------------------------------------------------- /** * Given a position in the world, return the nav area that is closest * and at the same height, or beneath it. * Used to find initial area if we start off of the mesh. */ CNavArea *CNavAreaGrid::GetNearestNavArea( const Vector *pos, bool anyZ ) const { if (m_grid == NULL) return NULL; CNavArea *close = NULL; float closeDistSq = 99999999.9f; // quick check close = GetNavArea( pos ); if (close) return close; // ensure source position is well behaved Vector source; source.x = pos->x; source.y = pos->y; if (GetGroundHeight( pos, &source.z ) == false) return NULL; source.z += HalfHumanHeight; /// @todo Step incrementally using grid for speed // find closest nav area for( NavAreaList::iterator iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); ++iter ) { CNavArea *area = *iter; Vector areaPos; area->GetClosestPointOnArea( &source, &areaPos ); float distSq = (areaPos - source).LengthSquared(); // keep the closest area if (distSq < closeDistSq) { // check LOS to area if (!anyZ) { TraceResult result; UTIL_TraceLine( source, areaPos + Vector( 0, 0, HalfHumanHeight ), ignore_monsters, ignore_glass, NULL, &result ); if (result.flFraction != 1.0f) continue; } closeDistSq = distSq; close = area; } } return close; } //-------------------------------------------------------------------------------------------------------------- /** * Given an ID, return the associated area */ CNavArea *CNavAreaGrid::GetNavAreaByID( unsigned int id ) const { if (id == 0) return NULL; int key = ComputeHashKey( id ); for( CNavArea *area = m_hashTable[key]; area; area = area->m_nextHash ) if (area->GetID() == id) return area; return NULL; } //-------------------------------------------------------------------------------------------------------------- /** * Return radio chatter place for given coordinate */ unsigned int CNavAreaGrid::GetPlace( const Vector *pos ) const { CNavArea *area = GetNearestNavArea( pos, true ); if (area) return area->GetPlace(); return UNDEFINED_PLACE; }