/* * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * In addition, as a special exception, the author gives permission to * link the code of this program with the Half-Life Game Engine ("HL * Engine") and Modified Game Libraries ("MODs") developed by Valve, * L.L.C ("Valve"). You must obey the GNU General Public License in all * respects for all of the code used other than the HL Engine and MODs * from Valve. If you modify this file, you may extend this exception * to your version of the file, but you are not obligated to do so. If * you do not wish to do so, delete this exception statement from your * version. * */ #pragma once #include class CNavArea; void DestroyHidingSpots(); void StripNavigationAreas(); bool SaveNavigationMap(const char *filename); NavErrorType LoadNavigationMap(); void DestroyNavigationMap(); enum NavEditCmdType { EDIT_NONE, EDIT_DELETE, // delete current area EDIT_SPLIT, // split current area EDIT_MERGE, // merge adjacent areas EDIT_JOIN, // define connection between areas EDIT_BREAK, // break connection between areas EDIT_MARK, // mark an area for further operations EDIT_ATTRIB_CROUCH, // toggle crouch attribute on current area EDIT_ATTRIB_JUMP, // toggle jump attribute on current area EDIT_ATTRIB_PRECISE, // toggle precise attribute on current area EDIT_ATTRIB_NO_JUMP, // toggle inhibiting discontinuity jumping in current area EDIT_BEGIN_AREA, // begin creating a new nav area EDIT_END_AREA, // end creation of the new nav area EDIT_CONNECT, // connect marked area to selected area EDIT_DISCONNECT, // disconnect marked area from selected area EDIT_SPLICE, // create new area in between marked and selected areas EDIT_TOGGLE_PLACE_MODE, // switch between normal and place editing EDIT_TOGGLE_PLACE_PAINTING, // switch between "painting" places onto areas EDIT_PLACE_FLOODFILL, // floodfill areas out from current area EDIT_PLACE_PICK, // "pick up" the place at the current area EDIT_MARK_UNNAMED, // mark an unnamed area for further operations EDIT_WARP_TO_MARK, // warp a spectating local player to the selected mark EDIT_SELECT_CORNER, // select a corner on the current area EDIT_RAISE_CORNER, // raise a corner on the current area EDIT_LOWER_CORNER, // lower a corner on the current area }; enum RouteType { FASTEST_ROUTE, SAFEST_ROUTE, }; union NavConnect { unsigned int id; CNavArea *area; bool operator==(const NavConnect &other) const { return (area == other.area) ? true : false; } }; typedef std::list NavConnectList; enum LadderDirectionType { LADDER_UP = 0, LADDER_DOWN, NUM_LADDER_DIRECTIONS }; class CNavLadder { public: CNavLadder() { m_topForwardArea = nullptr; m_topRightArea = nullptr; m_topLeftArea = nullptr; m_topBehindArea = nullptr; m_bottomArea = nullptr; m_entity = nullptr; } Vector m_top; Vector m_bottom; float m_length; NavDirType m_dir; Vector2D m_dirVector; CBaseEntity *m_entity; CNavArea *m_topForwardArea; CNavArea *m_topLeftArea; CNavArea *m_topRightArea; CNavArea *m_topBehindArea; CNavArea *m_bottomArea; bool m_isDangling; void OnDestroyNotify(CNavArea *dead) { if (dead == m_topForwardArea) m_topForwardArea = nullptr; if (dead == m_topLeftArea) m_topLeftArea = nullptr; if (dead == m_topRightArea) m_topRightArea = nullptr; if (dead == m_topBehindArea) m_topBehindArea = nullptr; if (dead == m_bottomArea) m_bottomArea = nullptr; } }; typedef std::list NavLadderList; extern NavLadderList TheNavLadderList; class HidingSpot { public: HidingSpot(); HidingSpot(const Vector *pos, unsigned char flags); enum { IN_COVER = 0x01, GOOD_SNIPER_SPOT = 0x02, IDEAL_SNIPER_SPOT = 0x04 }; bool HasGoodCover() const { return (m_flags & IN_COVER) ? true : false; } bool IsGoodSniperSpot() const { return (m_flags & GOOD_SNIPER_SPOT) ? true : false; } bool IsIdealSniperSpot() const { return (m_flags & IDEAL_SNIPER_SPOT) ? true : false; } void SetFlags(unsigned char flags) { m_flags |= flags; } unsigned char GetFlags() const { return m_flags; } void Save(int fd, unsigned int version) const; void Load(SteamFile *file, unsigned int version); const Vector *GetPosition() const { return &m_pos; } unsigned int GetID() const { return m_id; } void Mark() { m_marker = m_masterMarker; } bool IsMarked() const { return (m_marker == m_masterMarker) ? true : false; } static void ChangeMasterMarker() { m_masterMarker++; } private: friend void DestroyHidingSpots(); Vector m_pos; unsigned int m_id; unsigned int m_marker; unsigned char m_flags; static unsigned int m_nextID; static unsigned int m_masterMarker; }; typedef std::list HidingSpotList; extern HidingSpotList TheHidingSpotList; struct SpotOrder { float t; union { HidingSpot *spot; unsigned int id; }; }; typedef std::list SpotOrderList; struct SpotEncounter { NavConnect from; NavDirType fromDir; NavConnect to; NavDirType toDir; Ray path; // the path segment SpotOrderList spotList; // list of spots to look at, in order of occurrence }; typedef std::list SpotEncounterList; typedef std::list NavAreaList; // A CNavArea is a rectangular region defining a walkable area in the map class CNavArea { public: CNavArea(CNavNode *nwNode, CNavNode *neNode, CNavNode *seNode, CNavNode *swNode); CNavArea(); CNavArea(const Vector *corner, const Vector *otherCorner); CNavArea(const Vector *nwCorner, const Vector *neCorner, const Vector *seCorner, const Vector *swCorner); ~CNavArea(); void ConnectTo(CNavArea *area, NavDirType dir); // connect this area to given area in given direction void Disconnect(CNavArea *area); // disconnect this area from given area void Save(FILE *fp) const; void Save(int fd, unsigned int version); void Load(SteamFile *file, unsigned int version); NavErrorType PostLoad(); unsigned int GetID() const { return m_id; } void SetAttributes(unsigned char bits) { m_attributeFlags = bits; } unsigned char GetAttributes() const { return m_attributeFlags; } void SetPlace(Place place) { m_place = place; } // set place descriptor Place GetPlace() const { return m_place; } // get place descriptor bool IsOverlapping(const Vector *pos) const; // return true if 'pos' is within 2D extents of area bool IsOverlapping(const CNavArea *area) const; // return true if 'area' overlaps our 2D extents bool IsOverlappingX(const CNavArea *area) const; // return true if 'area' overlaps our X extent bool IsOverlappingY(const CNavArea *area) const; // return true if 'area' overlaps our Y extent int GetPlayerCount(int teamID = 0, CBasePlayer *ignore = nullptr) const; // return number of players with given teamID in this area (teamID == 0 means any/all) float GetZ(const Vector *pos) const; // return Z of area at (x,y) of 'pos' float GetZ(float x, float y) const; // return Z of area at (x,y) of 'pos' bool Contains(const Vector *pos) const; // return true if given point is on or above this area, but no others bool IsCoplanar(const CNavArea *area) const; // return true if this area and given area are approximately co-planar void GetClosestPointOnArea(const Vector *pos, Vector *close) const; // return closest point to 'pos' on this area - returned point in 'close' float GetDistanceSquaredToPoint(const Vector *pos) const; // return shortest distance between point and this area bool IsDegenerate() const; // return true if this area is badly formed bool IsEdge(NavDirType dir) const; // return true if there are no bi-directional links on the given side int GetAdjacentCount(NavDirType dir) const { return m_connect[dir].size(); } // return number of connected areas in given direction CNavArea *GetAdjacentArea(NavDirType dir, int i) const; // return the i'th adjacent area in the given direction CNavArea *GetRandomAdjacentArea(NavDirType dir) const; const NavConnectList *GetAdjacentList(NavDirType dir) const { return &m_connect[dir]; } bool IsConnected(const CNavArea *area, NavDirType dir) const; // return true if given area is connected in given direction float ComputeHeightChange(const CNavArea *area); // compute change in height from this area to given area const NavLadderList *GetLadderList(LadderDirectionType dir) const { return &m_ladder[dir]; } void ComputePortal(const CNavArea *to, NavDirType dir, Vector *center, float *halfWidth) const; // compute portal to adjacent area void ComputeClosestPointInPortal(const CNavArea *to, NavDirType dir, const Vector *fromPos, Vector *closePos) const; // compute closest point within the "portal" between to adjacent areas NavDirType ComputeDirection(Vector *point) const; // return direction from this area to the given point // for hunting algorithm void SetClearedTimestamp(int teamID) { m_clearedTimestamp[teamID] = gpGlobals->time; } // set this area's "clear" timestamp to now float GetClearedTimestamp(int teamID) { return m_clearedTimestamp[teamID]; } // get time this area was marked "clear" // hiding spots const HidingSpotList *GetHidingSpotList() const { return &m_hidingSpotList; } void ComputeHidingSpots(); // analyze local area neighborhood to find "hiding spots" in this area - for map learning void ComputeSniperSpots(); // analyze local area neighborhood to find "sniper spots" in this area - for map learning SpotEncounter *GetSpotEncounter(const CNavArea *from, const CNavArea *to); // given the areas we are moving between, return the spots we will encounter void ComputeSpotEncounters(); // compute spot encounter data - for map learning // danger void IncreaseDanger(int teamID, float amount); // increase the danger of this area for the given team float GetDanger(int teamID); // return the danger of this area (decays over time) float GetSizeX() const { return m_extent.hi.x - m_extent.lo.x; } float GetSizeY() const { return m_extent.hi.y - m_extent.lo.y; } const Extent *GetExtent() const { return &m_extent; } const Vector *GetCenter() const { return &m_center; } const Vector *GetCorner(NavCornerType corner) const; // approach areas struct ApproachInfo { NavConnect here; // the approach area NavConnect prev; // the area just before the approach area on the path NavTraverseType prevToHereHow; NavConnect next; // the area just after the approach area on the path NavTraverseType hereToNextHow; }; const ApproachInfo *GetApproachInfo(int i) const { return &m_approach[i]; } int GetApproachInfoCount() const { return m_approachCount; } void ComputeApproachAreas(); // determine the set of "approach areas" - for map learning // A* pathfinding algorithm static void MakeNewMarker() { if (++m_masterMarker == 0) m_masterMarker = 1; } void Mark() { m_marker = m_masterMarker; } BOOL IsMarked() const { return (m_marker == m_masterMarker) ? true : false; } void SetParent(CNavArea *parent, NavTraverseType how = NUM_TRAVERSE_TYPES) { m_parent = parent; m_parentHow = how; } CNavArea *GetParent() const { return m_parent; } NavTraverseType GetParentHow() const { return m_parentHow; } bool IsOpen() const; // true if on "open list" void AddToOpenList(); // add to open list in decreasing value order void UpdateOnOpenList(); // a smaller value has been found, update this area on the open list void RemoveFromOpenList(); static bool IsOpenListEmpty(); static CNavArea *PopOpenList(); // remove and return the first element of the open list bool IsClosed() const; // true if on "closed list" void AddToClosedList(); // add to the closed list void RemoveFromClosedList(); static void ClearSearchLists(); // clears the open and closed lists for a new search void SetTotalCost(float value) { m_totalCost = value; } float GetTotalCost() const { return m_totalCost; } void SetCostSoFar(float value) { m_costSoFar = value; } float GetCostSoFar() const { return m_costSoFar; } // editing void Draw(byte red, byte green, byte blue, int duration = 50); // draw area for debugging & editing void DrawConnectedAreas(); void DrawMarkedCorner(NavCornerType corner, byte red, byte green, byte blue, int duration = 50); bool SplitEdit(bool splitAlongX, float splitEdge, CNavArea **outAlpha = nullptr, CNavArea **outBeta = nullptr); // split this area into two areas at the given edge bool MergeEdit(CNavArea *adj); // merge this area and given adjacent area bool SpliceEdit(CNavArea *other); // create a new area between this area and given area void RaiseCorner(NavCornerType corner, int amount); // raise/lower a corner (or all corners if corner == NUM_CORNERS) // ladders void AddLadderUp(CNavLadder *ladder) { m_ladder[LADDER_UP].push_back(ladder); } void AddLadderDown(CNavLadder *ladder) { m_ladder[LADDER_DOWN].push_back(ladder); } private: friend void ConnectGeneratedAreas(); friend void MergeGeneratedAreas(); friend void MarkJumpAreas(); friend bool SaveNavigationMap(const char *filename); friend NavErrorType LoadNavigationMap(); friend void DestroyNavigationMap(); friend void DestroyHidingSpots(); friend void StripNavigationAreas(); friend class CNavAreaGrid; friend class CCSBotManager; void Initialize(); // to keep constructors consistent static bool m_isReset; // if true, don't bother cleaning up in destructor since everything is going away static unsigned int m_nextID; // used to allocate unique IDs unsigned int m_id; // unique area ID Extent m_extent; // extents of area in world coords (NOTE: lo.z is not necessarily the minimum Z, but corresponds to Z at point (lo.x, lo.y), etc Vector m_center; // centroid of area unsigned char m_attributeFlags; // set of attribute bit flags (see NavAttributeType) Place m_place; // place descriptor // height of the implicit corners float m_neZ; float m_swZ; enum { MAX_AREA_TEAMS = 2 }; // for hunting float m_clearedTimestamp[MAX_AREA_TEAMS]; // time this area was last "cleared" of enemies // danger float m_danger[MAX_AREA_TEAMS]; // danger of this area, allowing bots to avoid areas where they died in the past - zero is no danger float m_dangerTimestamp[MAX_AREA_TEAMS]; // time when danger value was set - used for decaying void DecayDanger(); // hiding spots HidingSpotList m_hidingSpotList; bool IsHidingSpotCollision(const Vector *pos) const; // returns true if an existing hiding spot is too close to given position // encounter spots SpotEncounterList m_spotEncounterList; // list of possible ways to move thru this area, and the spots to look at as we do void AddSpotEncounters(const CNavArea *from, NavDirType fromDir, const CNavArea *to, NavDirType toDir); // approach areas enum { MAX_APPROACH_AREAS = 16 }; ApproachInfo m_approach[MAX_APPROACH_AREAS]; unsigned char m_approachCount; void Strip(); // remove "analyzed" data from nav area // A* pathfinding algorithm static unsigned int m_masterMarker; unsigned int m_marker; // used to flag the area as visited CNavArea *m_parent; // the area just prior to this on in the search path NavTraverseType m_parentHow; // how we get from parent to us float m_totalCost; // the distance so far plus an estimate of the distance left float m_costSoFar; // distance travelled so far static CNavArea *m_openList; CNavArea *m_nextOpen, *m_prevOpen; // only valid if m_openMarker == m_masterMarker unsigned int m_openMarker; // if this equals the current marker value, we are on the open list // connections to adjacent areas NavConnectList m_connect[NUM_DIRECTIONS]; // a list of adjacent areas for each direction NavLadderList m_ladder[NUM_LADDER_DIRECTIONS]; // list of ladders leading up and down from this area CNavNode *m_node[NUM_CORNERS]; // nav nodes at each corner of the area void FinishMerge(CNavArea *adjArea); // recompute internal data once nodes have been adjusted during merge void MergeAdjacentConnections(CNavArea *adjArea); // for merging with "adjArea" - pick up all of "adjArea"s connections void AssignNodes(CNavArea *area); // assign internal nodes to the given area void FinishSplitEdit(CNavArea *newArea, NavDirType ignoreEdge); // given the portion of the original area, update its internal data NavAreaList m_overlapList; // list of areas that overlap this area void OnDestroyNotify(CNavArea *dead); // invoked when given area is going away CNavArea *m_prevHash, *m_nextHash; // for hash table in CNavAreaGrid }; extern NavAreaList TheNavAreaList; inline bool CNavArea::IsDegenerate() const { return (m_extent.lo.x >= m_extent.hi.x || m_extent.lo.y >= m_extent.hi.y); } inline CNavArea *CNavArea::GetAdjacentArea(NavDirType dir, int i) const { for (auto &con : m_connect[dir]) { if (i == 0) return con.area; i--; } return nullptr; } inline bool CNavArea::IsOpen() const { return (m_openMarker == m_masterMarker) ? true : false; } inline bool CNavArea::IsOpenListEmpty() { return m_openList ? false : true; } inline CNavArea *CNavArea::PopOpenList() { if (m_openList) { CNavArea *area = m_openList; // disconnect from list area->RemoveFromOpenList(); return area; } return nullptr; } inline bool CNavArea::IsClosed() const { if (IsMarked() && !IsOpen()) return true; return false; } inline void CNavArea::AddToClosedList() { Mark(); } inline void CNavArea::RemoveFromClosedList() { // since "closed" is defined as visited (marked) and not on open list, do nothing } // The CNavAreaGrid is used to efficiently access navigation areas by world position // Each cell of the grid contains a list of areas that overlap it // Given a world position, the corresponding grid cell is ( x/cellsize, y/cellsize ) class CNavAreaGrid { public: CNavAreaGrid(); ~CNavAreaGrid(); void Reset(); // clear the grid to empty void Initialize(float minX, float maxX, float minY, float maxY); // clear and reset the grid to the given extents void AddNavArea(CNavArea *area); // add an area to the grid void RemoveNavArea(CNavArea *area); // remove an area from the grid unsigned int GetNavAreaCount() const { return m_areaCount; } // return total number of nav areas CNavArea *GetNavArea(const Vector *pos, float beneathLimt = 120.0f) const; // given a position, return the nav area that IsOverlapping and is *immediately* beneath it CNavArea *GetNavAreaByID(unsigned int id) const; CNavArea *GetNearestNavArea(const Vector *pos, bool anyZ = false) const; Place GetPlace(const Vector *pos) const; // return radio chatter place for given coordinate private: const float m_cellSize; NavAreaList *m_grid; int m_gridSizeX; int m_gridSizeY; float m_minX; float m_minY; unsigned int m_areaCount; // total number of nav areas enum { HASH_TABLE_SIZE = 256 }; CNavArea *m_hashTable[HASH_TABLE_SIZE]; // hash table to optimize lookup by ID inline int ComputeHashKey(unsigned int id) const // returns a hash key for the given nav area ID { return id & 0xFF; } inline int WorldToGridX(float wx) const { int x = (wx - m_minX) / m_cellSize; if (x < 0) x = 0; else if (x >= m_gridSizeX) x = m_gridSizeX - 1; return x; } inline int WorldToGridY(float wy) const { int y = (wy - m_minY) / m_cellSize; if (y < 0) y = 0; else if (y >= m_gridSizeY) y = m_gridSizeY - 1; return y; } }; extern CNavAreaGrid TheNavAreaGrid; class ShortestPathCost { public: float operator()(CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder) { if (!fromArea) { // 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(); // if this is a "crouch" area, add penalty if (area->GetAttributes() & NAV_CROUCH) { const float crouchPenalty = 20.0f; cost += crouchPenalty * dist; } // if this is a "jump" area, add penalty if (area->GetAttributes() & NAV_JUMP) { const float jumpPenalty = 5.0f; cost += jumpPenalty * dist; } return cost; } } }; // Find path from startArea to goalArea via an A* search, using supplied cost heuristic. // If cost functor returns -1 for an area, that area is considered a dead end. // This doesn't actually build a path, but the path is defined by following parent // pointers back from goalArea to startArea. // If 'closestArea' is non-NULL, the closest area to the goal is returned (useful if the path fails). // If 'goalArea' is NULL, will compute a path as close as possible to 'goalPos'. // If 'goalPos' is NULL, will use the center of 'goalArea' as the goal position. // Returns true if a path exists. template bool NavAreaBuildPath(CNavArea *startArea, CNavArea *goalArea, const Vector *goalPos, CostFunctor &costFunc, CNavArea **closestArea = nullptr) { if (closestArea) *closestArea = nullptr; if (!startArea) return false; // If goalArea is NULL, this function will return the closest area to the goal. // However, if there is also no goal, we can't do anything. if (!goalArea && !goalPos) { return false; } startArea->SetParent(nullptr); // if we are already in the goal area, build trivial path if (startArea == goalArea) { goalArea->SetParent(nullptr); if (closestArea) *closestArea = goalArea; return true; } // determine actual goal position Vector actualGoalPos = (goalPos != nullptr) ? (*goalPos) : (*goalArea->GetCenter()); // start search CNavArea::ClearSearchLists(); // compute estimate of path length // TODO: Cost might work as "manhattan distance" startArea->SetTotalCost((*startArea->GetCenter() - actualGoalPos).Length()); real_t initCost = costFunc(startArea, nullptr, nullptr); if (initCost < 0.0f) return false; startArea->SetCostSoFar(initCost); startArea->AddToOpenList(); // keep track of the area we visit that is closest to the goal if (closestArea) *closestArea = startArea; float closestAreaDist = startArea->GetTotalCost(); // do A* search while (!CNavArea::IsOpenListEmpty()) { // get next area to check CNavArea *area = CNavArea::PopOpenList(); // check if we have found the goal area if (area == goalArea) { if (closestArea) *closestArea = goalArea; return true; } // search adjacent areas bool searchFloor = true; int dir = NORTH; const NavConnectList *floorList = area->GetAdjacentList(NORTH); auto floorIter = floorList->begin(); bool ladderUp = true; const NavLadderList *ladderList = nullptr; NavLadderList::const_iterator ladderIter; enum { AHEAD = 0, LEFT, RIGHT, BEHIND, NUM_TOP_DIRECTIONS }; int ladderTopDir; while (true) { CNavArea *newArea; NavTraverseType how; const CNavLadder *ladder = nullptr; // Get next adjacent area - either on floor or via ladder if (searchFloor) { // if exhausted adjacent connections in current direction, begin checking next direction if (floorIter == floorList->end()) { dir++; if (dir == NUM_DIRECTIONS) { // checked all directions on floor - check ladders next searchFloor = false; ladderList = area->GetLadderList(LADDER_UP); ladderIter = ladderList->begin(); ladderTopDir = AHEAD; } else { // start next direction floorList = area->GetAdjacentList((NavDirType)dir); floorIter = floorList->begin(); } continue; } newArea = (*floorIter).area; how = (NavTraverseType)dir; floorIter++; } // search ladders else { if (ladderIter == ladderList->end()) { if (!ladderUp) { // checked both ladder directions - done break; } else { // check down ladders ladderUp = false; ladderList = area->GetLadderList(LADDER_DOWN); ladderIter = ladderList->begin(); } continue; } if (ladderUp) { ladder = (*ladderIter); // cannot use this ladder if the ladder bottom is hanging above our head if (ladder->m_isDangling) { ladderIter++; continue; } // do not use BEHIND connection, as its very hard to get to when going up a ladder if (ladderTopDir == AHEAD) newArea = ladder->m_topForwardArea; else if (ladderTopDir == LEFT) newArea = ladder->m_topLeftArea; else if (ladderTopDir == RIGHT) newArea = ladder->m_topRightArea; else { ladderIter++; continue; } how = GO_LADDER_UP; ladderTopDir++; } else { newArea = (*ladderIter)->m_bottomArea; how = GO_LADDER_DOWN; ladder = (*ladderIter); ladderIter++; } if (!newArea) continue; } // don't backtrack if (newArea == area) continue; real_t newCostSoFar = costFunc(newArea, area, ladder); // check if cost functor says this area is a dead-end if (newCostSoFar < 0.0f) continue; if ((newArea->IsOpen() || newArea->IsClosed()) && newArea->GetCostSoFar() <= newCostSoFar) { // this is a worse path - skip it continue; } else { // compute estimate of distance left to go real_t newCostRemaining = (*newArea->GetCenter() - actualGoalPos).Length(); // track closest area to goal in case path fails if (closestArea && newCostRemaining < closestAreaDist) { *closestArea = newArea; closestAreaDist = newCostRemaining; } newArea->SetParent(area, how); newArea->SetCostSoFar(newCostSoFar); newArea->SetTotalCost(newCostSoFar + newCostRemaining); if (newArea->IsClosed()) newArea->RemoveFromClosedList(); if (newArea->IsOpen()) { // area already on open list, update the list order to keep costs sorted newArea->UpdateOnOpenList(); } else newArea->AddToOpenList(); } } // we have searched this area area->AddToClosedList(); } return false; } // Compute distance between two areas. Return -1 if can't reach 'endArea' from 'startArea'. template real_t NavAreaTravelDistance(CNavArea *startArea, CNavArea *endArea, CostFunctor &costFunc) { if (!startArea) return -1.0f; if (!endArea) return -1.0f; if (startArea == endArea) return 0.0f; // compute path between areas using given cost heuristic if (NavAreaBuildPath(startArea, endArea, nullptr, costFunc) == false) return -1.0f; // compute distance along path real_t distance = 0.0f; for (CNavArea *area = endArea; area->GetParent(); area = area->GetParent()) { distance += (*area->GetCenter() - *area->GetParent()->GetCenter()).Length(); } return distance; } // Compute distance from area to position. Return -1 if can't reach position. template float NavAreaTravelDistance(const Vector *startPos, CNavArea *startArea, const Vector *goalPos, CostFunctor &costFunc) { if (!startArea || !startPos || !goalPos) return -1.0f; // compute path between areas using given cost heuristic CNavArea *goalArea = nullptr; if (NavAreaBuildPath(startArea, TheNavAreaGrid.GetNearestNavArea(goalPos), goalPos, costFunc, &goalArea) == false) return -1.0f; if (!goalArea) return -1.0f; // compute distance along path if (!goalArea->GetParent()) { return (*goalPos - *startPos).Length(); } else { CNavArea *area = goalArea->GetParent(); float distance = (*goalPos - *area->GetCenter()).Length(); for (; area->GetParent(); area = area->GetParent()) { distance += (*area->GetCenter() - *area->GetParent()->GetCenter()).Length(); } return distance; } } // Do a breadth-first search, invoking functor on each area. // If functor returns 'true', continue searching from this area. // If functor returns 'false', the area's adjacent areas are not explored (dead end). // If 'maxRange' is 0 or less, no range check is done (all areas will be examined). // NOTE: Returns all areas that overlap range, even partially // TODO: Use ladder connections // // helper function inline void AddAreaToOpenList(CNavArea *area, CNavArea *parent, const Vector *startPos, float maxRange) { if (!area) return; if (!area->IsMarked()) { area->Mark(); area->SetTotalCost(0.0f); area->SetParent(parent); if (maxRange > 0.0f) { // make sure this area overlaps range Vector closePos; area->GetClosestPointOnArea(startPos, &closePos); if ((closePos - *startPos).Make2D().IsLengthLessThan(maxRange)) { // compute approximate distance along path to limit travel range, too float distAlong = parent->GetCostSoFar(); distAlong += (*area->GetCenter() - *parent->GetCenter()).Length(); area->SetCostSoFar(distAlong); // allow for some fudge due to large size areas if (distAlong <= 1.5f * maxRange) area->AddToOpenList(); } } else { // infinite range area->AddToOpenList(); } } } template void SearchSurroundingAreas(CNavArea *startArea, const Vector *startPos, Functor &func, float maxRange = -1.0f) { if (!startArea || !startPos) return; CNavArea::MakeNewMarker(); CNavArea::ClearSearchLists(); startArea->AddToOpenList(); startArea->SetTotalCost(0.0f); startArea->SetCostSoFar(0.0f); startArea->SetParent(nullptr); startArea->Mark(); while (!CNavArea::IsOpenListEmpty()) { // get next area to check CNavArea *area = CNavArea::PopOpenList(); // invoke functor on area if (func(area)) { // explore adjacent floor areas for (int dir = 0; dir < NUM_DIRECTIONS; dir++) { int count = area->GetAdjacentCount((NavDirType)dir); for (int i = 0; i < count; i++) { CNavArea *adjArea = area->GetAdjacentArea((NavDirType)dir, i); AddAreaToOpenList(adjArea, area, startPos, maxRange); } } // explore adjacent areas connected by ladders NavLadderList::const_iterator ladderIt; // check up ladders const NavLadderList *ladderList = area->GetLadderList(LADDER_UP); if (ladderList) { for (ladderIt = ladderList->begin(); ladderIt != ladderList->end(); ladderIt++) { const CNavLadder *ladder = *ladderIt; // cannot use this ladder if the ladder bottom is hanging above our head if (ladder->m_isDangling) { continue; } // do not use BEHIND connection, as its very hard to get to when going up a ladder AddAreaToOpenList(ladder->m_topForwardArea, area, startPos, maxRange); AddAreaToOpenList(ladder->m_topLeftArea, area, startPos, maxRange); AddAreaToOpenList(ladder->m_topRightArea, area, startPos, maxRange); } } // check down ladders ladderList = area->GetLadderList(LADDER_DOWN); if (ladderList) { for (ladderIt = ladderList->begin(); ladderIt != ladderList->end(); ladderIt++) { const CNavLadder *ladder = *ladderIt; AddAreaToOpenList(ladder->m_bottomArea, area, startPos, maxRange); } } } } } // Apply the functor to all navigation areas // If functor returns false, stop processing and return false. template bool ForAllAreas(Functor &func) { NavAreaList::iterator iter; for (iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); iter++) { CNavArea *area = (*iter); if (func(area) == false) return false; } return true; } // Fuctor that returns lowest cost for farthest away areas // For use with FindMinimumCostArea() class FarAwayFunctor { public: float operator()(CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder) { if (area == fromArea) return 9999999.9f; return 1.0f / (*fromArea->GetCenter() - *area->GetCenter()).Length(); } }; // Fuctor that returns lowest cost for farthest away areas // For use with FindMinimumCostArea() class FarAwayFromPositionFunctor { public: FarAwayFromPositionFunctor(const Vector *pos) { m_pos = pos; } float operator() (CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder) { return 1.0f / (*m_pos - *area->GetCenter()).Length(); } private: const Vector *m_pos; }; // Pick a low-cost area of "decent" size template CNavArea *FindMinimumCostArea(CNavArea *startArea, CostFunctor &costFunc) { const float minSize = 150.0f; // collect N low-cost areas of a decent size enum { NUM_CHEAP_AREAS = 32 }; struct { CNavArea *area; float cost; } cheapAreaSet[NUM_CHEAP_AREAS]; int cheapAreaSetCount = 0; 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->hi.x - extent->lo.x < minSize || extent->hi.y - extent->lo.y < minSize) continue; // compute cost of this area float cost = costFunc(area, startArea, nullptr); if (cheapAreaSetCount < NUM_CHEAP_AREAS) { cheapAreaSet[cheapAreaSetCount].area = area; cheapAreaSet[cheapAreaSetCount++].cost = cost; } else { // replace most expensive cost if this is cheaper int expensive = 0; for (int i = 1; i < NUM_CHEAP_AREAS; i++) { if (cheapAreaSet[i].cost > cheapAreaSet[expensive].cost) expensive = i; } if (cheapAreaSet[expensive].cost > cost) { cheapAreaSet[expensive].area = area; cheapAreaSet[expensive].cost = cost; } } } if (cheapAreaSetCount) { // pick one of the areas at random return cheapAreaSet[RANDOM_LONG(0, cheapAreaSetCount - 1)].area; } else { // degenerate case - no decent sized areas - pick a random area int numAreas = TheNavAreaList.size(); int which = RANDOM_LONG(0, numAreas - 1); NavAreaList::iterator iter; for (iter = TheNavAreaList.begin(); iter != TheNavAreaList.end(); iter++) { if (which-- == 0) break; } return (*iter); } } bool IsHidingSpotInCover(const Vector *spot); void ClassifySniperSpot(HidingSpot *spot); void DestroyHidingSpots(); void EditNavAreas(NavEditCmdType cmd); bool GetGroundHeight(const Vector *pos, float *height, Vector *normal = nullptr); bool GetSimpleGroundHeight(const Vector *pos, float *height, Vector *normal = nullptr); CNavArea *GetMarkedArea(); void EditNavAreasReset(); void DrawHidingSpots(const CNavArea *area); void IncreaseDangerNearby(int teamID, float amount, CNavArea *startArea, const Vector *pos, float maxRadius); void DrawDanger(); bool IsSpotOccupied(CBaseEntity *me, const Vector *pos); // if a player is at the given spot, return true const Vector *FindNearbyHidingSpot(CBaseEntity *me, const Vector *pos, CNavArea *startArea, float maxRange = 1000.0f, bool isSniper = false, bool useNearest = false); const Vector *FindNearbyRetreatSpot(CBaseEntity *me, const Vector *start, CNavArea *startArea, float maxRange = 1000.0f, int avoidTeam = 0, bool useCrouchAreas = true); // return true if moving from "start" to "finish" will cross a player's line of fire bool IsCrossingLineOfFire(const Vector &start, const Vector &finish, CBaseEntity *ignore = nullptr, int ignoreTeam = 0); const Vector *FindRandomHidingSpot(CBaseEntity *me, Place place, bool isSniper = false); HidingSpot *GetHidingSpotByID(unsigned int id); void ApproachAreaAnalysisPrep(); void CleanupApproachAreaAnalysisPrep(); void DestroyLadders(); void DestroyNavigationMap(); void StripNavigationAreas(); CNavArea *FindFirstAreaInDirection(const Vector *start, NavDirType dir, float range, float beneathLimit, CBaseEntity *traceIgnore, Vector *closePos); bool testJumpDown(const Vector *fromPos, const Vector *toPos); void ConnectGeneratedAreas(); void MergeGeneratedAreas(); bool IsAreaRoughlySquare(const CNavArea *area); void SplitX(CNavArea *area); void SplitY(CNavArea *area); void SquareUpAreas(); bool TestArea(CNavNode *node, int width, int height); int BuildArea(CNavNode *node, int width, int height); void BuildLadders(); void MarkJumpAreas(); void GenerateNavigationAreaMesh();