fix(Core/Travel): Audit fixes and probe correctness in MoveFarTo

This commit is contained in:
bash 2026-05-10 03:02:43 +02:00
parent 3710c35a41
commit 753248510a
6 changed files with 66 additions and 56 deletions

View File

@ -9,23 +9,6 @@
LastMovement::LastMovement() { clear(); }
LastMovement::LastMovement(LastMovement& other)
: taxiNodes(other.taxiNodes),
taxiMaster(other.taxiMaster),
lastFollow(other.lastFollow),
lastAreaTrigger(other.lastAreaTrigger),
lastMoveToX(other.lastMoveToX),
lastMoveToY(other.lastMoveToY),
lastMoveToZ(other.lastMoveToZ),
lastMoveToOri(other.lastMoveToOri),
lastFlee(other.lastFlee)
{
lastMoveShort = other.lastMoveShort;
nextTeleport = other.nextTeleport;
lastPath = other.lastPath;
priority = other.priority;
}
void LastMovement::clear()
{
lastMoveShort = WorldPosition();

View File

@ -27,20 +27,11 @@ class LastMovement
{
public:
LastMovement();
LastMovement(LastMovement& other);
LastMovement& operator=(LastMovement const& other)
{
taxiNodes = other.taxiNodes;
taxiMaster = other.taxiMaster;
lastFollow = other.lastFollow;
lastAreaTrigger = other.lastAreaTrigger;
lastMoveShort = other.lastMoveShort;
lastPath = other.lastPath;
nextTeleport = other.nextTeleport;
priority = other.priority;
return *this;
};
// Non-copyable: holds a std::future and several state fields the
// historical copy-helpers omitted. Always pass by reference.
LastMovement(LastMovement const&) = delete;
LastMovement& operator=(LastMovement const&) = delete;
void clear();
@ -75,7 +66,7 @@ public:
LastMovementValue(PlayerbotAI* botAI) : ManualSetValue<LastMovement&>(botAI, data) {}
private:
LastMovement data = LastMovement();
LastMovement data{};
};
class StayTimeValue : public ManualSetValue<time_t>

View File

@ -137,24 +137,40 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
bool const needsLongPath = (bot->GetMapId() != dest.GetMapId()) ||
(dis > sPlayerbotAIConfig.sightDistance);
// Skip travel-node planning inside battlegrounds — the graph is
// built for the open world and yields nonsense routes inside BGs.
// Mirrors cmangos MovementActions.cpp:705.
if (needsLongPath && sPlayerbotAIConfig.enableTravelNodes &&
!bot->InBattleground())
// needsLongPath branch — graph routing only. cmangos's getFullPath
// already attempts a 40-step mmap probe internally before falling
// back to A* node routing, so we don't need a second probe here.
// If the graph yields no plan, fall straight to the single-point
// fallback (cmangos addPoint(endPosition)). BG gate per
// MovementActions.cpp:705.
if (needsLongPath)
{
StartTravelPlan(dest);
if (botAI->rpgInfo.HasActiveTravelPlan())
if (sPlayerbotAIConfig.enableTravelNodes && !bot->InBattleground())
{
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
bot->GetName(), dis);
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return UpdateTravelPlan();
StartTravelPlan(dest);
if (botAI->rpgInfo.HasActiveTravelPlan())
{
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
bot->GetName(), dis);
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return UpdateTravelPlan();
}
}
// Long-path fallback. Cross-map can't be served by a same-map
// straight-line spline — bail rather than splining toward
// unreachable coords.
if (bot->GetMapId() != dest.GetMapId())
return false;
Movement::PointsArray fallback;
fallback.emplace_back(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
fallback.emplace_back(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return DispatchPathPoints(dest, fallback, "spline-long");
}
// 40-step chained mmap probe.
// Short-path branch — 40-step chained mmap probe.
WorldPosition botPos(bot);
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
@ -245,14 +261,12 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
lm.setPath(TravelPath(wpts));
}
// Item 5 — underwater fixup. Push waypoints submerged below the
// water surface up to the surface itself, unless the destination
// is itself underwater (e.g. fishing-loot quest GO, sunken ruins).
// Push submerged waypoints to the water surface unless dest is itself underwater.
if (Map* map = bot->GetMap())
{
WorldPosition destWp = dest;
if (!destWp.isUnderWater())
{
Map* map = bot->GetMap();
for (auto& pt : points)
{
WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z);
@ -307,11 +321,21 @@ bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
break;
}
}
if (clipAt > 0 && clipAt + 1 < points.size())
// Truncate after the first hostile waypoint. cmangos's
// analog at TravelNode.cpp:1158 sets endP=path.begin() if
// i=0 (mob right at start) and erases everything after,
// collapsing to a 1-point path. Our caller then sees
// points.size() < 2 and bails out — the bot just stops.
if (clipAt < points.size() && clipAt + 1 < points.size())
points.erase(points.begin() + clipAt + 1, points.end());
}
}
// After clip, points may have collapsed to one element (mob right
// at start). Bail rather than dispatching a degenerate spline.
if (points.size() < 2)
return false;
G3D::Vector3 const& last = points.back();
float totalDist = 0.f;

View File

@ -50,7 +50,7 @@ void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition fl
void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
{
startT = getMSTime();
Reset();
OutdoorPvP pvp;
pvp.capturePointSpawnId = capturePointSpawnId;
data = pvp;

View File

@ -873,14 +873,20 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
PathGenerator path(pathUnit);
path.AddExcludeFlag(NAV_GROUND_STEEP);
// Area-cost biases. Mirrors cmangos WorldPosition.cpp:1098-1100.
// NAV_WATER weighted 10x so A* prefers shore routes over wading
// through lakes when both are reachable.
path.SetAreaCost(NAV_WATER, 10.0f);
// Limit the pathfinding attempts
for (uint32 i = 0; i < maxAttempt; i++)
{
// Drop cached poly state from the previous step. AC's
// BuildPolyPath has a subpath-prefix optimization
// (PathGenerator.cpp:285-389) that keeps ~80% of the prior
// call's poly chain when the new startPoly is anywhere in
// it, then computes a suffix from there. For a chained
// walker this implicitly snaps the start to the previous
// corridor instead of using the explicit startPos we pass
// in, bending the route. Reset wipes the cache so each step
// is a fresh A*.
path.Clear();
// Try to pathfind to this position.
subPath = getPathStepFrom(currentPos, path);

View File

@ -1297,7 +1297,13 @@ bool TravelNodeMap::GetFullPath(TravelPlan& plan,
if (botPos.GetMapId() == destination.GetMapId())
{
std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
if (destination.isPathTo(probe, sPlayerbotAIConfig.spellDistance))
// Guard the degenerate case: getPathFromPath always returns at
// least {botPos}, and isPathTo(probe, spellDistance) succeeds
// when the bot is already within spellDistance of dest.
// Without this guard we'd emit a 1-element plan [botPos as
// PREPATH] and ExecuteTravelPlan would silently no-op every
// tick.
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)