feat(Core/Travel): Align MoveFarTo and probe pipeline with cmangos

This commit is contained in:
bash 2026-05-10 18:14:42 +02:00
parent 724b15e39f
commit 86f0be5b08
9 changed files with 344 additions and 185 deletions

View File

@ -3308,7 +3308,7 @@ bool MovementAction::GetTravelPlan(TravelPlan& plan, WorldPosition destination)
destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(), destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(),
destination.GetMapId(), botPos.fDist(destination)); 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) bool MovementAction::ExecuteTravelPlan(TravelPlan& state)

View File

@ -53,21 +53,29 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
return false; 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. // Let previously committed movement finish before recomputing.
// // If the bot is still actively walking toward its last committed
// MoveTo internally caps its stored delay at maxWaitForMove // point on the same map, just let the current spline finish.
// (default 5s), but a long path (200+ yd routed around a // Prevents oscillation when a re-resolve produces a slightly
// mountain) takes 30+ seconds to walk. After 5s // different partial-path endpoint mid-walk.
// 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.
{ {
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId()) if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId())
@ -120,24 +128,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
points.reserve(pts.size()); points.reserve(pts.size());
for (auto const& wp : pts) for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
for (auto& pt : points) return DispatchPathPoints(dest, points, "reuse");
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<uint32>((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;
} }
} }
// Path was cleared or collapsed — fall through to fresh planning. // 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 disToDest = bot->GetDistance(dest);
float dis = bot->GetExactDist(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. // needsLongPath gate — cross-map or > sightDistance go to graph.
// // BG gating: graph holds open-world routes only.
// 2. Long-distance move (>= nodeFirstDis) and travel nodes bool tryNodes = sPlayerbotAIConfig.enableTravelNodes &&
// enabled: try the node graph FIRST. The graph holds !bot->InBattleground() &&
// curated waypoints that avoid known bad terrain. ((bot->GetMapId() != dest.GetMapId()) ||
// (dis > sPlayerbotAIConfig.sightDistance));
// 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);
// If a node plan is already active, ride it — but only if its // If a node plan is already active, ride it — but only if its
// destination still matches the requested dest. Otherwise the // destination still matches the requested dest. Otherwise the
// old plan (e.g. built toward a quest objective POI) would keep // old plan (e.g. built toward a quest objective POI) would keep
// driving the bot after the caller switched targets (e.g. to a // driving the bot after the caller switched targets (e.g. to a
// turn-in NPC). cmangos's ResolveMovePath dodges this by being // turn-in NPC).
// stateless; we have a long-lived plan flag, so check explicitly.
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan()) if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
{ {
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f) if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
@ -199,8 +188,8 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
botAI->rpgInfo.ClearTravel(); botAI->rpgInfo.ClearTravel();
} }
// 40-step chained mmap probe — fallback when the node graph // 40-step chained mmap probe — primary for short moves and
// returned no plan (or for short moves below nodeFirstDis). // fallback when the node graph returned no plan.
WorldPosition botPos(bot); WorldPosition botPos(bot);
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot); std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
@ -235,24 +224,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
points.reserve(pts.size()); points.reserve(pts.size());
for (auto const& wp : pts) for (auto const& wp : pts)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
for (auto& pt : points) return DispatchPathPoints(dest, points, "regress-keep");
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<uint32>((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;
} }
} }
} }
@ -260,10 +232,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
} }
} }
// Walk the chained probe's full waypoint chain via MoveSplinePath. // Walk the chained probe's full waypoint chain via DispatchPathPoints.
// Handing the full waypoint vector to the motion master removes
// its discretion to introduce a straight-line shortcut between
// intermediate points.
if (!probe.empty() && probe.size() >= 2) if (!probe.empty() && probe.size() >= 2)
{ {
WorldPosition stepDest = probe.back(); WorldPosition stepDest = probe.back();
@ -271,56 +240,21 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
stepDest.GetPositionY(), stepDest.GetPositionZ()); stepDest.GetPositionY(), stepDest.GetPositionZ());
if (endDistToDest + 5.0f < disToDest) if (endDistToDest + 5.0f < disToDest)
{ {
// Convert WorldPosition probe to G3D::Vector3 array.
Movement::PointsArray points; Movement::PointsArray points;
points.reserve(probe.size()); points.reserve(probe.size());
for (auto const& wp : probe) for (auto const& wp : probe)
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ()); 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) if (points.size() >= 2)
{ {
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={}", LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={}",
bot->GetName(), dis, endDistToDest, (uint32)points.size()); 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. // Mount up if outdoors and not in combat.
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive()) if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true); botAI->DoSpecificAction("check mount state", Event(), true);
// Bulk dispatch: hand the full waypoint chain to the return DispatchPathPoints(dest, points, "mmap");
// 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<uint32>((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<WorldPosition> 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;
} }
} }
} }
@ -340,31 +274,184 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
} }
} }
// Empty / non-progressing path falls back to dispatching the // Empty-probe fallback — single-waypoint MoveTo. Engine
// destination as a single waypoint. Spline only when target is // MovePoint(generatePath=true) does the local PathGenerator
// line-of-sight: dispatching a straight line through walls // resolution. Cross-map can't be served by a single-map spline
// produces visible clipping/glitching. If LOS is blocked we // — bail rather than dispatching toward unreachable coords.
// refuse and let UnstuckAction (5/10 min) catch the stuck. if (bot->GetMapId() != dest.GetMapId())
bool const inLOS = bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); return false;
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.
}
EmitDebugMove("MoveFar", "spline", EmitDebugMove("MoveFar", "spline",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); 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(), return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
false, false, false, false); 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<WorldPosition> 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) void NewRpgBaseAction::StartTravelPlan(WorldPosition dest)
{ {
TravelPlan& plan = botAI->rpgInfo.travelPlan; TravelPlan& plan = botAI->rpgInfo.travelPlan;

View File

@ -76,19 +76,18 @@ protected:
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus); bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status); 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: private:
void StartTravelPlan(WorldPosition dest); void StartTravelPlan(WorldPosition dest);
bool UpdateTravelPlan(); 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 #endif

View File

@ -17,6 +17,7 @@
#include "ChatHelper.h" #include "ChatHelper.h"
#include "MapCollisionData.h" #include "MapCollisionData.h"
#include "MapMgr.h" #include "MapMgr.h"
#include "ModelIgnoreFlags.h"
#include "PathGenerator.h" #include "PathGenerator.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "RaceMgr.h" #include "RaceMgr.h"
@ -698,7 +699,6 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
if (!pathUnit) if (!pathUnit)
{ {
// Create a temporary creature for PathGenerator (same entry as DebugAction "show node")
Map* map = sMapMgr->FindBaseMap(startPos.GetMapId()); Map* map = sMapMgr->FindBaseMap(startPos.GetMapId());
if (!map) if (!map)
return {}; return {};
@ -714,52 +714,82 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
} }
pathUnit = tempCreature; 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(startPos.GetPositionX(), startPos.GetPositionY()));
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), 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); PathGenerator path(pathUnit);
path.AddExcludeFlag(NAV_GROUND_STEEP); 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> 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(), path.CalculatePath(startPos.GetPositionX(), startPos.GetPositionY(), startPos.GetPositionZ(),
GetPositionX(), GetPositionY(), GetPositionZ(), false); GetPositionX(), GetPositionY(), GetPositionZ(), false);
Movement::PointsArray points = path.GetPath(); Movement::PointsArray points = path.GetPath();
PathType type = path.GetPathType(); PathType type = path.GetPathType();
if (tempCreature) // PathType is a bitmask. AC's PathGenerator returns
delete tempCreature; // 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: std::vector<WorldPosition> retvec = fromPointsArray(points);
//
// 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 {}; // 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<WorldPosition>& path, float maxDistance) bool WorldPosition::cropPathTo(std::vector<WorldPosition>& path, float maxDistance)
@ -795,27 +825,60 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
std::vector<WorldPosition> subPath, fullPath = startPath; std::vector<WorldPosition> 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<HighGuid::Unit>(), 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 // Limit the pathfinding attempts
for (uint32 i = 0; i < maxAttempt; i++) for (uint32 i = 0; i < maxAttempt; i++)
{ {
// Try to pathfind to this position. // Reset cached poly state from the previous step so each call
subPath = getPathStepFrom(currentPos, bot); // 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) if (subPath.empty() || currentPos.distance(&subPath.back()) < sPlayerbotAIConfig.targetPosRecalcDistance)
break; 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()); fullPath.insert(fullPath.end(), std::next(subPath.begin(), 1), subPath.end());
// Are we there yet?
if (isPathTo(subPath)) if (isPathTo(subPath))
break; break;
// Continue pathfinding.
currentPos = subPath.back(); currentPos = subPath.back();
} }
if (tempCreature)
delete tempCreature;
return fullPath; return fullPath;
} }

View File

@ -19,6 +19,7 @@
class Creature; class Creature;
class GuidPosition; class GuidPosition;
class PathGenerator;
class ObjectGuid; class ObjectGuid;
class Quest; class Quest;
class Player; class Player;
@ -283,6 +284,7 @@ public:
// Pathfinding // Pathfinding
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot); std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, Unit* bot);
std::vector<WorldPosition> getPathStepFrom(WorldPosition startPos, PathGenerator& pathfinder);
std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40); std::vector<WorldPosition> getPathFromPath(std::vector<WorldPosition> startPath, Unit* bot, uint8 maxAttempt = 40);
std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot) std::vector<WorldPosition> getPathFrom(WorldPosition startPos, Unit* bot)

View File

@ -1285,18 +1285,25 @@ TravelNodeRoute TravelNodeMap::FindRouteNearestNodes(WorldPosition startPos, Wor
bool TravelNodeMap::GetFullPath(TravelPlan& plan, bool TravelNodeMap::GetFullPath(TravelPlan& plan,
WorldPosition botPos, uint32 botZoneId, WorldPosition botPos, uint32 botZoneId,
WorldPosition destination) WorldPosition destination, Unit* bot)
{ {
plan.Reset(); plan.Reset();
plan.destination = destination; plan.destination = destination;
// Short distance — direct walk, no nodes needed // mmap-probe-first. Run a 40-step chained probe; if it gets within
if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE && // spellDistance of dest, emit it as plan steps and skip the graph
botPos.GetMapId() == destination.GetMapId()) // 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); std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
plan.steps.addPoint(destination, PathNodeType::NODE_PATH); if (probe.size() >= 2 && destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
return true; {
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<std::shared_timed_mutex> guard(m_nMapMtx); std::shared_lock<std::shared_timed_mutex> guard(m_nMapMtx);

View File

@ -725,7 +725,7 @@ public:
std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const; std::vector<TravelNode*> const& GetNodesInZone(uint32 zoneId) const;
bool GetFullPath(TravelPlan& plan, WorldPosition botPos, 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) // Resolve A* route between two world positions (returns node vector)
std::vector<TravelNode*> ResolveRoute(WorldPosition startPos, std::vector<TravelNode*> ResolveRoute(WorldPosition startPos,

View File

@ -97,6 +97,7 @@ bool PlayerbotAIConfig::Initialize()
tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f); tooCloseDistance = sConfigMgr->GetOption<float>("AiPlayerbot.TooCloseDistance", 5.0f);
meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f); meleeDistance = sConfigMgr->GetOption<float>("AiPlayerbot.MeleeDistance", 0.75f);
followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f); followDistance = sConfigMgr->GetOption<float>("AiPlayerbot.FollowDistance", 1.5f);
walkDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WalkDistance", 5.0f);
whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f); whisperDistance = sConfigMgr->GetOption<float>("AiPlayerbot.WhisperDistance", 6000.0f);
contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f); contactDistance = sConfigMgr->GetOption<float>("AiPlayerbot.ContactDistance", 0.45f);
aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f); aoeRadius = sConfigMgr->GetOption<float>("AiPlayerbot.AoeRadius", 10.0f);

View File

@ -96,7 +96,7 @@ public:
bool dynamicReactDelay; bool dynamicReactDelay;
float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance, float sightDistance, spellDistance, reactDistance, grindDistance, lootDistance, shootDistance, fleeDistance,
tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance, tooCloseDistance, meleeDistance, followDistance, whisperDistance, contactDistance, aoeRadius, rpgDistance,
targetPosRecalcDistance, farDistance, healDistance, aggroDistance; targetPosRecalcDistance, farDistance, healDistance, aggroDistance, walkDistance;
uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth; uint32 criticalHealth, lowHealth, mediumHealth, almostFullHealth;
uint32 lowMana, mediumMana, highMana; uint32 lowMana, mediumMana, highMana;
bool autoSaveMana; bool autoSaveMana;