Compare commits

...

8 Commits

15 changed files with 147841 additions and 147673 deletions

View File

@ -330,7 +330,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle,
return false; return false;
} }
bool generatePath = !bot->IsFlying() && !bot->isSwimming(); bool generatePath = !bot->IsFlying() && !bot->isSwimming() && !bot->IsInWater();
bool disableMoveSplinePath = bool disableMoveSplinePath =
sPlayerbotAIConfig.disableMoveSplinePath >= 2 || sPlayerbotAIConfig.disableMoveSplinePath >= 2 ||
(sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground()); (sPlayerbotAIConfig.disableMoveSplinePath == 1 && bot->InBattleground());
@ -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

@ -9,23 +9,6 @@
LastMovement::LastMovement() { clear(); } 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() void LastMovement::clear()
{ {
lastMoveShort = WorldPosition(); lastMoveShort = WorldPosition();

View File

@ -27,20 +27,9 @@ class LastMovement
{ {
public: public:
LastMovement(); LastMovement();
LastMovement(LastMovement& other);
LastMovement& operator=(LastMovement const& other) LastMovement(LastMovement const&) = default;
{ LastMovement& operator=(LastMovement const&) = default;
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;
};
void clear(); void clear();
@ -66,7 +55,6 @@ public:
MovementPriority priority; MovementPriority priority;
TravelPath lastPath; TravelPath lastPath;
time_t nextTeleport; time_t nextTeleport;
std::future<TravelPath> future;
}; };
class LastMovementValue : public ManualSetValue<LastMovement&> class LastMovementValue : public ManualSetValue<LastMovement&>
@ -75,7 +63,7 @@ public:
LastMovementValue(PlayerbotAI* botAI) : ManualSetValue<LastMovement&>(botAI, data) {} LastMovementValue(PlayerbotAI* botAI) : ManualSetValue<LastMovement&>(botAI, data) {}
private: private:
LastMovement data = LastMovement(); LastMovement data{};
}; };
class StayTimeValue : public ManualSetValue<time_t> class StayTimeValue : public ManualSetValue<time_t>

View File

@ -52,32 +52,23 @@ 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. Stop any spline
// MoveTo internally caps its stored delay at maxWaitForMove // still running and clear the cached path if it points to here
// (default 5s), but a long path (200+ yd routed around a // — otherwise the bot keeps gliding past dest. Mirrors cmangos
// mountain) takes 30+ seconds to walk. After 5s // MovementActions.cpp:1095-1106.
// 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"); float const totalDistance = bot->GetExactDist(dest);
if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId()) if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance)
{ {
float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ); LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
if (remaining > 10.0f) if (!lastMove.lastPath.empty() &&
lastMove.lastPath.getBack().distance(dest) <= totalDistance)
{ {
EmitDebugMove("MoveFar", "spline-plan", lastMove.clear();
lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ);
return true;
} }
bot->StopMoving();
return false;
} }
} }
@ -119,24 +110,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.
@ -145,71 +119,62 @@ 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): // Mirrors cmangos ResolveMovePath: pick travel-node graph for
// long-distance / cross-map moves; mmap probe for short paths.
// //
// 1. Active node plan? Ride it. // 1. needsLongPath = cross-map OR dis > sightDistance
// // 2. If needsLongPath && travel nodes enabled → graph plan
// 2. Long-distance move (>= nodeFirstDis) and travel nodes // 3. Else 40-step chained mmap probe
// enabled: try the node graph FIRST. The graph holds // 4. Regression guard against the new probe — ride cached path
// curated waypoints that avoid known bad terrain. // if it ends at least as close to dest
// // 5. Dispatch probe (no reach gate — partial probes still make
// 3. If no node plan returned: run the 40-step chained mmap // progress and the next tick replans)
// probe and dispatch its waypoint chain. // 6. Empty probe → cmangos addPoint(endPosition) fallback:
// // single-waypoint MoveTo so PathGenerator resolves it.
// 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 bool const needsLongPath = (bot->GetMapId() != dest.GetMapId()) ||
// destination still matches the requested dest. Otherwise the (dis > sPlayerbotAIConfig.sightDistance);
// old plan (e.g. built toward a quest objective POI) would keep
// driving the bot after the caller switched targets (e.g. to a // needsLongPath branch — graph routing only. cmangos's getFullPath
// turn-in NPC). cmangos's ResolveMovePath dodges this by being // already attempts a 40-step mmap probe internally before falling
// stateless; we have a long-lived plan flag, so check explicitly. // back to A* node routing, so we don't need a second probe here.
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan()) // If the graph yields no plan, fall straight to the single-point
// fallback (cmangos addPoint(endPosition)). BG gate per
// MovementActions.cpp:705.
if (needsLongPath)
{ {
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f) if (sPlayerbotAIConfig.enableTravelNodes && !bot->InBattleground())
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}", StartTravelPlan(dest);
bot->GetName(), dis); if (botAI->rpgInfo.HasActiveTravelPlan())
EmitDebugMove("MoveFar", "travelplan", {
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
return UpdateTravelPlan(); bot->GetName(), dis);
EmitDebugMove("MoveFar", "travelplan",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
return UpdateTravelPlan();
}
} }
// Graph returned no plan — fall through to mmap probe.
} // Long-path fallback. Cross-map can't be served by a same-map
else if (botAI->rpgInfo.HasActiveTravelPlan()) // straight-line spline — bail rather than splining toward
{ // unreachable coords.
// Move dropped below node-first threshold — drop any leftover plan. if (bot->GetMapId() != dest.GetMapId())
botAI->rpgInfo.ClearTravel(); 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 — fallback when the node graph // Short-path branch — 40-step chained mmap probe.
// 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.
// lastPath ends at least as close to dest as the new probe's
// 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)
@ -234,24 +199,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;
} }
} }
} }
@ -259,109 +207,248 @@ 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.
if (!probe.empty() && probe.size() >= 2) if (!probe.empty() && probe.size() >= 2)
{ {
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());
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());
for (auto const& wp : probe)
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())
botAI->DoSpecificAction("check mount state", Event(), true);
return DispatchPathPoints(dest, points, "mmap");
}
}
// Empty-probe fallback — cmangos addPoint(endPosition) + DispatchMovement.
// Build a 2-point straight-line path (bot → dest) and route it through
// DispatchPathPoints so it gets the same clip / underwater fixup /
// setPath / WaitForReach / teleport / mode-flip treatment as the
// chained-mmap branches. Trade-off vs the old MoveTo path: lose the
// engine's MovePoint(generatePath=true) local PathGenerator resolution.
// For the empty-probe case the long mmap probe already failed, so the
// engine's local resolution would likely fail too.
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");
}
bool NewRpgBaseAction::DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label)
{
if (points.size() < 2)
return false;
// Save the planner's path BEFORE clip/fixup mutations, so the
// next-tick reuse/regress branches see the original intent (not
// a clip-truncated tail). Mirrors cmangos MovementActions.cpp:1117 +
// 1147 — both saves happen before ClipPath at :1150 and the
// underwater fixup loop at :1170.
{
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));
}
// Push submerged waypoints to the water surface unless dest is itself underwater.
if (Map* map = bot->GetMap())
{
WorldPosition destWp = dest;
if (!destWp.isUnderWater())
{
for (auto& pt : points) 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={}", WorldPosition wp(dest.GetMapId(), pt.x, pt.y, pt.z);
bot->GetName(), dis, endDistToDest, (uint32)points.size()); if (wp.isUnderWater())
EmitDebugMove("MoveFar", "mmap", {
points.back().x, points.back().y, points.back().z); float surface = map->GetWaterLevel(pt.x, pt.y);
if (surface != INVALID_HEIGHT && surface > pt.z)
// Mount up if outdoors and not in combat. pt.z = surface;
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<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 for (auto& pt : points)
// the user can see WHY mmap didn't dispatch. bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// ClipPath — truncate the path at the first hostile creature within
// its own attack range, so the bot stops walking into pulls instead
// of marching past mobs and aggroing them all at once. Skipped while
// already in combat or dead. Mirrors cmangos TravelNode.cpp:1118-1203
// in simplified form (no transport, no hazard list).
if (botAI->GetState() != BOT_STATE_COMBAT && bot->IsAlive())
{ {
bool const probeProgressed = !probe.empty() && probe.size() >= 2 && GuidVector targets = AI_VALUE(GuidVector, "possible targets");
(dest.GetExactDist(probe.back().GetPositionX(), if (!targets.empty())
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
if (!probeProgressed)
{ {
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress"; size_t clipAt = points.size();
EmitDebugMove("MoveFar", reason, for (size_t i = 0; i < points.size() && clipAt == points.size(); ++i)
dest.GetPositionX(), dest.GetPositionY(), {
dest.GetPositionZ()); 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;
// LOS guard — mobs behind walls don't actually
// pull, so clipping there over-truncates. Mirrors
// cmangos TravelNode.cpp:1155.
if (!unit->IsWithinLOSInMap(bot))
continue;
clipAt = i;
break;
}
}
// 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());
} }
} }
// Empty / non-progressing path falls back to dispatching the // After clip, points may have collapsed to one element (mob right
// destination as a single waypoint. Spline only when target is // at start). Bail rather than dispatching a degenerate spline.
// line-of-sight: dispatching a straight line through walls if (points.size() < 2)
// produces visible clipping/glitching. If LOS is blocked we return false;
// refuse and let UnstuckAction (5/10 min) catch the stuck.
bool const inLOS = bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); G3D::Vector3 const& last = points.back();
LOG_INFO("playerbots", "[MoveFar] {} spline | dis={:.0f} | probe.empty={} | LOS={}",
bot->GetName(), dis, float totalDist = 0.f;
probe.empty() ? "y" : "n", for (size_t i = 1; i < points.size(); ++i)
inLOS ? "y" : "n"); totalDist += (points[i] - points[i - 1]).length();
if (!inLOS)
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
// Item 2 — inactive-bot teleport. When the path is longer than
// reactDistance and there's no real player around to witness, jump
// to the path tail and schedule a cooldown. Skips cosmetic walking
// for unobserved random bots. Player-owned (self) bots are excluded
// so testing/observed sessions always see the real walk.
if (sRandomPlayerbotMgr.IsRandomBot(bot))
{ {
EmitDebugMove("MoveFar", "spline-blocked", WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z);
dest.GetPositionX(), dest.GetPositionY(), time_t now = time(nullptr);
dest.GetPositionZ()); if (totalDist > sPlayerbotAIConfig.reactDistance &&
return false; // Refuse to dispatch a straight line through geometry. 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));
}
} }
EmitDebugMove("MoveFar", "spline",
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); // masterWalking — match the master's walk pace when they're nearby
// Same exact_waypoint=false rationale as the mmap branch — terrain- // and walking. Lets a follower bot trail at walk speed instead of
// following spline, not a straight diagonal. // sprinting past. No-op for masterless RPG bots. Mirrors cmangos
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), // MovementActions.cpp:1212-1221. (Flying-mount mode flip deferred —
false, false, false, false); // requires takeoff/landing infrastructure we haven't ported.)
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;
}
}
}
// Debug-move beacon — when the `debug move` strategy is active in
// non-combat, summon a visible creature at every waypoint (white
// glow, red glow on the tail). Lets the operator visually verify
// ClipPath truncation, underwater fixup, masterWalking pace, etc.
// Mirrors cmangos MovementActions.cpp:1152-1161.
if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT))
{
for (size_t i = 0; i < points.size(); ++i)
{
G3D::Vector3 const& p = points[i];
if (Creature* wp = bot->SummonCreature(2334, p.x, p.y, p.z, 0,
TEMPSUMMON_TIMED_DESPAWN, 10000))
{
bot->AddAura(246, wp);
if (i + 1 == points.size())
bot->AddAura(1130, wp);
}
}
}
// Pre-dispatch state cleanup. Mirrors cmangos MovementActions.cpp:1186-1194:
// - Clear any looping emote so the bot doesn't run-while-waving
// - Stand up unconditionally (eating/sitting clients ignore moves)
// - Interrupt any non-melee cast so the spline can begin
bot->ClearEmoteState();
if (!bot->IsStandState())
bot->SetStandState(UNIT_STAND_STATE_STAND);
if (bot->IsNonMeleeSpellCast(true))
bot->InterruptNonMeleeSpells(true);
// Item 7 — DispatchMovement two-step. cmangos calls
// mm.MovePoint(last) followed by mm.MovePath(points). In AC,
// MotionMaster::Mutate at MOTION_SLOT_ACTIVE replaces (not queues)
// the previous generator, so both calls in sequence collapse to
// whichever ran last. We skip the redundant MovePoint and dispatch
// the smooth waypoint path directly.
bot->GetMotionMaster()->Clear();
bot->GetMotionMaster()->MoveSplinePath(&points, moveMode);
EmitDebugMove("MoveFar", label, last.x, last.y, last.z);
// Item 6 — WaitForReach scheduling.
// waitDist = (totalDist > reactDistance) ? totalDist - 10 : totalDist
// duration = 1000 * (waitDist / runSpeed) + reactDelay
// capped at maxWaitForMove. The 10y leaves a small buffer so the
// AI tick can intervene before the path strictly finishes.
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)

View File

@ -76,19 +76,20 @@ 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();
// Dispatches a chained mmap path. Applies cmangos-parity tweaks:
// underwater fixup (push submerged waypoints to the surface unless
// dest is itself underwater), inactive-bot teleport (jump to the
// tail when no players are nearby and the path is longer than
// reactDistance), and the WaitForReach formula
// (1000 * dist/speed + reactDelay, capped at maxWaitForMove).
// Returns true if dispatched or teleported.
bool DispatchPathPoints(WorldPosition const& dest,
Movement::PointsArray& points,
char const* label);
}; };
#endif #endif

View File

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

View File

@ -721,6 +721,22 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY())); map->EnsureGridCreated(Acore::ComputeGridCoord(GetPositionX(), GetPositionY()));
} }
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 (filters, area costs). Mirrors cmangos
// WorldPosition.cpp:958 which threads one PathFinder 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 (PathGenerator.h:67). Without this, // Explicit-start overload (PathGenerator.h:67). Without this,
// CalculatePath(destX,destY,destZ) defaults to the unit's // CalculatePath(destX,destY,destZ) defaults to the unit's
// current position as start — which means every iteration of // current position as start — which means every iteration of
@ -729,16 +745,12 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
// never advances. With explicit start, each step extends from // never advances. With explicit start, each step extends from
// the previous step's endpoint, giving the 40-attempt walker // the previous step's endpoint, giving the 40-attempt walker
// its intended multi-tile reach. // its intended multi-tile reach.
PathGenerator path(pathUnit);
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)
delete tempCreature;
// PathType is a bitmask. Two things to handle: // PathType is a bitmask. Two things to handle:
// //
// 1. AC's PathGenerator can return INCOMPLETE | FARFROMPOLY_END // 1. AC's PathGenerator can return INCOMPLETE | FARFROMPOLY_END
@ -754,11 +766,50 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
// To match cmangos's intent (never silently dispatch a // To match cmangos's intent (never silently dispatch a
// geometry-ignoring shortcut), reject any path with the // geometry-ignoring shortcut), reject any path with the
// NOT_USING_PATH bit set. // NOT_USING_PATH bit set.
if ((type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) if (!(type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE)) ||
&& !(type & PATHFIND_NOT_USING_PATH)) (type & PATHFIND_NOT_USING_PATH))
return fromPointsArray(points); return {};
return {}; std::vector<WorldPosition> retvec = fromPointsArray(points);
// Underwater path-extension. Mirrors cmangos WorldPosition.cpp:997-1014.
// When PATHFIND_INCOMPLETE ends within 50y of dest and both endpoints
// are underwater with LOS between them, extend by one 5y step (or
// straight to dest if <5y). Lets bots traverse navmesh-poor water
// volumes the same way real swimmers do.
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)
@ -794,11 +845,50 @@ 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.
// Mirrors cmangos WorldPosition.cpp:1091-1096. Avoids the per-step
// allocation and lets Detour reuse internal scratch.
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++)
{ {
// 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. // Try to pathfind to this position.
subPath = getPathStepFrom(currentPos, bot); subPath = getPathStepFrom(currentPos, path);
// If we could not find a path return what we have now. // 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)
@ -815,6 +905,9 @@ std::vector<WorldPosition> WorldPosition::getPathFromPath(std::vector<WorldPosit
currentPos = subPath.back(); currentPos = subPath.back();
} }
if (tempCreature)
delete tempCreature;
return fullPath; return fullPath;
} }

View File

@ -20,6 +20,7 @@
class Creature; class Creature;
class GuidPosition; class GuidPosition;
class ObjectGuid; class ObjectGuid;
class PathGenerator;
class Quest; class Quest;
class Player; class Player;
class PlayerbotAI; class PlayerbotAI;
@ -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,31 @@ 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 — mirrors cmangos getFullPath (TravelNode.cpp:1887-1895).
if (botPos.fDist(destination) < MAX_PATHFINDING_DISTANCE && // 40-step chained probe from bot; if it gets within spellDistance of dest
botPos.GetMapId() == destination.GetMapId()) // we 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); std::vector<WorldPosition> probe = destination.getPathFromPath({botPos}, bot, 40);
plan.steps.addPoint(destination, PathNodeType::NODE_PATH); // Guard the degenerate case: getPathFromPath always returns at
return true; // 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)
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

@ -98,6 +98,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

@ -89,7 +89,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;