From b19ab481b3e6ddd604f4d2457b70e55b80cedf9a Mon Sep 17 00:00:00 2001 From: bash Date: Sat, 2 May 2026 00:50:18 +0200 Subject: [PATCH] feat(Core/Travel): Enable travel node system for RPG pathfinding (#2312) --- conf/playerbots.conf.dist | 6 + src/Ai/Base/Actions/CheckValuesAction.cpp | 7 +- src/Ai/Base/Actions/DebugAction.cpp | 51 +- src/Ai/Base/Actions/FollowActions.cpp | 79 +- src/Ai/Base/Actions/MovementActions.cpp | 596 +++++++++++- src/Ai/Base/Actions/MovementActions.h | 33 + src/Ai/World/Rpg/Action/NewRpgAction.cpp | 53 +- src/Ai/World/Rpg/Action/NewRpgAction.h | 10 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 142 ++- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 7 +- src/Ai/World/Rpg/NewRpgInfo.cpp | 24 +- src/Ai/World/Rpg/NewRpgInfo.h | 11 +- src/Bot/PlayerbotAI.cpp | 15 + src/Bot/PlayerbotAI.h | 1 + src/Bot/RandomPlayerbotMgr.cpp | 9 +- src/Mgr/Travel/TravelMgr.cpp | 657 +++++++------ src/Mgr/Travel/TravelMgr.h | 76 +- src/Mgr/Travel/TravelNode.cpp | 957 +++++++++---------- src/Mgr/Travel/TravelNode.h | 364 ++++--- src/PlayerbotAIConfig.cpp | 1 + src/PlayerbotAIConfig.h | 1 + src/Script/PlayerbotCommandScript.cpp | 15 + 22 files changed, 1956 insertions(+), 1159 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 3d0866f44..3a6c82157 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1054,6 +1054,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/CheckValuesAction.cpp b/src/Ai/Base/Actions/CheckValuesAction.cpp index dce66bd47..19f8f21f5 100644 --- a/src/Ai/Base/Actions/CheckValuesAction.cpp +++ b/src/Ai/Base/Actions/CheckValuesAction.cpp @@ -6,10 +6,10 @@ #include "CheckValuesAction.h" #include "Event.h" +#include "ObjectGuid.h" #include "ServerFacade.h" #include "PlayerbotAI.h" -#include "TravelNode.h" #include "AiObjectContext.h" CheckValuesAction::CheckValuesAction(PlayerbotAI* botAI) : Action(botAI, "check values") {} @@ -21,11 +21,6 @@ bool CheckValuesAction::Execute(Event /*event*/) botAI->Ping(bot->GetPositionX(), bot->GetPositionY()); } - if (botAI->HasStrategy("map", BOT_STATE_NON_COMBAT) || botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT)) - { - TravelNodeMap::instance().manageNodes(bot, botAI->HasStrategy("map full", BOT_STATE_NON_COMBAT)); - } - GuidVector possible_targets = *context->GetValue("possible targets"); GuidVector all_targets = *context->GetValue("all targets"); GuidVector npcs = *context->GetValue("nearest npcs"); diff --git a/src/Ai/Base/Actions/DebugAction.cpp b/src/Ai/Base/Actions/DebugAction.cpp index edf1ef925..1ea8302f9 100644 --- a/src/Ai/Base/Actions/DebugAction.cpp +++ b/src/Ai/Base/Actions/DebugAction.cpp @@ -76,7 +76,7 @@ bool DebugAction::Execute(Event event) return false; std::vector beginPath, endPath; - TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot); + TravelNodeRoute route = TravelNodeMap::instance().FindRouteNearestNodes(botPos, *points.front(), beginPath, bot); std::ostringstream out; out << "Traveling to " << dest->getTitle() << ": "; @@ -196,18 +196,18 @@ bool DebugAction::Execute(Event event) { WorldPosition pos(bot); - std::string const name = "USER:" + text.substr(9); + std::string suffix = text.size() > 9 ? text.substr(9) : pos.getAreaName(); + std::string const name = "USER:" + suffix; - /* TravelNode* startNode = */ TravelNodeMap::instance().addNode(pos, name, false, false); // startNode not used, but addNode as side effect, fragment marked for removal. - - for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000)) { - endNode->setLinked(false); + std::lock_guard lock(TravelNodeMap::instance().m_nMapMtx); + TravelNodeMap::instance().addNode(pos, name, false, true); + + for (auto& endNode : TravelNodeMap::instance().getNodes(pos, 2000)) + endNode->setLinked(false); } - botAI->TellMasterNoFacing("Node " + name + " created."); - - TravelNodeMap::instance().setHasToGen(); + botAI->TellMasterNoFacing("Node " + name + " created. Use console command '.playerbots travel generatenode' to connect nodes."); return true; } @@ -223,14 +223,15 @@ bool DebugAction::Execute(Event event) if (startNode->isImportant()) { botAI->TellMasterNoFacing("Node can not be removed."); + return true; } - TravelNodeMap::instance().m_nMapMtx.lock(); - TravelNodeMap::instance().removeNode(startNode); - botAI->TellMasterNoFacing("Node removed."); - TravelNodeMap::instance().m_nMapMtx.unlock(); + { + std::lock_guard lock(TravelNodeMap::instance().m_nMapMtx); + TravelNodeMap::instance().removeNode(startNode); + } - TravelNodeMap::instance().setHasToGen(); + botAI->TellMasterNoFacing("Node removed. Use console command '.playerbots travel generatenode' to finalize nodes."); return true; } @@ -247,15 +248,17 @@ bool DebugAction::Execute(Event event) node->removeLinkTo(path.first, true); return true; } - else if (text.find("gen node") != std::string::npos) + else if (text.find("gen node") != std::string::npos || + text.find("gen path") != std::string::npos) { - // Pathfinder - TravelNodeMap::instance().generateNodes(); - return true; - } - else if (text.find("gen path") != std::string::npos) - { - TravelNodeMap::instance().generatePaths(); + // Disabled: generateAll() touches Map / grid / mmap state that is only + // safe to mutate on the world thread. Running it from a detached worker + // (or from a bot tick on a MapUpdater thread) races with world updates + // and freezes the server. Use the console command instead, which runs + // synchronously on the world thread: + // .playerbots travel generatenode + botAI->TellMasterNoFacing( + "Disabled in chat. Run '.playerbots travel generatenode' from the server console."); return true; } else if (text.find("crop path") != std::string::npos) @@ -275,7 +278,7 @@ bool DebugAction::Execute(Event event) [] { TravelNodeMap::instance().removeNodes(); - TravelNodeMap::instance().loadNodeStore(); + TravelNodeMap::instance().LoadNodeStore(); }); t.detach(); @@ -297,7 +300,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 f82b05368..12335e855 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 18e76ca72..696d13bd0 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); @@ -1719,6 +1721,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) { @@ -2959,4 +2974,571 @@ 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; + + // walkPoints may have been cleared by a map transfer or external reset + // while the spline was still flagged active; bail out safely. + if (state.walkPoints.empty()) + { + state.splineActive = false; + 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 + } + + // Spline finalized short of target — interrupted (combat/knockback/etc). + // Caller will re-launch. + 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 past any stored points the bot has already moved past — useful + // when a spline is interrupted (combat, knockback, mid-spline reissue) + // and we re-launch from a position later in the route. + 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); + + 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; + + 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()); + + LOG_DEBUG("playerbots", + "[TravelPlan] {} requesting plan: from ({:.0f},{:.0f},{:.0f}) map={} zone={} → " + "({:.0f},{:.0f},{:.0f}) map={} (straight={:.0f}yd)", + bot->GetName(), botPos.GetPositionX(), botPos.GetPositionY(), botPos.GetPositionZ(), + bot->GetMapId(), bot->GetZoneId(), + destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(), + destination.GetMapId(), botPos.fDist(destination)); + + return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination); +} + +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: + { + if (state.stepIdx + 1 >= state.steps.size()) + { + state.stepIdx++; + return true; + } + + float const botX = bot->GetPositionX(); + float const botY = bot->GetPositionY(); + float const botZ = bot->GetPositionZ(); + + // Walk forward through the route while distance keeps shrinking. + // Once it starts growing we're past the closest waypoint — break. + size_t bestIdx = state.stepIdx + 1; + float bestDistSq = FLT_MAX; + for (size_t i = state.stepIdx + 1; i < state.steps.size(); ++i) + { + const PathNodePoint& cand = state.steps[i]; + if (cand.type != PathNodeType::NODE_PATH && + cand.type != PathNodeType::NODE_NODE) + break; // stop at portal/transport/etc — can't walk past + + float const dx = cand.point.GetPositionX() - botX; + float const dy = cand.point.GetPositionY() - botY; + float const dz = cand.point.GetPositionZ() - botZ; + float const dSq = dx * dx + dy * dy + dz * dz; + if (dSq >= bestDistSq) + break; // moving away — closest waypoint already found + + bestDistSq = dSq; + bestIdx = i; + } + + constexpr float ARRIVAL_DIST = 5.0f; + + WorldPosition const& target = state.steps[bestIdx].point; + float const distToTarget = bot->GetExactDist( + target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); + + if (distToTarget < ARRIVAL_DIST) + { + state.stepIdx = bestIdx; + return true; + } + + return MoveTo(target.GetMapId(), + target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), + false, false, false, true /*exact_waypoint*/); + } + + case PathNodeType::NODE_PATH: + case PathNodeType::NODE_NODE: + { + // Batch consecutive walk points into one spline. Capped small 20 points per tick. + static constexpr uint32 MAX_SPLINE_POINTS = 20; + 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_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 — abort the plan and let the + // caller's stuck-recovery decide what to do. (cmangos + // doesn't teleport here either; an abandoned plan is + // recovered by the next MoveFarTo cycle.) + 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) + { + state.walkPoints.clear(); + state.Reset(); + return false; + } + } + // 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 didn't cross — natural collision missed. + // Abort the plan; stuck-recovery in MoveFarTo will decide + // whether to retry or teleport the bot. (cmangos doesn't + // teleport here either.) + state.Reset(); + return false; + } + + 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; + } + } + + TravelMgr::FlightMasterInfo const* fmInfo = sTravelMgr.GetNearestFlightMasterInfo(bot); + if (!fmInfo) + { + state.route.clear(); + state.stepIdx += 2; + return true; + } + + if (bot->GetDistance(fmInfo->pos) > INTERACTION_DISTANCE) + return MoveTo(fmInfo->pos.GetMapId(), fmInfo->pos.GetPositionX(), + fmInfo->pos.GetPositionY(), fmInfo->pos.GetPositionZ()); + + ObjectGuid fmGuid = ObjectGuid::Create(fmInfo->templateEntry, fmInfo->dbGuid); + Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid); + if (!flightMaster || !flightMaster->IsAlive()) + { + state.route.clear(); + state.stepIdx += 2; + return true; + } + + 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: + { + // Teleport-spell node (e.g. mage portals). Not implemented + // — abort the plan instead of silently teleporting the + // bot. The plan executor regards this node as terminal. + state.Reset(); + return false; + } + + case PathNodeType::NODE_FLYING_MOUNT: + { + // Flying-mount node not implemented — abort. cmangos + // produces these nodes during graph generation but their + // execution is server-specific; we treat them as + // unreachable rather than papering over with a teleport. + state.Reset(); + return false; + } + default: + { + LOG_ERROR("playerbots", + "[TravelPlan] Bot {} encountered unknown PathNodeType ({}); resetting plan", + bot->GetName(), static_cast(pt.type)); + state.Reset(); + return false; + } + } + return false; +} + +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 b1c566211..d3a3a5509 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 = PATHFIND_NORMAL | PATHFIND_INCOMPLETE; +constexpr uint32 RELAXED_PATH_ACCEPT_MASK = 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: @@ -66,6 +80,25 @@ 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); + protected: struct CheckAngle { diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index ca0ca2433..fe3aeeb58 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -3,6 +3,7 @@ #include #include +#include "AreaDefines.h" #include "BroadcastHelper.h" #include "ChatHelper.h" #include "G3D/Vector2.h" @@ -119,17 +120,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) } break; } - case RPG_TRAVEL_FLIGHT: - { - auto& data = std::get(info.data); - if (data.inFlight && !bot->IsInFlight()) - { - // flight arrival - info.ChangeToIdle(); - return true; - } - break; - } + // RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction + // so the flight action owns both take-off and landing transitions. case RPG_REST: { // REST -> IDLE @@ -463,31 +455,42 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) return false; auto& data = *dataPtr; + + // Arrival: we had boarded a flight (data.inFlight) and we're no longer in + // it → we just landed. Special-case Rut'theran: walk to the portal GO so + // it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS + // so this branch falls through to ChangeToIdle on the next tick. + if (data.inFlight && !bot->IsInFlight()) + { + if (bot->GetZoneId() == AREA_TELDRASSIL) + { + static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f); + return MoveFarTo(rutTheranPortalEntrance); + } + info.ChangeToIdle(); + return true; + } + if (bot->IsInFlight()) { data.inFlight = true; return false; } - Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster); + + if (bot->GetDistance(data.flightMasterPos) > INTERACTION_DISTANCE) + return MoveFarTo(data.flightMasterPos); + + Creature* flightMaster = bot->FindNearestCreature(data.flightMasterEntry, INTERACTION_DISTANCE * 3); if (!flightMaster || !flightMaster->IsAlive()) { - botAI->rpgInfo.ChangeToIdle(); + info.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(); + info.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..a8fefe7b5 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,27 @@ 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 +1015,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,25 +1062,31 @@ 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()); return dest; } -bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path) +bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path) { - flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot); - if (!flightMaster) + TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot); + if (!info) return false; std::vector> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot); if (availablePaths.empty()) return false; + flightMasterEntry = info->templateEntry; + flightMasterPos = info->pos; path = availablePaths[urand(0, availablePaths.size() - 1)]; LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", - bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size()); + bot->GetName(), flightMasterEntry, path[0], path[path.size() - 1], availablePaths.size()); return true; } @@ -1058,7 +1105,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 +1185,12 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta } case RPG_TRAVEL_FLIGHT: { - ObjectGuid flightMaster; + uint32 flightMasterEntry = 0; + WorldPosition flightMasterPos; std::vector path; - if (SelectRandomFlightTaxiNode(flightMaster, path)) + if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path)) { - botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path); + botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path); return true; } return false; @@ -1220,9 +1267,10 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) } case RPG_TRAVEL_FLIGHT: { - ObjectGuid flightMaster; + uint32 flightMasterEntry = 0; + WorldPosition flightMasterPos; std::vector path; - return SelectRandomFlightTaxiNode(flightMaster, path); + return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path); } case RPG_OUTDOOR_PVP: { diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index eaba72446..1a62d053c 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); @@ -54,7 +55,7 @@ protected: bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomCampPos(Player* bot); - bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path); + bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path); bool RandomChangeStatus(std::vector candidateStatus); bool CheckRpgStatusAvailable(NewRpgStatus status); @@ -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..150421752 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -6,42 +6,43 @@ void NewRpgInfo::ChangeToGoGrind(WorldPosition pos) { - startT = getMSTime(); + Reset(); data = GoGrind{pos}; } void NewRpgInfo::ChangeToGoCamp(WorldPosition pos) { - startT = getMSTime(); + Reset(); data = GoCamp{pos}; } void NewRpgInfo::ChangeToWanderNpc() { - startT = getMSTime(); + Reset(); data = WanderNpc{}; } void NewRpgInfo::ChangeToWanderRandom() { - startT = getMSTime(); + Reset(); data = WanderRandom{}; } void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) { - startT = getMSTime(); + Reset(); DoQuest do_quest; do_quest.questId = questId; do_quest.quest = quest; data = do_quest; } -void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path) +void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path) { - startT = getMSTime(); + Reset(); TravelFlight flight; - flight.fromFlightMaster = fromFlightMaster; + flight.flightMasterEntry = flightMasterEntry; + flight.flightMasterPos = flightMasterPos; flight.path = std::move(path); flight.inFlight = false; data = flight; @@ -57,13 +58,13 @@ void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId) void NewRpgInfo::ChangeToRest() { - startT = getMSTime(); + Reset(); data = Rest{}; } void NewRpgInfo::ChangeToIdle() { - startT = getMSTime(); + Reset(); data = Idle{}; } @@ -76,6 +77,7 @@ void NewRpgInfo::Reset() { data = Idle{}; startT = getMSTime(); + ClearTravel(); } void NewRpgInfo::SetMoveFarTo(WorldPosition pos) @@ -157,7 +159,7 @@ std::string NewRpgInfo::ToString() else if constexpr (std::is_same_v) { out << "TRAVEL_FLIGHT"; - out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry(); + out << "\nflightMasterEntry: " << arg.flightMasterEntry; out << "\nfromNode: " << arg.path[0]; out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index 9e6abdda4..9b3f51485 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -8,6 +8,7 @@ #include "Strategy.h" #include "Timer.h" #include "TravelMgr.h" +#include "TravelNode.h" using NewRpgStatusTransitionProb = std::vector>; @@ -49,7 +50,8 @@ struct NewRpgInfo // RPG_TRAVEL_FLIGHT struct TravelFlight { - ObjectGuid fromFlightMaster{}; + uint32 flightMasterEntry{0}; + WorldPosition flightMasterPos{}; std::vector path; bool inFlight{false}; }; @@ -74,7 +76,10 @@ struct NewRpgInfo uint32 stuckTs{0}; uint32 stuckAttempts{0}; WorldPosition moveFarPos; - // END MOVE_FAR + // Travel Node System + TravelPlan travelPlan; + bool HasActiveTravelPlan() const { return travelPlan.IsActive(); } + void ClearTravel() { travelPlan.Reset(); } using RpgData = std::variant< Idle, @@ -96,7 +101,7 @@ struct NewRpgInfo void ChangeToWanderNpc(); void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); - void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path); + void ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path); void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0); void ChangeToRest(); void ChangeToIdle(); diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 357678928..f5356b773 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -762,6 +762,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 dc7770ed8..796dd4181 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 5c0922fb9..4ce498f39 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..12fad2245 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -8,6 +8,7 @@ #include #include +#include "AreaDefines.h" #include "Creature.h" #include "Log.h" #include "ObjectAccessor.h" @@ -28,67 +29,60 @@ // Navigation data -enum class CityId : uint8 +struct Capital { - STORMWIND, - IRONFORGE, - DARNASSUS, - EXODAR, - ORGRIMMAR, - UNDERCITY, - THUNDER_BLUFF, - SILVERMOON_CITY, - SHATTRATH_CITY, - DALARAN + uint32 zoneId; + TeamId team; + char const* name; + std::vector bankers; }; -static const std::unordered_map> bankerToCity = { - {2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}}, - {2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}}, - {4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}}, - {17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}}, - {3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}}, - {4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}}, - {2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, - {17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, - {16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, - {19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, - {19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, - {30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}}, - {28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}} +static const std::vector capitals = { + { AREA_STORMWIND_CITY, TEAM_ALLIANCE, "Stormwind", {2455, 2456, 2457} }, + { AREA_IRONFORGE, TEAM_ALLIANCE, "Ironforge", {2460, 2461, 5099} }, + { AREA_DARNASSUS, TEAM_ALLIANCE, "Darnassus", {4155, 4208, 4209} }, + { AREA_THE_EXODAR, TEAM_ALLIANCE, "Exodar", {17773, 18350, 16710} }, + { AREA_ORGRIMMAR, TEAM_HORDE, "Orgrimmar", {3320, 3309, 3318} }, + { AREA_UNDERCITY, TEAM_HORDE, "Undercity", {4549, 2459, 2458, 4550} }, + { AREA_THUNDER_BLUFF, TEAM_HORDE, "Thunder Bluff", {2996, 8356, 8357} }, + { AREA_SILVERMOON_CITY, TEAM_HORDE, "Silvermoon", {17631, 17632, 17633, 16615, 16616, 16617} }, + { AREA_SHATTRATH_CITY, TEAM_NEUTRAL, "Shattrath", {19246, 19338, 19034, 19318} }, + { AREA_DALARAN, TEAM_NEUTRAL, "Dalaran", {30604, 30605, 30607, 28675, 28676, 28677, 29530} } }; -static const std::unordered_map> cityToBankers = { - {CityId::STORMWIND, {2455, 2456, 2457}}, - {CityId::IRONFORGE, {2460, 2461, 5099}}, - {CityId::DARNASSUS, {4155, 4208, 4209}}, - {CityId::EXODAR, {17773, 18350, 16710}}, - {CityId::ORGRIMMAR, {3320, 3309, 3318}}, - {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, - {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, - {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, - {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, - {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} -}; - -static int GetCityWeight(CityId city) +static Capital const* FindCapitalByZone(uint32 zoneId) { - int weight = 0; - switch (city) + for (Capital const& capital : capitals) + if (capital.zoneId == zoneId) + return &capital; + return nullptr; +} + +static Capital const* FindCapitalByBanker(uint16 bankerEntry) +{ + for (Capital const& capital : capitals) + for (uint16 bankerId : capital.bankers) + if (bankerId == bankerEntry) + return &capital; + return nullptr; +} + +static int GetCityWeight(uint32 zoneId) +{ + switch (zoneId) { - case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; - case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; - case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; - case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; - case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; - case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; - case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; - case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; - case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; - case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; - default: weight = 0; break; + case AREA_STORMWIND_CITY: return sPlayerbotAIConfig.weightTeleToStormwind; + case AREA_IRONFORGE: return sPlayerbotAIConfig.weightTeleToIronforge; + case AREA_DARNASSUS: return sPlayerbotAIConfig.weightTeleToDarnassus; + case AREA_THE_EXODAR: return sPlayerbotAIConfig.weightTeleToExodar; + case AREA_ORGRIMMAR: return sPlayerbotAIConfig.weightTeleToOrgrimmar; + case AREA_UNDERCITY: return sPlayerbotAIConfig.weightTeleToUndercity; + case AREA_THUNDER_BLUFF: return sPlayerbotAIConfig.weightTeleToThunderBluff; + case AREA_SILVERMOON_CITY: return sPlayerbotAIConfig.weightTeleToSilvermoonCity; + case AREA_SHATTRATH_CITY: return sPlayerbotAIConfig.weightTeleToShattrathCity; + case AREA_DALARAN: return sPlayerbotAIConfig.weightTeleToDalaran; } - return weight; + return 0; } WorldPosition::WorldPosition(std::string const str) @@ -687,93 +681,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 +693,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 +994,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 +2308,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 +2699,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 +2823,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,80 +4286,120 @@ void TravelMgr::Init() PrepareZone2LevelBracket(); PrepareDestinationCache(); } - sTravelNodeMap.InitTaxiGraph(); - LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built."); + sTravelNodeMap.Init(); } -Creature* TravelMgr::GetNearestFlightMaster(Player* bot) +TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const { - std::map& flightMasterCache = + auto const& flightMasterCache = (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; - Creature* nearestFlightMaster = nullptr; + FlightMasterInfo const* nearest = nullptr; float nearestDistance = std::numeric_limits::max(); - for (auto const& [entry, pos] : flightMasterCache) + for (auto const& [dbGuid, info] : flightMasterCache) { - if (pos.GetMapId() != bot->GetMapId()) + if (info.pos.GetMapId() != bot->GetMapId()) continue; - float distance = bot->GetExactDist2dSq(pos); - if (distance > nearestDistance) - continue; - - Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); - if (flightMaster) + float distance = bot->GetExactDist2dSq(info.pos); + if (distance < nearestDistance) { nearestDistance = distance; - nearestFlightMaster = flightMaster; + nearest = &info; } } - return nearestFlightMaster; + return nearest; } -ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot) +std::vector TravelMgr::GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode) const { - Creature* nearestFlightMaster = GetNearestFlightMaster(bot); - if (!nearestFlightMaster) - return ObjectGuid::Empty; - - return nearestFlightMaster->GetGUID(); + auto const& cache = (team == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; + std::unordered_set seen; + std::vector result; + for (auto const& [entry, info] : cache) + { + if (info.zoneId != zoneId || info.taxiNodeId == 0 || info.taxiNodeId == excludeNode) + continue; + if (seen.insert(info.taxiNodeId).second) + result.push_back(info.taxiNodeId); + } + return result; } std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot) { std::vector> validDestinations; - Creature* nearestFlightMaster = GetNearestFlightMaster(bot); - if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot); + if (!nearestFlightMaster) return validDestinations; - uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), - nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), - bot->GetTeamId()); + uint32 fromNode = nearestFlightMaster->taxiNodeId; if (!fromNode) return validDestinations; - std::vector candidateLocations; - if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) - candidateLocations = GetCityLocations(bot); + TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); + if (!startNode) + return validDestinations; - std::vector hubLocations = GetTravelHubs(bot); - candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end()); + uint32 botLevel = bot->GetLevel(); - for (auto const& loc : candidateLocations) + // Bots already in a capital shouldn't have another capital picked as a + // flight destination — that just shuffles them between cities. + bool botInCapital = false; + if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId())) + botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0; + + std::vector candidateZones; + if (botLevel >= 10 && !botInCapital && + urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) { - uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(), - loc.GetPositionZ(), loc.GetMapId(), - bot->GetTeamId()); - if (!candidateNode) - continue; - - std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode); - if (!path.empty()) - validDestinations.push_back(path); + TeamId botTeam = bot->GetTeamId(); + for (Capital const& capital : capitals) + { + if (capital.team != TEAM_NEUTRAL && capital.team != botTeam) + continue; + candidateZones.push_back(capital.zoneId); + } } + if (candidateZones.empty()) + { + for (auto const& [zoneId, bracket] : zone2LevelBracket) + { + if (botLevel < bracket.low || botLevel > bracket.high) + continue; + if (GetFlightNodesInZone(zoneId, bot->GetTeamId(), fromNode).empty()) + continue; + candidateZones.push_back(zoneId); + } + } + + if (candidateZones.empty()) + return validDestinations; + + while (!candidateZones.empty()) + { + uint32 zoneIndex = urand(0, candidateZones.size() - 1); + uint32 pickedZone = candidateZones[zoneIndex]; + + std::vector usableNodes = GetFlightNodesInZone(pickedZone, bot->GetTeamId(), fromNode); + + if (!usableNodes.empty()) + { + uint32 pickedNode = usableNodes[urand(0, usableNodes.size() - 1)]; + std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, pickedNode); + if (!path.empty()) + { + validDestinations.push_back(std::move(path)); + return validDestinations; + } + } + + candidateZones.erase(candidateZones.begin() + zoneIndex); + } + return validDestinations; } @@ -4472,34 +4433,34 @@ std::vector TravelMgr::GetCityLocations(Player* bot) return fallbackLocations; TeamId botTeamId = bot->GetTeamId(); - std::unordered_set validBankerCities; + std::unordered_set validBankerCities; for (auto& loc : bankerLocsPerLevelCache[level]) { - auto cityIt = bankerToCity.find(loc.entry); - if (cityIt == bankerToCity.end()) + Capital const* capital = FindCapitalByBanker(loc.entry); + if (!capital) continue; - TeamId cityTeamId = cityIt->second.second; + TeamId cityTeamId = capital->team; if (cityTeamId == botTeamId || (cityTeamId == TEAM_NEUTRAL) ) - validBankerCities.insert(cityIt->second.first); + validBankerCities.insert(capital->zoneId); } // Fallback if no valid cities if (validBankerCities.empty()) return fallbackLocations; // Apply weights to valid cities - std::vector weightedCities; - for (CityId city : validBankerCities) + std::vector weightedCities; + for (uint32 zoneId : validBankerCities) { - int weight = GetCityWeight(city); + int weight = GetCityWeight(zoneId); if (weight <= 0) continue; for (int i = 0; i < weight; ++i) - weightedCities.push_back(city); + weightedCities.push_back(zoneId); } // Fallback if no valid cities @@ -4507,9 +4468,11 @@ std::vector TravelMgr::GetCityLocations(Player* bot) return fallbackLocations; // Pick a weighted city randomly, then a random banker in that city - CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; - - auto const& bankers = cityToBankers.at(selectedCity); + uint32 selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; + Capital const* selectedCapital = FindCapitalByZone(selectedCity); + if (!selectedCapital) + return fallbackLocations; + auto const& bankers = selectedCapital->bankers; uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; auto locIt = bankerEntryToLocation.find(selectedBankerEntry); if (locIt != bankerEntryToLocation.end()) @@ -4518,80 +4481,108 @@ 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 - zone2LevelBracket[1] = {5, 12}; // Dun Morogh - zone2LevelBracket[12] = {5, 12}; // Elwynn Forest - zone2LevelBracket[14] = {5, 12}; // Durotar - zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades - zone2LevelBracket[141] = {5, 12}; // Teldrassil - zone2LevelBracket[215] = {5, 12}; // Mulgore - zone2LevelBracket[3430] = {5, 12}; // Eversong Woods - zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle + // Classic WoW - starter zones + zone2LevelBracket[AREA_DUN_MOROGH] = {5, 12}; + zone2LevelBracket[AREA_ELWYNN_FOREST] = {5, 12}; + zone2LevelBracket[AREA_DUROTAR] = {5, 12}; + zone2LevelBracket[AREA_TIRISFAL_GLADES] = {5, 12}; + zone2LevelBracket[AREA_TELDRASSIL] = {5, 12}; + zone2LevelBracket[AREA_MULGORE] = {5, 12}; + zone2LevelBracket[AREA_EVERSONG_WOODS] = {5, 12}; + zone2LevelBracket[AREA_AZUREMYST_ISLE] = {5, 12}; - // Classic WoW - Mid - level zones - zone2LevelBracket[17] = {10, 25}; // Barrens - zone2LevelBracket[38] = {10, 20}; // Loch Modan - zone2LevelBracket[40] = {10, 21}; // Westfall - zone2LevelBracket[130] = {10, 23}; // Silverpine Forest - zone2LevelBracket[148] = {10, 21}; // Darkshore - zone2LevelBracket[3433] = {10, 22}; // Ghostlands - zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle + // Classic WoW - low level zones + zone2LevelBracket[AREA_THE_BARRENS] = {10, 25}; + zone2LevelBracket[AREA_LOCH_MODAN] = {10, 20}; + zone2LevelBracket[AREA_WESTFALL] = {10, 21}; + zone2LevelBracket[AREA_SILVERPINE_FOREST] = {10, 23}; + zone2LevelBracket[AREA_DARKSHORE] = {10, 21}; + zone2LevelBracket[AREA_GHOSTLANDS] = {10, 22}; + zone2LevelBracket[AREA_BLOODMYST_ISLE] = {10, 21}; - // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Deadwind Pass - zone2LevelBracket[11] = {21, 30}; // Wetlands - zone2LevelBracket[44] = {16, 28}; // Redridge Mountains - zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills - zone2LevelBracket[331] = {18, 33}; // Ashenvale - zone2LevelBracket[400] = {24, 36}; // Thousand Needles - zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains + // Classic WoW - mid-level zones + zone2LevelBracket[AREA_DUSKWOOD] = {19, 33}; + zone2LevelBracket[AREA_WETLANDS] = {21, 30}; + zone2LevelBracket[AREA_REDRIDGE_MOUNTAINS] = {16, 28}; + zone2LevelBracket[AREA_HILLSBRAD_FOOTHILLS] = {20, 34}; + zone2LevelBracket[AREA_ASHENVALE] = {18, 33}; + zone2LevelBracket[AREA_THOUSAND_NEEDLES] = {24, 36}; + zone2LevelBracket[AREA_STONETALON_MOUNTAINS] = {16, 29}; - // Classic WoW - Higher - level zones - zone2LevelBracket[3] = {36, 46}; // Badlands - zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows - zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh - zone2LevelBracket[16] = {45, 52}; // Azshara - zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale - zone2LevelBracket[45] = {30, 42}; // Arathi Highlands - zone2LevelBracket[47] = {42, 51}; // Hinterlands - zone2LevelBracket[51] = {45, 51}; // Searing Gorge - zone2LevelBracket[357] = {40, 52}; // Feralas - zone2LevelBracket[405] = {30, 41}; // Desolace - zone2LevelBracket[440] = {41, 52}; // Tanaris + // Classic WoW - 30-52 zones + zone2LevelBracket[AREA_BADLANDS] = {36, 46}; + zone2LevelBracket[AREA_SWAMP_OF_SORROWS] = {36, 46}; + zone2LevelBracket[AREA_DUSTWALLOW_MARSH] = {35, 46}; + zone2LevelBracket[AREA_AZSHARA] = {45, 52}; + zone2LevelBracket[AREA_STRANGLETHORN_VALE] = {32, 47}; + zone2LevelBracket[AREA_ARATHI_HIGHLANDS] = {30, 42}; + zone2LevelBracket[AREA_THE_HINTERLANDS] = {42, 51}; + zone2LevelBracket[AREA_SEARING_GORGE] = {45, 51}; + zone2LevelBracket[AREA_FERALAS] = {40, 52}; + zone2LevelBracket[AREA_DESOLACE] = {30, 41}; + zone2LevelBracket[AREA_TANARIS] = {41, 52}; - // Classic WoW - Top - level zones - zone2LevelBracket[4] = {52, 57}; // Blasted Lands - zone2LevelBracket[28] = {50, 60}; // Western Plaguelands - zone2LevelBracket[46] = {51, 60}; // Burning Steppes - zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands - zone2LevelBracket[361] = {47, 57}; // Felwood - zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater - zone2LevelBracket[618] = {54, 61}; // Winterspring - zone2LevelBracket[1377] = {54, 63}; // Silithus + // Classic WoW - top level zones + zone2LevelBracket[AREA_BLASTED_LANDS] = {52, 57}; + zone2LevelBracket[AREA_WESTERN_PLAGUELANDS] = {50, 60}; + zone2LevelBracket[AREA_BURNING_STEPPES] = {51, 60}; + zone2LevelBracket[AREA_EASTERN_PLAGUELANDS] = {54, 62}; + zone2LevelBracket[361] = {47, 57}; // Felwood (no AREA_ define) + zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater (no AREA_ define) + zone2LevelBracket[AREA_WINTERSPRING] = {54, 61}; + zone2LevelBracket[AREA_SILITHUS] = {54, 63}; - // The Burning Crusade - Zones - zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula - zone2LevelBracket[3518] = {64, 70}; // Nagrand - zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest - zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley - zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh - zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains - zone2LevelBracket[3523] = {67, 73}; // Netherstorm - zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas + // The Burning Crusade zones + zone2LevelBracket[AREA_HELLFIRE_PENINSULA] = {58, 66}; + zone2LevelBracket[AREA_NAGRAND] = {64, 70}; + zone2LevelBracket[AREA_TEROKKAR_FOREST] = {62, 73}; + zone2LevelBracket[AREA_SHADOWMOON_VALLEY] = {66, 73}; + zone2LevelBracket[AREA_ZANGARMARSH] = {60, 67}; + zone2LevelBracket[AREA_BLADES_EDGE_MOUNTAINS] = {64, 73}; + zone2LevelBracket[AREA_NETHERSTORM] = {67, 73}; + zone2LevelBracket[AREA_ISLE_OF_QUEL_DANAS] = {68, 73}; - // Wrath of the Lich King - Zones - zone2LevelBracket[65] = {71, 77}; // Dragonblight - zone2LevelBracket[66] = {74, 80}; // Zul'Drak - zone2LevelBracket[67] = {77, 80}; // Storm Peaks - zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier - zone2LevelBracket[394] = {72, 78}; // Grizzly Hills - zone2LevelBracket[495] = {68, 74}; // Howling Fjord - zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest - zone2LevelBracket[3537] = {68, 75}; // Borean Tundra - zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin - zone2LevelBracket[4197] = {79, 80}; // Wintergrasp + // Wrath of the Lich King zones + zone2LevelBracket[AREA_DRAGONBLIGHT] = {71, 77}; + zone2LevelBracket[AREA_ZUL_DRAK] = {74, 80}; + zone2LevelBracket[AREA_THE_STORM_PEAKS] = {77, 80}; + zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier (no AREA_ define) + zone2LevelBracket[AREA_GRIZZLY_HILLS] = {72, 78}; + zone2LevelBracket[AREA_HOWLING_FJORD] = {68, 74}; + zone2LevelBracket[AREA_CRYSTALSONG_FOREST] = {77, 80}; + zone2LevelBracket[AREA_BOREAN_TUNDRA] = {68, 75}; + zone2LevelBracket[AREA_SHOLAZAR_BASIN] = {75, 80}; + zone2LevelBracket[AREA_WINTERGRASP] = {79, 80}; // Override with values from config for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) @@ -4604,6 +4595,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 @@ -4650,16 +4642,18 @@ void TravelMgr::PrepareDestinationCache() (creatureTemplate->unit_flags & 4096) == 0 && creatureTemplate->rank == 0) { - uint32 roundX = (x / 50.0f) * 10.0f; - uint32 roundY = (y / 50.0f) * 10.0f; - uint32 roundZ = (z / 50.0f) * 10.0f; + uint32 roundX = static_cast(std::round(x / 50.0f)); + uint32 roundY = static_cast(std::round(y / 50.0f)); + uint32 roundZ = static_cast(std::round(z / 50.0f)); tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData); tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z)); } // FLIGHT MASTERS + // Entry 29480 is Grimwing (Storm Peaks) — has FLIGHTMASTER flag but + // isn't a real usable flight master; skip it. else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && - creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) + creatureTemplate->Entry != 29480) { FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); bool forHorde = !(factionEntry->hostileMask & 4); @@ -4669,23 +4663,39 @@ void TravelMgr::PrepareDestinationCache() { WorldPosition pos(mapId, x, y, z, orient); if (forHorde) - hordeFlightMasterCache[guid] = pos; + { + FlightMasterInfo info; + info.pos = pos; + info.zoneId = areaId; + info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_HORDE); + info.templateEntry = templateEntry; + info.dbGuid = guid; + hordeFlightMasterCache[guid] = info; + } if (forAlliance) - allianceFlightMasterCache[guid] = pos; + { + FlightMasterInfo info; + info.pos = pos; + info.zoneId = areaId; + info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_ALLIANCE); + info.templateEntry = templateEntry; + info.dbGuid = guid; + allianceFlightMasterCache[guid] = info; + } flightMastersCount++; // Zones that have flight masters but no innkeepers — use flight master as hub static const std::set zonesWithoutInnkeeper = { - 4, // Blasted Lands (52-57) - 16, // Azshara (45-52) - 28, // Western Plaguelands (50-60) - 46, // Burning Steppes (51-60) - 51, // Searing Gorge (45-51) + AREA_BLASTED_LANDS, + AREA_AZSHARA, + AREA_WESTERN_PLAGUELANDS, + AREA_BURNING_STEPPES, + AREA_SEARING_GORGE, 361, // Felwood (47-57) 490, // Un'Goro Crater (49-56) - 2817, // Crystalsong Forest (77-80) - 4197 // Wintergrasp (79-80) + AREA_CRYSTALSONG_FOREST, + AREA_WINTERGRASP }; if (zonesWithoutInnkeeper.count(areaId)) { @@ -4728,7 +4738,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,22 +4761,63 @@ 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 for (auto const& [gridTuple, creatureDataList] : tempLocsCache) { - if (creatureDataList.size() > 2) + if (creatureDataList.size() >= 2) { 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 +4863,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..41cc4ecf8 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_TRAVELMGR_H #include +#include #include #include @@ -268,12 +269,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; } @@ -297,10 +292,26 @@ public: std::vector getPathTo(WorldPosition endPos, Unit* bot) { return endPos.getPathFrom(*this, bot); } - bool isPathTo(std::vector path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance) + // Cmangos-aligned (WorldPosition.h:317): the path "reaches" this + // position when its last point is on the same map, within + // maxDistance horizontally, and within maxZDistance vertically. + // 3D Euclidean distance would falsely accept paths that end the + // right horizontal distance from us but on a roof/floor below. + // maxDistance == 0 falls back to targetPosRecalcDistance (0.1y). + bool isPathTo(std::vector const& path, float const maxDistance = 0.0f, + float const maxZDistance = 2.0f) const { - return !path.empty() && distance(path.back()) < maxDistance; - }; + if (path.empty()) + return false; + WorldPosition const& back = path.back(); + if (back.GetMapId() != GetMapId()) + return false; + float const realMax = maxDistance > 0.0f ? maxDistance + : sPlayerbotAIConfig.targetPosRecalcDistance; + if (GetExactDist2dSq(&back) >= realMax * realMax) + return false; + return std::fabs(back.GetPositionZ() - GetPositionZ()) < maxZDistance; + } bool cropPathTo(std::vector& path, float maxDistance = sPlayerbotAIConfig.targetPosRecalcDistance); bool canPathTo(WorldPosition endPos, Unit* bot) { return endPos.isPathTo(getPathTo(endPos, bot)); } @@ -507,9 +518,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 +690,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 +863,21 @@ protected: class TravelMgr { public: + struct NpcLocation + { + WorldLocation loc; + uint32 entry; + }; + + struct FlightMasterInfo + { + WorldPosition pos; + uint32 zoneId; // resolved once at cache load + uint32 taxiNodeId; // DBC taxi node nearest to this flight master + uint32 templateEntry; // creature template ID (for ObjectGuid construction) + uint32 dbGuid; // DB spawn GUID (for ObjectGuid construction) + }; + static TravelMgr& instance() { static TravelMgr instance; @@ -858,12 +890,14 @@ public: // Navigation void Init(); - Creature* GetNearestFlightMaster(Player* bot); - ObjectGuid GetNearestFlightMasterGuid(Player* bot); + + FlightMasterInfo const* GetNearestFlightMasterInfo(Player* bot) const; std::vector> GetOptimalFlightDestinations(Player* bot); const std::vector GetTeleportLocations(Player* bot); const std::vector GetTravelHubs(Player* bot); std::vector GetCityLocations(Player* bot); + std::vector GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const; + bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer); const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } template @@ -968,18 +1002,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 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..77b3af5ae 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" @@ -112,25 +115,25 @@ float TravelNodePath::getCost(Player* bot, uint32 cGold) if (getPathType() == TravelNodePathType::flightPath && pathObject) { if (!bot->IsAlive()) - return -1; + return -1.0f; TaxiPathEntry const* taxiPath = sTaxiPathStore.LookupEntry(pathObject); if (!taxiPath) - return -1; + return -1.0f; if (!bot->isTaxiCheater() && taxiPath->price > cGold) - return -1; + return -1.0f; if (!bot->isTaxiCheater() && !bot->m_taxi.IsTaximaskNodeKnown(taxiPath->to)) - return -1; + return -1.0f; TaxiNodesEntry const* startTaxiNode = sTaxiNodesStore.LookupEntry(taxiPath->from); TaxiNodesEntry const* endTaxiNode = sTaxiNodesStore.LookupEntry(taxiPath->to); if (!startTaxiNode || !endTaxiNode || !startTaxiNode->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0] || !endTaxiNode->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0]) - return -1; + return -1.0f; } speed = bot->GetSpeed(MOVE_RUN); @@ -158,9 +161,19 @@ 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.0f; + + 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) - return -1; + else if (getPathType() == TravelNodePathType::flightPath || getPathType() == TravelNodePathType::flyingMount) + return -1.0f; if (getPathType() != TravelNodePathType::walk) timeCost = extraCost * modifier; @@ -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; @@ -498,7 +511,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 +559,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 +643,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 +655,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 +680,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 +691,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 +710,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 +762,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 +778,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 +807,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 +889,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 +953,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 +962,7 @@ void TravelNodeMap::removeNode(TravelNode* node) { node->removeLinkTo(nullptr, true); - for (auto& tnode : m_nodes) + for (auto& tnode : nodes) { if (tnode == node) { @@ -1099,7 +971,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 +987,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 +998,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 +1039,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 +1075,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 +1130,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 +1160,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::FindRouteNearestNodes(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 +1233,52 @@ 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, + 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 || 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()); - } - } + 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); - - 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) @@ -1477,7 +1309,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 +1340,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; @@ -1530,64 +1362,6 @@ TravelNode* TravelNodeMap::addRandomExtNode(TravelNode* startNode) return nullptr; } -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(); -} - void TravelNodeMap::generateNpcNodes() { std::unordered_map> bossMap; @@ -1869,7 +1643,7 @@ void TravelNodeMap::generateWalkPaths() for (auto& startNode : TravelNodeMap::instance().getNodes()) { - nodeMaps[startNode->getMapId()] = true; + nodeMaps[startNode->GetMapId()] = true; } for (auto& map : nodeMaps) @@ -1887,10 +1661,10 @@ void TravelNodeMap::generateWalkPaths() if (startNode->hasCompletePathTo(endNode)) continue; - if (startNode->getMapId() != endNode->getMapId()) + if (startNode->GetMapId() != endNode->GetMapId()) continue; - startNode->buildPath(endNode, nullptr, false); + startNode->BuildPath(endNode, nullptr, false); } startNode->setLinked(true); @@ -1983,7 +1757,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; while (true) { uint32 rem = 0; @@ -1998,7 +1772,6 @@ void TravelNodeMap::removeUselessPaths() break; hasToSave = true; - it++; LOG_INFO("playerbots", "Iteration {}, removed {}", it, rem); @@ -2026,14 +1799,20 @@ void TravelNodeMap::calculatePathCosts() LOG_INFO("playerbots", ">> Calculated pathcost for {} nodes.", TravelNodeMap::instance().getNodes().size()); } -void TravelNodeMap::generatePaths() +void TravelNodeMap::generatePaths(bool fullGen) { LOG_INFO("playerbots", "-Calculating walkable paths"); generateWalkPaths(); - LOG_INFO("playerbots", "-Removing useless nodes"); - removeLowNodes(); - LOG_INFO("playerbots", "-Removing useless paths"); - removeUselessPaths(); + + if (fullGen) + { + LOG_INFO("playerbots", "-Removing useless nodes"); + removeLowNodes(); + + LOG_INFO("playerbots", "-Removing useless paths"); + removeUselessPaths(); + } + LOG_INFO("playerbots", "-Calculating path costs"); calculatePathCosts(); LOG_INFO("playerbots", "-Generating taxi paths"); @@ -2042,22 +1821,37 @@ void TravelNodeMap::generatePaths() void TravelNodeMap::generateAll() { - if (hasToFullGen) - generateNodes(); + generatePaths(false); + hasToSave = true; + saveNodeStore(); - LOG_INFO("playerbots", "-Calculating mapoffset"); + BuildZoneIndex(); + PrecomputeReachability(); +} + +void TravelNodeMap::Init() +{ + InitTaxiGraph(); + + if (!sPlayerbotAIConfig.enableTravelNodes) + return; + + LoadNodeStore(); calcMapOffset(); - LOG_INFO("playerbots", "-Generating maptransfers"); - TravelMgr::instance().loadMapTransfers(); - if (hasToGen || hasToFullGen) { - generatePaths(); + if (hasToFullGen) + generateNodes(); + + generatePaths(hasToFullGen); hasToGen = false; hasToFullGen = false; - hasToSave = true; + saveNodeStore(); } + + BuildZoneIndex(); + PrecomputeReachability(); } void TravelNodeMap::printMap() @@ -2118,7 +1912,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 +1921,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()) @@ -2195,7 +1989,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 +2001,95 @@ 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++; + } + } + // 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 Paths, {} points.", paths, 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"; @@ -2306,12 +2133,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(), @@ -2341,15 +2171,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 +2214,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 +2228,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 +2285,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(); @@ -2467,11 +2300,11 @@ std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode); - if (!startNode || !endNode || startNode->map_id != endNode->map_id) + if (!startNode || !endNode) 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 +2316,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 +2331,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 +2351,7 @@ void TravelNodeMap::ComputeAllPaths() auto path = BuildPath(source, target, parentMap); if (!path.empty()) - taxiPathCache[source][target] = path; + m_taxiPathCache[source][target] = path; } } } @@ -2538,7 +2371,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 +2405,125 @@ 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); + } +} + +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); + } + } +} diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 9e05e2490..7c966762f 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,18 +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 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); + // Picks the nearest start/end nodes for two world positions and runs A* + // over the node graph to return a full route between them. + TravelNodeRoute FindRouteNearestNodes(WorldPosition startPos, + WorldPosition endPos, + std::vector& startPath, + Player* bot = nullptr); void setHasToGen() { hasToGen = true; } @@ -563,15 +668,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 +691,24 @@ 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, 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 +724,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 242febd17..1b5e6c98a 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -651,6 +651,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 210e03ef9..2a133f938 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -371,6 +371,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);