diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 41ca0b6b4..5ddc3500d 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -343,10 +343,6 @@ AiPlayerbot.MaxWaitForMove = 5000 # 2 - MoveSplinePath disabled everywhere AiPlayerbot.DisableMoveSplinePath = 0 -# Max search time for movement (higher for better movement on slopes) -# Default: 3 -AiPlayerbot.MaxMovementSearchTime = 3 - # Action expiration time AiPlayerbot.ExpireActionTime = 5000 @@ -1054,6 +1050,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/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..456ab96f3 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include "Corpse.h" +#include "DBCStores.h" #include "Event.h" #include "FleeManager.h" #include "G3D/Vector3.h" @@ -18,8 +20,11 @@ #include "LastMovementValue.h" #include "LootObjectStack.h" #include "Map.h" +#include "ModelIgnoreFlags.h" #include "MotionMaster.h" +#include "MoveSpline.h" #include "MoveSplineInitArgs.h" +#include "TravelNode.h" #include "MovementGenerator.h" #include "ObjectDefines.h" #include "ObjectGuid.h" @@ -36,6 +41,7 @@ #include "SpellInfo.h" #include "Stances.h" #include "Timer.h" +#include "Transport.h" #include "Unit.h" #include "Vehicle.h" #include "WaypointMovementGenerator.h" @@ -45,6 +51,146 @@ MovementAction::MovementAction(PlayerbotAI* botAI, std::string const name) : Act bot = botAI->GetBot(); } +void MovementAction::EmitDebugMove(char const* method, char const* generator, float x, float y, float z, char const* extra) +{ + if (!botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) + return; + + auto resolveName = [&](ObjectGuid guid) -> std::string + { + if (!guid) + return ""; + if (WorldObject* obj = botAI->GetWorldObject(guid)) + return obj->GetName(); + return ""; + }; + + NewRpgInfo& info = botAI->rpgInfo; + NewRpgStatus status = info.GetStatus(); + char const* statusName = + status == RPG_IDLE ? "idle" : + status == RPG_GO_GRIND ? "go-grind" : + status == RPG_GO_CAMP ? "go-camp" : + status == RPG_WANDER_NPC ? "wander-npc" : + status == RPG_WANDER_RANDOM ? "wander-random" : + status == RPG_REST ? "rest" : + status == RPG_DO_QUEST ? "do-quest" : + status == RPG_TRAVEL_FLIGHT ? "travel-flight" : + status == RPG_OUTDOOR_PVP ? "outdoor-pvp" : "?"; + + // Resolve a human-readable target name from the RPG context. When + // we can name the target (quest objective, wander NPC, flight + // master, travel-node hop, etc.), it replaces the loc=(x,y,z) + // field — names are far more useful than coordinates. When no + // target can be named (combat moves, follow, flee, ad-hoc), we + // fall through to loc=(x,y,z). + std::string targetName; + switch (status) + { + case RPG_DO_QUEST: + if (auto* data = std::get_if(&info.data)) + { + if (data->quest) + { + bool turnIn = data->questId && + bot->GetQuestStatus(data->questId) == QUEST_STATUS_COMPLETE; + if (turnIn) + { + std::ostringstream t; + t << "turn-in:" << data->quest->GetTitle() << "(" << data->questId << ")"; + targetName = t.str(); + } + else + { + Quest const* q = data->quest; + QuestStatusData const& qs = bot->getQuestStatusMap().at(data->questId); + std::string goal; + for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i) + { + int32 entry = q->RequiredNpcOrGo[i]; + if (entry != 0 && qs.CreatureOrGOCount[i] < q->RequiredNpcOrGoCount[i]) + { + if (entry > 0) + { + if (CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(entry)) + goal = "mob:" + ct->Name; + } + else + { + if (GameObjectTemplate const* gt = sObjectMgr->GetGameObjectTemplate(-entry)) + goal = "go:" + gt->name; + } + break; + } + uint32 item = q->RequiredItemId[i]; + if (item && bot->GetItemCount(item, true) < q->RequiredItemCount[i]) + { + if (ItemTemplate const* it = sObjectMgr->GetItemTemplate(item)) + goal = "item:" + it->Name1; + break; + } + } + if (goal.empty()) + { + std::ostringstream t; + t << "quest:" << q->GetTitle() << "(" << data->questId << ")"; + goal = t.str(); + } + targetName = goal; + } + } + } + break; + case RPG_WANDER_NPC: + if (auto* data = std::get_if(&info.data)) + { + std::string n = resolveName(data->npcOrGo); + if (!n.empty()) + targetName = "npc:" + n; + } + break; + case RPG_TRAVEL_FLIGHT: + if (auto* data = std::get_if(&info.data)) + { + if (CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(data->flightMasterEntry)) + targetName = "flightmaster:" + ct->Name; + } + break; + case RPG_GO_GRIND: targetName = "grind-pos"; break; + case RPG_GO_CAMP: targetName = "camp-pos"; break; + case RPG_WANDER_RANDOM: targetName = "wander-random"; break; + default: break; + } + + // Travel-plan override: when actively routing through the node + // graph, prefer the next-hop node name over any RPG-level target. + if (info.HasActiveTravelPlan()) + { + TravelPlan const& plan = info.travelPlan; + if (plan.stepIdx < plan.steps.GetPathRef().size()) + { + PathNodePoint const& pnt = plan.steps.GetPathRef()[plan.stepIdx]; + if (pnt.type == PathNodeType::NODE_NODE || pnt.type == PathNodeType::NODE_PATH) + { + if (TravelNode* n = sTravelNodeMap.getNode(pnt.point, nullptr, 5.0f)) + targetName = "node:" + n->getName(); + } + } + } + + float dis = bot->GetExactDist(x, y, z); + std::ostringstream out; + out << "[MOVE] meth=" << method + << " | via=" << (generator && *generator ? generator : "-") + << " | rpg=" << statusName + << " | d=" << dis << "y" + << " | targ=" << (targetName.empty() ? "-" : targetName.c_str()); + if (extra && *extra) + out << " | " << extra; + botAI->TellMasterNoFacing(out); +} + + void MovementAction::CreateWp(Player* wpOwner, float x, float y, float z, float o, uint32 entry, bool important) { float dist = wpOwner->GetDistance(x, y, z); @@ -83,6 +229,7 @@ bool MovementAction::JumpTo(uint32 mapId, float x, float y, float z, MovementPri bool MovementAction::MoveNear(uint32 mapId, float x, float y, float z, float distance, MovementPriority priority) { float angle = GetFollowAngle(); + EmitDebugMove("MoveNear", "mmap", x, y, z); return MoveTo(mapId, x + cos(angle) * distance, y + sin(angle) * distance, z, false, false, false, false, priority); } @@ -126,12 +273,11 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged) float x = target->GetPositionX(); float y = target->GetPositionY(); float z = target->GetPositionZ(); + EmitDebugMove("MoveToLOS", "mmap", x, y, z); // 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 +286,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); @@ -239,32 +385,29 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, } else { - float modifiedZ; - Movement::PointsArray path = - SearchForBestPath(x, y, z, modifiedZ, sPlayerbotAIConfig.maxMovementSearchTime, normal_only); - if (modifiedZ == INVALID_HEIGHT) - return false; - float distance = bot->GetExactDist(x, y, modifiedZ); + // Direct dispatch — engine MovePoint(generatePath=true) handles + // path-finding internally. Previously called SearchForBestPath + // here to probe ±step around the target z; that helped find + // polygons when the input z was several yards off the navmesh, + // but its "shortest path" preference would shift modifiedZ to + // an unreachable nearby polygon (upper terrace, ledge above) + // and then the engine's straight-spline NOPATH fallback would + // air-walk the bot up to it. cmangos doesn't have an + // equivalent — single-z PathFinder call is sufficient. + float distance = bot->GetExactDist(x, y, z); if (distance > 0.01f) { if (bot->IsSitState()) bot->SetStandState(UNIT_STAND_STATE_STAND); - // if (bot->IsNonMeleeSpellCast(true)) - // { - // bot->CastStop(); - // botAI->InterruptSpell(); - // } - DoMovePoint(bot, x, y, modifiedZ, generatePath, backwards); + DoMovePoint(bot, x, y, z, generatePath, backwards); float delay = 1000.0f * MoveDelay(distance, backwards); if (lessDelay) - { delay -= botAI->GetReactDelay(); - } delay = std::max(.0f, delay); delay = std::min((float)sPlayerbotAIConfig.maxWaitForMove, delay); AI_VALUE(LastMovement&, "last movement") - .Set(mapId, x, y, modifiedZ, bot->GetOrientation(), delay, priority); + .Set(mapId, x, y, z, bot->GetOrientation(), delay, priority); return true; } } @@ -851,8 +994,15 @@ bool MovementAction::ReachCombatTo(Unit* target, float distance) path.ShortenPathUntilDist(G3D::Vector3(tx, ty, tz), shortenTo); G3D::Vector3 endPos = path.GetPath().back(); - return MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true); + bool moved = MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true); + // Only emit on a successful new commit — combat ticks call this + // many times per second and MoveTo internally suppresses while a + // prior spline is still playing. Emitting before the suppression + // check produces per-tick whisper spam. + if (moved) + EmitDebugMove("ReachCombatTo", "mmap", endPos.x, endPos.y, endPos.z); + return moved; } float MovementAction::GetFollowAngle() @@ -1184,6 +1334,8 @@ bool MovementAction::Follow(Unit* target, float distance, float angle) if (ServerFacade::instance().IsDistanceGreaterOrEqualThan(ServerFacade::instance().GetDistance2d(bot, target), sPlayerbotAIConfig.sightDistance)) { + EmitDebugMove("Follow", "mmap", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ()); + if (target->GetGUID().IsPlayer()) { Player* pTarget = (Player*)target; @@ -1269,6 +1421,7 @@ bool MovementAction::Follow(Unit* target, float distance, float angle) if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FOLLOW_MOTION_TYPE) bot->GetMotionMaster()->Clear(); + EmitDebugMove("Follow", "follow", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ()); bot->GetMotionMaster()->MoveFollow(target, distance, angle); return true; } @@ -1280,6 +1433,9 @@ bool MovementAction::ChaseTo(WorldObject* obj, float distance) return false; } + if (obj) + EmitDebugMove("ChaseTo", "chase", obj->GetPositionX(), obj->GetPositionY(), obj->GetPositionZ()); + if (Vehicle* vehicle = bot->GetVehicle()) { VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot); @@ -1368,6 +1524,8 @@ bool MovementAction::Flee(Unit* target) if (!sPlayerbotAIConfig.fleeingEnabled) return false; + EmitDebugMove("Flee", "flee", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ()); + if (!IsMovingAllowed()) { botAI->TellError("I am stuck while fleeing"); @@ -1547,6 +1705,7 @@ bool MovementAction::MoveAway(Unit* target, float distance, bool backwards) { return false; } + EmitDebugMove("MoveAway", "mmap", target->GetPositionX(), target->GetPositionY(), target->GetPositionZ()); float init_angle = target->GetAngle(bot); for (float delta = 0; delta <= M_PI / 2; delta += M_PI / 8) { @@ -1626,6 +1785,7 @@ bool MovementAction::MoveFromGroup(float distance) y /= count; // x and y are now average position of the group members float angle = bot->GetAngle(x, y) + M_PI; + EmitDebugMove("MoveFromGroup", "mmap", x, y, bot->GetPositionZ()); return Move(angle, distance - closestDist); } } @@ -1654,6 +1814,7 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d { return false; } + EmitDebugMove("MoveInside", "mmap", x, y, z); return MoveNear(mapId, x, y, z, distance, priority); } @@ -1719,72 +1880,16 @@ bool MovementAction::MoveInside(uint32 mapId, float x, float y, float z, float d // return current_z; // } -const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y, float z, float& modified_z, - int maxSearchCount, bool normal_only, float step) +PathResult MovementAction::GeneratePath(float x, float y, float z, uint32 acceptMask, bool forceDestination) { - bool found = false; - modified_z = INVALID_HEIGHT; - float tempZ = bot->GetMapHeight(x, y, z); + PathResult result; PathGenerator gen(bot); - gen.CalculatePath(x, y, tempZ); - Movement::PointsArray result = gen.GetPath(); - float min_length = gen.getPathLength(); - int typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE; - if ((gen.GetPathType() & typeOk) && abs(tempZ - z) < 0.5f) - { - modified_z = tempZ; - return result; - } - // Start searching - if (gen.GetPathType() & typeOk) - { - modified_z = tempZ; - found = true; - } - int count = 1; - for (float delta = step; count < maxSearchCount / 2 + 1; count++, delta += step) - { - tempZ = bot->GetMapHeight(x, y, z + delta); - if (tempZ == INVALID_HEIGHT) - { - continue; - } - PathGenerator gen(bot); - gen.CalculatePath(x, y, tempZ); - if ((gen.GetPathType() & typeOk) && gen.getPathLength() < min_length) - { - found = true; - min_length = gen.getPathLength(); - result = gen.GetPath(); - modified_z = tempZ; - } - } - for (float delta = -step; count < maxSearchCount; count++, delta -= step) - { - tempZ = bot->GetMapHeight(x, y, z + delta); - if (tempZ == INVALID_HEIGHT) - { - continue; - } - PathGenerator gen(bot); - gen.CalculatePath(x, y, tempZ); - if ((gen.GetPathType() & typeOk) && gen.getPathLength() < min_length) - { - found = true; - min_length = gen.getPathLength(); - result = gen.GetPath(); - modified_z = tempZ; - } - } - if (!found && normal_only) - { - modified_z = INVALID_HEIGHT; - return Movement::PointsArray{}; - } - if (!found && !normal_only) - { - return result; - } + 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; } @@ -2229,6 +2334,7 @@ bool MovementAction::FleePosition(Position pos, float radius, uint32 minInterval } if (bestPos != Position()) { + EmitDebugMove("FleePosition", "mmap", bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ()); if (MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), false, false, true, false, MovementPriority::MOVEMENT_COMBAT)) { @@ -2959,4 +3065,727 @@ 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; + } + + // Re-clamp cached waypoints to current valid Z. Rows in + // playerbots_travelnode_path store absolute coords baked at + // offline generation; if the live navmesh has shifted since + // (mmap regen, terrain change, vmap update), the stored z can + // be above ground — MoveSplinePath plays back coords verbatim + // and the bot looks like it's walking through the air. + // UpdateAllowedPositionZ factors mmap polygon Z, water surface, + // swimming, flying and transport state, so cave floors above + // the terrain plane snap correctly. + for (auto& pt : state.walkPoints) + bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); + + // 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; + + G3D::Vector3 const& last = state.walkPoints.back(); + + // Update LastMovement so MoveFarTo's spline-active early-out + // knows about this in-flight walk and won't recompute the path + // mid-spline. Mirror what MoveTo does after dispatching a spline. + { + float delay = static_cast(state.expectedDuration); + delay = std::min(delay, static_cast(sPlayerbotAIConfig.maxWaitForMove)); + delay = std::max(delay, 0.f); + LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, + bot->GetOrientation(), delay, MovementPriority::MOVEMENT_NORMAL); + + // Cache the dispatched waypoint chain so MoveFarTo's 10% + // lastPath reuse and "no worse" reuse can pick it up next tick. + std::vector wpts; + wpts.reserve(state.walkPoints.size()); + for (auto const& pt : state.walkPoints) + wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z); + lastMove.setPath(TravelPath(wpts)); + } + + EmitDebugMove("TravelPlan:walk-start", "mmap", last.x, last.y, last.z); + + return false; // Walking +} + +bool MovementAction::RefineWalkPoints(std::vector& walkPoints) +{ + if (walkPoints.size() < 2) + return true; + + std::vector refined; + refined.reserve(walkPoints.size() * 4); + + uint32 const mapId = bot->GetMapId(); + + for (size_t i = 0; i + 1 < walkPoints.size(); ++i) + { + G3D::Vector3 const& a = walkPoints[i]; + G3D::Vector3 const& b = walkPoints[i + 1]; + + WorldPosition aPos(mapId, a.x, a.y, a.z); + WorldPosition bPos(mapId, b.x, b.y, b.z); + + // Per-segment mmap query against the live navmesh. The + // travel-node graph stores offline-baked waypoints; if the + // straight line A->B crosses geometry the live navmesh has + // (mountain, ledge, model edit since offline gen), this + // returns either an mmap-routed path around it (NORMAL/ + // INCOMPLETE) or empty (NOT_USING_PATH was rejected as + // "would walk through walls"). + std::vector segPath = bPos.getPathStepFrom(aPos, bot); + + if (segPath.empty()) + { + // Live mmap refuses A->B. Caller should abort the plan + // and let MoveFarTo's own probe re-derive a route. + return false; + } + + // Reject "pathfinder cheating" — same checks the offline gen + // applies to BuildPath. Catches cached segments where the + // live navmesh still produces a near-vertical hop or a + // 2-point straight line through geometry. + if (TravelPath::IsPathCheating(segPath, aPos.distance(bPos))) + { + return false; + } + + // First segment: include its start point so the spline + // begins from the original A. Later segments: skip the first + // point — it duplicates the previous segment's tail. + size_t startK = (i == 0) ? 0 : 1; + for (size_t k = startK; k < segPath.size(); ++k) + refined.emplace_back(segPath[k].GetPositionX(), + segPath[k].GetPositionY(), + segPath[k].GetPositionZ()); + } + + walkPoints = std::move(refined); + return true; +} + +bool MovementAction::MoveToSpline(TravelPlan& state, WorldPosition target) +{ + if (!IsMovingAllowed()) + return false; + + EmitDebugMove("TravelPlan:walk-waypoint", "mmap", target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); + + // Generate path + state.walkPoints.clear(); + PathResult path = GeneratePath(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); + // Reject paths that PathGenerator marked unreachable. The default + // accept mask is NORMAL | INCOMPLETE; anything else (NOT_USING_PATH + // from BuildShortcut on invalid polys, NOPATH, etc.) means the + // dispatched waypoints would either be a straight-line through + // geometry or stop short of the target. Abort the plan instead so + // MoveFarTo can re-derive via its own probe. + if (!path.reachable) + { + state.walkPoints.clear(); + return false; + } + 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; + + // Per-step labels (`walk`, `segment`, `flight`, `transport-*`, + // `teleport(reason)`) cover every actual movement decision; emitting + // an executor-ran-this-tick label here would whisper every tick + // while the plan is active. + + // 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; + } + + // Validate the path before MoveTo. PathGenerator can + // return NORMAL | NOT_USING_PATH when start or end poly + // is invalid (BuildShortcut → 2-point straight line). + // PointMovementGenerator would then dispatch the bot + // straight through any geometry between bot and target. + // The default accept mask (NORMAL | INCOMPLETE) rejects + // NOT_USING_PATH, so abort the plan and let MoveFarTo + // re-derive instead of walking a known-bad shortcut. + PathResult validate = GeneratePath( + target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), + DEFAULT_PATH_ACCEPT_MASK, false); + if (!validate.reachable) + { + EmitDebugMove("TravelPlan", "prepath-unreachable", + target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); + state.Reset(); + return false; + } + + 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 at + // 20 points per dispatch as a cheap upper bound on per-tick + // work; stepIdx advances exactly in step with what's + // dispatched, so the next tick picks up from the cutoff. + 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. 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; + } + + // Re-validate each consecutive (A, B) pair against the + // live navmesh. The graph's offline-baked coords can + // produce a chain whose straight-line interpolation + // passes through geometry (mountains, ledges, model + // edits). RefineWalkPoints substitutes mmap-routed + // sub-paths between each pair; if any segment is + // unwalkable, abort the plan so MoveFarTo's own probe + // can re-derive a route. + if (!RefineWalkPoints(state.walkPoints)) + { + G3D::Vector3 const& failPt = state.walkPoints.empty() + ? G3D::Vector3(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()) + : state.walkPoints.front(); + EmitDebugMove("TravelPlan", "segment-unwalkable", + failPt.x, failPt.y, failPt.z); + state.walkPoints.clear(); + state.Reset(); + return false; + } + + 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. + 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. The graph + // generator produces these 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(); + EmitDebugMove("TravelPlan:transport-board", "teleport", transport->GetPositionX(), + transport->GetPositionY(), transport->GetPositionZ()); + 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); + EmitDebugMove("TravelPlan:transport-walk", "spline", destX, destY, destZ); + } + + return false; +} diff --git a/src/Ai/Base/Actions/MovementActions.h b/src/Ai/Base/Actions/MovementActions.h index b1c566211..c041d1cc7 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,12 +23,33 @@ 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: MovementAction(PlayerbotAI* botAI, std::string const name); protected: + // Emit a one-line trace describing the imminent movement. No-op + // unless the bot has the "debug move" non-combat strategy. + // Subclasses (e.g. NewRpgBaseAction) may override to append richer + // context such as RPG status and target name. Optional `extra` + // is appended verbatim (use it to attach hop labels like + // "node:Stormwind innkeeper" or fallback reasons). + virtual void EmitDebugMove(char const* method, char const* generator, float x, float y, float z, char const* extra = nullptr); + bool JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); bool MoveNear(uint32 mapId, float x, float y, float z, float distance = sPlayerbotAIConfig.contactDistance, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); @@ -66,6 +88,31 @@ 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 = false); + + 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); + // Per-segment mmap refinement of a travel-node-graph walk batch. + // The graph stores offline-baked coords whose straight-line + // interpolation may pass through geometry the bot can't actually + // traverse. Returns false if any segment is unwalkable per the + // live navmesh, in which case the caller should abort the plan. + bool RefineWalkPoints(std::vector& walkPoints); + protected: struct CheckAngle { @@ -74,10 +121,6 @@ protected: }; private: - // float SearchBestGroundZForPath(float x, float y, float z, bool generatePath, float range = 20.0f, bool - // normal_only = false, float step = 8.0f); - const Movement::PointsArray SearchForBestPath(float x, float y, float z, float& modified_z, int maxSearchCount = 5, - bool normal_only = false, float step = 8.0f); bool wasMovementRestricted = false; void DoMovePoint(Unit* unit, float x, float y, float z, bool generatePath, bool backwards); }; diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 5937f0ef6..4de890b64 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -763,6 +763,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()) @@ -805,7 +820,7 @@ void PlayerbotAI::HandleTeleportAck() bot->StopMoving(); } - // simulate far teleport latency (cmangos-style) + // simulate far teleport latency SetNextCheckDelay(urand(2000, 5000)); return; } diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index 01924c46f..7837c5a53 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 3c0a9054c..1e919efa3 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 7192c2c26..2a01141d6 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -681,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; @@ -780,36 +693,69 @@ 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())); + } + + // Explicit-start overload (PathGenerator.h:67). Without this, + // CalculatePath(destX,destY,destZ) defaults to the unit's + // current position as start — which means every iteration of + // getPathFromPath's "chain" begins from the bot's same real + // location and produces the same ~296y partial path. The chain + // never advances. With explicit start, each step extends from + // the previous step's endpoint, giving the 40-attempt walker + // its intended multi-tile reach. + PathGenerator path(pathUnit); + path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(), + GetPositionX(), GetPositionY(), GetPositionZ(), false); 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 (tempCreature) + delete tempCreature; - 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 (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL) + // PathType is a bitmask. Two things to handle: + // + // 1. AC's PathGenerator can return INCOMPLETE | FARFROMPOLY_END + // (0x84) etc. — strict `== PATHFIND_INCOMPLETE` would reject + // these perfectly usable partial paths. Use bitwise to accept + // NORMAL/INCOMPLETE plus auxiliary flags. + // + // 2. AC's PathGenerator at PathGenerator.cpp:177-188 returns + // NORMAL | NOT_USING_PATH for player units when start or end + // polygon is INVALID_POLYREF (BuildShortcut → 2-point straight + // line through whatever's in the way). cmangos by contrast + // returns NOPATH for the same case (PathFinder.cpp:437-441). + // To match cmangos's intent (never silently dispatch a + // geometry-ignoring shortcut), reject any path with the + // NOT_USING_PATH bit set. + if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) + && !(type & PATHFIND_NOT_USING_PATH)) return fromPointsArray(points); return {}; @@ -1073,6 +1019,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) @@ -2379,9 +2333,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; @@ -2772,7 +2724,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); @@ -2896,7 +2848,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); @@ -4359,8 +4311,7 @@ void TravelMgr::Init() PrepareZone2LevelBracket(); PrepareDestinationCache(); } - sTravelNodeMap.InitTaxiGraph(); - LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built."); + sTravelNodeMap.Init(); } TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const @@ -4407,7 +4358,7 @@ std::vector> TravelMgr::GetOptimalFlightDestinations(Player* std::vector> validDestinations; FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot); - if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f) + if (!nearestFlightMaster) return validDestinations; uint32 fromNode = nearestFlightMaster->taxiNodeId; @@ -4426,9 +4377,9 @@ std::vector> TravelMgr::GetOptimalFlightDestinations(Player* if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId())) botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0; - //Simplify destination delection. Its either target cities (Based on config value) or target world. std::vector candidateZones; - if (botLevel >= 10 && !botInCapital && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + if (botLevel >= 10 && !botInCapital && + urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) { TeamId botTeam = bot->GetTeamId(); for (Capital const& capital : capitals) @@ -4555,6 +4506,34 @@ std::vector TravelMgr::GetCityLocations(Player* bot) return fallbackLocations; } +bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer) +{ + uint16 botMapId = bot->GetMapId(); + auto const& cache = (bot->GetTeamId() == TEAM_HORDE) ? hordeAuctioneerCache : allianceAuctioneerCache; + + auto mapIt = cache.find(botMapId); + if (mapIt == cache.end() || mapIt->second.empty()) + return false; + + // Collect all areas on this map that have auctioneers + std::vector areaIds; + areaIds.reserve(mapIt->second.size()); + for (auto const& [areaId, npcs] : mapIt->second) + { + if (!npcs.empty()) + areaIds.push_back(areaId); + } + + if (areaIds.empty()) + return false; + + // Pick a random area, then a random auctioneer in that area + uint32 selectedArea = areaIds[urand(0, areaIds.size() - 1)]; + auto const& auctioneers = mapIt->second.at(selectedArea); + outAuctioneer = auctioneers[urand(0, auctioneers.size() - 1)]; + return true; +} + void TravelMgr::PrepareZone2LevelBracket() { // Classic WoW - starter zones @@ -4641,6 +4620,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 @@ -4687,18 +4667,18 @@ void TravelMgr::PrepareDestinationCache() (creatureTemplate->unit_flags & 4096) == 0 && creatureTemplate->rank == 0) { - int32 roundX = static_cast(std::lround(x / 50.0f)); - int32 roundY = static_cast(std::lround(y / 50.0f)); - int32 roundZ = static_cast(std::lround(z / 50.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) - // Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one. + // 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); @@ -4783,7 +4763,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; @@ -4806,6 +4786,31 @@ void TravelMgr::PrepareDestinationCache() } bankerCount++; } + // === AUCTIONEERS === + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_AUCTIONEER) + { + FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); + if (!factionEntry) + continue; + + bool forHorde = !(factionEntry->hostileMask & 4); + bool forAlliance = !(factionEntry->hostileMask & 2); + + if (!forHorde && !forAlliance) + continue; + + NpcLocation aLoc; + aLoc.loc = WorldLocation(mapId, x + cos(orient) * 3.0f, y + sin(orient) * 3.0f, z + 0.5f, orient + M_PI); + aLoc.entry = templateEntry; + + if (forHorde) + hordeAuctioneerCache[mapId][areaId].push_back(aLoc); + + if (forAlliance) + allianceAuctioneerCache[mapId][areaId].push_back(aLoc); + + auctioneerCount++; + } } // Process temporary caches @@ -4815,16 +4820,29 @@ void TravelMgr::PrepareDestinationCache() { CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + + float totalX = 0.0f; + float totalY = 0.0f; + float totalZ = 0.0f; + for (CreatureData const& creatureData : creatureDataList) + { + totalX += creatureData.posX; + totalY += creatureData.posY; + totalZ += creatureData.posZ; + } + + float avgX = totalX / creatureDataList.size(); + float avgY = totalY / creatureDataList.size(); + float avgZ = totalZ / creatureDataList.size(); + uint32 mapId = std::get<0>(gridTuple); + for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) { if (l < 1 || l > maxLevel) continue; - locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple), - static_cast(std::get<1>(gridTuple)) * 50.0f, - static_cast(std::get<2>(gridTuple)) * 50.0f, - static_cast(std::get<3>(gridTuple)) * 50.0f)); + locsPerLevelCache[(uint8)l].push_back(WorldLocation(mapId, avgX, avgY, avgZ, 0.0f)); } } } @@ -4870,5 +4888,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 99c8c8e4c..877b0744b 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) + // 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: @@ -985,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> 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 9d25d4ea7..df77344ac 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. @@ -213,13 +226,21 @@ TravelNodePath* TravelNode::buildPath(TravelNode* endNode, Unit* bot, bool postP bool canPath = endPos->isPathTo(path); // Check if we reached our destination. + // Reject "pathfinder cheating" — too-short or too-steep results + // that mmap accepts but a player can't actually walk. Without this, + // the segment gets cached + saved to playerbots_travelnode_path + // and dispatched at runtime as straight-line spline through whatever + // mountain/cliff sat between A and B (cmangos parity). + if (canPath && TravelPath::IsPathCheating(path, getPosition()->distance(endNode->getPosition()))) + canPath = false; + if (!canPath && endNode->hasLinkTo(this)) // Unable to find a path? See if the reverse is possible. { TravelNodePath backNodePath = *endNode->getPathTo(this); 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 +420,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 +519,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 +567,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 +651,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 +663,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); @@ -673,9 +686,42 @@ void TravelNode::print([[maybe_unused]] bool printFailed) } // Attempts to move ahead of the path. -bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) +bool TravelPath::IsPathCheating(std::vector const& path, float endpointDistance) { - if (getPath().empty()) + if (path.empty()) + return false; + + // Guard 1: 2-point path for >5y is navmesh "gave up" — straight + // line through whatever's between A and B. + if (path.size() == 2 && endpointDistance > 5.0f) + return true; + + // Guard 2: steep slope at start or end suggests the pathfinder + // hopped through a near-vertical step. >10y drop with >2:1 slope + // is too steep to walk. + if (path.size() > 2) + { + WorldPosition const& a = path.front(); + WorldPosition const& b = path[1]; + float vDist = std::fabs(a.GetPositionZ() - b.GetPositionZ()); + float hDist = a.GetExactDist2d(b.GetPositionX(), b.GetPositionY()); + if (vDist > 10.0f && (hDist == 0.0f || vDist / hDist > 2.0f)) + return true; + + WorldPosition const& c = path.back(); + WorldPosition const& d = path[path.size() - 2]; + float vDist2 = std::fabs(c.GetPositionZ() - d.GetPositionZ()); + float hDist2 = c.GetExactDist2d(d.GetPositionX(), d.GetPositionY()); + if (vDist2 > 10.0f && (hDist2 == 0.0f || vDist2 / hDist2 > 2.0f)) + return true; + } + + return false; +} + +bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot) +{ + if (GetPath().empty()) return false; float maxDistSq = maxDist * maxDist; @@ -686,10 +732,10 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) for (auto& p : fullPath) // cycle over the full path { - // if (p.point.getMapId() != startPos.getMapId()) - // continue; - - if (p.point.GetMapId() == startPos.GetMapId()) + // Walkability filter (cmangos parity): portals/transports/taxis + // aren't valid anchor points — picking one as the new start of + // the trimmed path would leave the bot anchored on a hop. + if (p.point.GetMapId() == startPos.GetMapId() && p.isWalkable()) { float curDist = p.point.sqDistance(startPos); @@ -705,7 +751,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; @@ -732,8 +778,10 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) WorldPosition beginPos = newPath.begin()->point; - // The old path seems to be the best. - if (beginPos.distance(firstNode) < sPlayerbotAIConfig.tooCloseDistance) + // The old path seems to be the best — either the closest walkable + // point IS the original front, or it's within tooCloseDistance. + if (newPath.front() == fullPath.front() || + beginPos.distance(firstNode) < sPlayerbotAIConfig.tooCloseDistance) return false; // We are (nearly) on the new path. Just follow the rest. @@ -743,7 +791,11 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist) return true; } - std::vector toPath = startPos.getPathTo(beginPos, nullptr); + // Pass the bot into getPathTo so PathGenerator picks up its + // collision / swimming / flying flags. cmangos parity — passing + // nullptr here drops to a default mover and can produce a path + // the bot itself can't actually walk. + std::vector toPath = startPos.getPathTo(beginPos, bot); // We can not reach the new begin position. Follow the complete path. if (!beginPos.isPathTo(toPath)) @@ -757,152 +809,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 +825,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 +854,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 +936,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 +1000,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 +1009,7 @@ void TravelNodeMap::removeNode(TravelNode* node) { node->removeLinkTo(nullptr, true); - for (auto& tnode : m_nodes) + for (auto& tnode : nodes) { if (tnode == node) { @@ -1099,7 +1018,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 +1034,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 +1045,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 +1086,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 +1122,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 +1177,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 +1207,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 +1280,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 +1356,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 +1387,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 +1409,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 +1690,7 @@ void TravelNodeMap::generateWalkPaths() for (auto& startNode : TravelNodeMap::instance().getNodes()) { - nodeMaps[startNode->getMapId()] = true; + nodeMaps[startNode->GetMapId()] = true; } for (auto& map : nodeMaps) @@ -1887,10 +1708,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 +1804,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 +1819,6 @@ void TravelNodeMap::removeUselessPaths() break; hasToSave = true; - it++; LOG_INFO("playerbots", "Iteration {}, removed {}", it, rem); @@ -2026,14 +1846,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 +1868,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 +1959,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 +1968,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 +2036,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 +2048,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 +2180,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 +2218,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 +2261,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 +2275,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 +2332,7 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId) return WorldPosition(mapId, 0, 0, 0, 0); } -// ============================================================ // TravelNodeMap taxi graph (BFS-based flight path lookup) -// ============================================================ - void TravelNodeMap::InitTaxiGraph() { BuildTaxiGraph(); @@ -2470,8 +2350,8 @@ std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) 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 +2363,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 +2378,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 +2398,7 @@ void TravelNodeMap::ComputeAllPaths() auto path = BuildPath(source, target, parentMap); if (!path.empty()) - taxiPathCache[source][target] = path; + m_taxiPathCache[source][target] = path; } } } @@ -2538,7 +2418,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 +2452,134 @@ 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; +} + +std::vector const& TravelNodeMap::GetNodesInZone(uint32 zoneId) const +{ + static std::vector const empty; + auto it = m_zoneIndex.find(zoneId); + if (it == m_zoneIndex.end()) + return empty; + return it->second; +} + +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..780898a51 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,38 +418,56 @@ 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; + + bool operator==(const PathNodePoint& p1) const + { + return point == p1.point && type == p1.type && entry == p1.entry; + } + // A "walkable" node is one we traverse on foot. Portals/transports/ + // taxis/teleports are entry/exit hops, not points to anchor a + // shortcut on. Used by makeShortCut to skip them when picking the + // closest-point-on-path to the bot. + bool isWalkable() const { return (uint8)type <= (uint8)PathNodeType::NODE_NODE; } }; // A complete list of points the bots has to walk to or teleport to. class TravelPath { public: - TravelPath(){}; - TravelPath(std::vector fullPath1) { fullPath = fullPath1; } - TravelPath(std::vector path, PathNodeType type = 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 +475,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 +489,22 @@ 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); + bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr); + + // Detect "pathfinder cheating" — paths that PathGenerator accepts + // but a player can't actually walk: + // * a 2-point path for an endpoint distance > 5y means navmesh + // gave up and returned the straight A->B line. + // * a vertical drop > 10y combined with a slope steeper than + // 2:1 at either start or end means the pathfinder hopped + // through a near-vertical step the navmesh permits but a + // player wouldn't survive. + // cmangos applies the same two checks in TravelNode::buildPath + // before caching a node-to-node segment. + static bool IsPathCheating(std::vector const& path, + float endpointDistance); std::ostringstream const print(); @@ -438,17 +517,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 +554,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 +606,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 +625,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 +666,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 +691,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 +714,28 @@ public: void InitTaxiGraph(); std::vector FindTaxiPath(uint32 fromNode, uint32 toNode); + void BuildZoneIndex(); + void PrecomputeReachability(); + + TravelNode* GetNearestNodeInZone(WorldPosition pos, uint32 zoneId); + TravelNode* GetNearestNodeOnMap(WorldPosition pos); + + // All nodes registered to a zone (post-BuildZoneIndex). Returns an + // empty static vector for unknown zones. + std::vector const& GetNodesInZone(uint32 zoneId) const; + + 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 +751,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;