mod-playerbots/src/Mgr/Travel/TravelNode.h

768 lines
25 KiB
C++

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, 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 <shared_mutex>
#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.
//
// <S> ---> [N1] ---> [N2] ---> [N3] ---> <E>
//
// Bot at <S> wants to move to <E>
// [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)
// portal(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)
// flyingMount (7) — Use Bots Flying mount to travel (Not currently enabled)
//
// 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).
// ExecuteTravelPlan iterates the path by stepIdx, dispatching on each point's PathNodeType.
// 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.
//
constexpr float MAX_PATHFINDING_DISTANCE = 296.0f;
enum class TravelNodePathType : uint8
{
none = 0,
walk = 1,
portal = 2,
transport = 3,
flightPath = 4,
teleportSpell = 5,
staticPortal = 6,
flyingMount = 7
};
// 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<uint8> 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<WorldPosition> 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<uint8> 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<WorldPosition> path1) { path = path1; }
void setPathAndCost(std::vector<WorldPosition> 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<WorldPosition> 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<uint8> 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<TravelNode*, TravelNodePath>* getPaths() { return &paths; }
std::unordered_map<TravelNode*, TravelNodePath*>* 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::portal ||
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<TravelNode*> getNodeMap(bool importantOnly = false,
std::vector<TravelNode*> 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<TravelNode*, TravelNodePath> paths;
// List of links to other nodes.
std::unordered_map<TravelNode*, TravelNodePath*> links;
// List of nodes and if there is 'any' route possible
std::unordered_map<TravelNode*, bool> 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_PORTAL = 3,
NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5,
NODE_TELEPORT = 6,
NODE_FLYING_MOUNT = 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<PathNodePoint> fullPath1)
{
fullPath = fullPath1;
}
TravelPath(std::vector<WorldPosition> 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<WorldPosition> path,
PathNodeType type = PathNodeType::NODE_PATH,
uint32 entry = 0)
{
for (auto& p : path)
fullPath.push_back(PathNodePoint{p, type, entry});
}
void addPath(std::vector<PathNodePoint> 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<PathNodePoint> GetPath() { return fullPath; }
const std::vector<PathNodePoint>& GetPathRef() const { return fullPath; }
WorldPosition getFront() { return fullPath.front().point; }
WorldPosition getBack() { return fullPath.back().point; }
std::vector<WorldPosition> getPointPath()
{
std::vector<WorldPosition> retVec;
for (auto const& p : fullPath)
retVec.push_back(p.point);
return retVec;
}
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
// 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<WorldPosition> const& path,
float endpointDistance);
std::ostringstream const print();
private:
std::vector<PathNodePoint> fullPath;
};
// An stored A* search that gives a complete route from one node to another.
class TravelNodeRoute
{
public:
TravelNodeRoute() {}
TravelNodeRoute(std::vector<TravelNode*> nodes1)
{
nodes = nodes1;
}
bool isEmpty() { return nodes.empty(); }
bool hasNode(TravelNode* node)
{
return findNode(node) != nodes.end();
}
float getTotalDistance();
std::vector<TravelNode*> getNodes() { return nodes; }
TravelPath BuildPath(
std::vector<WorldPosition> pathToStart = {},
std::vector<WorldPosition> pathToEnd = {},
Unit* bot = nullptr);
std::ostringstream const print();
private:
std::vector<TravelNode*>::iterator findNode(TravelNode* node)
{
return std::find(nodes.begin(), nodes.end(), node);
}
std::vector<TravelNode*> 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;
};
struct TravelPlan
{
WorldPosition destination;
// Flat waypoint path built upfront by GetFullPath:
TravelPath steps;
uint32 stepIdx{0};
// Spline scratch (used by executor):
std::vector<G3D::Vector3> walkPoints;
bool splineActive{false};
uint32 splineStartTime{0};
uint32 expectedDuration{0};
// Taxi scratch:
std::vector<uint32> route;
bool IsActive() const { return !steps.empty(); }
void Reset()
{
destination = WorldPosition();
steps.clear();
stepIdx = 0;
walkPoints.clear();
splineActive = false;
splineStartTime = 0;
expectedDuration = 0;
route.clear();
}
};
// 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<TravelNode*> getNodes() { return nodes; }
std::vector<TravelNode*> 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<WorldPosition>& ppath,
Unit* bot = nullptr, float range = -1);
TravelNode* getNode(WorldPosition pos, Unit* bot = nullptr,
float range = -1)
{
std::vector<WorldPosition> ppath;
return getNode(pos, ppath, bot, range);
}
// Get Random Node
TravelNode* getRandomNode(WorldPosition pos)
{
std::vector<TravelNode*> 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<WorldPosition>& 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();
bool cropUselessNode(TravelNode* startNode);
TravelNode* addZoneLinkNode(TravelNode* startNode);
TravelNode* addRandomExtNode(TravelNode* startNode);
void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId);
// Taxi graph (BFS-based path lookup between taxi nodes)
void InitTaxiGraph();
std::vector<uint32> 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<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
bool GetFullPath(TravelPlan& plan, WorldPosition botPos,
uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr);
// Resolve A* route between two world positions (returns node vector)
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,
WorldPosition endPos);
// Get stored walk points for one edge (from→to). Empty if no path.
std::vector<G3D::Vector3> 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<uint32, uint32> BFS(uint32 startNode);
std::vector<uint32> BuildPath(
uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap);
std::unordered_map<uint32, std::vector<uint32>> m_taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>>
m_taxiPathCache;
std::vector<TravelNode*> nodes;
std::unordered_map<uint32, std::vector<TravelNode*>> m_zoneIndex;
std::unordered_map<uint32, std::vector<TravelNode*>> m_mapIndex;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;
bool hasToSave = false;
bool hasToGen = false;
bool hasToFullGen = false;
};
#define sTravelNodeMap TravelNodeMap::instance()
#endif