mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
refactor(Core/RPG): Remove MoveFarTo loop-breaker (cmangos has no equivalent)
This commit is contained in:
parent
ab8524a91d
commit
9f8d0b3a74
@ -98,12 +98,8 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
float distFromBotToBack = bot->GetExactDist(&lastBack);
|
float distFromBotToBack = bot->GetExactDist(&lastBack);
|
||||||
if (lastBack.distance(dest) < maxDistChange && distFromBotToBack > 10.0f)
|
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",
|
EmitDebugMove("MoveFar", "reuse",
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), fails);
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,90 +115,37 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
//
|
//
|
||||||
// 2. Long-distance move (>= nodeFirstDis) and travel nodes
|
// 2. Long-distance move (>= nodeFirstDis) and travel nodes
|
||||||
// enabled: try the node graph FIRST. The graph holds
|
// enabled: try the node graph FIRST. The graph holds
|
||||||
// curated waypoints that avoid known bad terrain (fence
|
// curated waypoints that avoid known bad terrain.
|
||||||
// edges, off-mesh holes, etc.); the chained mmap probe
|
|
||||||
// doesn't and routinely picks "longest reachable mesh"
|
|
||||||
// over "geometrically toward dest".
|
|
||||||
//
|
//
|
||||||
// 3. If no node plan returned (or loop-breaker forced mmap):
|
// 3. If no node plan returned: run the 40-step chained mmap
|
||||||
// run the 40-step chained mmap probe and dispatch its
|
// probe and dispatch its waypoint chain.
|
||||||
// waypoint chain.
|
|
||||||
//
|
//
|
||||||
// 4. Empty / non-progressing probe: fall back to single-
|
// 4. Empty / non-progressing probe: fall back to single-
|
||||||
// waypoint spline at dest.
|
// waypoint spline at dest.
|
||||||
bool tryNodes = (dis >= nodeFirstDis && sPlayerbotAIConfig.enableTravelNodes);
|
bool tryNodes = (dis >= nodeFirstDis && sPlayerbotAIConfig.enableTravelNodes);
|
||||||
|
|
||||||
// Loop-breaker: count recent attempts of each strategy to this
|
// If a node plan is already active, ride it.
|
||||||
// dest. If 3 of one strategy → flip to the other. If both have
|
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
// 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)
|
|
||||||
{
|
|
||||||
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 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();
|
return UpdateTravelPlan();
|
||||||
|
|
||||||
// PRIORITY: try the travel-node graph FIRST when the move is
|
// PRIORITY: try the travel-node graph FIRST when the move is
|
||||||
// long enough to need it. Mirrors cmangos ResolveMovePath:
|
// long enough to need it.
|
||||||
// curated graph paths avoid the "longest reachable mesh"
|
if (tryNodes)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
StartTravelPlan(dest);
|
StartTravelPlan(dest);
|
||||||
if (botAI->rpgInfo.HasActiveTravelPlan())
|
if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
{
|
{
|
||||||
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f} | mmapFails={} nodeFails={} | flags={}{}{}",
|
LOG_INFO("playerbots", "[MoveFar] {} nodetravel | dis={:.0f}",
|
||||||
bot->GetName(), dis,
|
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));
|
|
||||||
EmitDebugMove("MoveFar", "travelplan",
|
EmitDebugMove("MoveFar", "travelplan",
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), fails);
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/true);
|
|
||||||
return UpdateTravelPlan();
|
return UpdateTravelPlan();
|
||||||
}
|
}
|
||||||
// Graph returned no plan — fall through to mmap probe.
|
// Graph returned no plan — fall through to mmap probe.
|
||||||
}
|
}
|
||||||
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
{
|
{
|
||||||
// We're forcing mmap (loop-breaker) or move dropped below
|
// Move dropped below node-first threshold — drop any leftover plan.
|
||||||
// node-first threshold — drop any leftover plan.
|
|
||||||
botAI->rpgInfo.ClearTravel();
|
botAI->rpgInfo.ClearTravel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,11 +157,8 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
// Walk the chained probe's full waypoint chain via MoveSplinePath.
|
// Walk the chained probe's full waypoint chain via MoveSplinePath.
|
||||||
// Handing the full waypoint vector to the motion master removes
|
// Handing the full waypoint vector to the motion master removes
|
||||||
// its discretion to introduce a straight-line shortcut between
|
// its discretion to introduce a straight-line shortcut between
|
||||||
// intermediate points — that shortcut produced the diagonal-
|
// intermediate points.
|
||||||
// through-air bug when we used MoveTo(endpoint) and let the
|
if (!probe.empty() && probe.size() >= 2)
|
||||||
// motion master replan.
|
|
||||||
// Skip when both routing strategies have failed 3 times each.
|
|
||||||
if (!probe.empty() && !bothExhausted && probe.size() >= 2)
|
|
||||||
{
|
{
|
||||||
WorldPosition stepDest = probe.back();
|
WorldPosition stepDest = probe.back();
|
||||||
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
|
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
|
||||||
@ -261,22 +201,10 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
points.resize(cutoff);
|
points.resize(cutoff);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={} | mmapFails={} nodeFails={} | flags={}{}{}",
|
LOG_INFO("playerbots", "[MoveFar] {} mmap-path | dis={:.0f} | endDist={:.0f} | wp={}",
|
||||||
bot->GetName(), dis, endDistToDest, (uint32)points.size(),
|
bot->GetName(), dis, endDistToDest, (uint32)points.size());
|
||||||
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",
|
EmitDebugMove("MoveFar", "mmap",
|
||||||
points.back().x, points.back().y, points.back().z, fails);
|
points.back().x, points.back().y, points.back().z);
|
||||||
}
|
|
||||||
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/false);
|
|
||||||
|
|
||||||
// Mount up if outdoors and not in combat.
|
// Mount up if outdoors and not in combat.
|
||||||
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
|
||||||
@ -316,23 +244,17 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Probe failed or didn't progress — emit visibility whisper so
|
// Probe failed or didn't progress — emit visibility whisper so
|
||||||
// the user can see WHY mmap didn't dispatch. Without this the
|
// the user can see WHY mmap didn't dispatch.
|
||||||
// do-quest action's `MoveRandomNear` nudge appears with no
|
|
||||||
// preceding MoveFar whisper, and the failure mode is invisible.
|
|
||||||
{
|
{
|
||||||
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
|
bool const probeProgressed = !probe.empty() && probe.size() >= 2 &&
|
||||||
(dest.GetExactDist(probe.back().GetPositionX(),
|
(dest.GetExactDist(probe.back().GetPositionX(),
|
||||||
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
|
probe.back().GetPositionY(), probe.back().GetPositionZ()) + 5.0f < disToDest);
|
||||||
if (!probeProgressed)
|
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";
|
char const* reason = (probe.empty() || probe.size() < 2) ? "mmap-empty" : "mmap-noprogress";
|
||||||
EmitDebugMove("MoveFar", reason,
|
EmitDebugMove("MoveFar", reason,
|
||||||
dest.GetPositionX(), dest.GetPositionY(),
|
dest.GetPositionX(), dest.GetPositionY(),
|
||||||
dest.GetPositionZ(), fails);
|
dest.GetPositionZ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,35 +264,19 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
// produces visible clipping/glitching. If LOS is blocked we
|
// produces visible clipping/glitching. If LOS is blocked we
|
||||||
// refuse and let UnstuckAction (5/10 min) catch the stuck.
|
// refuse and let UnstuckAction (5/10 min) catch the stuck.
|
||||||
bool const inLOS = bot->IsWithinLOS(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
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,
|
bot->GetName(), dis,
|
||||||
probe.empty() ? "y" : "n",
|
probe.empty() ? "y" : "n",
|
||||||
inLOS ? "y" : "n",
|
inLOS ? "y" : "n");
|
||||||
botAI->rpgInfo.CountRecentAttempts(dest, false),
|
|
||||||
botAI->rpgInfo.CountRecentAttempts(dest, true),
|
|
||||||
forceMmapOverNodes ? "F-mmap " : "",
|
|
||||||
forceNodesOverMmap ? "F-nodes " : "",
|
|
||||||
bothExhausted ? "EXHAUST " : "");
|
|
||||||
if (!inLOS)
|
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",
|
EmitDebugMove("MoveFar", "spline-blocked",
|
||||||
dest.GetPositionX(), dest.GetPositionY(),
|
dest.GetPositionX(), dest.GetPositionY(),
|
||||||
dest.GetPositionZ(), fails);
|
dest.GetPositionZ());
|
||||||
return false; // Refuse to dispatch a straight line through geometry.
|
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",
|
EmitDebugMove("MoveFar", "spline",
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), fails);
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
}
|
|
||||||
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/false);
|
|
||||||
// Same exact_waypoint=false rationale as the mmap branch — terrain-
|
// Same exact_waypoint=false rationale as the mmap branch — terrain-
|
||||||
// following spline, not a straight diagonal.
|
// 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(),
|
||||||
|
|||||||
@ -78,50 +78,6 @@ void NewRpgInfo::Reset()
|
|||||||
data = Idle{};
|
data = Idle{};
|
||||||
startT = getMSTime();
|
startT = getMSTime();
|
||||||
ClearTravel();
|
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()
|
NewRpgStatus NewRpgInfo::GetStatus()
|
||||||
|
|||||||
@ -83,22 +83,6 @@ struct NewRpgInfo
|
|||||||
bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
|
bool HasActiveTravelPlan() const { return travelPlan.IsActive(); }
|
||||||
void ClearTravel() { travelPlan.Reset(); }
|
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<
|
using RpgData = std::variant<
|
||||||
Idle,
|
Idle,
|
||||||
GoGrind,
|
GoGrind,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user