/* * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it * and/or modify it under version 3 of the License, or (at your option), any later version. */ #ifndef _PLAYERBOT_TRAVELNODE_H #define _PLAYERBOT_TRAVELNODE_H #include #include "G3D/Vector3.h" #include "TravelMgr.h" // THEORY // // Pathfinding uses the detour recast navmesh engine for mob, npc, and bot movement. // Because mobs and npc movement is based on following a player or a set path the PathGenerator is limited to 296y. // This means that when trying to find a path from A to B distances beyond 296y will be a best guess often moving in a // straight path. Bots would get stuck moving from Northshire to Stormwind because there is no 296y path that doesn't // go (initially) the wrong direction. // // To remedy this limitation without altering the PathGenerator limits too much this node system was introduced. // // ---> [N1] ---> [N2] ---> [N3] ---> // // Bot at wants to move to // [N1],[N2],[N3] are predefined nodes for which we know we can move from [N1] to [N2] and from [N2] to [N3] but not // from [N1] to [N3]. If we can move from [S] to [N1] and from [N3] to [E] we have a complete route to travel. // // Terminology: // Node: A location on a map for which we know bots are likely to want to travel to or need to travel past to reach // other nodes. Stored in DB table `playerbots_travelnode`. // Link: The connection between two nodes. A link signifies that the bot can travel from one node to another. // A link is one-directional. Stored in `playerbots_travelnode_link`. // Path: The waypoint path returned by the standard PathGenerator to move from one node (or position) to another. // A path can be incomplete or empty which means there is no link. Stored in `playerbots_travelnode_path`. // Route: The list of nodes that give the shortest route from a node to a distant node. Routes are calculated using // a standard A* search based on links. // // Edge types (TravelNodePathType): // walk(1) — Walk via navmesh waypoints (stored in DB) // areaTrigger(2) — AreaTrigger teleport (auto-discovered at startup) // transport(3) — Boat/zeppelin (auto-discovered from MO_TRANSPORT) // flightPath(4) — Taxi flight between flight masters // teleportSpell(5) — Spell-based teleport (e.g. mage portals) // staticPortal(6) — Manually defined teleport link (DB only, not pruned by generation) // // On server start saved nodes and links are loaded via TravelNodeMap::Init(). An index of nodes by zone is prepared // (instead of scanning all ~4000 nodes), precomputes connected components for O(1) reachability checks, and builds // a taxi BFS graph. Paths and routes are calculated on the fly and saved for future use. Nodes are only added at // startup or via the console `.generate` command — runtime mutation was removed because taking a unique_lock // caused 100-250ms contention spikes against bot threads. // // Initially the current nodes have been made: // Flightmasters and Inns (Bots can use these to fast-travel so eventually they will be included in the route // calculation) WorldBosses and Unique bosses in instances (These are logical places bots might want to go in // instances) Player start spawns (Obviously all lvl1 bots will spawn and move from here) Area triggers locations with // teleport and their teleport destinations (These used to travel in or between maps) Transports including elevators // (Again used to travel in and in maps) (sub)Zone means (These are the center most point for each sub-zone which is // good for global coverage). // // To increase coverage/linking extra nodes must be manually created via the "playerbot travel generatenode" // console command after importing the specified node. Current implementation places nodes on paths (including // complete) at sub-zone transitions or randomly. After calculating possible links the node is removed if it // does not create local coverage (.fullgenerate only). // // Travel Flow: // // GetFullPath finds nearest nodes (zone-indexed), runs A* to get a node route, then // BuildPath assembles a flat TravelPath with typed waypoints (walk, portal, transport, flight). // MoveFarTo re-resolves a fresh TravelPath each tick; UpcommingSpecialMovement cuts // to the head segment when special; HandleSpecialMovement dispatches the matching // action (portal interact, area-trigger marker, transport board, flight taxi). // Cross-map travel is handled naturally by portal/transport edges in the A* graph. // // If setup cannot resolve (no node, no route, no flight), the bot teleports directly to the destination // as a fallback. // // The use of hearthstones and mage teleporting was removed — it caused route mutations requiring locking that no longer made sense. Mage portals may be future item. // // Thread Safety: // // The node graph is immutable at runtime (no adds/removes after Init). A shared_timed_mutex (m_nMapMtx) still // exists and shared_locks are taken in GetFullPath and GenerateWalkPath for safety, but since there are no // runtime mutations these are effectively uncontested. The only exclusive locks are taken at startup // (saveNodeStore) and by the debug dump command. // enum class TravelNodePathType : uint8 { none = 0, walk = 1, areaTrigger = 2, transport = 3, flightPath = 4, // value 5 reserved (was teleportSpell — removed) staticPortal = 6 }; // A connection between two nodes. class TravelNodePath { public: // Constructor TravelNodePath(float distance = 0.1f, float extraCost = 0, uint8 pathType = (uint8)TravelNodePathType::walk, uint32 pathObject = 0, bool calculated = false, std::vector maxLevelCreature = {0, 0, 0}, float swimDistance = 0) : extraCost(extraCost), calculated(calculated), distance(distance), maxLevelCreature(maxLevelCreature), swimDistance(swimDistance), pathType(TravelNodePathType(pathType)), pathObject(pathObject) // reorder args - whipowill { if (pathType != (uint8)TravelNodePathType::walk) complete = true; } TravelNodePath(TravelNodePath* basePath) { complete = basePath->complete; path = basePath->path; extraCost = basePath->extraCost; calculated = basePath->calculated; distance = basePath->distance; maxLevelCreature = basePath->maxLevelCreature; swimDistance = basePath->swimDistance; pathType = basePath->pathType; pathObject = basePath->pathObject; } // Getters bool getComplete() { return complete || pathType != TravelNodePathType::walk; } std::vector GetPath() { return path; } TravelNodePathType getPathType() { return pathType; } uint32 getPathObject() { return pathObject; } float getDistance() { return distance; } float getSwimDistance() { return swimDistance; } float getExtraCost() { return extraCost; } std::vector getMaxLevelCreature() { return maxLevelCreature; } void setCalculated(bool calculated1 = true) { calculated = calculated1; } bool getCalculated() { return calculated; } std::string const print(); // Setters void setComplete(bool complete1) { complete = complete1; } void setPath(std::vector path1) { path = path1; } void setPathAndCost(std::vector path1, float speed) { setPath(path1); calculateCost(true); extraCost = distance / speed; } void setPathType(TravelNodePathType pathType1) { pathType = pathType1; } void setPathObject(uint32 pathObject1) { pathObject = pathObject1; } void calculateCost(bool distanceOnly = false); float getCost(Player* bot = nullptr, uint32 cGold = 0); uint32 getPrice(); private: // Does the path have all the points to get to the destination? bool complete = false; // List of WorldPositions to get to the destination. std::vector path = {}; // The extra (loading/transport) time it takes to take this path. float extraCost = 0; bool calculated = false; // Derived distance in yards float distance = 0.1f; // Calculated mobs level along the way. std::vector maxLevelCreature = {0, 0, 0}; // mobs, horde, alliance // Calculated swiming distances along the way. float swimDistance = 0; TravelNodePathType pathType = TravelNodePathType::walk; uint32 pathObject = 0; /* //Is the path a portal/teleport to the destination? bool portal = false; //Area trigger Id uint32 portalId = 0; //Is the path transport based? bool transport = false; // Is the path a flightpath? bool flightPath = false; */ }; // A waypoint to travel from or to. // Each node knows which other nodes can be reached without help. class TravelNode { public: // Constructors TravelNode() {} TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false) { nodeName = nodeName1; point = point1; important = important1; } TravelNode(TravelNode* baseNode) { nodeName = baseNode->nodeName; point = baseNode->point; important = baseNode->important; } // Setters void setLinked(bool linked1) { linked = linked1; } void setPoint(WorldPosition point1) { point = point1; } // Getters std::string const getName() { return nodeName; } WorldPosition* getPosition() { return &point; } std::unordered_map* getPaths() { return &paths; } std::unordered_map* getLinks() { return &links; } bool isImportant() { return important; } bool isLinked() { return linked; } bool isTransport() { for (auto const& link : *getLinks()) if (link.second->getPathType() == TravelNodePathType::transport) return true; return false; } uint32 getTransportId() { for (auto const& link : *getLinks()) if (link.second->getPathType() == TravelNodePathType::transport) return link.second->getPathObject(); return false; } bool isPortal() { for (auto const& link : *getLinks()) if (link.second->getPathType() == TravelNodePathType::areaTrigger || link.second->getPathType() == TravelNodePathType::staticPortal) return true; return false; } bool isWalking() { for (auto link : *getLinks()) if (link.second->getPathType() == TravelNodePathType::walk) return true; return false; } // WorldLocation shortcuts uint32 GetMapId() { return point.GetMapId(); } float getX() { return point.GetPositionX(); } float getY() { return point.GetPositionY(); } float getZ() { return point.GetPositionZ(); } float getO() { return point.GetOrientation(); } float getDistance(WorldPosition pos) { return point.distance(pos); } float getDistance(TravelNode* node) { return point.distance(node->getPosition()); } float fDist(TravelNode* node) { return point.fDist(node->getPosition()); } float fDist(WorldPosition pos) { return point.fDist(pos); } TravelNodePath* setPathTo(TravelNode* node, TravelNodePath path = TravelNodePath(), bool isLink = true) { if (this != node) { paths[node] = path; if (isLink) links[node] = &paths[node]; return &paths[node]; } return nullptr; } bool hasPathTo(TravelNode* node) { return paths.find(node) != paths.end(); } TravelNodePath* getPathTo(TravelNode* node) { return &paths[node]; } bool hasCompletePathTo(TravelNode* node) { return hasPathTo(node) && getPathTo(node)->getComplete(); } TravelNodePath* BuildPath(TravelNode* endNode, Unit* bot, bool postProcess = false); void setLinkTo(TravelNode* node, float distance = 0.1f) { if (this != node) { if (!hasPathTo(node)) setPathTo(node, TravelNodePath(distance)); else links[node] = &paths[node]; } } bool hasLinkTo(TravelNode* node) { return links.find(node) != links.end(); } float linkCostTo(TravelNode* node) { return paths.find(node)->second.getDistance(); } float linkDistanceTo(TravelNode* node) { return paths.find(node)->second.getDistance(); } void removeLinkTo(TravelNode* node, bool removePaths = false); bool isEqual(TravelNode* compareNode); // Removes links to other nodes that can also be reached by passing another node. bool isUselessLink(TravelNode* farNode); void cropUselessLink(TravelNode* farNode); bool cropUselessLinks(); // Returns all nodes that can be reached from this node. std::vector getNodeMap(bool importantOnly = false, std::vector ignoreNodes = {}); // Checks if it is even possible to route to this node. bool hasRouteTo(TravelNode* node) { if (routes.empty()) for (auto mNode : getNodeMap()) routes[mNode] = true; return routes.find(node) != routes.end(); } void clearRoutes() { routes.clear(); } void setRouteTo(TravelNode* node) { routes[node] = true; } void print(bool printFailed = true); protected: // Logical name of the node std::string nodeName; // WorldPosition of the node. WorldPosition point; // List of paths to other nodes. std::unordered_map paths; // List of links to other nodes. std::unordered_map links; // List of nodes and if there is 'any' route possible std::unordered_map routes; // This node should not be removed bool important = false; // This node has been checked for nearby links bool linked = false; // This node is a (moving) transport. // bool transport = false; // Entry of transport. // uint32 transportId = 0; }; // Route step type enum class PathNodeType : uint8 { NODE_PREPATH = 0, NODE_PATH = 1, NODE_NODE = 2, NODE_AREA_TRIGGER = 3, NODE_TRANSPORT = 4, NODE_FLIGHTPATH = 5, // value 6 reserved (was NODE_TELEPORT — removed with teleportSpell) NODE_STATIC_PORTAL = 7 }; struct PathNodePoint { WorldPosition point; PathNodeType type = PathNodeType::NODE_PATH; uint32 entry = 0; bool operator==(const PathNodePoint& p1) const { return point == p1.point && type == p1.type && entry == p1.entry; } // A "walkable" node is one we traverse on foot. Portals/transports/ // taxis/teleports are entry/exit hops, not points to anchor a // shortcut on. Used by makeShortCut to skip them when picking the // closest-point-on-path to the bot. bool isWalkable() const { return (uint8)type <= (uint8)PathNodeType::NODE_NODE; } }; // A complete list of points the bots has to walk to or teleport to. class TravelPath { public: TravelPath() {} TravelPath(std::vector fullPath1) { fullPath = fullPath1; } TravelPath(std::vector path, PathNodeType type = PathNodeType::NODE_PATH, uint32 entry = 0) { addPath(path, type, entry); } void addPoint(PathNodePoint point) { fullPath.push_back(point); } void addPoint(WorldPosition point, PathNodeType type = PathNodeType::NODE_PATH, uint32 entry = 0) { fullPath.push_back(PathNodePoint{point, type, entry}); } void addPath(std::vector path, PathNodeType type = PathNodeType::NODE_PATH, uint32 entry = 0) { for (auto& p : path) fullPath.push_back(PathNodePoint{p, type, entry}); } void addPath(std::vector newPath) { fullPath.insert(fullPath.end(), newPath.begin(), newPath.end()); } void clear() { fullPath.clear(); } bool empty() const { return fullPath.empty(); } size_t size() const { return fullPath.size(); } const PathNodePoint& operator[](size_t idx) const { return fullPath[idx]; } std::vector GetPath() { return fullPath; } const std::vector& GetPathRef() const { return fullPath; } WorldPosition getFront() { return fullPath.front().point; } WorldPosition getBack() { return fullPath.back().point; } std::vector getPointPath() { std::vector retVec; for (auto const& p : fullPath) retVec.push_back(p.point); return retVec; } bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr); // Trim the path up to (and optionally including) the given point. // Returns true if the point was found. Used by upcoming special- // movement detection to advance the path past a portal/transport/ // area-trigger node once the bot reaches it. bool cutTo(PathNodePoint point, bool including); // Returns true if the next reachable segment is a special-handling // node (portal / area-trigger / transport / flightpath / teleport) // and the bot is close enough / positioned right to handle it now. // Trims the path up to that segment as a side effect. Caller then // dispatches the matching special-movement handler on the new head. bool UpcommingSpecialMovement(WorldPosition startPos, float maxDist, bool onTransport); // Truncate the path at the first waypoint that would put the bot in // range of a hostile creature (within attack range, in LOS, level-cap // sane), at a non-walkable hop, after drifting beyond reactDistance // from the start, or across a > 125-sqDist jump. Set ignoreEnemyTargets // to suppress the hostile-target check (used by combat repositioning). void ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets = false); // Reject paths the navmesh accepts but a player can't walk: // 2-point shortcut over 5y, or > 10y vertical drop with slope steeper than 2:1. static bool IsPathCheating(std::vector const& path, float endpointDistance); std::ostringstream const print(); private: // Returns the next-best-point iterator within maxDist from startPos: // skips waypoints behind the bot, advances while shouldMoveToNextPoint // allows, projects onto current segment to decide if the bot has // already passed it. std::vector::iterator getNextPoint(WorldPosition startPos, float maxDist, bool onTransport); // Heuristic for getNextPoint: decides whether the iterator should // step forward to nextP. Stops at special nodes (area triggers, // portals, transports, flight paths), at map boundaries, and when // accumulated distance exceeds maxDist. bool shouldMoveToNextPoint(WorldPosition startPos, std::vector::iterator beg, std::vector::iterator ed, std::vector::iterator p, float& moveDist, float maxDist); std::vector fullPath; }; // An stored A* search that gives a complete route from one node to another. class TravelNodeRoute { public: TravelNodeRoute() {} TravelNodeRoute(std::vector nodes1) { nodes = nodes1; } bool isEmpty() { return nodes.empty(); } bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); } float getTotalDistance(); std::vector getNodes() { return nodes; } TravelPath BuildPath( std::vector pathToStart = {}, std::vector pathToEnd = {}, Unit* bot = nullptr); std::ostringstream const print(); private: std::vector::iterator findNode(TravelNode* node) { return std::find(nodes.begin(), nodes.end(), node); } std::vector nodes; }; // A node container to aid A* calculations with nodes. class TravelNodeStub { public: TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; } TravelNode* dataNode; float totalCost = 0.0; float costFromStart = 0.0; float heuristic = 0.0; bool open = false; bool closed = false; TravelNodeStub* parent = nullptr; uint32 currentGold = 0; }; // The container of all nodes. class TravelNodeMap { public: static TravelNodeMap& instance() { static TravelNodeMap instance; return instance; } TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false, bool checkDuplicate = true, bool transport = false, uint32 transportId = 0); void removeNode(TravelNode* node); bool removeNodes() { if (m_nMapMtx.try_lock_for(std::chrono::seconds(10))) { for (auto& node : nodes) removeNode(node); m_nMapMtx.unlock(); return true; } return false; } void fullLinkNode(TravelNode* startNode, Unit* bot); // Get all nodes std::vector getNodes() { return nodes; } std::vector getNodes(WorldPosition pos, float range = -1); // Find nearest node. TravelNode* getNode(TravelNode* sameNode) { for (auto& node : nodes) { if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition()) return node; } return nullptr; } TravelNode* getNode(WorldPosition pos, std::vector& ppath, Unit* bot = nullptr, float range = -1); TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr, float range = -1) { std::vector ppath; return getNode(pos, ppath, bot, range); } // Get Random Node TravelNode* getRandomNode(WorldPosition pos) { std::vector rNodes = getNodes(pos); if (rNodes.empty()) return nullptr; return rNodes[urand(0, rNodes.size() - 1)]; } // Finds the best nodePath between two nodes (A* over the node graph) TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal, Player* bot); // Picks the nearest start/end nodes for two world positions and runs A* // over the node graph to return a full route between them. TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos, WorldPosition endPos, std::vector& startPath, Player* bot = nullptr); void setHasToGen() { hasToGen = true; } void generateNpcNodes(); void generateStartNodes(); void generateAreaTriggerNodes(); void generateNodes(); void generateTransportNodes(); void generateZoneMeanNodes(); void generateWalkPaths(); void removeLowNodes(); void removeUselessPaths(); void calculatePathCosts(); void generateTaxiPaths(); void generatePaths(bool fullGen = false); void generateAll(); void Init(); void printMap(); void printNodeStore(); void saveNodeStore(); void LoadNodeStore(); void calcMapOffset(); WorldPosition getMapOffset(uint32 mapId); // Taxi graph (BFS-based path lookup between taxi nodes) void InitTaxiGraph(); std::vector FindTaxiPath(uint32 fromNode, uint32 toNode); void BuildZoneIndex(); void PrecomputeReachability(); TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId); TravelNode* GetNearestNodeOnMap(WorldPosition pos); // All nodes registered to a zone (post-BuildZoneIndex). Returns an // empty static vector for unknown zones. std::vector const& GetNodesInZone(uint32 zoneId) const; // Resolve a full TravelPath from botPos to destination. Returns an // empty TravelPath if no graph route + mmap stitch is reachable; // the caller is then expected to fall back to a single-point path. TravelPath GetFullPath(WorldPosition botPos, uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr); // Resolve A* route between two world positions (returns node vector) std::vector ResolveRoute(WorldPosition startPos, WorldPosition endPos); // Get stored walk points for one edge (from→to). Empty if no path. std::vector GetEdgeWalkPoints(TravelNode* from, TravelNode* to); std::shared_timed_mutex m_nMapMtx; private: TravelNodeMap() = default; ~TravelNodeMap() = default; TravelNodeMap(const TravelNodeMap&) = delete; TravelNodeMap& operator=(const TravelNodeMap&) = delete; TravelNodeMap(TravelNodeMap&&) = delete; TravelNodeMap& operator=(TravelNodeMap&&) = delete; // Taxi graph internals void BuildTaxiGraph(); void ComputeAllPaths(); std::unordered_map BFS(uint32 startNode); std::vector BuildPath( uint32 fromNode, uint32 toNode, const std::unordered_map& parentMap); std::unordered_map> m_taxiGraph; std::map>> m_taxiPathCache; std::vector nodes; std::unordered_map> m_zoneIndex; std::unordered_map> m_mapIndex; std::vector> mapOffsets; bool hasToSave = false; bool hasToGen = false; bool hasToFullGen = false; }; #define sTravelNodeMap TravelNodeMap::instance() #endif