From 40cd8088864c8654759c6e16756a0acab13e4b71 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:28:15 -0700 Subject: [PATCH] Isolate travel changes from branch RpgGoCity --- conf/playerbots.conf.dist | 6 + src/Ai/Base/Actions/DebugAction.cpp | 4 +- src/Ai/Base/Actions/FollowActions.cpp | 79 +- src/Ai/Base/Actions/MovementActions.cpp | 534 ++++++++- src/Ai/Base/Actions/MovementActions.h | 34 + src/Ai/World/Rpg/Action/NewRpgAction.cpp | 19 +- src/Ai/World/Rpg/Action/NewRpgAction.h | 10 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 129 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 5 + src/Ai/World/Rpg/NewRpgInfo.cpp | 14 + src/Ai/World/Rpg/NewRpgInfo.h | 4 +- src/Bot/PlayerbotAI.cpp | 15 + src/Bot/PlayerbotAI.h | 1 + src/Bot/RandomPlayerbotMgr.cpp | 9 +- src/Mgr/Travel/TravelMgr.cpp | 234 ++-- src/Mgr/Travel/TravelMgr.h | 35 +- src/Mgr/Travel/TravelNode.cpp | 1110 ++++++++++-------- src/Mgr/Travel/TravelNode.h | 360 ++++-- src/PlayerbotAIConfig.cpp | 1 + src/PlayerbotAIConfig.h | 1 + src/Script/PlayerbotCommandScript.cpp | 15 + 21 files changed, 1725 insertions(+), 894 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 88920a050..bd2f4476a 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1028,6 +1028,12 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2 # Default: 1 (enabled) AiPlayerbot.EnableNewRpgStrategy = 1 +# Use pre-computed travel node paths for long-distance movement (>300 yards). +# When enabled, bots use the travel node graph (A*, flight paths, transports) +# instead of repeated mmap hops. Experimental. +# Default: 0 (disabled) +AiPlayerbot.EnableTravelNodes = 0 + # Control probability weights for RPG status of bots. Takes effect only when the status meets its premise.​ # Sum of weights need not be 100. Set to 0 to disable the status. # diff --git a/src/Ai/Base/Actions/DebugAction.cpp b/src/Ai/Base/Actions/DebugAction.cpp index edf1ef925..b79707077 100644 --- a/src/Ai/Base/Actions/DebugAction.cpp +++ b/src/Ai/Base/Actions/DebugAction.cpp @@ -275,7 +275,7 @@ bool DebugAction::Execute(Event event) [] { TravelNodeMap::instance().removeNodes(); - TravelNodeMap::instance().loadNodeStore(); + TravelNodeMap::instance().LoadNodeStore(); }); t.detach(); @@ -297,7 +297,7 @@ bool DebugAction::Execute(Event event) // uint32 time = 60 * IN_MILLISECONDS; //not used, line marked for removal. - std::vector ppath = l.second->getPath(); + std::vector ppath = l.second->GetPath(); for (auto p : ppath) { diff --git a/src/Ai/Base/Actions/FollowActions.cpp b/src/Ai/Base/Actions/FollowActions.cpp index 48ce74ad3..1100ab460 100644 --- a/src/Ai/Base/Actions/FollowActions.cpp +++ b/src/Ai/Base/Actions/FollowActions.cpp @@ -19,83 +19,8 @@ #include "Transport.h" #include "Map.h" -namespace -{ - Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z) - { - if (!map || !ref) - return nullptr; - - std::array const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f }; - for (float const pz : probes) - { - if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref)) - return t; - } - - return nullptr; - } - - // Attempts to find a point on the leader's transport that is closer to the bot, - // by probing along the segment from master -> bot and returning the last point - // that is still detected as being on the expected transport. - bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref, - float masterX, float masterY, float masterZ, - float botX, float botY, float botZ, - float& outX, float& outY, float& outZ) - { - if (!map || !expectedTransport || !ref) - return false; - - uint32 const phaseMask = ref->GetPhaseMask(); - - // Ensure master is actually detected on that transport (tolerant). - if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport) - return false; - - // The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z. - float const probeZ = std::max(masterZ, botZ); - - // Adaptive step count: small platforms need tighter sampling. - float const dx2 = botX - masterX; - float const dy2 = botY - masterY; - float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2); - int32 const steps = std::clamp(static_cast(dist2d / 0.75f), 10, 28); - - float const dx = (botX - masterX) / static_cast(steps); - float const dy = (botY - masterY) / static_cast(steps); - - // Master must actually be on the expected transport for this to work. - if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport) - return false; - - float lastX = masterX; - float lastY = masterY; - bool found = false; - - for (int32 i = 1; i <= steps; ++i) - { - float const px = masterX + dx * i; - float const py = masterY + dy * i; - - Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ); - if (t != expectedTransport) - break; - - lastX = px; - lastY = py; - found = true; - } - - if (!found) - return false; - - outX = lastX; - outY = lastY; - outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat - return true; - } -} +// Transport helpers (GetTransportForPosTolerant, FindBoardingPointOnTransport, +// BoardTransport) are now on MovementAction — inherited by FollowAction. bool FollowAction::Execute(Event /*event*/) { diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 85855f60d..81d3c6da2 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -11,6 +11,7 @@ #include #include "Corpse.h" +#include "DBCStores.h" #include "Event.h" #include "FleeManager.h" #include "G3D/Vector3.h" @@ -19,7 +20,9 @@ #include "LootObjectStack.h" #include "Map.h" #include "MotionMaster.h" +#include "MoveSpline.h" #include "MoveSplineInitArgs.h" +#include "TravelNode.h" #include "MovementGenerator.h" #include "ObjectDefines.h" #include "ObjectGuid.h" @@ -36,6 +39,7 @@ #include "SpellInfo.h" #include "Stances.h" #include "Timer.h" +#include "Transport.h" #include "Unit.h" #include "Vehicle.h" #include "WaypointMovementGenerator.h" @@ -128,10 +132,8 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged) float z = target->GetPositionZ(); // Use standard PathGenerator to find a route. - PathGenerator path(bot); - path.CalculatePath(x, y, z, false); - PathType type = path.GetPathType(); - if (type != PATHFIND_NORMAL && type != PATHFIND_INCOMPLETE) + PathResult path = GeneratePath(x, y, z, DEFAULT_PATH_ACCEPT_MASK, false); + if (!path.reachable) return false; if (!ranged) @@ -140,9 +142,9 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged) float dist = FLT_MAX; PositionInfo dest; - if (!path.GetPath().empty()) + if (!path.points.empty()) { - for (auto& point : path.GetPath()) + for (auto& point : path.points) { if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) CreateWp(bot, point.x, point.y, point.z, 0.0, 2334); @@ -1732,6 +1734,19 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d // return current_z; // } +PathResult MovementAction::GeneratePath(float x, float y, float z, uint32 acceptMask, bool forceDestination) +{ + PathResult result; + PathGenerator gen(bot); + gen.CalculatePath(x, y, z, forceDestination); + result.pathType = gen.GetPathType(); + result.reachable = !(result.pathType & (~acceptMask)); + result.points = gen.GetPath(); + result.actualEnd = gen.GetActualEndPosition(); + result.end = gen.GetEndPosition(); + return result; +} + const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y, float z, float& modified_z, int maxSearchCount, bool normal_only, float step) { @@ -2972,4 +2987,509 @@ bool MoveAwayFromPlayerWithDebuffAction::Execute(Event /*event*/) return false; } -bool MoveAwayFromPlayerWithDebuffAction::isPossible() { return bot->CanFreeMove(); } +bool MoveAwayFromPlayerWithDebuffAction::isPossible() +{ + return bot->CanFreeMove(); +} + +bool MovementAction::CheckSplineProgress(TravelPlan& state) +{ + if (!state.splineActive) + return false; + + if (bot->movespline->Finalized()) + { + G3D::Vector3 const& endPt = state.walkPoints.back(); + float distToEnd = bot->GetExactDist(endPt.x, endPt.y, endPt.z); + + if (distToEnd < 10.0f) + { + state.splineActive = false; + state.walkPoints.clear(); + return true; // Arrived + } + + // If we havent arrived to destination, but are done moving then something interrupted it. + // Need to restart. Reset state + state.splineActive = false; + return false; + } + + // Stuck detection + if (state.splineStartTime && + GetMSTimeDiffToNow(state.splineStartTime) > state.expectedDuration * 2 + (30 * IN_MILLISECONDS)) + { + G3D::Vector3 const& endPt = state.walkPoints.back(); + botAI->TeleportTo(WorldLocation(bot->GetMapId(), endPt.x, endPt.y, endPt.z)); + state.splineActive = false; + state.walkPoints.clear(); + return true; + } + + return false; // Still moving +} + +bool MovementAction::LaunchWalkSpline(TravelPlan& state) +{ + if (state.walkPoints.size() < 2) + { + state.walkPoints.clear(); + return false; + } + + // Trim to current position + G3D::Vector3 botPos(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + float closestDist = FLT_MAX; + size_t closestIdx = 0; + for (size_t i = 0; i < state.walkPoints.size(); ++i) + { + float distance = (state.walkPoints[i] - botPos).squaredLength(); + if (distance < closestDist) + { + closestDist = distance; + closestIdx = i; + } + } + if (closestIdx > 0) + state.walkPoints.erase(state.walkPoints.begin(), state.walkPoints.begin() + closestIdx); + state.walkPoints.insert(state.walkPoints.begin(), botPos); + + if (state.walkPoints.size() < 2) + { + state.walkPoints.clear(); + return true; + } + + // Mount up + if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive()) + botAI->DoSpecificAction("check mount state", Event(), true); + + float totalDist = 0; + for (size_t i = 1; i < state.walkPoints.size(); ++i) + totalDist += (state.walkPoints[i] - state.walkPoints[i - 1]).length(); + + float speed = bot->GetSpeed(MOVE_RUN); + state.expectedDuration = static_cast((totalDist / speed) * IN_MILLISECONDS); + + bot->GetMotionMaster()->MoveSplinePath(&state.walkPoints, FORCED_MOVEMENT_RUN); + + state.splineStartTime = getMSTime(); + state.splineActive = true; + + LOG_DEBUG("playerbots", "[TravelPlan] Bot {} walk spline: {} points, {:.0f}y", + bot->GetName(), state.walkPoints.size(), totalDist); + + return false; // Walking +} + +bool MovementAction::MoveToSpline(TravelPlan& state, WorldPosition target) +{ + if (!IsMovingAllowed()) + return false; + + // Generate path + state.walkPoints.clear(); + PathResult path = GeneratePath(target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ()); + for (auto const& pt : path.points) + state.walkPoints.push_back(G3D::Vector3(pt.x, pt.y, pt.z)); + + if (state.walkPoints.size() < 2) + { + state.walkPoints.clear(); + return false; + } + + // Launch spline movement + LaunchWalkSpline(state); + return true; +} + +bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination) +{ + WorldPosition botPos(bot->GetMapId(), bot->GetPositionX(), + bot->GetPositionY(), bot->GetPositionZ()); + + bool havePlan = sTravelNodeMap.GetFullPath(plan, botPos, + bot->GetZoneId(), bot->GetTeamId(), + destination); + + if (!havePlan) + TeleportFallback(plan, destination, "no plan"); + + return havePlan; +} + +bool MovementAction::ExecuteTravelPlan(TravelPlan& state) +{ + if (!state.IsActive()) + return false; + + if (bot->IsInFlight()) + return true; + + // Handle active spline + if (state.splineActive) + { + if (!CheckSplineProgress(state)) + { + if (state.splineActive) + return true; // Still moving + else + LaunchWalkSpline(state); // Interrupted, re-launch + } + return true; + } + + if (state.stepIdx >= state.steps.size()) + { + state.Reset(); + return true; + } + + const PathNodePoint& pt = state.steps[state.stepIdx]; + + switch (pt.type) + { + case PathNodeType::NODE_PREPATH: + case PathNodeType::NODE_PATH: + case PathNodeType::NODE_NODE: + { + // Batch consecutive walk points into one spline, capped at 100 points per tick. + static constexpr uint32 MAX_SPLINE_POINTS = 100; + state.walkPoints.clear(); + while (state.stepIdx < state.steps.size() && state.walkPoints.size() < MAX_SPLINE_POINTS) + { + const PathNodePoint& wp = state.steps[state.stepIdx]; + if (wp.type != PathNodeType::NODE_PREPATH && wp.type != PathNodeType::NODE_PATH && wp.type != PathNodeType::NODE_NODE) + break; + state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(), wp.point.GetPositionY(), wp.point.GetPositionZ())); + state.stepIdx++; + } + + if (state.walkPoints.empty()) + return true; + + // Already near end of batch? + G3D::Vector3 const& last = state.walkPoints.back(); + float dist = bot->GetExactDist(last.x, last.y, last.z); + if (dist < 10.0f) + { + state.walkPoints.clear(); + return true; + } + + // Too far from first point — teleport + if (state.walkPoints.size() >= 2) + { + G3D::Vector3 const& first = state.walkPoints.front(); + float distToFirst = bot->GetExactDist( first.x, first.y, first.z); + if (distToFirst > MAX_PATHFINDING_DISTANCE) + { + TeleportFallback(state, WorldPosition(bot->GetMapId(), last.x, last.y, last.z), "walk batch too far"); + state.walkPoints.clear(); + return true; + } + } + // Single point — use PathGenerator directly + if (state.walkPoints.size() < 2) + { + WorldPosition target(bot->GetMapId(), last.x, last.y, last.z); + MoveToSpline(state, target); + state.walkPoints.clear(); + return true; + } + LaunchWalkSpline(state); + return true; + } + + case PathNodeType::NODE_PORTAL: + { + // Pair: source (pointIdx) + dest (pointIdx+1) + if (state.stepIdx + 1 >= state.steps.size()) + { + state.Reset(); + return false; + } + + const PathNodePoint& src = state.steps[state.stepIdx]; + const PathNodePoint& dst = state.steps[state.stepIdx + 1]; + + // Already on destination map? + if (bot->GetMapId() == dst.point.GetMapId()) + { + state.stepIdx += 2; + return true; + } + // Walk to portal source + float dist = bot->GetExactDist(src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ()); + if (dist > INTERACTION_DISTANCE) + return MoveTo(src.point.GetMapId(), src.point.GetPositionX(), src.point.GetPositionY(), src.point.GetPositionZ()); + + // At portal, but havent teleported though + TeleportFallback(state, dst.point, "portal walk-through"); + state.stepIdx += 2; + return true; + } + + case PathNodeType::NODE_TRANSPORT: + { + if (state.stepIdx + 1 >= state.steps.size()) + { + state.Reset(); + return false; + } + + const PathNodePoint& board = state.steps[state.stepIdx]; + const PathNodePoint& arrive = state.steps[state.stepIdx + 1]; + // Arrived at destination? + if (bot->GetMapId() == arrive.point.GetMapId() && !bot->GetTransport()) + { + state.stepIdx += 2; + return true; + } + // On transport — wait + if (bot->GetTransport()) + { + if (bot->GetMapId() == arrive.point.GetMapId()) + { + bot->GetTransport()->RemovePassenger(bot); + bot->StopMovingOnCurrentPos(); + state.stepIdx += 2; + } + return true; + } + + // Walk to boarding point + float dist = bot->GetExactDist(board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ()); + if (dist > 60.0f) + return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ()); + + // Try to board + if (board.entry) + { + Map* map = bot->GetMap(); + if (map) + { + Transport* transport = + GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), board.point.GetPositionX(), + board.point.GetPositionY(), board.point.GetPositionZ()); + if (transport && transport->GetEntry() == board.entry) + { + BoardTransport(transport); + return true; + } + } + } + // Wait at boarding point + if (dist > INTERACTION_DISTANCE) + return MoveTo(board.point.GetMapId(), board.point.GetPositionX(), board.point.GetPositionY(), board.point.GetPositionZ()); + return true; + } + + case PathNodeType::NODE_FLIGHTPATH: + { + if (state.stepIdx + 1 >= state.steps.size()) + { + state.Reset(); + return false; + } + + const PathNodePoint& dep = state.steps[state.stepIdx]; + const PathNodePoint& arr = state.steps[state.stepIdx + 1]; + + if (bot->IsInFlight()) + return true; + + // Resolve taxi path + if (state.route.empty()) + { + uint32 fromTaxi = sObjectMgr->GetNearestTaxiNode(dep.point.GetPositionX(), dep.point.GetPositionY(), + dep.point.GetPositionZ(), dep.point.GetMapId(), bot->GetTeamId()); + uint32 toTaxi = sObjectMgr->GetNearestTaxiNode(arr.point.GetPositionX(), arr.point.GetPositionY(), + arr.point.GetPositionZ(), arr.point.GetMapId(), bot->GetTeamId()); + + if (fromTaxi && toTaxi && fromTaxi != toTaxi) + state.route = sTravelNodeMap.FindTaxiPath(fromTaxi, toTaxi); + + if (state.route.empty()) + { + state.stepIdx += 2; + return true; + } + } + + Creature* flightMaster = sTravelMgr.GetNearestFlightMaster(bot); + if (!flightMaster || !flightMaster->IsAlive()) + { + state.route.clear(); + state.stepIdx += 2; + return true; + } + + if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) + return MoveTo(flightMaster, INTERACTION_DISTANCE); + + botAI->RemoveShapeshift(); + if (bot->IsMounted()) + bot->Dismount(); + + if (bot->ActivateTaxiPathTo(state.route, flightMaster, 0)) + LOG_DEBUG("playerbots","[TravelPlan] Bot {} taking flight ({} nodes)", bot->GetName(), state.route.size()); + + state.route.clear(); + state.stepIdx += 2; + return true; + } + + case PathNodeType::NODE_TELEPORT: + { + if (state.stepIdx + 1 >= state.steps.size()) + { + state.Reset(); + return false; + } + + const PathNodePoint& dst = state.steps[state.stepIdx + 1]; + TeleportFallback(state, dst.point, "teleport spell"); + state.stepIdx += 2; + return true; + } + + case PathNodeType::NODE_FLYING_MOUNT: + { + if (state.stepIdx + 1 >= state.steps.size()) + { + state.Reset(); + return false; + } + + const PathNodePoint& dst = state.steps[state.stepIdx + 1]; + TeleportFallback(state, dst.point, "flying mount not implemented"); + state.stepIdx += 2; + return true; + } + } + return false; +} + +void MovementAction::TeleportFallback(TravelPlan& state, WorldPosition target, char const* reason) +{ + LOG_INFO("playerbots", "[TravelPlan] Bot {} teleport fallback ({}): from map={} ({:.0f},{:.0f},{:.0f}) to map={} ({:.0f},{:.0f},{:.0f})", + bot->GetName(), reason, bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), target.GetMapId(), target.GetPositionX(), + target.GetPositionY(), target.GetPositionZ()); + + botAI->TeleportTo(target); +} + +Transport* MovementAction::GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z) +{ + if (!map || !ref) + return nullptr; + + std::array const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f }; + for (float const pz : probes) + { + if (Transport* transport = map->GetTransportForPos(phaseMask, x, y, pz, ref)) + return transport; + } + return nullptr; +} + +bool MovementAction::FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref, + float refX, float refY, float refZ, float botX, float botY, float botZ, float& outX, float& outY, float& outZ) +{ + if (!map || !expectedTransport || !ref) + return false; + + uint32 const phaseMask = ref->GetPhaseMask(); + if (GetTransportForPosTolerant(map, ref, phaseMask, refX, refY, refZ) + != expectedTransport) + return false; + + float const probeZ = std::max(refZ, botZ); + float const dx2 = botX - refX; + float const dy2 = botY - refY; + float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2); + int32 const steps = std::clamp(static_cast(dist2d / 0.75f), 10, 28); + float const dx = (botX - refX) / static_cast(steps); + float const dy = (botY - refY) / static_cast(steps); + + if (map->GetTransportForPos(phaseMask, refX, refY, probeZ, ref) != expectedTransport) + return false; + + float lastX = refX; + float lastY = refY; + bool found = false; + + for (int32 i = 1; i <= steps; ++i) + { + float const px = refX + dx * i; + float const py = refY + dy * i; + Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ); + if (t != expectedTransport) + break; + lastX = px; + lastY = py; + found = true; + } + + if (!found) + return false; + + outX = lastX; + outY = lastY; + outZ = refZ; + return true; +} + +bool MovementAction::BoardTransport(Transport* transport) +{ + if (!transport || transport->IsStaticTransport()) + return false; + + Map* map = bot->GetMap(); + if (!map) + return false; + + // Already on this transport + if (bot->GetTransport() == transport) + return true; + + // Check if bot is on the transport surface + float probeZ = std::max(bot->GetPositionZ(), transport->GetPositionZ()); + Transport* surface = GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), bot->GetPositionX(), + bot->GetPositionY(), probeZ); + + if (surface == transport) + { + transport->AddPassenger(bot, true); + bot->StopMovingOnCurrentPos(); + return true; + } + // Not on surface — move toward the transport + float destX = transport->GetPositionX(); + float destY = transport->GetPositionY(); + float destZ = transport->GetPositionZ(); + + // Try to find nearest boarding edge + float edgeX, edgeY, edgeZ; + if (FindBoardingPointOnTransport(map, transport, transport, transport->GetPositionX(), transport->GetPositionY(), + transport->GetPositionZ(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), edgeX, edgeY, edgeZ)) + { + destX = edgeX; + destY = edgeY; + destZ = edgeZ; + } + + // MovePoint without pathfinding (transport is a moving object) + if (MotionMaster* mm = bot->GetMotionMaster()) + { + if (bot->IsSitState()) + bot->SetStandState(UNIT_STAND_STATE_STAND); + + mm->MovePoint(0, destX, destY, destZ, FORCED_MOVEMENT_NONE, 0.0f, 0.0f, false, false); + } + + return false; +} diff --git a/src/Ai/Base/Actions/MovementActions.h b/src/Ai/Base/Actions/MovementActions.h index 377e8360a..a0d160610 100644 --- a/src/Ai/Base/Actions/MovementActions.h +++ b/src/Ai/Base/Actions/MovementActions.h @@ -10,6 +10,7 @@ #include "Action.h" #include "LastMovementValue.h" +#include "PathGenerator.h" #include "PlayerbotAIConfig.h" class Player; @@ -22,6 +23,19 @@ class Position; #define ANGLE_90_DEG M_PI_2 #define ANGLE_120_DEG (2.f * static_cast(M_PI) / 3.f) +// Default acceptable path types for GeneratePath +constexpr uint32 DEFAULT_PATH_ACCEPT_MASK = 0x01 /*PATHFIND_NORMAL*/ | 0x04 /*PATHFIND_INCOMPLETE*/; +uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; + +struct PathResult +{ + Movement::PointsArray points; + G3D::Vector3 actualEnd; + G3D::Vector3 end; + PathType pathType; + bool reachable; +}; + class MovementAction : public Action { public: @@ -67,6 +81,26 @@ protected: bool FleePosition(Position pos, float radius, uint32 minInterval = 1000); bool CheckLastFlee(float curAngle, std::list& infoList); + PathResult GeneratePath(float x, float y, float z, uint32 acceptMask = DEFAULT_PATH_ACCEPT_MASK, bool forceDestination = true); + + bool GetTravelPlan(TravelPlan& plan, WorldPosition destination); + bool ExecuteTravelPlan(TravelPlan& state); + + // Transport boarding helpers (shared by FollowAction and travel plan) + static Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, + uint32 phaseMask, float x, float y, float z); + static bool FindBoardingPointOnTransport(Map* map, Transport* transport, + WorldObject* ref, float refX, float refY, float refZ, + float botX, float botY, float botZ, + float& outX, float& outY, float& outZ); + bool BoardTransport(Transport* transport); + +private: + bool LaunchWalkSpline(TravelPlan& state); + bool CheckSplineProgress(TravelPlan& state); + bool MoveToSpline(TravelPlan& state, WorldPosition target); + void TeleportFallback(TravelPlan& state, WorldPosition target, char const* reason); + protected: struct CheckAngle { diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index ca0ca2433..f8797ab22 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -468,26 +468,21 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) data.inFlight = true; return false; } - Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster); + + if (bot->GetDistance(data.fromPos) > INTERACTION_DISTANCE) + return MoveFarTo(data.fromPos); + + Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMasterGuid); if (!flightMaster || !flightMaster->IsAlive()) { botAI->rpgInfo.ChangeToIdle(); return true; } - if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) - return MoveFarTo(flightMaster); - std::vector nodes = data.path; - - botAI->RemoveShapeshift(); - if (bot->IsMounted()) - bot->Dismount(); - - if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0)) + if (!TakeFlight(data.path, flightMaster)) { - LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(), - flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]); botAI->rpgInfo.ChangeToIdle(); + return true; } return true; } diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.h b/src/Ai/World/Rpg/Action/NewRpgAction.h index 83594204b..5433284d1 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgAction.h @@ -47,11 +47,11 @@ public: protected: // static NewRpgStatusTransitionProb transitionMat; - const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ; - const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ; - const int32 statusRestDuration = 30 * IN_MILLISECONDS ; - const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ; - const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ; + const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS; + const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS; + const int32 statusRestDuration = 30 * IN_MILLISECONDS; + const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS; + const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS; }; class NewRpgGoGrindAction : public NewRpgBaseAction diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 092b11538..eb989dd42 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -96,25 +96,35 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) botAI->rpgInfo.stuckAttempts = 0; const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId()); std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry); - LOG_DEBUG( - "playerbots", - "[New RPG] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})", + LOG_DEBUG("playerbots","[New RPG] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})", bot->GetName(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(), - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), bot->GetZoneId(), - zone_name); - bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); - return bot->TeleportTo(dest); + dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), bot->GetZoneId(), zone_name); + botAI->TeleportTo(dest); + return true; } float dis = bot->GetExactDist(dest); + + // Long distance + travel nodes enabled: use the pre-computed node graph + // (A*, flight paths, transports) instead of repeated mmap hops. + if (dis > MAX_PATHFINDING_DISTANCE && sPlayerbotAIConfig.enableTravelNodes) + { + if (!botAI->rpgInfo.HasActiveTravelPlan()) + StartTravelPlan(dest); + + return UpdateTravelPlan(); + } + + // Crossed below the travel-node threshold — clear any leftover plan + if (botAI->rpgInfo.HasActiveTravelPlan()) + botAI->rpgInfo.ClearTravel(); + + // Short range: close enough for a single mmap call if (dis < pathFinderDis) { return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false, false, true); } - - const uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; - // Primary strategy: ask mmap for a route to the TRUE destination. // If mmap can reach it directly (PATHFIND_NORMAL) or partially // (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap @@ -126,23 +136,18 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) // subsequent ticks early-out via IsWaitingForLastMove and no // further PathGenerator calls fire until the bot arrives. { - PathGenerator path(bot); - path.CalculatePath(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - PathType type = path.GetPathType(); - bool canReach = !(type & (~typeOk)); - if (canReach) + PathResult path = GeneratePath(dest.GetPositionX(), dest.GetPositionY(), + dest.GetPositionZ(), RELAXED_PATH_ACCEPT_MASK); + if (path.reachable) { - const G3D::Vector3& endPos = path.GetActualEndPosition(); // Only commit if the mmap endpoint actually makes progress // toward the destination. For pathological INCOMPLETE // results (e.g. disconnected polys that still report // INCOMPLETE) the endpoint can land right under the bot; // fall through to cone sampling in that case. - float endDistToDest = dest.GetExactDist(endPos.x, endPos.y, endPos.z); + float endDistToDest = dest.GetExactDist(path.actualEnd.x, path.actualEnd.y, path.actualEnd.z); if (endDistToDest + 5.0f < disToDest) - { - return MoveTo(bot->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, true); - } + return MoveTo(bot->GetMapId(), path.actualEnd.x, path.actualEnd.y, path.actualEnd.z, false, false, false, true); } } @@ -166,18 +171,14 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) float dx = x + cos(angle) * sampleDis; float dy = y + sin(angle) * sampleDis; float dz = z + 0.5f; - PathGenerator path(bot); - path.CalculatePath(dx, dy, dz); - PathType type = path.GetPathType(); - bool canReach = !(type & (~typeOk)); + PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK); - if (canReach && fabs(delta) <= minDelta) + if (path.reachable && fabs(delta) <= minDelta) { found = true; - const G3D::Vector3& endPos = path.GetActualEndPosition(); - rx = endPos.x; - ry = endPos.y; - rz = endPos.z; + rx = path.actualEnd.x; + ry = path.actualEnd.y; + rz = path.actualEnd.z; minDelta = fabs(delta); } } @@ -188,12 +189,31 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) return false; } +void NewRpgBaseAction::StartTravelPlan(WorldPosition dest) +{ + TravelPlan& plan = botAI->rpgInfo.travelPlan; + GetTravelPlan(plan, dest); + + LOG_DEBUG("playerbots","[New RPG] Bot {} starting travel plan to ({:.0f},{:.0f},{:.0f}) map={}, {} points", + bot->GetName(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.GetMapId(), plan.steps.size()); +} + +bool NewRpgBaseAction::UpdateTravelPlan() +{ + TravelPlan& plan = botAI->rpgInfo.travelPlan; + + bool result = ExecuteTravelPlan(plan); + + if (!plan.IsActive()) + botAI->rpgInfo.ClearTravel(); + + return result; +} + bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) { if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) - { return false; - } WorldObject* object = botAI->GetWorldObject(guid); if (!object) @@ -245,13 +265,9 @@ bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority, float dy = y + distance * sin(angle); float dz = z; - PathGenerator path(bot); - path.CalculatePath(dx, dy, dz); - PathType type = path.GetPathType(); - uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; - bool canReach = !(type & (~typeOk)); + PathResult path = GeneratePath(dx, dy, dz, RELAXED_PATH_ACCEPT_MASK); - if (!canReach) + if (!path.reachable) continue; if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz)) @@ -276,6 +292,28 @@ bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority) return true; } + +bool NewRpgBaseAction::TakeFlight(std::vector const& taxiNodes, Creature* flightMaster) +{ + if (taxiNodes.size() < 2 || !flightMaster || !flightMaster->IsAlive()) + return false; + + botAI->RemoveShapeshift(); + if (bot->IsMounted()) + bot->Dismount(); + + if (!bot->ActivateTaxiPathTo(taxiNodes, flightMaster, 0)) + { + LOG_DEBUG("playerbots", "[New RPG] Bot {} flight ({} nodes, {} to {}) failed", + bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back()); + return false; + } + + LOG_DEBUG("playerbots", "[New RPG] Bot {} taking flight ({} nodes, {} to {})", + bot->GetName(), taxiNodes.size(), taxiNodes.front(), taxiNodes.back()); + return true; +} + /// @TODO: Fix redundant code /// Quest related method refer to TalkToQuestGiverAction.h bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid) @@ -978,6 +1016,10 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) uint32 idx = urand(0, lo_prepared_locs.size() - 1); dest = lo_prepared_locs[idx]; } + + if (!dest.IsValid()) + return dest; + LOG_DEBUG("playerbots", "[New RPG] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})", bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size()); @@ -1021,6 +1063,10 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) uint32 idx = urand(0, prepared_locs.size() - 1); dest = prepared_locs[idx]; } + + if (!dest.IsValid()) + return dest; + LOG_DEBUG("playerbots", "[New RPG] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})", bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), prepared_locs.size(), locs.size()); @@ -1058,7 +1104,6 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta probSum += sPlayerbotAIConfig.RpgStatusProbWeight[status]; } } - // Safety check. Default to "rest" if all RPG weights = 0 if (availableStatus.empty() || probSum == 0) { botAI->rpgInfo.ChangeToRest(); @@ -1139,11 +1184,11 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta } case RPG_TRAVEL_FLIGHT: { - ObjectGuid flightMaster; + ObjectGuid flightMasterGuid; std::vector path; - if (SelectRandomFlightTaxiNode(flightMaster, path)) + if (SelectRandomFlightTaxiNode(flightMasterGuid, path)) { - botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path); + botAI->rpgInfo.ChangeToTravelFlight(flightMasterGuid, path); return true; } return false; @@ -1240,3 +1285,5 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) } return false; } + + diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index eaba72446..245d78ece 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -33,6 +33,7 @@ protected: bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE); bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr); bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); + bool TakeFlight(std::vector const& taxiNodes, Creature* flightMaster); /* QUEST RELATED CHECK */ ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f); @@ -69,6 +70,10 @@ protected: // the teleport fires, but long enough that a genuine long // walk that is slowly making progress never triggers it. const uint32 stuckTime = 90 * 1000; + +private: + void StartTravelPlan(WorldPosition dest); + bool UpdateTravelPlan(); }; #endif diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp index 4935503fc..83c780647 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -4,33 +4,43 @@ #include "Timer.h" +void NewRpgInfo::ClearTravel() +{ + travelPlan.Reset(); +} + void NewRpgInfo::ChangeToGoGrind(WorldPosition pos) { startT = getMSTime(); + ClearTravel(); data = GoGrind{pos}; } void NewRpgInfo::ChangeToGoCamp(WorldPosition pos) { startT = getMSTime(); + ClearTravel(); data = GoCamp{pos}; } void NewRpgInfo::ChangeToWanderNpc() { startT = getMSTime(); + ClearTravel(); data = WanderNpc{}; } void NewRpgInfo::ChangeToWanderRandom() { startT = getMSTime(); + ClearTravel(); data = WanderRandom{}; } void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) { startT = getMSTime(); + ClearTravel(); DoQuest do_quest; do_quest.questId = questId; do_quest.quest = quest; @@ -40,6 +50,7 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path) { startT = getMSTime(); + ClearTravel(); TravelFlight flight; flight.fromFlightMaster = fromFlightMaster; flight.path = std::move(path); @@ -58,12 +69,14 @@ void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId) void NewRpgInfo::ChangeToRest() { startT = getMSTime(); + ClearTravel(); data = Rest{}; } void NewRpgInfo::ChangeToIdle() { startT = getMSTime(); + ClearTravel(); data = Idle{}; } @@ -76,6 +89,7 @@ void NewRpgInfo::Reset() { data = Idle{}; startT = getMSTime(); + ClearTravel(); } void NewRpgInfo::SetMoveFarTo(WorldPosition pos) diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index 9e6abdda4..4a9188489 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -74,7 +74,9 @@ struct NewRpgInfo uint32 stuckTs{0}; uint32 stuckAttempts{0}; WorldPosition moveFarPos; - // END MOVE_FAR + TravelPlan travelPlan; + bool HasActiveTravelPlan() const { return travelPlan.IsActive(); } + void ClearTravel(); using RpgData = std::variant< Idle, diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index e9de581de..288ee0180 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -735,6 +735,21 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr } } +void PlayerbotAI::TeleportTo(WorldLocation loc, bool resetAI) +{ + if (!bot || bot->IsBeingTeleported() || !bot->IsInWorld()) + return; + + bot->GetMotionMaster()->Clear(); + if (resetAI) + Reset(true); + else + InterruptSpell(); + bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); + bot->TeleportTo(loc.GetMapId(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ(), 0); + bot->SendMovementFlagUpdate(); +} + void PlayerbotAI::HandleTeleportAck() { if (!bot || !bot->GetSession()) diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index cfa27ed4e..1aafcdefc 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -396,6 +396,7 @@ public: void HandleMasterIncomingPacket(WorldPacket const& packet); void HandleMasterOutgoingPacket(WorldPacket const& packet); void HandleTeleportAck(); + void TeleportTo(WorldLocation loc, bool resetAI = false); void ChangeEngine(BotState type); void ChangeEngineOnCombat(); void ChangeEngineOnNonCombat(); diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index ee06c58f8..04297822c 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -1696,14 +1696,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& break; } - bot->GetMotionMaster()->Clear(); - PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); - if (botAI) - botAI->Reset(true); - bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); - bot->TeleportTo(loc.GetMapId(), x, y, z, 0); - bot->SendMovementFlagUpdate(); - + botAI->TeleportTo(WorldLocation(loc.GetMapId(), x, y, z, 0), true); if (pmo) pmo->finish(); diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 1868bc2e3..5d0e014f7 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -687,93 +687,6 @@ std::vector WorldPosition::frommGridCoord(mGridCoord GridCoord) return retVec; } -// TODO: Cleanup — make this actually work. -void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y) -{ - std::string const fileName = "load_map_grid.csv"; -/* - if (isOverworld() && false || false) - { - if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y)) - if (sPlayerbotAIConfig.hasLog(fileName)) - { - std::ostringstream out; - out << sPlayerbotAIConfig.GetTimestampStr(); - out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1") - << ","; - printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true); - sPlayerbotAIConfig.log(fileName, out.str().c_str()); - } - } - else - { - // This needs to be disabled or maps will not load. - // Needs more testing to check for impact on movement. - if (false) - if (!TravelMgr::instance().isBadVmap(mapId, x, y)) - { - // load VMAPs for current map/grid... - const MapEntry* i_mapEntry = sMapStore.LookupEntry(mapId); - //const char* mapName = i_mapEntry ? i_mapEntry->name[sWorld->GetDefaultDbcLocale()] : "UNNAMEDMAP\x0"; //not used, (usage are commented out below), line marked for removal. - - int vmapLoadResult = VMAP::VMapFactory::createOrGetVMapMgr()->loadMap( - (sWorld->GetDataPath() + "vmaps").c_str(), mapId, x, y); - switch (vmapLoadResult) - { - case VMAP::VMAP_LOAD_RESULT_OK: - // LOG_ERROR("playerbots", "VMAP loaded name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})", - // mapName, mapId, x, y, x, y); - break; - case VMAP::VMAP_LOAD_RESULT_ERROR: - // LOG_ERROR("playerbots", "Could not load VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, - // y:{})", mapName, mapId, x, y, x, y); - TravelMgr::instance().addBadVmap(mapId, x, y); - break; - case VMAP::VMAP_LOAD_RESULT_IGNORED: - TravelMgr::instance().addBadVmap(mapId, x, y); - // LOG_INFO("playerbots", "Ignored VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})", - // mapName, mapId, x, y, x, y); - break; - } - - if (sPlayerbotAIConfig.hasLog(fileName)) - { - std::ostringstream out; - out << sPlayerbotAIConfig.GetTimestampStr(); - out << "+00,\"vmap\", " << x << "," << y << ", " << (TravelMgr::instance().isBadVmap(mapId, x, y) ? "0" : "1") - << ","; - printWKT(frommGridCoord(mGridCoord(x, y)), out, 1, true); - sPlayerbotAIConfig.log(fileName, out.str().c_str()); - } - } -*/ - if (!TravelMgr::instance().isBadMmap(mapId, x, y)) - { - // load navmesh - Map* map = getMap(); - if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR) - TravelMgr::instance().addBadMmap(mapId, x, y); - - if (sPlayerbotAIConfig.hasLog(fileName)) - { - std::ostringstream out; - out << sPlayerbotAIConfig.GetTimestampStr(); - out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1") - << ","; - printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true); - sPlayerbotAIConfig.log(fileName, out.str().c_str()); - } - } -} - -void WorldPosition::loadMapAndVMaps(WorldPosition secondPos) -{ - for (auto& grid : getmGridCoords(secondPos)) - { - loadMapAndVMap(GetMapId(), grid.first, grid.second); - } -} - std::vector WorldPosition::fromPointsArray(std::vector path) { std::vector retVec; @@ -786,34 +699,42 @@ std::vector WorldPosition::fromPointsArray(std::vector WorldPosition::getPathStepFrom(WorldPosition startPos, Unit* bot) { - if (!bot) - return {}; + Unit* pathUnit = bot; + Creature* tempCreature = nullptr; - // Load mmaps and vmaps between the two points. - loadMapAndVMaps(startPos); + if (!pathUnit) + { + // Create a temporary creature for PathGenerator (same entry as DebugAction "show node") + Map* map = sMapMgr->FindBaseMap(startPos.GetMapId()); + if (!map) + return {}; - PathGenerator path(bot); - path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ()); + tempCreature = new Creature(); + if (!tempCreature->Create(map->GenerateLowGuid(), map, + PHASEMASK_NORMAL, 1 /*entry*/, 0, + startPos.GetPositionX(), startPos.GetPositionY(), + startPos.GetPositionZ(), 0)) + { + delete tempCreature; + return {}; + } + pathUnit = tempCreature; + + // Ensure grids are created at both endpoints so mmap tiles are available. + // EnsureGridCreated loads terrain + vmaps + mmaps but NOT objects, + // which is all PathGenerator needs. + map->EnsureGridCreated(Acore::ComputeGridCoord(startPos.GetPositionX(), startPos.GetPositionY())); + map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY())); + } + + PathGenerator path(pathUnit); + path.CalculatePath(GetPositionX(), GetPositionY(), GetPositionZ()); Movement::PointsArray points = path.GetPath(); PathType type = path.GetPathType(); - if (sPlayerbotAIConfig.hasLog("pathfind_attempt_point.csv")) - { - std::ostringstream out; - out << std::fixed << std::setprecision(1); - printWKT({startPos, *this}, out); - sPlayerbotAIConfig.log("pathfind_attempt_point.csv", out.str().c_str()); - } - - if (sPlayerbotAIConfig.hasLog("pathfind_attempt.csv") && (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)) - { - std::ostringstream out; - out << sPlayerbotAIConfig.GetTimestampStr() << "+00,"; - out << std::fixed << std::setprecision(1) << type << ","; - printWKT(fromPointsArray(points), out, 1); - sPlayerbotAIConfig.log("pathfind_attempt.csv", out.str().c_str()); - } + if (tempCreature) + delete tempCreature; if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL) return fromPointsArray(points); @@ -1079,6 +1000,14 @@ GuidPosition::GuidPosition(GameObjectData const& goData) loadedFromDB = true; } +TravelDestination::~TravelDestination() +{ + for (WorldPosition* point : points) + delete point; + + points.clear(); +} + std::vector TravelDestination::getPoints(bool ignoreFull) { if (ignoreFull) @@ -2385,9 +2314,7 @@ void TravelMgr::LoadQuestTravelTable() sPlayerbotAIConfig.openLog("unload_grid.csv", "w"); sPlayerbotAIConfig.openLog("unload_obj.csv", "w"); - TravelNodeMap::instance().loadNodeStore(); - - TravelNodeMap::instance().generateAll(); + // Node loading/generation is handled by TravelNodeMap::Init() called from TravelMgr::Init(). /* bool fullNavPointReload = false; @@ -2778,7 +2705,7 @@ void TravelMgr::LoadQuestTravelTable() //if (preloadUnlinkedPaths && !startNode->hasLinkTo(endNode) && startNode->isUselessLink(endNode)) // continue; - startNode->buildPath(endNode, nullptr, false); + startNode->BuildPath(endNode, nullptr, false); //if (startNode->hasLinkTo(endNode) && !startNode->getPathTo(endNode)->getComplete()) //startNode->removeLinkTo(endNode); @@ -2902,7 +2829,7 @@ void TravelMgr::LoadQuestTravelTable() TravelNodePath nodePath = *path.second; - std::vector pPath = nodePath.getPath(); + std::vector pPath = nodePath.GetPath(); std::reverse(pPath.begin(), pPath.end()); nodePath.setPath(pPath); @@ -4365,8 +4292,7 @@ void TravelMgr::Init() PrepareZone2LevelBracket(); PrepareDestinationCache(); } - sTravelNodeMap.InitTaxiGraph(); - LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built."); + sTravelNodeMap.Init(); } Creature* TravelMgr::GetNearestFlightMaster(Player* bot) @@ -4518,6 +4444,34 @@ std::vector TravelMgr::GetCityLocations(Player* bot) return fallbackLocations; } +bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer) +{ + uint16 botMapId = bot->GetMapId(); + auto const& cache = (bot->GetTeamId() == TEAM_HORDE) ? hordeAuctioneerCache : allianceAuctioneerCache; + + auto mapIt = cache.find(botMapId); + if (mapIt == cache.end() || mapIt->second.empty()) + return false; + + // Collect all areas on this map that have auctioneers + std::vector areaIds; + areaIds.reserve(mapIt->second.size()); + for (auto const& [areaId, npcs] : mapIt->second) + { + if (!npcs.empty()) + areaIds.push_back(areaId); + } + + if (areaIds.empty()) + return false; + + // Pick a random area, then a random auctioneer in that area + uint32 selectedArea = areaIds[urand(0, areaIds.size() - 1)]; + auto const& auctioneers = mapIt->second.at(selectedArea); + outAuctioneer = auctioneers[urand(0, auctioneers.size() - 1)]; + return true; +} + void TravelMgr::PrepareZone2LevelBracket() { // Classic WoW - Low - level zones @@ -4604,6 +4558,7 @@ void TravelMgr::PrepareDestinationCache() uint32 flightMastersCount = 0; uint32 innkeepersCount = 0; uint32 bankerCount = 0; + uint32 auctioneerCount = 0; LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); // Temporary map to group creatures by entry and area @@ -4728,7 +4683,7 @@ void TravelMgr::PrepareDestinationCache() creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 && creatureTemplate->Entry != 29282) { - BankerLocation bLoc; + NpcLocation bLoc; bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); bLoc.entry = templateEntry; uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; @@ -4751,6 +4706,31 @@ void TravelMgr::PrepareDestinationCache() } bankerCount++; } + // === AUCTIONEERS === + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_AUCTIONEER) + { + FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); + if (!factionEntry) + continue; + + bool forHorde = !(factionEntry->hostileMask & 4); + bool forAlliance = !(factionEntry->hostileMask & 2); + + if (!forHorde && !forAlliance) + continue; + + NpcLocation aLoc; + aLoc.loc = WorldLocation(mapId, x + cos(orient) * 3.0f, y + sin(orient) * 3.0f, z + 0.5f, orient + M_PI); + aLoc.entry = templateEntry; + + if (forHorde) + hordeAuctioneerCache[mapId][areaId].push_back(aLoc); + + if (forAlliance) + allianceAuctioneerCache[mapId][areaId].push_back(aLoc); + + auctioneerCount++; + } } // Process temporary caches @@ -4760,13 +4740,29 @@ void TravelMgr::PrepareDestinationCache() { CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + + float totalX = 0.0f; + float totalY = 0.0f; + float totalZ = 0.0f; + for (CreatureData const& creatureData : creatureDataList) + { + totalX += creatureData.posX; + totalY += creatureData.posY; + totalZ += creatureData.posZ; + } + + float avgX = totalX / creatureDataList.size(); + float avgY = totalY / creatureDataList.size(); + float avgZ = totalZ / creatureDataList.size(); + uint32 mapId = std::get<0>(gridTuple); + for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) { if (l < 1 || l > maxLevel) continue; - locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple))); + locsPerLevelCache[(uint8)l].push_back(WorldLocation(mapId, avgX, avgY, avgZ, 0.0f)); } } } @@ -4812,5 +4808,5 @@ void TravelMgr::PrepareDestinationCache() break; } } - LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); + LOG_INFO("playerbots", ">> {} flight masters, {} innkeepers, {} bankers, {} auctioneers collected.", flightMastersCount, innkeepersCount, bankerCount, auctioneerCount); } diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index f300ae636..98d86c36a 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -268,12 +268,6 @@ public: std::vector getmGridCoords(WorldPosition secondPos); std::vector frommGridCoord(mGridCoord GridCoord); - void loadMapAndVMap(uint32 mapId, uint8 x, uint8 y); - - void loadMapAndVMap() { loadMapAndVMap(GetMapId(), getmGridCoord().first, getmGridCoord().second); } - - void loadMapAndVMaps(WorldPosition secondPos); - // Display functions WorldPosition getDisplayLocation(); float getDisplayX() { return getDisplayLocation().GetPositionY() * -1.0; } @@ -507,9 +501,15 @@ public: radiusMin = radiusMin1; radiusMax = radiusMax1; } - virtual ~TravelDestination() = default; + virtual ~TravelDestination(); - void addPoint(WorldPosition* pos) { points.push_back(pos); } + void addPoint(WorldPosition* pos) + { + if (!pos) + return; + + points.push_back(new WorldPosition(*pos)); + } void setExpireDelay(uint32 delay) { expireDelay = delay; } @@ -673,7 +673,7 @@ public: bool isActive(Player* bot) override; virtual CreatureTemplate const* GetCreatureTemplate(); std::string const getName() override { return "RpgTravelDestination"; } - int32 getEntry() override { return 0; } + int32 getEntry() override { return entry; } std::string const getTitle() override; protected: @@ -846,6 +846,12 @@ protected: class TravelMgr { public: + struct NpcLocation + { + WorldLocation loc; + uint32 entry; + }; + static TravelMgr& instance() { static TravelMgr instance; @@ -864,6 +870,7 @@ public: const std::vector GetTeleportLocations(Player* bot); const std::vector GetTravelHubs(Player* bot); std::vector GetCityLocations(Player* bot); + bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer); const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } template @@ -968,18 +975,14 @@ private: bool InsideBracket(uint32 val) const { return val >= low && val <= high; } }; - struct BankerLocation - { - WorldLocation loc; - uint32 entry; - }; - // Navigation caches std::map allianceFlightMasterCache; std::map hordeFlightMasterCache; std::map> allianceHubsPerLevelCache; std::map> hordeHubsPerLevelCache; - std::map> bankerLocsPerLevelCache; + std::map> bankerLocsPerLevelCache; + std::unordered_map>> hordeAuctioneerCache; + std::unordered_map>> allianceAuctioneerCache; std::unordered_map bankerEntryToLocation; std::map> locsPerLevelCache; std::unordered_map> creatureSpawnsByTemplate; diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 3b4996e97..d4bdd21ee 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -5,11 +5,14 @@ #include "TravelNode.h" +#include #include +#include #include #include #include "BudgetValues.h" +#include "MapMgr.h" #include "PathGenerator.h" #include "Playerbots.h" #include "RaceMgr.h" @@ -158,8 +161,18 @@ float TravelNodePath::getCost(Player* bot, uint32 cGold) if (factionAnnoyance > 0) modifier += 0.3 * factionAnnoyance; // For each level the whole path takes 10% longer. } + if (getPathType() == TravelNodePathType::flyingMount) + { + if (!bot->IsAlive() || bot->GetLevel() < 70 || !bot->CanFly()) + return -1; + + float flySpeed = bot->GetSpeed(MOVE_FLIGHT); + if (flySpeed < 1.0f) + flySpeed = 20.0f; // 280% base flying speed fallback + return (distance / flySpeed) * modifier; + } } - else if (getPathType() == TravelNodePathType::flightPath) + else if (getPathType() == TravelNodePathType::flightPath || getPathType() == TravelNodePathType::flyingMount) return -1; if (getPathType() != TravelNodePathType::walk) @@ -187,9 +200,9 @@ uint32 TravelNodePath::getPrice() } // Creates or appends the path from one node to another. Returns if the path. -TravelNodePath* TravelNode::buildPath(TravelNode* endNode, Unit* bot, bool postProcess) +TravelNodePath* TravelNode::BuildPath(TravelNode* endNode, Unit* bot, bool postProcess) { - if (getMapId() != endNode->getMapId()) + if (GetMapId() != endNode->GetMapId()) return nullptr; TravelNodePath* returnNodePath; @@ -202,7 +215,7 @@ TravelNodePath* TravelNode::buildPath(TravelNode* endNode, Unit* bot, bool postP if (returnNodePath->getComplete()) // Path is already complete. Return it. return returnNodePath; - std::vector path = returnNodePath->getPath(); + std::vector path = returnNodePath->GetPath(); if (path.empty()) path = {*getPosition()}; // Start the path from the current Node. @@ -219,7 +232,7 @@ TravelNodePath* TravelNode::buildPath(TravelNode* endNode, Unit* bot, bool postP if (backNodePath.getPathType() == TravelNodePathType::walk) { - std::vector bPath = backNodePath.getPath(); + std::vector bPath = backNodePath.GetPath(); if (!backNodePath.getComplete()) // Build it if it's not already complete. { @@ -399,7 +412,7 @@ bool TravelNode::isUselessLink(TravelNode* farNode) } else { - TravelNodeRoute route = TravelNodeMap::instance().getRoute(nearNode, farNode, nullptr); + TravelNodeRoute route = TravelNodeMap::instance().GetNodeRoute(nearNode, farNode, nullptr); if (route.isEmpty()) continue; @@ -431,6 +444,8 @@ bool TravelNode::cropUselessLinks() TravelNode* farNode = firstLink.first; if (this->hasLinkTo(farNode) && this->isUselessLink(farNode)) { + LOG_DEBUG("playerbots", "[CropLink] '{}' → '{}' (dist {:.0f}) — redundant, removing", + getName(), farNode->getName(), getPathTo(farNode)->getDistance()); this->removeLinkTo(farNode); hasRemoved = true; @@ -448,6 +463,8 @@ bool TravelNode::cropUselessLinks() if (farNode->hasLinkTo(this) && farNode->isUselessLink(this)) { + LOG_DEBUG("playerbots", "[CropLink] '{}' → '{}' (dist {:.0f}) — redundant, removing", + farNode->getName(), getName(), farNode->getPathTo(this)->getDistance()); farNode->removeLinkTo(this); hasRemoved = true; @@ -498,7 +515,7 @@ bool TravelNode::cropUselessLinks() } else { - TravelNodeRoute route = TravelNodeMap::instance().getRoute(firstNode, secondNode, false); + TravelNodeRoute route = TravelNodeMap::instance().GetNodeRoute(firstNode, secondNode, nullptr); if (route.isEmpty()) continue; @@ -546,7 +563,7 @@ bool TravelNode::cropUselessLinks() } else { - TravelNodeRoute route = TravelNodeMap::instance().getRoute(firstNode, secondNode, false); + TravelNodeRoute route = TravelNodeMap::instance().GetNodeRoute(firstNode, secondNode, nullptr); if (route.isEmpty()) continue; @@ -630,7 +647,7 @@ void TravelNode::print([[maybe_unused]] bool printFailed) if (!hasLinkTo(endNode) && urand(0, 20) && !printFailed) continue; - ppath = path->getPath(); + ppath = path->GetPath(); if (ppath.size() < 2 && hasLinkTo(endNode)) { @@ -642,19 +659,11 @@ void TravelNode::print([[maybe_unused]] bool printFailed) { std::ostringstream out; - uint32 pathType = 1; + uint32 pathType = static_cast(path->getPathType()); if (!hasLinkTo(endNode)) pathType = 0; - else if (path->getPathType() == TravelNodePathType::transport) - pathType = 2; - else if (path->getPathType() == TravelNodePathType::portal && getMapId() == endNode->getMapId()) - pathType = 3; - else if (path->getPathType() == TravelNodePathType::portal) - pathType = 4; - else if (path->getPathType() == TravelNodePathType::flightPath) - pathType = 5; else if (!path->getComplete()) - pathType = 6; + pathType = 0; out << pathType << ","; out << std::fixed << std::setprecision(2); @@ -675,7 +684,7 @@ void TravelNode::print([[maybe_unused]] bool printFailed) // Attempts to move ahead of the path. bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) { - if (getPath().empty()) + if (GetPath().empty()) return false; float maxDistSq = maxDist * maxDist; @@ -686,7 +695,7 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) for (auto& p : fullPath) // cycle over the full path { - // if (p.point.getMapId() != startPos.getMapId()) + // if (p.point.GetMapId() != startPos.GetMapId()) // continue; if (p.point.GetMapId() == startPos.GetMapId()) @@ -705,7 +714,7 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) newPath.clear(); } - if (p.type != NODE_PREPATH) // Only look at the part after the first node and in the same map. + if (p.type != PathNodeType::NODE_PREPATH) // Only look at the part after the first node and in the same map. { if (!firstNode) firstNode = p.point; @@ -757,152 +766,6 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) return true; } -bool TravelPath::shouldMoveToNextPoint(WorldPosition startPos, std::vector::iterator beg, - std::vector::iterator ed, std::vector::iterator p, - float& moveDist, float maxDist) -{ - if (p == ed) // We are the end. Stop now. - return false; - - auto nextP = std::next(p); - - // We are moving to a area trigger node and want to move to the next teleport node. - if (p->type == NODE_PORTAL && nextP->type == NODE_PORTAL && p->entry == nextP->entry) - { - return false; // Move to teleport and activate area trigger. - } - - // We are using a hearthstone. - if (p->type == NODE_TELEPORT && nextP->type == NODE_TELEPORT && p->entry == nextP->entry) - { - return false; // Move to teleport and activate area trigger. - } - - // We are almost at a transport node. Move to the node before this. - if (nextP->type == NODE_TRANSPORT && nextP->entry && moveDist > INTERACTION_DISTANCE) - { - return false; - } - - // We are moving to a transport node. - if (p->type == NODE_TRANSPORT && p->entry) - { - if (nextP->type != NODE_TRANSPORT && p != beg && - std::prev(p)->type != NODE_TRANSPORT) // We are not using the transport. Skip it. - return true; - - return false; // Teleport to exit of transport. - } - - // We are moving to a flightpath and want to fly. - if (p->type == NODE_FLIGHTPATH && nextP->type == NODE_FLIGHTPATH) - { - return false; - } - - float nextMove = p->point.distance(nextP->point); - - if (p->point.GetMapId() != startPos.GetMapId() || - ((moveDist + nextMove > maxDist || startPos.distance(nextP->point) > maxDist) && moveDist > 0)) - { - return false; - } - - moveDist += nextMove; - - return true; -} - -// Next position to move to -WorldPosition TravelPath::getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, - uint32& entry) -{ - if (getPath().empty()) - return WorldPosition(); - - auto beg = fullPath.begin(); - auto ed = fullPath.end(); - - float minDist = 0.0f; - auto startP = beg; - - // Get the closest point on the path to start from. - for (auto p = startP; p != ed; p++) - { - if (p->point.GetMapId() != startPos.GetMapId()) - continue; - - float curDist = p->point.distance(startPos); - - if (curDist <= minDist || p == beg) - { - minDist = curDist; - startP = p; - } - } - - float moveDist = startP->point.distance(startPos); - - // Move as far as we are allowed - for (auto p = startP; p != ed; p++) - { - if (shouldMoveToNextPoint(startPos, beg, ed, p, moveDist, maxDist)) - continue; - - startP = p; - - break; - } - - // We are moving towards a teleport. Move to portal an activate area trigger - if (startP->type == NODE_PORTAL) - { - pathType = TravelNodePathType::portal; - entry = startP->entry; - return startP->point; - } - - // We are using a hearthstone - if (startP->type == NODE_TELEPORT) - { - pathType = TravelNodePathType::teleportSpell; - entry = startP->entry; - return startP->point; - } - - // We are moving towards a flight path. Move to flight master and activate flight path. - if (startP->type == NODE_FLIGHTPATH && startPos.distance(startP->point) < INTERACTION_DISTANCE) - { - pathType = TravelNodePathType::flightPath; - entry = startP->entry; - return startP->point; - } - - // We are moving towards transport. Teleport to next normal point instead. - if (startP->type == NODE_TRANSPORT) - { - for (auto p = startP + 1; p != ed; p++) - { - if (p->type != NODE_TRANSPORT) - { - pathType = TravelNodePathType::portal; - entry = 0; - return p->point; - } - } - } - - // We have to move far for next point. Try to make a cropped path. - if (moveDist < sPlayerbotAIConfig.targetPosRecalcDistance && std::next(startP) != ed) - { - // std::vector path = startPos.getPathTo(std::next(startP)->point, nullptr); - // startP->point = startPos.lastInRange(path, -1, maxDist); - return WorldPosition(); - } - - return startP->point; -} - std::ostringstream const TravelPath::print() { std::ostringstream out; @@ -919,22 +782,23 @@ std::ostringstream const TravelPath::print() float TravelNodeRoute::getTotalDistance() { + if (nodes.size() < 2) + return 0; + float totalLength = 0; - for (uint32 i = 0; i < nodes.size() - 2; i++) - { + for (uint32 i = 0; i < nodes.size() - 1; i++) totalLength += nodes[i]->linkDistanceTo(nodes[i + 1]); - } return totalLength; } -TravelPath TravelNodeRoute::buildPath(std::vector pathToStart, std::vector pathToEnd, +TravelPath TravelNodeRoute::BuildPath(std::vector pathToStart, std::vector pathToEnd, [[maybe_unused]] Unit* bot) { TravelPath travelPath; if (!pathToStart.empty()) // From start position to start of path. - travelPath.addPath(pathToStart, NODE_PREPATH); + travelPath.addPath(pathToStart, PathNodeType::NODE_PREPATH); TravelNode* prevNode = nullptr; for (auto& node : nodes) @@ -947,69 +811,81 @@ TravelPath TravelNodeRoute::buildPath(std::vector pathToStart, st if (!nodePath || !nodePath->getComplete()) // Build the path to the next node if it doesn't exist. { - if (!prevNode->isTransport()) - nodePath = prevNode->buildPath(node, nullptr); - else // For transports we have no proper path since the node is in air/water. Instead we build a - // reverse path and follow that. + // Only attempt runtime path building when we have a bot entity. + if (bot) { - node->buildPath(prevNode, nullptr); // Reverse build to get proper path. - nodePath = prevNode->getPathTo(node); + if (!prevNode->isTransport()) + nodePath = prevNode->BuildPath(node, bot); + else + { + node->BuildPath(prevNode, bot); + nodePath = prevNode->getPathTo(node); + } } } TravelNodePath returnNodePath; - if (!nodePath || !nodePath->getComplete()) // It looks like we can't properly path to our node. Make a - // temporary reverse path and see if that works instead. + if (!nodePath || !nodePath->getComplete()) { - returnNodePath = - *node->buildPath(prevNode, nullptr); // Build reverse path and save it to a temporary variable. - std::vector path = returnNodePath.getPath(); - std::reverse(path.begin(), path.end()); // Reverse the path - returnNodePath.setPath(path); - nodePath = &returnNodePath; + if (bot) + { + returnNodePath = + *node->BuildPath(prevNode, bot); + std::vector path = returnNodePath.GetPath(); + std::reverse(path.begin(), path.end()); + returnNodePath.setPath(path); + nodePath = &returnNodePath; + } } if (!nodePath || !nodePath->getComplete()) // If we can not build a path just try to move to the node. { - travelPath.addPoint(*prevNode->getPosition(), NODE_NODE); + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_NODE); prevNode = node; continue; } - if (nodePath->getPathType() == TravelNodePathType::portal) // Teleport to next node. + if (nodePath->getPathType() == TravelNodePathType::portal || + nodePath->getPathType() == TravelNodePathType::staticPortal) // Teleport to next node. { - travelPath.addPoint(*prevNode->getPosition(), NODE_PORTAL, nodePath->getPathObject()); // Entry point - travelPath.addPoint(*node->getPosition(), NODE_PORTAL, nodePath->getPathObject()); // Exit point + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_PORTAL, nodePath->getPathObject()); // Entry point + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_PORTAL, nodePath->getPathObject()); // Exit point } else if (nodePath->getPathType() == TravelNodePathType::transport) // Move onto transport { - travelPath.addPoint(*prevNode->getPosition(), NODE_TRANSPORT, + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TRANSPORT, nodePath->getPathObject()); // Departure point - travelPath.addPoint(*node->getPosition(), NODE_TRANSPORT, nodePath->getPathObject()); // Arrival point + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TRANSPORT, nodePath->getPathObject()); // Arrival point } else if (nodePath->getPathType() == TravelNodePathType::flightPath) // Use the flightpath { - travelPath.addPoint(*prevNode->getPosition(), NODE_FLIGHTPATH, + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject()); // Departure point - travelPath.addPoint(*node->getPosition(), NODE_FLIGHTPATH, nodePath->getPathObject()); // Arrival point + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject()); // Arrival point } else if (nodePath->getPathType() == TravelNodePathType::teleportSpell) { - travelPath.addPoint(*prevNode->getPosition(), NODE_TELEPORT, nodePath->getPathObject()); - travelPath.addPoint(*node->getPosition(), NODE_TELEPORT, nodePath->getPathObject()); + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject()); + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject()); + } + else if (nodePath->getPathType() == TravelNodePathType::flyingMount) + { + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_FLYING_MOUNT, 0); + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_FLYING_MOUNT, 0); } else { - std::vector path = nodePath->getPath(); + std::vector path = nodePath->GetPath(); if (path.size() > 1 && node != nodes.back()) // Remove the last point since that will also be the start of the next path. path.pop_back(); if (path.size() > 1 && prevNode->isPortal() && - nodePath->getPathType() != TravelNodePathType::portal) // Do not move to the area trigger if we - // don't plan to take the portal. + nodePath->getPathType() != TravelNodePathType::portal && + nodePath->getPathType() != TravelNodePathType::staticPortal) // Do not move to the area trigger if we + // don't plan to take the portal. path.erase(path.begin()); if (path.size() > 1 && prevNode->isTransport() && @@ -1017,14 +893,14 @@ TravelPath TravelNodeRoute::buildPath(std::vector pathToStart, st TravelNodePathType::transport) // Do not move to the transport if we aren't going to take it. path.erase(path.begin()); - travelPath.addPath(path, NODE_PATH); + travelPath.addPath(path, PathNodeType::NODE_PATH); } } prevNode = node; } if (!pathToEnd.empty()) - travelPath.addPath(pathToEnd, NODE_PATH); + travelPath.addPath(pathToEnd, PathNodeType::NODE_PATH); return travelPath; } @@ -1081,7 +957,7 @@ TravelNode* TravelNodeMap::addNode(WorldPosition pos, std::string const prefered newNode = new TravelNode(pos, finalName, isImportant); - m_nodes.push_back(newNode); + nodes.push_back(newNode); return newNode; } @@ -1090,7 +966,7 @@ void TravelNodeMap::removeNode(TravelNode* node) { node->removeLinkTo(nullptr, true); - for (auto& tnode : m_nodes) + for (auto& tnode : nodes) { if (tnode == node) { @@ -1099,7 +975,7 @@ void TravelNodeMap::removeNode(TravelNode* node) } } - m_nodes.erase(std::remove(m_nodes.begin(), m_nodes.end(), nullptr), m_nodes.end()); + nodes.erase(std::remove(nodes.begin(), nodes.end(), nullptr), nodes.end()); } void TravelNodeMap::fullLinkNode(TravelNode* startNode, Unit* bot) @@ -1115,8 +991,8 @@ void TravelNodeMap::fullLinkNode(TravelNode* startNode, Unit* bot) if (startNode->hasLinkTo(endNode)) continue; - startNode->buildPath(endNode, bot); - endNode->buildPath(startNode, bot); + startNode->BuildPath(endNode, bot); + endNode->BuildPath(startNode, bot); } startNode->setLinked(true); @@ -1126,9 +1002,9 @@ std::vector TravelNodeMap::getNodes(WorldPosition pos, float range) { std::vector retVec; - for (auto& node : m_nodes) + for (auto& node : nodes) { - if (node->getMapId() == pos.GetMapId()) + if (node->GetMapId() == pos.GetMapId()) if (range == -1 || node->getDistance(pos) <= range) retVec.push_back(node); } @@ -1167,14 +1043,15 @@ TravelNode* TravelNodeMap::getNode(WorldPosition pos, [[maybe_unused]] std::vect return nullptr; } -TravelNodeRoute TravelNodeMap::getRoute(TravelNode* start, TravelNode* goal, Player* bot) +TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal, + Player* bot) { float botSpeed = bot ? bot->GetSpeed(MOVE_RUN) : 7.0f; if (start == goal) return TravelNodeRoute(); - // Basic A* algoritm + // Basic A* algorithm std::unordered_map m_stubs; TravelNodeStub* startStub = &m_stubs.insert(std::make_pair(start, TravelNodeStub(start))).first->second; @@ -1202,62 +1079,35 @@ TravelNodeRoute TravelNodeMap::getRoute(TravelNode* start, TravelNode* goal, Pla } else startStub->currentGold = bot->GetMoney(); - - if (!bot->HasSpellCooldown(8690) && bot->IsAlive()) - { - AiObjectContext* context = botAI->GetAiObjectContext(); - - TravelNode* homeNode = TravelNodeMap::instance().getNode(AI_VALUE(WorldPosition, "home bind"), nullptr, 10.0f); - if (homeNode) - { - PortalNode* portNode = (PortalNode*)TravelNodeMap::instance().teleportNodes[bot->GetGUID()][8690]; - { - portNode = new PortalNode(start); - - TravelNodeMap::instance().teleportNodes[bot->GetGUID()][8690] = portNode; - } - - portNode->SetPortal(start, homeNode, 8690); - - childNode = &m_stubs.insert(std::make_pair(portNode, TravelNodeStub(portNode))).first->second; - - childNode->m_g = 10 * MINUTE; - childNode->m_h = childNode->dataNode->fDist(goal) / botSpeed; - childNode->m_f = childNode->m_g + childNode->m_h; - // childNode->parent = startStub; - - open.push_back(childNode); - std::push_heap(open.begin(), open.end(), - [](TravelNodeStub* i, TravelNodeStub* j) { return i->m_f < j->m_f; }); - childNode->open = true; - } - } } - if (open.size() == 0 && !start->hasRouteTo(goal)) + if (!start->hasRouteTo(goal)) return TravelNodeRoute(); - std::make_heap(open.begin(), open.end(), [](TravelNodeStub* i, TravelNodeStub* j) { return i->m_f < j->m_f; }); + // Min-heap: smallest f at front + auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; }; open.push_back(startStub); - std::push_heap(open.begin(), open.end(), [](TravelNodeStub* i, TravelNodeStub* j) { return i->m_f < j->m_f; }); + std::push_heap(open.begin(), open.end(), heapComp); startStub->open = true; + constexpr uint32 MAX_A_STAR_EXPLORED = 500; + uint32 nodesExplored = 0; + while (!open.empty()) { - std::sort(open.begin(), open.end(), [](TravelNodeStub* i, TravelNodeStub* j) { return i->m_f < j->m_f; }); + if (++nodesExplored > MAX_A_STAR_EXPLORED) + return TravelNodeRoute(); - currentNode = open.front(); // pop n node from open for which f is minimal - - std::pop_heap(open.begin(), open.end(), [](TravelNodeStub* i, TravelNodeStub* j) { return i->m_f < j->m_f; }); + std::pop_heap(open.begin(), open.end(), heapComp); + currentNode = open.back(); open.pop_back(); currentNode->open = false; - currentNode->close = true; + currentNode->closed = true; closed.push_back(currentNode); - if (currentNode->dataNode == goal || - (currentNode->dataNode->getMapId() != start->getMapId() && currentNode->dataNode->isWalking())) + if (currentNode->dataNode == goal) { TravelNodeStub* parent = currentNode->parent; @@ -1284,29 +1134,28 @@ TravelNodeRoute TravelNodeMap::getRoute(TravelNode* start, TravelNode* goal, Pla continue; childNode = &m_stubs.insert(std::make_pair(linkNode, TravelNodeStub(linkNode))).first->second; - g = currentNode->m_g + linkCost; // stance from start + distance between the two nodes - if ((childNode->open || childNode->close) && - childNode->m_g <= g) // n' is already in opend or closed with a lower cost g(n') + g = currentNode->costFromStart + linkCost; // stance from start + distance between the two nodes + if ((childNode->open || childNode->closed) && + childNode->costFromStart <= g) // n' is already in opend or closed with a lower cost g(n') continue; // consider next successor h = childNode->dataNode->fDist(goal) / botSpeed; - f = g + h; // compute f(n') - childNode->m_f = f; - childNode->m_g = g; - childNode->m_h = h; + f = g + h; // compute f(n') + childNode->totalCost = f; + childNode->costFromStart = g; + childNode->heuristic = h; childNode->parent = currentNode; if (bot && !bot->isTaxiCheater()) childNode->currentGold = currentNode->currentGold - link.second->getPrice(); - if (childNode->close) - childNode->close = false; + if (childNode->closed) + childNode->closed = false; if (!childNode->open) { open.push_back(childNode); - std::push_heap(open.begin(), open.end(), - [](TravelNodeStub* i, TravelNodeStub* j) { return i->m_f < j->m_f; }); + std::push_heap(open.begin(), open.end(), heapComp); childNode->open = true; } } @@ -1315,55 +1164,69 @@ TravelNodeRoute TravelNodeMap::getRoute(TravelNode* start, TravelNode* goal, Pla return TravelNodeRoute(); } -TravelNodeRoute TravelNodeMap::getRoute(WorldPosition startPos, WorldPosition endPos, - std::vector& startPath, Player* bot) +TravelNodeRoute TravelNodeMap::GetNearestNodes(WorldPosition startPos, WorldPosition endPos, + std::vector& startPath, Player* bot) { - if (m_nodes.empty()) + if (nodes.empty() || !bot) return TravelNodeRoute(); - std::vector newStartPath; - std::vector startNodes = m_nodes, endNodes = m_nodes; + constexpr uint32 K = 3; + if (nodes.size() < K) + return TravelNodeRoute(); - if (!startNodes.size() || !endNodes.size()) - return TravelNodeRoute(); + // Single copy of the node list, find closest K for start and end + std::vector nodesCopy = this->nodes; - // Partial sort to get the closest 5 nodes at the begin of the array. - std::partial_sort(startNodes.begin(), startNodes.begin() + 5, startNodes.end(), - [startPos](TravelNode* i, TravelNode* j) { return i->fDist(startPos) < j->fDist(startPos); }); + // nth_element is O(n) — partitions so the first K are the closest (unordered) + std::nth_element(nodesCopy.begin(), nodesCopy.begin() + K, nodesCopy.end(), + [startPos](TravelNode* i, TravelNode* j) { return i->fDist(startPos) < j->fDist(startPos); }); + // Sort just the K closest + std::sort(nodesCopy.begin(), nodesCopy.begin() + K, + [startPos](TravelNode* i, TravelNode* j) { return i->fDist(startPos) < j->fDist(startPos); }); - std::partial_sort(endNodes.begin(), endNodes.begin() + 5, endNodes.end(), - [endPos](TravelNode* i, TravelNode* j) { return i->fDist(endPos) < j->fDist(endPos); }); + // Save the K closest start nodes before reusing the vector for end nodes + std::array startNodes; + std::copy_n(nodesCopy.begin(), K, startNodes.begin()); - // Cycle over the combinations of these 5 nodes. + std::nth_element(nodesCopy.begin(), nodesCopy.begin() + K, nodesCopy.end(), + [endPos](TravelNode* i, TravelNode* j) { return i->fDist(endPos) < j->fDist(endPos); }); + std::sort(nodesCopy.begin(), nodesCopy.begin() + K, + [endPos](TravelNode* i, TravelNode* j) { return i->fDist(endPos) < j->fDist(endPos); }); + + std::array endNodes; + std::copy_n(nodesCopy.begin(), K, endNodes.begin()); + + // Cycle over the combinations of these K nodes. uint32 startI = 0, endI = 0; - while (startI < 5 && endI < 5) + while (startI < K && endI < K) { TravelNode* startNode = startNodes[startI]; TravelNode* endNode = endNodes[endI]; WorldPosition startNodePosition = *startNode->getPosition(); - WorldPosition endNodePosition = *endNode->getPosition(); - float maxStartDistance = startNode->isTransport() ? 20.0f : sPlayerbotAIConfig.targetPosRecalcDistance; - - TravelNodeRoute route = getRoute(startNode, endNode, bot); + TravelNodeRoute route = GetNodeRoute(startNode, endNode, bot); if (!route.isEmpty()) { - // Check if the bot can actually walk to this start position. - newStartPath = startPath; - if (startNodePosition.cropPathTo(newStartPath, maxStartDistance) || - startNode->getPosition()->isPathTo(newStartPath = startPos.getPathTo(startNodePosition, nullptr), - maxStartDistance)) + // Check if the bot can actually walk to this start node using mmap pathfinding. + if (startNodePosition.GetMapId() == bot->GetMapId()) { - startPath = newStartPath; - return route; - } + PathGenerator path(bot); + path.CalculatePath(startNodePosition.GetPositionX(), startNodePosition.GetPositionY(), startNodePosition.GetPositionZ()); + PathType type = path.GetPathType(); + bool reachable = !(type & ~(PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY)); + if (reachable) + { + startPath = {startPos, startNodePosition}; + return route; + } + } startI++; } - // Prefer a differnt end-node. + // Prefer a different end-node. endI++; // Cycle to a different start-node if needed. @@ -1374,79 +1237,58 @@ TravelNodeRoute TravelNodeMap::getRoute(WorldPosition startPos, WorldPosition en } } - if (bot && !bot->HasSpellCooldown(8690)) - { - startPath.clear(); - TravelNode* botNode = TravelNodeMap::instance().teleportNodes[bot->GetGUID()][0]; - { - botNode = new TravelNode(startPos, "Bot Pos", false); - TravelNodeMap::instance().teleportNodes[bot->GetGUID()][0] = botNode; - } - - botNode->setPoint(startPos); - - endI = 0; - while (endI < 5) - { - TravelNode* endNode = endNodes[endI]; - TravelNodeRoute route = getRoute(botNode, endNode, bot); - - if (!route.isEmpty()) - return route; - endI++; - } - } - return TravelNodeRoute(); } -TravelPath TravelNodeMap::getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot) +bool TravelNodeMap::GetFullPath(TravelPlan& plan, + WorldPosition botPos, uint32 botZoneId, + uint32 teamId, WorldPosition destination) { - TravelPath movePath; - PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); - std::vector beginPath, endPath; + plan.Reset(); + plan.destination = destination; - beginPath = endPos.getPathFromPath({startPos}, nullptr, 40); + // Short distance — direct walk, no nodes needed + if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE && + botPos.GetMapId() == destination.GetMapId()) + { + plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH); + plan.steps.addPoint(destination, PathNodeType::NODE_PATH); + return true; + } - if (endPos.isPathTo(beginPath)) - return TravelPath(beginPath); + std::shared_lock guard(m_nMapMtx); - //[[Node pathfinding system]] - // We try to find nodes near the bot and near the end position that have a route between them. - // Then bot has to move towards/along the route. - TravelNodeMap::instance().m_nMapMtx.lock_shared(); + // Find nearest nodes (zone-indexed, fast) + TravelNode* startNode = GetNearestNodeInZone(botPos, botZoneId); + if (!startNode) + startNode = GetNearestNodeOnMap(botPos); - // Find the route of nodes starting at a node closest to the start position and ending at a node closest to the - // endposition. Also returns longPath: The path from the start position to the first node in the route. - TravelNodeRoute route = TravelNodeMap::instance().getRoute(startPos, endPos, beginPath, bot); + uint32 destZone = sMapMgr->GetZoneId(PHASEMASK_NORMAL, destination); + TravelNode* endNode = GetNearestNodeInZone(destination, destZone); + if (!endNode) + endNode = GetNearestNodeOnMap(destination); + if (!startNode || !endNode) + return false; + + if (!startNode->hasRouteTo(endNode)) + return false; + + TravelNodeRoute route = GetNodeRoute(startNode, endNode, nullptr); if (route.isEmpty()) - return movePath; + return false; - if (sPlayerbotAIConfig.hasLog("bot_pathfinding.csv")) - { - if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) - { - sPlayerbotAIConfig.openLog("bot_pathfinding.csv", "w"); - sPlayerbotAIConfig.log("bot_pathfinding.csv", route.print().str().c_str()); - } - } + // Build flat waypoint path from A* route + std::vector pathToStart = {botPos}; + std::vector pathToEnd = {destination}; + plan.steps = route.BuildPath(pathToStart, pathToEnd, nullptr); - endPath = route.getNodes().back()->getPosition()->getPathTo(endPos, nullptr); - movePath = route.buildPath(beginPath, endPath); + LOG_DEBUG("playerbots", + "[TravelPlan] '{}' → '{}', {} points", + startNode->getName(), endNode->getName(), + plan.steps.size()); - if (sPlayerbotAIConfig.hasLog("bot_pathfinding.csv")) - { - if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) - { - sPlayerbotAIConfig.openLog("bot_pathfinding.csv", "w"); - sPlayerbotAIConfig.log("bot_pathfinding.csv", movePath.print().str().c_str()); - } - } - - TravelNodeMap::instance().m_nMapMtx.unlock_shared(); - - return movePath; + return !plan.steps.empty(); } bool TravelNodeMap::cropUselessNode(TravelNode* startNode) @@ -1465,6 +1307,9 @@ bool TravelNodeMap::cropUselessNode(TravelNode* startNode) return false; } + LOG_INFO("playerbots", "[CropNode] Removing useless node '{}' (map {}, {:.0f},{:.0f},{:.0f}) — no neighbor depends on it", + startNode->getName(), startNode->GetMapId(), startNode->getX(), startNode->getY(), startNode->getZ()); + removeNode(startNode); return true; @@ -1477,7 +1322,7 @@ TravelNode* TravelNodeMap::addZoneLinkNode(TravelNode* startNode) //TravelNode* endNode = path.first; //not used, line marked for removal. std::string zoneName = startNode->getPosition()->getAreaName(true, true); - for (auto& pos : path.second.getPath()) + for (auto& pos : path.second.GetPath()) { std::string const newZoneName = pos.getAreaName(true, true); if (zoneName != newZoneName) @@ -1508,7 +1353,7 @@ TravelNode* TravelNodeMap::addRandomExtNode(TravelNode* startNode) auto random_it = std::next(std::begin(paths), urand(0, paths.size() - 1)); TravelNode* endNode = random_it->first; - std::vector path = random_it->second.getPath(); + std::vector path = random_it->second.GetPath(); if (path.empty()) continue; @@ -1532,60 +1377,10 @@ TravelNode* TravelNodeMap::addRandomExtNode(TravelNode* startNode) void TravelNodeMap::manageNodes(Unit* bot, bool mapFull) { - bool rePrint = false; - - if (!bot->GetMap()) - return; - - if (m_nMapMtx.try_lock()) - { - TravelNode* startNode; - TravelNode* newNode; - - for (auto startNode : m_nodes) - { - cropUselessNode(startNode); - } - - // Pick random Node - for (uint32 i = 0; i < (mapFull ? (uint32)20 : (uint32)1); i++) - { - std::vector rnodes = getNodes(WorldPosition(bot)); - - if (!rnodes.empty()) - { - uint32 j = urand(0, rnodes.size() - 1); - - startNode = rnodes[j]; - newNode = nullptr; - - bool nodeDone = false; - - if (!nodeDone) - nodeDone = cropUselessNode(startNode); - - if (!nodeDone && !urand(0, 20)) - newNode = addZoneLinkNode(startNode); - - if (!nodeDone && !newNode && !urand(0, 20)) - newNode = addRandomExtNode(startNode); - - rePrint = nodeDone || rePrint || newNode; - } - } - - if (rePrint && (mapFull || !urand(0, 20))) - printMap(); - - m_nMapMtx.unlock(); - } - - TravelNodeMap::instance().m_nMapMtx.lock_shared(); - - if (!rePrint && mapFull) - printMap(); - - m_nMapMtx.unlock_shared(); + // Runtime node mutation disabled — the graph is fully built at startup via Init() + // and .generate. Taking a unique_lock here caused 100-250ms contention spikes for + // all bot threads holding shared_locks in TravelPlan. + // Node pruning/creation is still available via the .generate console command. } void TravelNodeMap::generateNpcNodes() @@ -1867,18 +1662,26 @@ void TravelNodeMap::generateWalkPaths() std::map nodeMaps; + uint32 totalProcessed = 0, totalSkipped = 0; + for (auto& startNode : TravelNodeMap::instance().getNodes()) { - nodeMaps[startNode->getMapId()] = true; + nodeMaps[startNode->GetMapId()] = true; } for (auto& map : nodeMaps) { + uint32 mapProcessed = 0, mapSkipped = 0; for (auto& startNode : TravelNodeMap::instance().getNodes(WorldPosition(map.first, 1, 1))) { if (startNode->isLinked()) + { + mapSkipped++; continue; + } + uint32 nearby = 0; + uint32 linked = 0; for (auto& endNode : TravelNodeMap::instance().getNodes(*startNode->getPosition(), 2000.0f)) { if (startNode == endNode) @@ -1887,17 +1690,30 @@ void TravelNodeMap::generateWalkPaths() if (startNode->hasCompletePathTo(endNode)) continue; - if (startNode->getMapId() != endNode->getMapId()) + if (startNode->GetMapId() != endNode->GetMapId()) continue; - startNode->buildPath(endNode, nullptr, false); + nearby++; + startNode->BuildPath(endNode, nullptr, false); + if (startNode->hasCompletePathTo(endNode)) + linked++; } + LOG_INFO("playerbots", " Node '{}' (map {}): {} nearby, {} linked", + startNode->getName(), startNode->GetMapId(), nearby, linked); + startNode->setLinked(true); + mapProcessed++; } + + LOG_INFO("playerbots", "[WalkPaths] Map {}: processed {} nodes, skipped {} (already linked)", + map.first, mapProcessed, mapSkipped); + totalProcessed += mapProcessed; + totalSkipped += mapSkipped; } - LOG_INFO("playerbots", ">> Generated paths for {} nodes.", TravelNodeMap::instance().getNodes().size()); + LOG_INFO("playerbots", ">> Generated paths for {} nodes ({} processed, {} skipped as already linked).", + TravelNodeMap::instance().getNodes().size(), totalProcessed, totalSkipped); } void TravelNodeMap::generateTaxiPaths() @@ -1951,11 +1767,14 @@ void TravelNodeMap::removeLowNodes() { std::vector goodNodes; std::vector remNodes; + uint32 totalOverworld = 0; for (auto& node : TravelNodeMap::instance().getNodes()) { if (!node->getPosition()->isOverworld()) continue; + totalOverworld++; + if (std::find(goodNodes.begin(), goodNodes.end(), node) != goodNodes.end()) continue; @@ -1965,17 +1784,32 @@ void TravelNodeMap::removeLowNodes() std::vector nodes = node->getNodeMap(true); if (nodes.size() < 5) + { + std::vector allInCluster = node->getNodeMap(); + LOG_INFO("playerbots", "[RemoveLow] Cluster starting at '{}' (map {}) has only {} important nodes, {} total — marking for removal:", + node->getName(), node->GetMapId(), nodes.size(), allInCluster.size()); + for (auto& rn : allInCluster) + LOG_INFO("playerbots", "[RemoveLow] - '{}' (map {}, {:.0f},{:.0f},{:.0f}) important={}", + rn->getName(), rn->GetMapId(), rn->getX(), rn->getY(), rn->getZ(), rn->isImportant()); remNodes.insert(remNodes.end(), nodes.begin(), nodes.end()); + } else goodNodes.insert(goodNodes.end(), nodes.begin(), nodes.end()); } + LOG_INFO("playerbots", "[RemoveLow] {} overworld nodes evaluated, {} good, {} to remove", + totalOverworld, goodNodes.size(), remNodes.size()); + for (auto& node : remNodes) TravelNodeMap::instance().removeNode(node); } void TravelNodeMap::removeUselessPaths() { + uint32 linksBefore = 0; + for (auto& n : TravelNodeMap::instance().getNodes()) + linksBefore += n->getLinks()->size(); + // Clean up node links for (auto& startNode : TravelNodeMap::instance().getNodes()) { @@ -1983,7 +1817,7 @@ void TravelNodeMap::removeUselessPaths() if (path.second.getComplete() && startNode->hasLinkTo(path.first)) ASSERT(true); } - uint32 it = 0/*, rem = 0*/; //rem not used in this scope, (shadowing) fragment marked for removal. + uint32 it = 0, totalRemoved = 0; while (true) { uint32 rem = 0; @@ -1998,11 +1832,18 @@ void TravelNodeMap::removeUselessPaths() break; hasToSave = true; - + totalRemoved += rem; it++; - LOG_INFO("playerbots", "Iteration {}, removed {}", it, rem); + LOG_INFO("playerbots", "[RemoveUseless] Iteration {}, removed {} links from nodes", it, rem); } + + uint32 linksAfter = 0; + for (auto& n : TravelNodeMap::instance().getNodes()) + linksAfter += n->getLinks()->size(); + + LOG_INFO("playerbots", "[RemoveUseless] Done: {} iterations, links {} → {} (removed {})", + it, linksBefore, linksAfter, linksBefore - linksAfter); } void TravelNodeMap::calculatePathCosts() @@ -2026,38 +1867,103 @@ void TravelNodeMap::calculatePathCosts() LOG_INFO("playerbots", ">> Calculated pathcost for {} nodes.", TravelNodeMap::instance().getNodes().size()); } -void TravelNodeMap::generatePaths() +void TravelNodeMap::generatePaths(bool fullGen) { + uint32 totalLinks = 0; + uint32 totalPathPoints = 0; + for (auto& n : TravelNodeMap::instance().getNodes()) + { + totalLinks += n->getLinks()->size(); + for (auto& l : *n->getLinks()) + totalPathPoints += l.second->GetPath().size(); + } + LOG_INFO("playerbots", "[GenPaths] ENTRY (fullGen={}): {} nodes, {} links, {} path points", + fullGen, TravelNodeMap::instance().getNodes().size(), totalLinks, totalPathPoints); + LOG_INFO("playerbots", "-Calculating walkable paths"); generateWalkPaths(); - LOG_INFO("playerbots", "-Removing useless nodes"); - removeLowNodes(); - LOG_INFO("playerbots", "-Removing useless paths"); - removeUselessPaths(); + + totalLinks = 0; + for (auto& n : TravelNodeMap::instance().getNodes()) + totalLinks += n->getLinks()->size(); + LOG_INFO("playerbots", "[GenPaths] After generateWalkPaths: {} nodes, {} links", + TravelNodeMap::instance().getNodes().size(), totalLinks); + + if (fullGen) + { + LOG_INFO("playerbots", "-Removing useless nodes"); + removeLowNodes(); + + totalLinks = 0; + for (auto& n : TravelNodeMap::instance().getNodes()) + totalLinks += n->getLinks()->size(); + LOG_INFO("playerbots", "[GenPaths] After removeLowNodes: {} nodes, {} links", + TravelNodeMap::instance().getNodes().size(), totalLinks); + + LOG_INFO("playerbots", "-Removing useless paths"); + removeUselessPaths(); + + totalLinks = 0; + for (auto& n : TravelNodeMap::instance().getNodes()) + totalLinks += n->getLinks()->size(); + LOG_INFO("playerbots", "[GenPaths] After removeUselessPaths: {} nodes, {} links", + TravelNodeMap::instance().getNodes().size(), totalLinks); + } + else + LOG_INFO("playerbots", "-Skipping node/path pruning (incremental generation)"); + LOG_INFO("playerbots", "-Calculating path costs"); calculatePathCosts(); LOG_INFO("playerbots", "-Generating taxi paths"); generateTaxiPaths(); + + totalLinks = 0; + totalPathPoints = 0; + for (auto& n : TravelNodeMap::instance().getNodes()) + { + totalLinks += n->getLinks()->size(); + for (auto& l : *n->getLinks()) + totalPathPoints += l.second->GetPath().size(); + } + LOG_INFO("playerbots", "[GenPaths] EXIT: {} nodes, {} links, {} path points", + TravelNodeMap::instance().getNodes().size(), totalLinks, totalPathPoints); } void TravelNodeMap::generateAll() { - if (hasToFullGen) - generateNodes(); + LOG_INFO("playerbots", "[GenerateAll] Regenerating: {} nodes", nodes.size()); - LOG_INFO("playerbots", "-Calculating mapoffset"); + generatePaths(false); + saveNodeStore(); + + BuildZoneIndex(); + PrecomputeReachability(); + LOG_INFO("playerbots", "[GenerateAll] Done: {} nodes, indexes rebuilt.", nodes.size()); +} + +void TravelNodeMap::Init() +{ + LoadNodeStore(); calcMapOffset(); - LOG_INFO("playerbots", "-Generating maptransfers"); - TravelMgr::instance().loadMapTransfers(); - if (hasToGen || hasToFullGen) { - generatePaths(); + LOG_INFO("playerbots", "[Init] Generating paths (fullGen={}, {} nodes)...", hasToFullGen, nodes.size()); + + if (hasToFullGen) + generateNodes(); + + generatePaths(hasToFullGen); hasToGen = false; hasToFullGen = false; - hasToSave = true; + saveNodeStore(); } + + BuildZoneIndex(); + PrecomputeReachability(); + InitTaxiGraph(); + LOG_INFO("playerbots", "TravelNodeMap initialized: {} nodes, zone index and reachability built.", + nodes.size()); } void TravelNodeMap::printMap() @@ -2118,7 +2024,7 @@ void TravelNodeMap::printNodeStore() // struct addNode {uint32 node; WorldPosition point; std::string const name; bool isPortal; bool // isTransport; uint32 transportId; }; out << std::fixed << std::setprecision(2) << " addNodes.push_back(addNode{" << i << ","; - out << "WorldPosition(" << node->getMapId() << ", " << node->getX() << "f, " << node->getY() << "f, " + out << "WorldPosition(" << node->GetMapId() << ", " << node->getX() << "f, " << node->getY() << "f, " << node->getZ() << "f, " << node->getO() << "f),"; out << "\"" << name << "\""; if (node->isTransport()) @@ -2127,7 +2033,7 @@ void TravelNodeMap::printNodeStore() /* out << std::fixed << std::setprecision(2) << " nodes[" << i << "] = - TravelNodeMap::instance().addNode(&WorldPosition(" << node->getMapId() << "," << node->getX() << "f," << node->getY() + TravelNodeMap::instance().addNode(&WorldPosition(" << node->GetMapId() << "," << node->getX() << "f," << node->getY() << "f," << node->getZ() << "f,"<< node->getO() <<"f), \"" << name << "\", " << (node->isImportant() ? "true" : "false") << ", true"; if (node->isTransport()) @@ -2172,7 +2078,10 @@ void TravelNodeMap::printNodeStore() void TravelNodeMap::saveNodeStore() { if (!hasToSave) + { + LOG_INFO("playerbots", "[SaveNodes] Skipped — hasToSave is false"); return; + } hasToSave = false; @@ -2195,7 +2104,7 @@ void TravelNodeMap::saveNodeStore() PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_TRAVELNODE); stmt->SetData(0, i); stmt->SetData(1, name); - stmt->SetData(2, node->getMapId()); + stmt->SetData(2, node->GetMapId()); stmt->SetData(3, node->getX()); stmt->SetData(4, node->getY()); stmt->SetData(5, node->getZ()); @@ -2207,62 +2116,97 @@ void TravelNodeMap::saveNodeStore() LOG_INFO("playerbots", ">> Saved {} travelNodes.", anodes.size()); + uint32 paths = 0; + for (uint32 i = 0; i < anodes.size(); i++) { - uint32 paths = 0, points = 0; - for (uint32 i = 0; i < anodes.size(); i++) + TravelNode* node = anodes[i]; + + for (auto& link : *node->getLinks()) { - TravelNode* node = anodes[i]; + TravelNodePath* path = link.second; - for (auto& link : *node->getLinks()) + PlayerbotsDatabasePreparedStatement* stmt = + PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_TRAVELNODE_LINK); + stmt->SetData(0, i); + stmt->SetData(1, saveNodes.find(link.first)->second); + stmt->SetData(2, static_cast(path->getPathType())); + stmt->SetData(3, path->getPathObject()); + stmt->SetData(4, path->getDistance()); + stmt->SetData(5, path->getSwimDistance()); + stmt->SetData(6, path->getExtraCost()); + stmt->SetData(7, path->getCalculated()); + stmt->SetData(8, path->getMaxLevelCreature()[0]); + stmt->SetData(9, path->getMaxLevelCreature()[1]); + stmt->SetData(10, path->getMaxLevelCreature()[2]); + trans->Append(stmt); + + paths++; + } + } + LOG_INFO("playerbots", ">> Saved {} travelNode links.", paths); + + // Path points: bulk raw SQL multi-row INSERTs (~500 rows each) instead of + // 1M+ individual prepared statements. Appended to the same transaction so + // ordering is guaranteed. + constexpr uint32 BATCH_SIZE = 500; + uint32 points = 0; + std::ostringstream ss; + uint32 batchCount = 0; + + auto flushBatch = [&]() + { + if (batchCount == 0) + return; + + std::string sql = ss.str(); + sql.back() = ';'; // Replace trailing comma + trans->Append(sql.c_str()); + ss.str(""); + ss.clear(); + batchCount = 0; + }; + + for (uint32 i = 0; i < anodes.size(); i++) + { + TravelNode* node = anodes[i]; + + for (auto& link : *node->getLinks()) + { + TravelNodePath* path = link.second; + uint32 toId = saveNodes.find(link.first)->second; + std::vector ppath = path->GetPath(); + + for (uint32 j = 0; j < ppath.size(); j++) { - TravelNodePath* path = link.second; + WorldPosition& point = ppath[j]; - PlayerbotsDatabasePreparedStatement* stmt = - PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_TRAVELNODE_LINK); - stmt->SetData(0, i); - stmt->SetData(1, saveNodes.find(link.first)->second); - stmt->SetData(2, static_cast(path->getPathType())); - stmt->SetData(3, path->getPathObject()); - stmt->SetData(4, path->getDistance()); - stmt->SetData(5, path->getSwimDistance()); - stmt->SetData(6, path->getExtraCost()); - stmt->SetData(7, path->getCalculated()); - stmt->SetData(8, path->getMaxLevelCreature()[0]); - stmt->SetData(9, path->getMaxLevelCreature()[1]); - stmt->SetData(10, path->getMaxLevelCreature()[2]); - trans->Append(stmt); + if (batchCount == 0) + ss << "INSERT INTO `playerbots_travelnode_path` (`node_id`,`to_node_id`,`nr`,`map_id`,`x`,`y`,`z`) VALUES "; - paths++; + ss << std::fixed << std::setprecision(4) + << "(" << i << "," << toId << "," << j << "," + << point.GetMapId() << "," + << point.GetPositionX() << "," + << point.GetPositionY() << "," + << point.GetPositionZ() << "),"; - std::vector ppath = path->getPath(); + batchCount++; + points++; - for (uint32 j = 0; j < ppath.size(); j++) - { - WorldPosition point = ppath[j]; - - PlayerbotsDatabasePreparedStatement* stmt = - PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_TRAVELNODE_PATH); - stmt->SetData(0, i); - stmt->SetData(1, saveNodes.find(link.first)->second); - stmt->SetData(2, j); - stmt->SetData(3, point.GetMapId()); - stmt->SetData(4, point.GetPositionX()); - stmt->SetData(5, point.GetPositionY()); - stmt->SetData(6, point.GetPositionZ()); - trans->Append(stmt); - - points++; - } + if (batchCount >= BATCH_SIZE) + flushBatch(); } } - - LOG_INFO("playerbots", ">> Saved {} travelNode Paths, {} points.", paths, points); } + flushBatch(); + + LOG_INFO("playerbots", ">> Saved {} travelNode path points.", points); + PlayerbotsDatabase.CommitTransaction(trans); } -void TravelNodeMap::loadNodeStore() +void TravelNodeMap::LoadNodeStore() { std::string const query = "SELECT id, name, map_id, x, y, z, linked FROM playerbots_travelnode"; @@ -2283,7 +2227,11 @@ void TravelNodeMap::loadNodeStore() if (fields[6].Get()) node->setLinked(true); else + { hasToGen = true; + LOG_INFO("playerbots", "[LoadNodes] Node '{}' (id={}) is unlinked — will trigger generation", + fields[1].Get(), fields[0].Get()); + } saveNodes.insert(std::make_pair(fields[0].Get(), node)); @@ -2306,12 +2254,15 @@ void TravelNodeMap::loadNodeStore() { Field* fields = result->Fetch(); - TravelNode* startNode = saveNodes.find(fields[0].Get())->second; - TravelNode* endNode = saveNodes.find(fields[1].Get())->second; + auto startIt = saveNodes.find(fields[0].Get()); + auto endIt = saveNodes.find(fields[1].Get()); - if (!startNode || !endNode) + if (startIt == saveNodes.end() || endIt == saveNodes.end()) continue; + TravelNode* startNode = startIt->second; + TravelNode* endNode = endIt->second; + startNode->setPathTo( endNode, TravelNodePath(fields[4].Get(), fields[6].Get(), fields[2].Get(), @@ -2321,11 +2272,15 @@ void TravelNodeMap::loadNodeStore() true); if (!fields[7].Get()) + { hasToGen = true; + LOG_DEBUG("playerbots", "[LoadNodes] Link {}->{} not calculated — will trigger generation", + fields[0].Get(), fields[1].Get()); + } } while (result->NextRow()); - LOG_INFO("playerbots", ">> Loaded {} travelNode paths.", result->GetRowCount()); + LOG_INFO("playerbots", ">> Loaded {} travelNode links.", result->GetRowCount()); } else { @@ -2341,15 +2296,21 @@ void TravelNodeMap::loadNodeStore() { Field* fields = result->Fetch(); - TravelNode* startNode = saveNodes.find(fields[0].Get())->second; - TravelNode* endNode = saveNodes.find(fields[1].Get())->second; + auto startIt = saveNodes.find(fields[0].Get()); + auto endIt = saveNodes.find(fields[1].Get()); - if (!startNode || !endNode || !startNode->hasPathTo(endNode)) + if (startIt == saveNodes.end() || endIt == saveNodes.end()) + continue; + + TravelNode* startNode = startIt->second; + TravelNode* endNode = endIt->second; + + if (!startNode->hasPathTo(endNode)) continue; TravelNodePath* path = startNode->getPathTo(endNode); - std::vector ppath = path->getPath(); + std::vector ppath = path->GetPath(); ppath.push_back(WorldPosition(fields[3].Get(), fields[4].Get(), fields[5].Get(), fields[6].Get())); @@ -2378,11 +2339,11 @@ void TravelNodeMap::calcMapOffset() std::vector mapIds; - for (auto& node : m_nodes) + for (auto& node : nodes) { if (!node->getPosition()->isOverworld()) - if (std::find(mapIds.begin(), mapIds.end(), node->getMapId()) == mapIds.end()) - mapIds.push_back(node->getMapId()); + if (std::find(mapIds.begin(), mapIds.end(), node->GetMapId()) == mapIds.end()) + mapIds.push_back(node->GetMapId()); } std::sort(mapIds.begin(), mapIds.end()); @@ -2392,9 +2353,9 @@ void TravelNodeMap::calcMapOffset() for (auto& mapId : mapIds) { bool doPush = true; - for (auto& node : m_nodes) + for (auto& node : nodes) { - if (node->getMapId() != mapId) + if (node->GetMapId() != mapId) continue; if (doPush) @@ -2449,10 +2410,7 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId) return WorldPosition(mapId, 0, 0, 0, 0); } -// ============================================================ // TravelNodeMap taxi graph (BFS-based flight path lookup) -// ============================================================ - void TravelNodeMap::InitTaxiGraph() { BuildTaxiGraph(); @@ -2470,8 +2428,8 @@ std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) if (!startNode || !endNode || startNode->map_id != endNode->map_id) return {}; - auto cacheItr = taxiPathCache.find(fromNode); - if (cacheItr == taxiPathCache.end()) + auto cacheItr = m_taxiPathCache.find(fromNode); + if (cacheItr == m_taxiPathCache.end()) return {}; auto toNodeItr = cacheItr->second.find(toNode); @@ -2483,7 +2441,7 @@ std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) void TravelNodeMap::BuildTaxiGraph() { - taxiGraph.clear(); + m_taxiGraph.clear(); std::unordered_map> tempGraph; for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i) { @@ -2498,13 +2456,13 @@ void TravelNodeMap::BuildTaxiGraph() tempGraph[path->to].insert(path->from); } for (auto const& [node, neighbors] : tempGraph) - taxiGraph[node] = std::vector(neighbors.begin(), neighbors.end()); + m_taxiGraph[node] = std::vector(neighbors.begin(), neighbors.end()); } void TravelNodeMap::ComputeAllPaths() { std::set allNodes; - for (auto const& [source, neighbors] : taxiGraph) + for (auto const& [source, neighbors] : m_taxiGraph) allNodes.insert(source); for (uint32 source : allNodes) @@ -2518,7 +2476,7 @@ void TravelNodeMap::ComputeAllPaths() auto path = BuildPath(source, target, parentMap); if (!path.empty()) - taxiPathCache[source][target] = path; + m_taxiPathCache[source][target] = path; } } } @@ -2538,7 +2496,7 @@ std::unordered_map TravelNodeMap::BFS(uint32 fromNode) uint32 current = workQueue.front(); workQueue.pop(); - for (uint32 next : taxiGraph.at(current)) + for (uint32 next : m_taxiGraph.at(current)) { if (visited.count(next)) continue; @@ -2572,3 +2530,171 @@ std::vector TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode, std::reverse(path.begin(), path.end()); return path; } + +void TravelNodeMap::BuildZoneIndex() +{ + m_zoneIndex.clear(); + m_mapIndex.clear(); + + for (auto* node : nodes) + { + if (!node) + continue; + + WorldPosition* pos = node->getPosition(); + uint32 mapId = pos->GetMapId(); + + m_mapIndex[mapId].push_back(node); + + uint32 zoneId = sMapMgr->GetZoneId(PHASEMASK_NORMAL, *pos); + if (zoneId) + m_zoneIndex[zoneId].push_back(node); + } + + LOG_INFO("playerbots", "[ZoneIndex] Built index: {} zones, {} maps", m_zoneIndex.size(), m_mapIndex.size()); +} + +TravelNode* TravelNodeMap::GetNearestNodeInZone(WorldPosition pos, uint32 zoneId) +{ + auto it = m_zoneIndex.find(zoneId); + if (it == m_zoneIndex.end() || it->second.empty()) + return GetNearestNodeOnMap(pos); // Fallback to map-wide + + TravelNode* bestNode = nullptr; + float bestDist = FLT_MAX; + + for (auto* node : it->second) + { + if (!node || node->GetMapId() != pos.GetMapId()) + continue; + float dist = node->fDist(pos); + if (dist < bestDist) + { + bestDist = dist; + bestNode = node; + } + } + + if (!bestNode) + return GetNearestNodeOnMap(pos); + + return bestNode; +} + +TravelNode* TravelNodeMap::GetNearestNodeOnMap(WorldPosition pos) +{ + auto it = m_mapIndex.find(pos.GetMapId()); + if (it == m_mapIndex.end() || it->second.empty()) + return nullptr; + + TravelNode* bestNode = nullptr; + float bestDist = FLT_MAX; + + for (auto* node : it->second) + { + if (!node) + continue; + float d = node->fDist(pos); + if (d < bestDist) + { + bestDist = d; + bestNode = node; + } + } + + return bestNode; +} + +void TravelNodeMap::PrecomputeReachability() +{ + // Find connected components via BFS + std::unordered_set visited; + std::vector> components; + + for (auto* node : nodes) + { + if (!node || visited.count(node)) + continue; + + // BFS from this node + std::vector component; + std::queue q; + q.push(node); + visited.insert(node); + + while (!q.empty()) + { + TravelNode* current = q.front(); + q.pop(); + component.push_back(current); + + for (auto const& link : *current->getLinks()) + { + TravelNode* neighbor = link.first; + if (neighbor && !visited.count(neighbor)) + { + visited.insert(neighbor); + q.push(neighbor); + } + } + } + + components.push_back(std::move(component)); + } + + // Populate routes: every node in a component can reach every other node + // in the same component + for (auto const& comp : components) + { + for (auto* node : comp) + { + node->clearRoutes(); + for (auto* other : comp) + node->setRouteTo(other); + } + } + + uint32 totalNodes = 0; + for (auto const& c : components) + totalNodes += c.size(); + + // Sort components by size descending for logging + std::sort(components.begin(), components.end(), + [](auto const& a, auto const& b) { return a.size() > b.size(); }); + + uint32 singletons = 0; + for (auto const& c : components) + { + if (c.size() == 1) + singletons++; + } + + LOG_INFO("playerbots", "[Reachability] {} nodes in {} connected components ({} singletons)", + totalNodes, components.size(), singletons); + + // Log top 10 largest components + for (uint32 i = 0; i < std::min(10, components.size()); ++i) + { + auto const& c = components[i]; + std::string sampleName = c.front() ? c.front()->getName() : "?"; + uint32 mapId = c.front() ? c.front()->GetMapId() : 0; + LOG_INFO("playerbots", " Component {}: {} nodes, map={}, sample='{}'", i, c.size(), mapId, sampleName); + } + if (singletons > 0) + { + LOG_INFO("playerbots", " Singleton nodes (no links):"); + uint32 logged = 0; + for (auto const& c : components) + { + if (c.size() == 1 && logged < 20) + { + auto* n = c.front(); + LOG_INFO("playerbots", " '{}' map={} pos=({:.0f},{:.0f},{:.0f})", n->getName(), n->GetMapId(), + n->getPosition()->GetPositionX(), n->getPosition()->GetPositionY(), n->getPosition()->GetPositionZ()); + logged++; + } + } + if (singletons > 20) + LOG_INFO("playerbots", " ... and {} more singletons", singletons - 20); + } +} diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 9e05e2490..b6d1d48ed 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -8,11 +8,12 @@ #include +#include "G3D/Vector3.h" #include "TravelMgr.h" // THEORY // -// Pathfinding in (c)mangos is based on detour recast an opensource nashmesh creation and pathfinding codebase. +// Pathfinding in (c)mangos is based on detour recast, an opensource navmesh creation and pathfinding codebase. // This system is used for mob and npc pathfinding and in this codebase also for bots. // 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 @@ -24,33 +25,68 @@ // ---> [N1] ---> [N2] ---> [N3] ---> // // Bot at wants to move to -// [N1],[N2],[N3] are predefined nodes for wich we know we can move from [N1] to [N2] and from [N2] to [N3] but not -// from [N1] to [N3] If we can move fom [S] to [N1] and from [N3] to [E] we have a complete route to travel. +// [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. // -// Termonology: -// 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. Link: the connection between two nodes. A link signifies that the bot can travel from one node to -// another. A link is one-directional. Path: the waypointpath returned by the standard PathGenerator to move from one -// node (or position) to another. A path can be imcomplete or empty which means there is no link. 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. +// 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. // -// On server start saved nodes and links are loaded. Paths and routes are calculated on the fly but saved for future -// use. Nodes can be added and removed realtime however because bots access the nodes from different threads this -// requires a locking mechanism. +// 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 a logical places bots might want to go in +// 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) +// good for global coverage). // -// To increase coverage/linking extra nodes can be automatically be created. -// Current implentation 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. +// 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 { @@ -59,21 +95,20 @@ enum class TravelNodePathType : uint8 portal = 2, transport = 3, flightPath = 4, - teleportSpell = 5 + teleportSpell = 5, + staticPortal = 6, + flyingMount = 7 }; // A connection between two nodes. class TravelNodePath { public: - // Legacy Constructor for travelnodestore - // TravelNodePath(float distance1, float extraCost1, bool portal1 = false, uint32 portalId1 = 0, bool transport1 = - // false, bool calculated = false, uint8 maxLevelMob1 = 0, uint8 maxLevelAlliance1 = 0, uint8 maxLevelHorde1 = 0, - // float swimDistance1 = 0, bool flightPath1 = false); - // 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}, + 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), @@ -85,7 +120,7 @@ public: { if (pathType != (uint8)TravelNodePathType::walk) complete = true; - }; + } TravelNodePath(TravelNodePath* basePath) { @@ -98,11 +133,11 @@ public: swimDistance = basePath->swimDistance; pathType = basePath->pathType; pathObject = basePath->pathObject; - }; + } // Getters bool getComplete() { return complete || pathType != TravelNodePathType::walk; } - std::vector getPath() { return path; } + std::vector GetPath() { return path; } TravelNodePathType getPathType() { return pathType; } uint32 getPathObject() { return pathObject; } @@ -130,9 +165,6 @@ public: extraCost = distance / speed; } - // void setPortal(bool portal1, uint32 portalId1 = 0) { portal = portal1; portalId = portalId1; } - // void setTransport(bool transport1) { transport = transport1; } - void setPathType(TravelNodePathType pathType1) { pathType = pathType1; } void setPathObject(uint32 pathObject1) { pathObject = pathObject1; } @@ -186,9 +218,10 @@ class TravelNode { public: // Constructors - TravelNode(){}; + TravelNode() {} - TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", bool important1 = false) + TravelNode(WorldPosition point1, std::string const nodeName1 = "Travel Node", + bool important1 = false) { nodeName = nodeName1; point = point1; @@ -207,11 +240,11 @@ public: void setPoint(WorldPosition point1) { point = point1; } // Getters - std::string const getName() { return nodeName; }; - WorldPosition* getPosition() { return &point; }; + 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 isImportant() { return important; } bool isLinked() { return linked; } bool isTransport() @@ -235,7 +268,8 @@ public: bool isPortal() { for (auto const& link : *getLinks()) - if (link.second->getPathType() == TravelNodePathType::portal) + if (link.second->getPathType() == TravelNodePathType::portal || + link.second->getPathType() == TravelNodePathType::staticPortal) return true; return false; @@ -251,17 +285,25 @@ public: } // WorldLocation shortcuts - uint32 getMapId() { return point.GetMapId(); } + 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 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) + TravelNodePath* setPathTo(TravelNode* node, + TravelNodePath path = TravelNodePath(), + bool isLink = true) { if (this != node) { @@ -275,10 +317,20 @@ public: 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); + 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) { @@ -291,9 +343,18 @@ public: } } - 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(); } + 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); @@ -304,7 +365,8 @@ public: bool cropUselessLinks(); // Returns all nodes that can be reached from this node. - std::vector getNodeMap(bool importantOnly = false, std::vector ignoreNodes = {}); + std::vector getNodeMap(bool importantOnly = false, + std::vector ignoreNodes = {}); // Checks if it is even possible to route to this node. bool hasRouteTo(TravelNode* node) @@ -314,7 +376,10 @@ public: 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); @@ -344,24 +409,8 @@ protected: // uint32 transportId = 0; }; -class PortalNode : public TravelNode -{ -public: - PortalNode(TravelNode* baseNode) : TravelNode(baseNode){}; - - void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell) - { - nodeName = baseNode->getName(); - point = *baseNode->getPosition(); - paths.clear(); - links.clear(); - TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, portalSpell, true); - setPathTo(endNode, path); - }; -}; - // Route step type -enum PathNodeType +enum class PathNodeType : uint8 { NODE_PREPATH = 0, NODE_PATH = 1, @@ -369,13 +418,14 @@ enum PathNodeType NODE_PORTAL = 3, NODE_TRANSPORT = 4, NODE_FLIGHTPATH = 5, - NODE_TELEPORT = 6 + NODE_TELEPORT = 6, + NODE_FLYING_MOUNT = 7 }; struct PathNodePoint { WorldPosition point; - PathNodeType type = NODE_PATH; + PathNodeType type = PathNodeType::NODE_PATH; uint32 entry = 0; }; @@ -383,24 +433,31 @@ struct PathNodePoint class TravelPath { public: - TravelPath(){}; - TravelPath(std::vector fullPath1) { fullPath = fullPath1; } - TravelPath(std::vector path, PathNodeType type = NODE_PATH, uint32 entry = 0) + 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 = NODE_PATH, uint32 entry = 0) + 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 = NODE_PATH, uint32 entry = 0) + 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) { @@ -408,8 +465,11 @@ public: } void clear() { fullPath.clear(); } - bool empty() { return fullPath.empty(); } - std::vector getPath() { return fullPath; } + 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; } @@ -419,13 +479,9 @@ public: for (auto const& p : fullPath) retVec.push_back(p.point); return retVec; - }; + } bool makeShortCut(WorldPosition startPos, float maxDist); - bool shouldMoveToNextPoint(WorldPosition startPos, std::vector::iterator beg, - std::vector::iterator ed, std::vector::iterator p, - float& moveDist, float maxDist); - WorldPosition getNextPoint(WorldPosition startPos, float maxDist, TravelNodePathType& pathType, uint32& entry); std::ostringstream const print(); @@ -438,17 +494,25 @@ class TravelNodeRoute { public: TravelNodeRoute() {} - TravelNodeRoute(std::vector nodes1) { nodes = nodes1; /*currentNode = route.begin();*/ } + TravelNodeRoute(std::vector nodes1) + { + nodes = nodes1; + } bool isEmpty() { return nodes.empty(); } - bool hasNode(TravelNode* node) { return findNode(node) != nodes.end(); } + 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); + TravelPath BuildPath( + std::vector pathToStart = {}, + std::vector pathToEnd = {}, + Unit* bot = nullptr); std::ostringstream const print(); @@ -467,12 +531,47 @@ public: TravelNodeStub(TravelNode* dataNode1) { dataNode = dataNode1; } TravelNode* dataNode; - float m_f = 0.0, m_g = 0.0, m_h = 0.0; - bool open = false, close = false; + 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 walkPoints; + bool splineActive{false}; + uint32 splineStartTime{0}; + uint32 expectedDuration{0}; + + // Taxi scratch: + std::vector 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 { @@ -484,14 +583,18 @@ public: return instance; } - TravelNode* addNode(WorldPosition pos, std::string const preferedName = "Travel Node", bool isImportant = false, - bool checkDuplicate = true, bool transport = false, uint32 transportId = 0); + 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 : m_nodes) + for (auto& node : nodes) removeNode(node); m_nMapMtx.unlock(); @@ -499,28 +602,32 @@ public: } return false; - }; + } void fullLinkNode(TravelNode* startNode, Unit* bot); // Get all nodes - std::vector getNodes() { return m_nodes; } + std::vector getNodes() { return nodes; } std::vector getNodes(WorldPosition pos, float range = -1); // Find nearest node. TravelNode* getNode(TravelNode* sameNode) { - for (auto& node : m_nodes) + for (auto& node : nodes) { - if (node->getName() == sameNode->getName() && node->getPosition() == sameNode->getPosition()) + 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) + 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); @@ -536,15 +643,16 @@ public: return rNodes[urand(0, rNodes.size() - 1)]; } - // Finds the best nodePath between two nodes - TravelNodeRoute getRoute(TravelNode* start, TravelNode* goal, Player* bot = nullptr); + // Finds the best nodePath between two nodes (A* over the node graph) + TravelNodeRoute GetNodeRoute(TravelNode* start, TravelNode* goal, + Player* bot); - // Find the best node between two positions - TravelNodeRoute getRoute(WorldPosition startPos, WorldPosition endPos, std::vector& startPath, - Player* bot = nullptr); + // Find the nearest start/end nodes for two world positions + TravelNodeRoute GetNearestNodes(WorldPosition startPos, + WorldPosition endPos, + std::vector& startPath, + Player* bot = nullptr); - // Find the full path between those locations - static TravelPath getFullPath(WorldPosition startPos, WorldPosition endPos, Player* bot = nullptr); // Manage/update nodes void manageNodes(Unit* bot, bool mapFull = false); @@ -563,15 +671,17 @@ public: void removeUselessPaths(); void calculatePathCosts(); void generateTaxiPaths(); - void generatePaths(); + void generatePaths(bool fullGen = false); void generateAll(); + void Init(); + void printMap(); void printNodeStore(); void saveNodeStore(); - void loadNodeStore(); + void LoadNodeStore(); bool cropUselessNode(TravelNode* startNode); TravelNode* addZoneLinkNode(TravelNode* startNode); @@ -584,8 +694,25 @@ public: void InitTaxiGraph(); std::vector FindTaxiPath(uint32 fromNode, uint32 toNode); + void BuildZoneIndex(); + void PrecomputeReachability(); + + TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId); + TravelNode* GetNearestNodeOnMap(WorldPosition pos); + + bool GetFullPath(TravelPlan& plan, WorldPosition botPos, + uint32 botZoneId, uint32 teamId, + WorldPosition destination); + + // 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; - std::unordered_map> teleportNodes; private: TravelNodeMap() = default; @@ -601,13 +728,18 @@ private: void BuildTaxiGraph(); void ComputeAllPaths(); std::unordered_map BFS(uint32 startNode); - std::vector BuildPath(uint32 fromNode, uint32 toNode, - const std::unordered_map& parentMap); + std::vector BuildPath( + uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap); - std::unordered_map> taxiGraph; - std::map>> taxiPathCache; + std::unordered_map> m_taxiGraph; + std::map>> + m_taxiPathCache; - std::vector m_nodes; + std::vector nodes; + + std::unordered_map> m_zoneIndex; + std::unordered_map> m_mapIndex; std::vector> mapOffsets; diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 8c8343db2..4c8979ace 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -644,6 +644,7 @@ bool PlayerbotAIConfig::Initialize() autoTeleportForLevel = sConfigMgr->GetOption("AiPlayerbot.AutoTeleportForLevel", false); autoDoQuests = sConfigMgr->GetOption("AiPlayerbot.AutoDoQuests", true); enableNewRpgStrategy = sConfigMgr->GetOption("AiPlayerbot.EnableNewRpgStrategy", true); + enableTravelNodes = sConfigMgr->GetOption("AiPlayerbot.EnableTravelNodes", false); RpgStatusProbWeight[RPG_WANDER_RANDOM] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.WanderRandom", 15); RpgStatusProbWeight[RPG_WANDER_NPC] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.WanderNpc", 20); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 4e758e79e..8b5dc0922 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -368,6 +368,7 @@ public: bool autoLearnTrainerSpells; bool autoDoQuests; bool enableNewRpgStrategy; + bool enableTravelNodes; std::unordered_map RpgStatusProbWeight; bool syncLevelWithPlayers; bool autoLearnQuestSpells; diff --git a/src/Script/PlayerbotCommandScript.cpp b/src/Script/PlayerbotCommandScript.cpp index a7a073952..105f8a28f 100644 --- a/src/Script/PlayerbotCommandScript.cpp +++ b/src/Script/PlayerbotCommandScript.cpp @@ -20,6 +20,7 @@ #include "PlayerbotMgr.h" #include "RandomPlayerbotMgr.h" #include "ScriptMgr.h" +#include "TravelNode.h" using namespace Acore::ChatCommands; @@ -41,11 +42,16 @@ public: {"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No}, }; + static ChatCommandTable playerbotsTravelCommandTable = { + {"generatenode", HandleGenerateTravelNodesCommand, SEC_GAMEMASTER, Console::Yes}, + }; + static ChatCommandTable playerbotsCommandTable = { {"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No}, {"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes}, {"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes}, {"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes}, + {"travel", playerbotsTravelCommandTable}, {"debug", playerbotsDebugCommandTable}, {"account", playerbotsAccountCommandTable}, }; @@ -106,6 +112,15 @@ public: return true; } + static bool HandleGenerateTravelNodesCommand(ChatHandler* handler, char const* /*args*/) + { + handler->PSendSysMessage("Regenerating travel node paths..."); + LOG_INFO("playerbots", "Manual travel node regeneration started via console command."); + sTravelNodeMap.generateAll(); + handler->PSendSysMessage("Travel node regeneration complete. Paths saved to database."); + return true; + } + static bool HandleDebugBGCommand(ChatHandler* handler, char const* args) { return BGTactics::HandleConsoleCommand(handler, args);