From 86f0be5b081d29b4669d02da69c0e46da9a9e283 Mon Sep 17 00:00:00 2001 From: bash Date: Sun, 10 May 2026 18:14:42 +0200 Subject: [PATCH] feat(Core/Travel): Align MoveFarTo and probe pipeline with cmangos --- src/Ai/Base/Actions/MovementActions.cpp | 2 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 339 ++++++++++++------- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 19 +- src/Mgr/Travel/TravelMgr.cpp | 141 +++++--- src/Mgr/Travel/TravelMgr.h | 2 + src/Mgr/Travel/TravelNode.cpp | 21 +- src/Mgr/Travel/TravelNode.h | 2 +- src/PlayerbotAIConfig.cpp | 1 + src/PlayerbotAIConfig.h | 2 +- 9 files changed, 344 insertions(+), 185 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 456ab96f3..6fae3d3f4 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -3308,7 +3308,7 @@ bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination) destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(), destination.GetMapId(), botPos.fDist(destination)); - return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination); + return sTravelNodeMap.GetFullPath(plan, botPos, bot->GetZoneId(), destination, bot); } bool MovementAction::ExecuteTravelPlan(TravelPlan& state) diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index a59c16d1a..d8ba2774a 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -53,21 +53,29 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) return false; + // Already-at-dest short-stop. Below targetPosRecalcDistance the + // move is effectively done — stop any active spline and clear + // the cached path if it pointed here, so we don't keep gliding. + { + float const totalDistance = bot->GetExactDist(dest); + if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance) + { + LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + if (!lastMove.lastPath.empty() && + lastMove.lastPath.getBack().distance(dest) <= totalDistance) + { + lastMove.clear(); + } + bot->StopMoving(); + return false; + } + } + // Let previously committed movement finish before recomputing. - // - // MoveTo internally caps its stored delay at maxWaitForMove - // (default 5s), but a long path (200+ yd routed around a - // mountain) takes 30+ seconds to walk. After 5s - // IsWaitingForLastMove returns false and MoveFarTo re-enters. - // Without this gate, DoMovePoint would call mm->Clear() and - // reissue MovePoint from the new bot position — and from a new - // position mmap's partial-path endpoint often differs, so the - // bot gets clobbered mid-walk and ends up oscillating around an - // unreachable destination. - // - // If the bot is still actively walking toward its last - // committed point on the same map, just let the current spline - // finish. + // If the bot is still actively walking toward its last committed + // point on the same map, just let the current spline finish. + // Prevents oscillation when a re-resolve produces a slightly + // different partial-path endpoint mid-walk. { LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId()) @@ -120,24 +128,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) points.reserve(pts.size()); for (auto const& wp : pts) points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); - for (auto& pt : points) - bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - bot->GetMotionMaster()->Clear(); - bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN); - - G3D::Vector3 const& last = points.back(); - float totalChainDist = 0.f; - for (size_t i = 1; i < points.size(); ++i) - totalChainDist += (points[i] - points[i - 1]).length(); - float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); - uint32 expectedMs = static_cast((totalChainDist / speed) * IN_MILLISECONDS); - uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove); - lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, - bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL); - - EmitDebugMove("MoveFar", "reuse", - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - return true; + return DispatchPathPoints(dest, points, "reuse"); } } // Path was cleared or collapsed — fall through to fresh planning. @@ -149,27 +140,25 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) float disToDest = bot->GetDistance(dest); float dis = bot->GetExactDist(dest); - // Decision tree (cmangos ResolveMovePath order — travel nodes first): + // Decision tree: + // 1. Active node plan with matching dest → ride it. + // 2. Long-distance / cross-map: try the node graph FIRST. + // Graph internally probes mmap and falls back to A* route. + // 3. Else: 40-step chained mmap probe + regression guard. + // 4. Empty / non-progressing probe: single-waypoint MoveTo. // - // 1. Active node plan? Ride it. - // - // 2. Long-distance move (>= nodeFirstDis) and travel nodes - // enabled: try the node graph FIRST. The graph holds - // curated waypoints that avoid known bad terrain. - // - // 3. If no node plan returned: run the 40-step chained mmap - // probe and dispatch its waypoint chain. - // - // 4. Empty / non-progressing probe: fall back to single- - // waypoint spline at dest. - bool tryNodes = (dis >= nodeFirstDis && sPlayerbotAIConfig.enableTravelNodes); + // needsLongPath gate — cross-map or > sightDistance go to graph. + // BG gating: graph holds open-world routes only. + bool tryNodes = sPlayerbotAIConfig.enableTravelNodes && + !bot->InBattleground() && + ((bot->GetMapId() != dest.GetMapId()) || + (dis > sPlayerbotAIConfig.sightDistance)); // If a node plan is already active, ride it — but only if its // destination still matches the requested dest. Otherwise the // old plan (e.g. built toward a quest objective POI) would keep // driving the bot after the caller switched targets (e.g. to a - // turn-in NPC). cmangos's ResolveMovePath dodges this by being - // stateless; we have a long-lived plan flag, so check explicitly. + // turn-in NPC). if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan()) { if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f) @@ -199,8 +188,8 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) botAI->rpgInfo.ClearTravel(); } - // 40-step chained mmap probe — fallback when the node graph - // returned no plan (or for short moves below nodeFirstDis). + // 40-step chained mmap probe — primary for short moves and + // fallback when the node graph returned no plan. WorldPosition botPos(bot); std::vector probe = botPos.getPathTo(dest, bot); @@ -235,24 +224,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) points.reserve(pts.size()); for (auto const& wp : pts) points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); - for (auto& pt : points) - bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - bot->GetMotionMaster()->Clear(); - bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN); - - G3D::Vector3 const& last = points.back(); - float totalChainDist = 0.f; - for (size_t i = 1; i < points.size(); ++i) - totalChainDist += (points[i] - points[i - 1]).length(); - float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); - uint32 expectedMs = static_cast((totalChainDist / speed) * IN_MILLISECONDS); - uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove); - lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, - bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL); - - EmitDebugMove("MoveFar", "regress-keep", - dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - return true; + return DispatchPathPoints(dest, points, "regress-keep"); } } } @@ -260,10 +232,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) } } - // Walk the chained probe's full waypoint chain via MoveSplinePath. - // Handing the full waypoint vector to the motion master removes - // its discretion to introduce a straight-line shortcut between - // intermediate points. + // Walk the chained probe's full waypoint chain via DispatchPathPoints. if (!probe.empty() && probe.size() >= 2) { WorldPosition stepDest = probe.back(); @@ -271,56 +240,21 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) stepDest.GetPositionY(), stepDest.GetPositionZ()); if (endDistToDest + 5.0f < disToDest) { - // Convert WorldPosition probe to G3D::Vector3 array. Movement::PointsArray points; points.reserve(probe.size()); for (auto const& wp : probe) points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); - // Per-waypoint Z-snap to current ground. - for (auto& pt : points) - bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); - if (points.size() >= 2) { LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={}", bot->GetName(), dis, endDistToDest, (uint32)points.size()); - EmitDebugMove("MoveFar", "mmap", - points.back().x, points.back().y, points.back().z); // Mount up if outdoors and not in combat. if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive()) botAI->DoSpecificAction("check mount state", Event(), true); - // Bulk dispatch: hand the full waypoint chain to the - // motion master via MoveSplinePath. Motion master plays - // every point in sequence — no per-tick re-dispatching. - bot->GetMotionMaster()->Clear(); - bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN); - - // Update LastMovement to the chain endpoint so spline- - // active early-exit at the top of MoveFarTo silences - // recompute attempts during the walk. - G3D::Vector3 const& last = points.back(); - float totalDist = 0.f; - for (size_t i = 1; i < points.size(); ++i) - totalDist += (points[i] - points[i - 1]).length(); - float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); - uint32 expectedMs = static_cast((totalDist / speed) * IN_MILLISECONDS); - uint32 cappedMs = std::min(expectedMs, (uint32)sPlayerbotAIConfig.maxWaitForMove); - LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); - lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, - bot->GetOrientation(), cappedMs, MovementPriority::MOVEMENT_NORMAL); - - // Cache full chain for downstream consumers - // (LastLongMoveValue) and the lastPath reuse check. - std::vector wpts; - wpts.reserve(points.size()); - for (auto const& pt : points) - wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z); - lastMove.setPath(TravelPath(wpts)); - - return true; + return DispatchPathPoints(dest, points, "mmap"); } } } @@ -340,31 +274,184 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) } } - // Empty / non-progressing path falls back to dispatching the - // destination as a single waypoint. Spline only when target is - // line-of-sight: dispatching a straight line through walls - // produces visible clipping/glitching. If LOS is blocked we - // refuse and let UnstuckAction (5/10 min) catch the stuck. - bool const inLOS = bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - LOG_INFO("playerbots", "[MoveFar] {} spline | dis={:.0f} | probe.empty={} | LOS={}", - bot->GetName(), dis, - probe.empty() ? "y" : "n", - inLOS ? "y" : "n"); - if (!inLOS) - { - EmitDebugMove("MoveFar", "spline-blocked", - dest.GetPositionX(), dest.GetPositionY(), - dest.GetPositionZ()); - return false; // Refuse to dispatch a straight line through geometry. - } + // Empty-probe fallback — single-waypoint MoveTo. Engine + // MovePoint(generatePath=true) does the local PathGenerator + // resolution. Cross-map can't be served by a single-map spline + // — bail rather than dispatching toward unreachable coords. + if (bot->GetMapId() != dest.GetMapId()) + return false; + EmitDebugMove("MoveFar", "spline", dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); - // Same exact_waypoint=false rationale as the mmap branch — terrain- - // following spline, not a straight diagonal. return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false, false, false); } +bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest, + Movement::PointsArray& points, + char const* label) +{ + if (points.size() < 2) + return false; + + // Save planner output BEFORE any clip/fixup mutation so next-tick + // reuse/regress branches see the original intent, not a clip- + // truncated tail. + { + LastMovement& lm = AI_VALUE(LastMovement&, "last movement"); + std::vector wpts; + wpts.reserve(points.size()); + for (auto const& pt : points) + wpts.emplace_back(dest.GetMapId(), pt.x, pt.y, pt.z); + lm.setPath(TravelPath(wpts)); + } + + // Underwater fixup — push waypoints submerged below the water + // surface up to the surface itself, unless the destination is + // itself underwater. + if (Map* map = bot->GetMap()) + { + WorldPosition destWp = dest; + if (!destWp.isUnderWater()) + { + for (auto& pt : points) + { + WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z); + if (wp.isUnderWater()) + { + float surface = map->GetWaterLevel(pt.x, pt.y); + if (surface != INVALID_HEIGHT && surface > pt.z) + pt.z = surface; + } + } + } + } + + for (auto& pt : points) + bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z); + + // ClipPath — truncate path at first hostile creature within its + // own attack range. Skipped while in combat or dead. + if (botAI->GetState() != BOT_STATE_COMBAT && bot->IsAlive()) + { + GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + if (!targets.empty()) + { + size_t clipAt = points.size(); + for (size_t i = 0; i < points.size() && clipAt == points.size(); ++i) + { + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + Creature* cre = unit->ToCreature(); + if (!cre) + continue; + if (unit->GetLevel() > bot->GetLevel() + 5) + continue; + float range = cre->GetAttackDistance(bot); + float dx = unit->GetPositionX() - points[i].x; + float dy = unit->GetPositionY() - points[i].y; + float dz = unit->GetPositionZ() - points[i].z; + if (dx * dx + dy * dy + dz * dz > range * range) + continue; + if (!unit->IsWithinLOSInMap(bot)) + continue; + clipAt = i; + break; + } + } + if (clipAt < points.size() && clipAt + 1 < points.size()) + points.erase(points.begin() + clipAt + 1, points.end()); + } + } + + if (points.size() < 2) + return false; + + G3D::Vector3 const& last = points.back(); + + float totalDist = 0.f; + for (size_t i = 1; i < points.size(); ++i) + totalDist += (points[i] - points[i - 1]).length(); + + LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + + // Inactive-bot teleport — when the path is longer than reactDistance + // and no real player is around to witness, jump to the path tail and + // schedule a cooldown. Skips cosmetic walking for unobserved random + // bots. Self-bots are excluded so observed sessions always walk. + if (sRandomPlayerbotMgr.IsRandomBot(bot)) + { + WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z); + time_t now = time(nullptr); + if (totalDist > sPlayerbotAIConfig.reactDistance && + lastMove.nextTeleport <= now && + !botAI->HasPlayerNearby(&tail)) + { + float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f); + lastMove.nextTeleport = now + (time_t)(totalDist / speed); + + EmitDebugMove("MoveFar", "teleport", + tail.GetPositionX(), tail.GetPositionY(), tail.GetPositionZ()); + + WorldPosition botPos(bot); + return bot->TeleportTo(dest.GetMapId(), + tail.GetPositionX(), tail.GetPositionY(), + tail.GetPositionZ(), + botPos.getAngleTo(tail)); + } + } + + // masterWalking — match the master's walk pace when they're nearby + // and walking. Lets a follower bot trail at walk speed instead of + // sprinting past. No-op for masterless RPG bots. + ForcedMovement moveMode = FORCED_MOVEMENT_RUN; + if (sPlayerbotAIConfig.walkDistance > 0.0f) + { + if (Player* master = botAI->GetMaster()) + { + if (bot->IsFriendlyTo(master) && master->IsWalking() && + bot->GetExactDist2d(master) < sPlayerbotAIConfig.walkDistance) + { + moveMode = FORCED_MOVEMENT_WALK; + } + } + } + + // Pre-dispatch state cleanup. Clear emote / stand up / interrupt + // any non-melee cast so the spline can begin without state conflicts. + bot->ClearEmoteState(); + if (!bot->IsStandState()) + bot->SetStandState(UNIT_STAND_STATE_STAND); + if (bot->IsNonMeleeSpellCast(true)) + bot->InterruptNonMeleeSpells(true); + + bot->GetMotionMaster()->Clear(); + bot->GetMotionMaster()->MoveSplinePath(&points, moveMode); + + EmitDebugMove("MoveFar", label, last.x, last.y, last.z); + + // WaitForReach scheduling. + // waitDist = (totalDist > reactDistance) ? totalDist - 10 : totalDist + // duration = 1000 * (waitDist / speed) + reactDelay, capped at maxWaitForMove + float waitDist = totalDist > sPlayerbotAIConfig.reactDistance + ? std::max(totalDist - 10.0f, 0.0f) : totalDist; + UnitMoveType const speedType = (moveMode == FORCED_MOVEMENT_WALK) ? MOVE_WALK : MOVE_RUN; + float speed = std::max(bot->GetSpeed(speedType), 0.1f); + float duration = 1000.0f * (waitDist / speed) + sPlayerbotAIConfig.reactDelay; + duration = std::min(duration, (float)sPlayerbotAIConfig.maxWaitForMove); + if (duration < 0.0f) + duration = 0.0f; + + lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, + bot->GetOrientation(), (uint32)duration, + MovementPriority::MOVEMENT_NORMAL); + + return true; +} + void NewRpgBaseAction::StartTravelPlan(WorldPosition dest) { TravelPlan& plan = botAI->rpgInfo.travelPlan; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 2a9df1692..52eebae8a 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -76,19 +76,18 @@ protected: bool RandomChangeStatus(std::vector candidateStatus); bool CheckRpgStatusAvailable(NewRpgStatus status); -protected: - /* FOR MOVE FAR */ - // Distance at which MoveFarTo considers the travel-node graph as - // a routing option. Below this, the move is short enough that - // mmap handles it directly. Above this, mmap is *still probed - // first* via the 40-step chained pathfinder; the node graph - // only takes over if mmap can't get within spellDistance of - // the destination. - const float nodeFirstDis = 75.0f; - private: void StartTravelPlan(WorldPosition dest); bool UpdateTravelPlan(); + + // Centralized dispatch helper. Applies underwater fixup, ClipPath + // (truncate at first hostile in attack range with LOS, level+5 cap), + // inactive-bot teleport (with self-bot carve-out), masterWalking + // mode, pre-dispatch state cleanup, then dispatches via + // MoveSplinePath and schedules via WaitForReach formula. + bool DispatchPathPoints(WorldPosition const& dest, + Movement::PointsArray& points, + char const* label); }; #endif diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index c1979f1c3..ded52bbcf 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -17,6 +17,7 @@ #include "ChatHelper.h" #include "MapCollisionData.h" #include "MapMgr.h" +#include "ModelIgnoreFlags.h" #include "PathGenerator.h" #include "Playerbots.h" #include "RaceMgr.h" @@ -698,7 +699,6 @@ std::vector WorldPosition::getPathStepFrom(WorldPosition startPos if (!pathUnit) { - // Create a temporary creature for PathGenerator (same entry as DebugAction "show node") Map* map = sMapMgr->FindBaseMap(startPos.GetMapId()); if (!map) return {}; @@ -714,52 +714,82 @@ std::vector WorldPosition::getPathStepFrom(WorldPosition startPos } 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.AddExcludeFlag(NAV_GROUND_STEEP); + auto result = getPathStepFrom(startPos, path); + + if (tempCreature) + delete tempCreature; + + return result; +} + +// Pathfinder-reuse overload — caller owns the PathGenerator and any +// per-call configuration. Used by getPathFromPath to thread one +// PathGenerator through the whole 40-step chain instead of +// constructing a new one per step. +std::vector WorldPosition::getPathStepFrom(WorldPosition startPos, PathGenerator& path) +{ + // Explicit-start overload. Without this, the chain begins from the + // unit's current position every step and never advances. path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(), GetPositionX(), GetPositionY(), GetPositionZ(), false); Movement::PointsArray points = path.GetPath(); PathType type = path.GetPathType(); - if (tempCreature) - delete tempCreature; + // PathType is a bitmask. AC's PathGenerator returns + // NORMAL | NOT_USING_PATH when start/end poly is INVALID_POLYREF + // (BuildShortcut produces a 2-point straight line through whatever's + // in the way). Reject those to avoid silently dispatching a + // geometry-ignoring shortcut. + if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) || + (type & PATHFIND_NOT_USING_PATH)) + return {}; - // 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); + std::vector retvec = fromPointsArray(points); - return {}; + // Underwater path-extension. When PATHFIND_INCOMPLETE ends within + // 50y of dest and both endpoints are underwater with LOS, extend + // by one 5y step (or straight to dest if <5y). Lets bots traverse + // navmesh-poor water volumes. + if (type & PATHFIND_INCOMPLETE) + { + WorldPosition end = *this; + WorldPosition lastPoint = retvec.back(); + float dist = lastPoint.distance(&end); + + if (dist < 50.0f && lastPoint.isUnderWater() && end.isUnderWater()) + { + Map* m = end.getMap(); + bool inLos = m && m->isInLineOfSight( + lastPoint.GetPositionX(), lastPoint.GetPositionY(), lastPoint.GetPositionZ() + 1.0f, + end.GetPositionX(), end.GetPositionY(), end.GetPositionZ() + 1.0f, + PHASEMASK_NORMAL, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing); + if (inLos) + { + if (dist < 5.0f) + retvec.push_back(end); + else + { + float dx = end.GetPositionX() - lastPoint.GetPositionX(); + float dy = end.GetPositionY() - lastPoint.GetPositionY(); + float dz = end.GetPositionZ() - lastPoint.GetPositionZ(); + float scale = 5.0f / dist; + retvec.emplace_back(end.GetMapId(), + lastPoint.GetPositionX() + dx * scale, + lastPoint.GetPositionY() + dy * scale, + lastPoint.GetPositionZ() + dz * scale); + } + } + } + } + + return retvec; } bool WorldPosition::cropPathTo(std::vector& path, float maxDistance) @@ -795,27 +825,60 @@ std::vector WorldPosition::getPathFromPath(std::vector subPath, fullPath = startPath; + // Construct ONE PathGenerator and thread it through every step + // to avoid the per-step alloc cost. AC's BuildPolyPath has a + // subpath-prefix optimization that can bend chained probes, so + // call Clear() before each step to reset the poly cache. + Unit* pathUnit = bot; + Creature* tempCreature = nullptr; + if (!pathUnit) + { + Map* map = sMapMgr->FindBaseMap(GetMapId()); + if (!map) + return fullPath; + + tempCreature = new Creature(); + if (!tempCreature->Create(map->GenerateLowGuid(), map, + PHASEMASK_NORMAL, 1 /*entry*/, 0, + currentPos.GetPositionX(), currentPos.GetPositionY(), + currentPos.GetPositionZ(), 0)) + { + delete tempCreature; + return fullPath; + } + pathUnit = tempCreature; + map->EnsureGridCreated(Acore::ComputeGridCoord(currentPos.GetPositionX(), currentPos.GetPositionY())); + map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY())); + } + + PathGenerator path(pathUnit); + path.AddExcludeFlag(NAV_GROUND_STEEP); + // Limit the pathfinding attempts for (uint32 i = 0; i < maxAttempt; i++) { - // Try to pathfind to this position. - subPath = getPathStepFrom(currentPos, bot); + // Reset cached poly state from the previous step so each call + // is a fresh A* (otherwise the prefix-recycling at + // PathGenerator.cpp BuildPolyPath snaps the start to the + // cached corridor, bending the chain). + path.Clear(); + + subPath = getPathStepFrom(currentPos, path); - // If we could not find a path return what we have now. if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance) break; - // Append the path excluding the start (this should be the same as the end of the startPath) fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end()); - // Are we there yet? if (isPathTo(subPath)) break; - // Continue pathfinding. currentPos = subPath.back(); } + if (tempCreature) + delete tempCreature; + return fullPath; } diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 877b0744b..98afd8a33 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -19,6 +19,7 @@ class Creature; class GuidPosition; +class PathGenerator; class ObjectGuid; class Quest; class Player; @@ -283,6 +284,7 @@ public: // Pathfinding std::vector getPathStepFrom(WorldPosition startPos, Unit* bot); + std::vector getPathStepFrom(WorldPosition startPos, PathGenerator& pathfinder); std::vector getPathFromPath(std::vector startPath, Unit* bot, uint8 maxAttempt = 40); std::vector getPathFrom(WorldPosition startPos, Unit* bot) diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index df77344ac..6b9ecea4b 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -1285,18 +1285,25 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor bool TravelNodeMap::GetFullPath(TravelPlan& plan, WorldPosition botPos, uint32 botZoneId, - WorldPosition destination) + WorldPosition destination, Unit* bot) { plan.Reset(); plan.destination = destination; - // Short distance — direct walk, no nodes needed - if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE && - botPos.GetMapId() == destination.GetMapId()) + // mmap-probe-first. Run a 40-step chained probe; if it gets within + // spellDistance of dest, emit it as plan steps and skip the graph + // entirely (a short walk is always better than a node hop). When + // the probe falls short, fall through to graph routing. + if (botPos.GetMapId() == destination.GetMapId()) { - plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH); - plan.steps.addPoint(destination, PathNodeType::NODE_PATH); - return true; + std::vector probe = destination.getPathFromPath({botPos}, bot, 40); + if (probe.size() >= 2 && destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance)) + { + plan.steps.addPoint(botPos, PathNodeType::NODE_PREPATH); + for (size_t i = 1; i < probe.size(); ++i) + plan.steps.addPoint(probe[i], PathNodeType::NODE_PATH); + return true; + } } std::shared_lock guard(m_nMapMtx); diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 780898a51..2481ac2cd 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -725,7 +725,7 @@ public: std::vector const& GetNodesInZone(uint32 zoneId) const; bool GetFullPath(TravelPlan& plan, WorldPosition botPos, - uint32 botZoneId, WorldPosition destination); + uint32 botZoneId, WorldPosition destination, Unit* bot = nullptr); // Resolve A* route between two world positions (returns node vector) std::vector ResolveRoute(WorldPosition startPos, diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 8afebf9f1..4d29c237c 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -97,6 +97,7 @@ bool PlayerbotAIConfig::Initialize() tooCloseDistance = sConfigMgr->GetOption("AiPlayerbot.TooCloseDistance", 5.0f); meleeDistance = sConfigMgr->GetOption("AiPlayerbot.MeleeDistance", 0.75f); followDistance = sConfigMgr->GetOption("AiPlayerbot.FollowDistance", 1.5f); + walkDistance = sConfigMgr->GetOption("AiPlayerbot.WalkDistance", 5.0f); whisperDistance = sConfigMgr->GetOption("AiPlayerbot.WhisperDistance", 6000.0f); contactDistance = sConfigMgr->GetOption("AiPlayerbot.ContactDistance", 0.45f); aoeRadius = sConfigMgr->GetOption("AiPlayerbot.AoeRadius", 10.0f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 84c8aec99..55fccb65c 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -96,7 +96,7 @@ public: bool dynamicReactDelay; float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance, tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance, - targetPosRecalcDistance, farDistance, healDistance, aggroDistance; + targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance; uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth; uint32 lowMana, mediumMana, highMana; bool autoSaveMana;