mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Core/Travel): Match cmangos MoveFarTo flow (mmap-first, 25y reach, single-point fallback)
This commit is contained in:
parent
823b08d86c
commit
cc3ac5fd7a
@ -52,34 +52,11 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
|
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Let previously committed movement finish before recomputing.
|
// Already-at-dest short-stop. Below targetPosRecalcDistance
|
||||||
//
|
// (default 0.1y) the move is effectively done — no need to
|
||||||
// MoveTo internally caps its stored delay at maxWaitForMove
|
// recompute or dispatch.
|
||||||
// (default 5s), but a long path (200+ yd routed around a
|
if (bot->GetExactDist(dest) < sPlayerbotAIConfig.targetPosRecalcDistance)
|
||||||
// mountain) takes 30+ seconds to walk. After 5s
|
return false;
|
||||||
// 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");
|
|
||||||
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId())
|
|
||||||
{
|
|
||||||
float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
|
|
||||||
if (remaining > 10.0f)
|
|
||||||
{
|
|
||||||
EmitDebugMove("MoveFar", "spline-plan",
|
|
||||||
lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10% lastPath reuse — if the cached path's endpoint is still
|
// 10% lastPath reuse — if the cached path's endpoint is still
|
||||||
// close (within 10%) to the new dest, trim the cached path to
|
// close (within 10%) to the new dest, trim the cached path to
|
||||||
@ -145,71 +122,25 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float disToDest = bot->GetDistance(dest);
|
float const dis = bot->GetExactDist(dest);
|
||||||
float dis = bot->GetExactDist(dest);
|
|
||||||
|
|
||||||
// Decision tree (cmangos ResolveMovePath order — travel nodes first):
|
// Decision tree:
|
||||||
//
|
//
|
||||||
// 1. Active node plan? Ride it.
|
// 1. 40-step chained mmap probe FIRST.
|
||||||
//
|
// 2. Regression guard — if cached lastPath ends ≤ as close to
|
||||||
// 2. Long-distance move (>= nodeFirstDis) and travel nodes
|
// dest as the new probe, ride the cached path instead.
|
||||||
// enabled: try the node graph FIRST. The graph holds
|
// 3. If probe reaches dest within 25y, dispatch probe waypoints.
|
||||||
// curated waypoints that avoid known bad terrain.
|
// 4. Else if travel nodes enabled, try the node graph as fallback.
|
||||||
//
|
// 5. Else dispatch the destination as a single-waypoint spline
|
||||||
// 3. If no node plan returned: run the 40-step chained mmap
|
// via MoveTo — engine MovePoint(generatePath=true) resolves
|
||||||
// probe and dispatch its waypoint chain.
|
// the local route via PathGenerator.
|
||||||
//
|
|
||||||
// 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
|
// 40-step chained mmap probe.
|
||||||
// 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.
|
|
||||||
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
|
|
||||||
{
|
|
||||||
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
|
|
||||||
botAI->rpgInfo.ClearTravel();
|
|
||||||
else
|
|
||||||
return UpdateTravelPlan();
|
|
||||||
}
|
|
||||||
|
|
||||||
// PRIORITY: try the travel-node graph FIRST when the move is
|
|
||||||
// long enough to need it.
|
|
||||||
if (tryNodes)
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
// Graph returned no plan — fall through to mmap probe.
|
|
||||||
}
|
|
||||||
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
|
||||||
{
|
|
||||||
// Move dropped below node-first threshold — drop any leftover plan.
|
|
||||||
botAI->rpgInfo.ClearTravel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 40-step chained mmap probe — fallback when the node graph
|
|
||||||
// returned no plan (or for short moves below nodeFirstDis).
|
|
||||||
WorldPosition botPos(bot);
|
WorldPosition botPos(bot);
|
||||||
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
||||||
|
|
||||||
// Regression guard (cmangos ResolveMovePath parity): if a cached
|
// Regression guard: if a cached lastPath ends at least as close
|
||||||
// lastPath ends at least as close to dest as the new probe's
|
// to dest as the new probe's endpoint, ride the cached path.
|
||||||
// endpoint, prefer the cached path. The 10% reuse block above
|
|
||||||
// already returned early when cached was within 10% of dest;
|
|
||||||
// this catches "cached is far (>10%) but still better than the
|
|
||||||
// probe" — typically when the probe got blocked by geometry and
|
|
||||||
// ended much farther from dest than where cached had reached.
|
|
||||||
{
|
{
|
||||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||||
if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2)
|
if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2)
|
||||||
@ -259,107 +190,74 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk the chained probe's full waypoint chain via MoveSplinePath.
|
// Probe dispatch — only if the probe reaches dest within 25y.
|
||||||
// Handing the full waypoint vector to the motion master removes
|
// Partial probes that fall short go through the graph fallback
|
||||||
// its discretion to introduce a straight-line shortcut between
|
// below instead of being dispatched as-is.
|
||||||
// intermediate points.
|
constexpr float PROBE_REACH = 25.0f;
|
||||||
if (!probe.empty() && probe.size() >= 2)
|
if (!probe.empty() && probe.size() >= 2 && dest.isPathTo(probe, PROBE_REACH))
|
||||||
{
|
{
|
||||||
WorldPosition stepDest = probe.back();
|
Movement::PointsArray points;
|
||||||
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
|
points.reserve(probe.size());
|
||||||
stepDest.GetPositionY(), stepDest.GetPositionZ());
|
for (auto const& wp : probe)
|
||||||
if (endDistToDest + 5.0f < disToDest)
|
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||||
|
|
||||||
|
for (auto& pt : points)
|
||||||
|
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
||||||
|
|
||||||
|
if (points.size() >= 2)
|
||||||
{
|
{
|
||||||
// Convert WorldPosition probe to G3D::Vector3 array.
|
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | wp={}",
|
||||||
Movement::PointsArray points;
|
bot->GetName(), dis, (uint32)points.size());
|
||||||
points.reserve(probe.size());
|
EmitDebugMove("MoveFar", "mmap",
|
||||||
for (auto const& wp : probe)
|
points.back().x, points.back().y, points.back().z);
|
||||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
|
||||||
|
|
||||||
// Per-waypoint Z-snap to current ground.
|
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
||||||
for (auto& pt : points)
|
botAI->DoSpecificAction("check mount state", Event(), true);
|
||||||
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
|
||||||
|
|
||||||
if (points.size() >= 2)
|
bot->GetMotionMaster()->Clear();
|
||||||
{
|
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
|
||||||
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.
|
G3D::Vector3 const& last = points.back();
|
||||||
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
float totalDist = 0.f;
|
||||||
botAI->DoSpecificAction("check mount state", Event(), true);
|
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);
|
||||||
|
|
||||||
// Bulk dispatch: hand the full waypoint chain to the
|
std::vector<WorldPosition> wpts;
|
||||||
// motion master via MoveSplinePath. Motion master plays
|
wpts.reserve(points.size());
|
||||||
// every point in sequence — no per-tick re-dispatching.
|
for (auto const& pt : points)
|
||||||
bot->GetMotionMaster()->Clear();
|
wpts.emplace_back(bot->GetMapId(), pt.x, pt.y, pt.z);
|
||||||
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
|
lastMove.setPath(TravelPath(wpts));
|
||||||
|
|
||||||
// Update LastMovement to the chain endpoint so spline-
|
return true;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Probe failed or didn't progress — emit visibility whisper so
|
// Travel-node graph fallback — fires when the probe didn't reach
|
||||||
// the user can see WHY mmap didn't dispatch.
|
// dest within PROBE_REACH and the graph is enabled.
|
||||||
|
if (sPlayerbotAIConfig.enableTravelNodes)
|
||||||
{
|
{
|
||||||
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
|
StartTravelPlan(dest);
|
||||||
(dest.GetExactDist(probe.back().GetPositionX(),
|
if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
|
|
||||||
if (!probeProgressed)
|
|
||||||
{
|
{
|
||||||
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
|
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
|
||||||
EmitDebugMove("MoveFar", reason,
|
bot->GetName(), dis);
|
||||||
dest.GetPositionX(), dest.GetPositionY(),
|
EmitDebugMove("MoveFar", "travelplan",
|
||||||
dest.GetPositionZ());
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
|
return UpdateTravelPlan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty / non-progressing path falls back to dispatching the
|
// Final fallback: dispatch the destination as a single waypoint.
|
||||||
// destination as a single waypoint. Spline only when target is
|
// MoveTo's MovePoint(generatePath=true) lets the engine resolve the
|
||||||
// line-of-sight: dispatching a straight line through walls
|
// local route via PathGenerator. Nothing dispatched if MoveTo refuses.
|
||||||
// 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.
|
|
||||||
}
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,16 +76,6 @@ 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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user