mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
Compare commits
12 Commits
cee4a067fa
...
b1fab01c48
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1fab01c48 | ||
|
|
9464d5cc98 | ||
|
|
de64d9780c | ||
|
|
7b60310c4e | ||
|
|
82bf33f3d4 | ||
|
|
21b68caffc | ||
|
|
9d2416a40a | ||
|
|
e892d10eaa | ||
|
|
cfd5012b13 | ||
|
|
edd27ee8e1 | ||
|
|
eb416ca7e5 | ||
|
|
bbd814347c |
@ -79,7 +79,11 @@ bool OpenLootAction::Execute(Event /*event*/)
|
||||
bool result = DoLoot(lootObject);
|
||||
if (result)
|
||||
{
|
||||
AI_VALUE(LootObjectStack*, "available loot")->Remove(lootObject.guid);
|
||||
// MarkCompleted (not Remove) — "add all loot" reads
|
||||
// "nearest corpses" without a lootable filter, so a plain
|
||||
// Remove lets the same corpse re-enter the stack on the next
|
||||
// tick. The completed set blocks re-add for ~5 min.
|
||||
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(lootObject.guid);
|
||||
context->GetValue<LootObject>("loot target")->Set(LootObject());
|
||||
}
|
||||
return result;
|
||||
@ -514,7 +518,7 @@ bool StoreLootAction::Execute(Event event)
|
||||
BroadcastHelper::BroadcastLootingItem(botAI, bot, proto);
|
||||
}
|
||||
|
||||
AI_VALUE(LootObjectStack*, "available loot")->Remove(guid);
|
||||
AI_VALUE(LootObjectStack*, "available loot")->MarkCompleted(guid);
|
||||
|
||||
// release loot
|
||||
WorldPacket* packet = new WorldPacket(CMSG_LOOT_RELEASE, 8);
|
||||
|
||||
@ -3334,33 +3334,18 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state)
|
||||
case PathNodeType::NODE_NODE:
|
||||
{
|
||||
// Batch consecutive walk points into one spline. Capped at
|
||||
// 20 points OR ~70y of accumulated distance — whichever
|
||||
// comes first. The distance cap gives the planner regular
|
||||
// re-evaluation points without committing the whole
|
||||
// remaining route up front; stepIdx advances exactly in
|
||||
// step with what's actually dispatched, so the next tick
|
||||
// picks up from the truncation point.
|
||||
// 20 points per dispatch as a cheap upper bound on per-tick
|
||||
// work; stepIdx advances exactly in step with what's
|
||||
// dispatched, so the next tick picks up from the cutoff.
|
||||
static constexpr uint32 MAX_SPLINE_POINTS = 20;
|
||||
static constexpr float MAX_BATCH_LENGTH = 70.0f;
|
||||
state.walkPoints.clear();
|
||||
float accumulated = 0.f;
|
||||
while (state.stepIdx < state.steps.size() && state.walkPoints.size() < MAX_SPLINE_POINTS)
|
||||
{
|
||||
const PathNodePoint& wp = state.steps[state.stepIdx];
|
||||
if (wp.type != PathNodeType::NODE_PATH && wp.type != PathNodeType::NODE_NODE)
|
||||
break;
|
||||
G3D::Vector3 next(wp.point.GetPositionX(), wp.point.GetPositionY(), wp.point.GetPositionZ());
|
||||
if (!state.walkPoints.empty())
|
||||
{
|
||||
accumulated += (next - state.walkPoints.back()).length();
|
||||
if (accumulated >= MAX_BATCH_LENGTH)
|
||||
{
|
||||
state.walkPoints.push_back(next);
|
||||
state.stepIdx++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
state.walkPoints.push_back(next);
|
||||
state.walkPoints.push_back(G3D::Vector3(wp.point.GetPositionX(),
|
||||
wp.point.GetPositionY(), wp.point.GetPositionZ()));
|
||||
state.stepIdx++;
|
||||
}
|
||||
|
||||
|
||||
@ -492,6 +492,15 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data)
|
||||
data.lastReachPOI = 0;
|
||||
data.pos = pos;
|
||||
data.objectiveIdx = -1;
|
||||
|
||||
// Drop the spline + lastPath that DoIncompleteQuest committed
|
||||
// to the now-completed objective. Without this, MoveFarTo on
|
||||
// the next tick hits the bot->isMoving() / lastPath-reuse
|
||||
// early-exits at the top of MoveFarTo and rides the stale
|
||||
// path instead of replanning toward the turn-in POI. (This
|
||||
// is what `.playerbot bot self` masks by recreating the AI.)
|
||||
bot->GetMotionMaster()->Clear();
|
||||
AI_VALUE(LastMovement&, "last movement").clear();
|
||||
}
|
||||
|
||||
if (data.pos == WorldPosition())
|
||||
|
||||
@ -73,21 +73,23 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
{
|
||||
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 — route commitment across combat
|
||||
// interruptions. If the cached path's endpoint is still close
|
||||
// (within 10%) to the new dest AND bot is mid-flight toward it
|
||||
// (>10y away AND currently moving), reuse silently. The
|
||||
// bot->isMoving() guard prevents reuse from short-circuiting
|
||||
// when bot is stopped between dispatches — in that case we
|
||||
// MUST fall through to dispatch the next leg, not return true
|
||||
// and stand still.
|
||||
// 10% lastPath reuse — if the cached path's endpoint is still
|
||||
// close (within 10%) to the new dest, trim the cached path to
|
||||
// the bot's current position via makeShortCut and re-dispatch.
|
||||
// Mirrors cmangos ResolveMovePath: per-tick re-dispatch of the
|
||||
// (trimmed) last path keeps the bot on-route after interrupts
|
||||
// (knockback, combat, manual move) without needing a full replan.
|
||||
{
|
||||
LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement");
|
||||
if (bot->isMoving() && !lastMove.lastPath.empty())
|
||||
if (!lastMove.lastPath.empty())
|
||||
{
|
||||
WorldPosition lastBack = lastMove.lastPath.getBack();
|
||||
if (lastBack.GetMapId() == dest.GetMapId())
|
||||
@ -97,13 +99,47 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
float distFromBotToBack = bot->GetExactDist(&lastBack);
|
||||
if (lastBack.distance(dest) < maxDistChange && distFromBotToBack > 10.0f)
|
||||
{
|
||||
char fails[32];
|
||||
snprintf(fails, sizeof(fails), "mF=%d nF=%d",
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true));
|
||||
EmitDebugMove("MoveFar", "reuse",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), fails);
|
||||
return true;
|
||||
WorldPosition botPos(bot);
|
||||
lastMove.lastPath.makeShortCut(botPos, sPlayerbotAIConfig.reactDistance, bot);
|
||||
|
||||
// makeShortCut may clear the path if the bot drifted
|
||||
// too far off (>reactDistance from any waypoint). In
|
||||
// that case fall through to fresh planning.
|
||||
if (lastMove.lastPath.empty())
|
||||
{
|
||||
EmitDebugMove("MoveFar", "reuse-trim-failed",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
}
|
||||
if (!lastMove.lastPath.empty())
|
||||
{
|
||||
std::vector<WorldPosition> const& pts = lastMove.lastPath.getPointPath();
|
||||
if (pts.size() >= 2)
|
||||
{
|
||||
Movement::PointsArray points;
|
||||
points.reserve(pts.size());
|
||||
for (auto const& wp : pts)
|
||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||
for (auto& pt : points)
|
||||
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
||||
bot->GetMotionMaster()->Clear();
|
||||
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
|
||||
|
||||
G3D::Vector3 const& last = points.back();
|
||||
float totalChainDist = 0.f;
|
||||
for (size_t i = 1; i < points.size(); ++i)
|
||||
totalChainDist += (points[i] - points[i - 1]).length();
|
||||
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
|
||||
uint32 expectedMs = static_cast<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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -118,90 +154,47 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
//
|
||||
// 2. Long-distance move (>= nodeFirstDis) and travel nodes
|
||||
// enabled: try the node graph FIRST. The graph holds
|
||||
// curated waypoints that avoid known bad terrain (fence
|
||||
// edges, off-mesh holes, etc.); the chained mmap probe
|
||||
// doesn't and routinely picks "longest reachable mesh"
|
||||
// over "geometrically toward dest".
|
||||
// curated waypoints that avoid known bad terrain.
|
||||
//
|
||||
// 3. If no node plan returned (or loop-breaker forced mmap):
|
||||
// run the 40-step chained mmap probe and dispatch its
|
||||
// waypoint chain.
|
||||
// 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);
|
||||
|
||||
// Loop-breaker: count recent attempts of each strategy to this
|
||||
// dest. If 3 of one strategy → flip to the other. If both have
|
||||
// failed 3 times each → both exhausted; fall through to
|
||||
// MoveFar:spline and rely on UnstuckAction (5/10 min) for the
|
||||
// eventual hearthstone-out. Without the "both exhausted" branch
|
||||
// we'd flip-flop forever as the buffer evicts.
|
||||
bool forceMmapOverNodes = false; // 3 nodes failed -> try mmap
|
||||
bool forceNodesOverMmap = false; // 3 mmap failed -> try nodes
|
||||
bool bothExhausted = false;
|
||||
if (tryNodes)
|
||||
// If a node plan is already active, ride it — but only if its
|
||||
// destination still matches the requested dest. Otherwise the
|
||||
// old plan (e.g. built toward a quest objective POI) would keep
|
||||
// driving the bot after the caller switched targets (e.g. to a
|
||||
// turn-in NPC). cmangos's ResolveMovePath dodges this by being
|
||||
// stateless; we have a long-lived plan flag, so check explicitly.
|
||||
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
|
||||
{
|
||||
int nodeFails = botAI->rpgInfo.CountRecentAttempts(dest, /*wasNodeTravel=*/true);
|
||||
int mmapFails = botAI->rpgInfo.CountRecentAttempts(dest, /*wasNodeTravel=*/false);
|
||||
|
||||
if (nodeFails >= 3 && mmapFails >= 3)
|
||||
bothExhausted = true; // give up, spline at dest
|
||||
else if (nodeFails >= 3)
|
||||
forceMmapOverNodes = true;
|
||||
else if (mmapFails >= 3)
|
||||
forceNodesOverMmap = true;
|
||||
|
||||
if (forceMmapOverNodes || forceNodesOverMmap || bothExhausted)
|
||||
{
|
||||
// Drop the in-flight plan if any; we're about to flip
|
||||
// (or give up). Buffer is intentionally NOT cleared so
|
||||
// we remember which strategies have already been tried
|
||||
// — otherwise we'd flip-flop indefinitely as the buffer
|
||||
// evicts old entries.
|
||||
if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||
botAI->rpgInfo.ClearTravel();
|
||||
}
|
||||
if (botAI->rpgInfo.travelPlan.destination.distance(dest) > 10.0f)
|
||||
botAI->rpgInfo.ClearTravel();
|
||||
else
|
||||
return UpdateTravelPlan();
|
||||
}
|
||||
|
||||
// If a node plan is already active, ride it. The plan executor
|
||||
// owns its own per-step transitions.
|
||||
if (tryNodes && !forceMmapOverNodes && !bothExhausted && botAI->rpgInfo.HasActiveTravelPlan())
|
||||
return UpdateTravelPlan();
|
||||
|
||||
// PRIORITY: try the travel-node graph FIRST when the move is
|
||||
// long enough to need it. Mirrors cmangos ResolveMovePath:
|
||||
// curated graph paths avoid the "longest reachable mesh"
|
||||
// failure mode of the raw chained mmap probe (e.g. routing
|
||||
// a bot up a tree because the wooden road extends 191y while
|
||||
// the leftward terrain has a navmesh seam).
|
||||
if (tryNodes && !forceMmapOverNodes && !bothExhausted)
|
||||
// long enough to need it.
|
||||
if (tryNodes)
|
||||
{
|
||||
StartTravelPlan(dest);
|
||||
if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||
{
|
||||
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f} | mmapFails={} nodeFails={} | flags={}{}{}",
|
||||
bot->GetName(), dis,
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true),
|
||||
forceMmapOverNodes ? "F-mmap " : "",
|
||||
forceNodesOverMmap ? "F-nodes " : "",
|
||||
bothExhausted ? "EXHAUST " : "");
|
||||
char fails[32];
|
||||
snprintf(fails, sizeof(fails), "mF=%d nF=%d",
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true));
|
||||
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
|
||||
bot->GetName(), dis);
|
||||
EmitDebugMove("MoveFar", "travelplan",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), fails);
|
||||
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/true);
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
return UpdateTravelPlan();
|
||||
}
|
||||
// Graph returned no plan — fall through to mmap probe.
|
||||
}
|
||||
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||
{
|
||||
// We're forcing mmap (loop-breaker) or move dropped below
|
||||
// node-first threshold — drop any leftover plan.
|
||||
// Move dropped below node-first threshold — drop any leftover plan.
|
||||
botAI->rpgInfo.ClearTravel();
|
||||
}
|
||||
|
||||
@ -210,14 +203,67 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
WorldPosition botPos(bot);
|
||||
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
||||
|
||||
// Regression guard (cmangos ResolveMovePath parity): if a cached
|
||||
// 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");
|
||||
if (!lastMove.lastPath.empty() && !probe.empty() && probe.size() >= 2)
|
||||
{
|
||||
WorldPosition lastBack = lastMove.lastPath.getBack();
|
||||
if (lastBack.GetMapId() == dest.GetMapId())
|
||||
{
|
||||
float cachedToDest = lastBack.distance(dest);
|
||||
float probeToDest = dest.GetExactDist(probe.back().GetPositionX(),
|
||||
probe.back().GetPositionY(),
|
||||
probe.back().GetPositionZ());
|
||||
if (cachedToDest <= probeToDest)
|
||||
{
|
||||
WorldPosition botPosNow(bot);
|
||||
lastMove.lastPath.makeShortCut(botPosNow, sPlayerbotAIConfig.reactDistance, bot);
|
||||
if (!lastMove.lastPath.empty())
|
||||
{
|
||||
std::vector<WorldPosition> const& pts = lastMove.lastPath.getPointPath();
|
||||
if (pts.size() >= 2)
|
||||
{
|
||||
Movement::PointsArray points;
|
||||
points.reserve(pts.size());
|
||||
for (auto const& wp : pts)
|
||||
points.emplace_back(wp.GetPositionX(), wp.GetPositionY(), wp.GetPositionZ());
|
||||
for (auto& pt : points)
|
||||
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
|
||||
bot->GetMotionMaster()->Clear();
|
||||
bot->GetMotionMaster()->MoveSplinePath(&points, FORCED_MOVEMENT_RUN);
|
||||
|
||||
G3D::Vector3 const& last = points.back();
|
||||
float totalChainDist = 0.f;
|
||||
for (size_t i = 1; i < points.size(); ++i)
|
||||
totalChainDist += (points[i] - points[i - 1]).length();
|
||||
float speed = std::max(bot->GetSpeed(MOVE_RUN), 0.1f);
|
||||
uint32 expectedMs = static_cast<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 — that shortcut produced the diagonal-
|
||||
// through-air bug when we used MoveTo(endpoint) and let the
|
||||
// motion master replan.
|
||||
// Skip when both routing strategies have failed 3 times each.
|
||||
if (!probe.empty() && !bothExhausted && probe.size() >= 2)
|
||||
// intermediate points.
|
||||
if (!probe.empty() && probe.size() >= 2)
|
||||
{
|
||||
WorldPosition stepDest = probe.back();
|
||||
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
|
||||
@ -236,46 +282,10 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
|
||||
if (points.size() >= 2)
|
||||
{
|
||||
// Cap dispatched path length at ~70y. MoveFarTo's
|
||||
// early-exit (top of function) lets the active spline
|
||||
// run until bot is within 10y of its endpoint, then
|
||||
// replans from the new position. Capping per-dispatch
|
||||
// distance gives the planner regular re-evaluation
|
||||
// points without the per-tick replan cost of fully
|
||||
// unbounded chunks.
|
||||
{
|
||||
constexpr float maxDispatchLength = 70.0f;
|
||||
float accumulated = 0.f;
|
||||
size_t cutoff = points.size();
|
||||
for (size_t i = 1; i < points.size(); ++i)
|
||||
{
|
||||
accumulated += (points[i] - points[i - 1]).length();
|
||||
if (accumulated >= maxDispatchLength)
|
||||
{
|
||||
cutoff = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cutoff < points.size())
|
||||
points.resize(cutoff);
|
||||
}
|
||||
|
||||
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={} | mmapFails={} nodeFails={} | flags={}{}{}",
|
||||
bot->GetName(), dis, endDistToDest, (uint32)points.size(),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true),
|
||||
forceMmapOverNodes ? "F-mmap " : "",
|
||||
forceNodesOverMmap ? "F-nodes " : "",
|
||||
bothExhausted ? "EXHAUST " : "");
|
||||
{
|
||||
char fails[32];
|
||||
snprintf(fails, sizeof(fails), "mF=%d nF=%d",
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true));
|
||||
EmitDebugMove("MoveFar", "mmap",
|
||||
points.back().x, points.back().y, points.back().z, fails);
|
||||
}
|
||||
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/false);
|
||||
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={}",
|
||||
bot->GetName(), dis, endDistToDest, (uint32)points.size());
|
||||
EmitDebugMove("MoveFar", "mmap",
|
||||
points.back().x, points.back().y, points.back().z);
|
||||
|
||||
// Mount up if outdoors and not in combat.
|
||||
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
||||
@ -315,23 +325,17 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
}
|
||||
|
||||
// Probe failed or didn't progress — emit visibility whisper so
|
||||
// the user can see WHY mmap didn't dispatch. Without this the
|
||||
// do-quest action's `MoveRandomNear` nudge appears with no
|
||||
// preceding MoveFar whisper, and the failure mode is invisible.
|
||||
// the user can see WHY mmap didn't dispatch.
|
||||
{
|
||||
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
|
||||
(dest.GetExactDist(probe.back().GetPositionX(),
|
||||
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
|
||||
if (!probeProgressed)
|
||||
{
|
||||
char fails[32];
|
||||
snprintf(fails, sizeof(fails), "mF=%d nF=%d",
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true));
|
||||
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
|
||||
EmitDebugMove("MoveFar", reason,
|
||||
dest.GetPositionX(), dest.GetPositionY(),
|
||||
dest.GetPositionZ(), fails);
|
||||
dest.GetPositionZ());
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,35 +345,19 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
||||
// 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={} | mmapFails={} nodeFails={} | flags={}{}{}",
|
||||
LOG_INFO("playerbots", "[MoveFar] {} spline | dis={:.0f} | probe.empty={} | LOS={}",
|
||||
bot->GetName(), dis,
|
||||
probe.empty() ? "y" : "n",
|
||||
inLOS ? "y" : "n",
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true),
|
||||
forceMmapOverNodes ? "F-mmap " : "",
|
||||
forceNodesOverMmap ? "F-nodes " : "",
|
||||
bothExhausted ? "EXHAUST " : "");
|
||||
inLOS ? "y" : "n");
|
||||
if (!inLOS)
|
||||
{
|
||||
char fails[32];
|
||||
snprintf(fails, sizeof(fails), "mF=%d nF=%d",
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true));
|
||||
EmitDebugMove("MoveFar", "spline-blocked",
|
||||
dest.GetPositionX(), dest.GetPositionY(),
|
||||
dest.GetPositionZ(), fails);
|
||||
dest.GetPositionZ());
|
||||
return false; // Refuse to dispatch a straight line through geometry.
|
||||
}
|
||||
{
|
||||
char fails[32];
|
||||
snprintf(fails, sizeof(fails), "mF=%d nF=%d",
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
||||
botAI->rpgInfo.CountRecentAttempts(dest, true));
|
||||
EmitDebugMove("MoveFar", "spline",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), fails);
|
||||
}
|
||||
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/false);
|
||||
EmitDebugMove("MoveFar", "spline",
|
||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||
// Same exact_waypoint=false rationale as the mmap branch — terrain-
|
||||
// following spline, not a straight diagonal.
|
||||
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
||||
@ -473,6 +461,7 @@ bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority,
|
||||
}
|
||||
}
|
||||
|
||||
EmitDebugMove("MoveRandomNear", "all-fail", x, y, z);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -78,50 +78,6 @@ void NewRpgInfo::Reset()
|
||||
data = Idle{};
|
||||
startT = getMSTime();
|
||||
ClearTravel();
|
||||
// recentMoveFarAttempts is intentionally NOT cleared. Reset() runs
|
||||
// on every state change (ChangeToDoQuest, ChangeToIdle, etc.) and
|
||||
// the do-quest action oscillates through transitions during a
|
||||
// failure cycle — wiping the deque here would prevent the
|
||||
// MoveFarTo loop-breaker (nF >= 3 AND mF >= 3 → bothExhausted)
|
||||
// from converging. CountRecentAttempts already filters by
|
||||
// destination (within 10y), so stale entries for previous quests
|
||||
// don't affect new ones.
|
||||
}
|
||||
|
||||
void NewRpgInfo::RecordMoveFarAttempt(WorldPosition const& dest, bool wasNodeTravel)
|
||||
{
|
||||
// Cap at 6 (3 node + 3 mmap). The loop-breaker in MoveFarTo
|
||||
// requires nF >= 3 AND mF >= 3 to declare bothExhausted. Each
|
||||
// MoveFarTo failure cycle records BOTH a node attempt and a mmap
|
||||
// attempt, so a single 3-cap deque would pop the older type
|
||||
// before its count reached 3, structurally preventing
|
||||
// bothExhausted from triggering.
|
||||
if (recentMoveFarAttempts.size() >= 6)
|
||||
recentMoveFarAttempts.pop_front();
|
||||
MoveFarAttempt a;
|
||||
a.dest = dest;
|
||||
a.wasNodeTravel = wasNodeTravel;
|
||||
a.timestamp = getMSTime();
|
||||
recentMoveFarAttempts.push_back(a);
|
||||
}
|
||||
|
||||
int NewRpgInfo::CountRecentAttempts(WorldPosition const& dest, bool wasNodeTravel) const
|
||||
{
|
||||
int count = 0;
|
||||
for (auto const& a : recentMoveFarAttempts)
|
||||
{
|
||||
if (a.wasNodeTravel != wasNodeTravel)
|
||||
continue;
|
||||
// Treat destinations within 10y as "same dest" — small jitter
|
||||
// from quest objective re-resolution shouldn't reset the loop
|
||||
// detector.
|
||||
if (a.dest.GetMapId() != dest.GetMapId())
|
||||
continue;
|
||||
if (a.dest.GetExactDist2dSq(&dest) > 10.0f * 10.0f)
|
||||
continue;
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
NewRpgStatus NewRpgInfo::GetStatus()
|
||||
|
||||
@ -83,22 +83,6 @@ struct NewRpgInfo
|
||||
bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
|
||||
void ClearTravel() { travelPlan.Reset(); }
|
||||
|
||||
// MoveFar attempt history. Records the last 3 path commits
|
||||
// (node plan or mmap) so MoveFarTo can detect when the same
|
||||
// dest + strategy has failed repeatedly and force the
|
||||
// alternative routing this tick. Breaks deterministic-loop
|
||||
// scenarios where the chained probe (or node graph) keeps
|
||||
// returning the same dead-end path.
|
||||
struct MoveFarAttempt
|
||||
{
|
||||
WorldPosition dest; // requested destination
|
||||
bool wasNodeTravel{false}; // true=node plan, false=mmap/spline
|
||||
uint32 timestamp{0};
|
||||
};
|
||||
std::deque<MoveFarAttempt> recentMoveFarAttempts;
|
||||
void RecordMoveFarAttempt(WorldPosition const& dest, bool wasNodeTravel);
|
||||
int CountRecentAttempts(WorldPosition const& dest, bool wasNodeTravel) const;
|
||||
|
||||
using RpgData = std::variant<
|
||||
Idle,
|
||||
GoGrind,
|
||||
|
||||
@ -362,6 +362,13 @@ bool LootObject::IsLootPossible(Player* bot)
|
||||
|
||||
bool LootObjectStack::Add(ObjectGuid guid)
|
||||
{
|
||||
// expire old completed entries so a despawn/respawn with a reused
|
||||
// guid can still be looted later
|
||||
completedLoot.shrink(time(nullptr) - 300);
|
||||
|
||||
if (completedLoot.find(guid) != completedLoot.end())
|
||||
return false;
|
||||
|
||||
if (availableLoot.size() >= MAX_LOOT_OBJECT_COUNT)
|
||||
{
|
||||
availableLoot.shrink(time(nullptr) - 30);
|
||||
@ -385,7 +392,17 @@ void LootObjectStack::Remove(ObjectGuid guid)
|
||||
availableLoot.erase(i);
|
||||
}
|
||||
|
||||
void LootObjectStack::Clear() { availableLoot.clear(); }
|
||||
void LootObjectStack::MarkCompleted(ObjectGuid guid)
|
||||
{
|
||||
Remove(guid);
|
||||
completedLoot.insert(guid);
|
||||
}
|
||||
|
||||
void LootObjectStack::Clear()
|
||||
{
|
||||
availableLoot.clear();
|
||||
completedLoot.clear();
|
||||
}
|
||||
|
||||
bool LootObjectStack::CanLoot(float maxDistance)
|
||||
{
|
||||
|
||||
@ -76,6 +76,7 @@ public:
|
||||
|
||||
bool Add(ObjectGuid guid);
|
||||
void Remove(ObjectGuid guid);
|
||||
void MarkCompleted(ObjectGuid guid);
|
||||
void Clear();
|
||||
bool CanLoot(float maxDistance);
|
||||
LootObject GetLoot(float maxDistance = 0);
|
||||
@ -85,6 +86,9 @@ private:
|
||||
|
||||
Player* bot;
|
||||
LootTargetList availableLoot;
|
||||
// Guids we already opened loot on; blocks "add all loot" from
|
||||
// re-adding the same corpse before it despawns.
|
||||
LootTargetList completedLoot;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@ -678,7 +678,7 @@ void TravelNode::print([[maybe_unused]] bool printFailed)
|
||||
}
|
||||
|
||||
// Attempts to move ahead of the path.
|
||||
bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist)
|
||||
bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot)
|
||||
{
|
||||
if (GetPath().empty())
|
||||
return false;
|
||||
@ -691,10 +691,10 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist)
|
||||
|
||||
for (auto& p : fullPath) // cycle over the full path
|
||||
{
|
||||
// if (p.point.GetMapId() != startPos.GetMapId())
|
||||
// continue;
|
||||
|
||||
if (p.point.GetMapId() == startPos.GetMapId())
|
||||
// Walkability filter (cmangos parity): portals/transports/taxis
|
||||
// aren't valid anchor points — picking one as the new start of
|
||||
// the trimmed path would leave the bot anchored on a hop.
|
||||
if (p.point.GetMapId() == startPos.GetMapId() && p.isWalkable())
|
||||
{
|
||||
float curDist = p.point.sqDistance(startPos);
|
||||
|
||||
@ -737,8 +737,10 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist)
|
||||
|
||||
WorldPosition beginPos = newPath.begin()->point;
|
||||
|
||||
// The old path seems to be the best.
|
||||
if (beginPos.distance(firstNode) < sPlayerbotAIConfig.tooCloseDistance)
|
||||
// The old path seems to be the best — either the closest walkable
|
||||
// point IS the original front, or it's within tooCloseDistance.
|
||||
if (newPath.front() == fullPath.front() ||
|
||||
beginPos.distance(firstNode) < sPlayerbotAIConfig.tooCloseDistance)
|
||||
return false;
|
||||
|
||||
// We are (nearly) on the new path. Just follow the rest.
|
||||
@ -748,7 +750,11 @@ bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist)
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<WorldPosition> toPath = startPos.getPathTo(beginPos, nullptr);
|
||||
// Pass the bot into getPathTo so PathGenerator picks up its
|
||||
// collision / swimming / flying flags. cmangos parity — passing
|
||||
// nullptr here drops to a default mover and can produce a path
|
||||
// the bot itself can't actually walk.
|
||||
std::vector<WorldPosition> toPath = startPos.getPathTo(beginPos, bot);
|
||||
|
||||
// We can not reach the new begin position. Follow the complete path.
|
||||
if (!beginPos.isPathTo(toPath))
|
||||
|
||||
@ -427,6 +427,16 @@ struct PathNodePoint
|
||||
WorldPosition point;
|
||||
PathNodeType type = PathNodeType::NODE_PATH;
|
||||
uint32 entry = 0;
|
||||
|
||||
bool operator==(const PathNodePoint& p1) const
|
||||
{
|
||||
return point == p1.point && type == p1.type && entry == p1.entry;
|
||||
}
|
||||
// A "walkable" node is one we traverse on foot. Portals/transports/
|
||||
// taxis/teleports are entry/exit hops, not points to anchor a
|
||||
// shortcut on. Used by makeShortCut to skip them when picking the
|
||||
// closest-point-on-path to the bot.
|
||||
bool isWalkable() const { return (uint8)type <= (uint8)PathNodeType::NODE_NODE; }
|
||||
};
|
||||
|
||||
// A complete list of points the bots has to walk to or teleport to.
|
||||
@ -481,7 +491,7 @@ public:
|
||||
return retVec;
|
||||
}
|
||||
|
||||
bool makeShortCut(WorldPosition startPos, float maxDist);
|
||||
bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr);
|
||||
|
||||
std::ostringstream const print();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user