mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
feat(Core/RPG): MoveFarTo loop detection with strategy flip + grinding throttle
Per-bot ring buffer of last 3 path attempts on RpgInfo. When 3 mmap or 3 nodetravel attempts to the same dest fail, force the alternative routing strategy on the next tick. When both strategies have failed 3 times each (bothExhausted), fall through to MoveFar:spline rather than flip-flopping forever. Also drops the 10%-per-tick opportunistic combat engage during do-quest travel — the multiplier (0.20x) is the right knob; the random yield was overriding it and producing the 'still grinding too much while traveling' symptom.
This commit is contained in:
parent
3f078b7c97
commit
553c7739e8
@ -335,23 +335,16 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data)
|
|||||||
if (HasNearbyQuestMob(15.0f))
|
if (HasNearbyQuestMob(15.0f))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Occasional yield so attack-anything can pick off a passing
|
// Note: previously yielded ~10%/tick when any hostile was
|
||||||
// hostile. Gated on "hostile actually in range" so we don't
|
// within 25y. That overrode the do-quest multiplier in
|
||||||
// burn ticks yielding into nothing, and rate-limited so we
|
// practice (combined with bots getting aggroed on the way,
|
||||||
// don't fight every mob we walk past — multiplier still
|
// which ALSO bypasses the multiplier via combat engine) and
|
||||||
// dominates, this just opens an occasional window.
|
// bots ended up grinding their way to POIs instead of
|
||||||
GuidVector nearbyTargets = AI_VALUE(GuidVector, "possible targets");
|
// travelling. Quest-mob exception above is kept so we don't
|
||||||
for (ObjectGuid guid : nearbyTargets)
|
// walk past a quest target while gathering. Anything else
|
||||||
{
|
// hostile is the multiplier's job to throttle — and bots
|
||||||
Unit* u = botAI->GetUnit(guid);
|
// that DO get aggroed switch to combat engine where the
|
||||||
if (!u || !u->IsAlive())
|
// class strategy handles it.
|
||||||
continue;
|
|
||||||
if (bot->GetDistance(u) > 25.0f)
|
|
||||||
continue;
|
|
||||||
if (urand(0, 9) == 0) // 10% per tick when a hostile is in range
|
|
||||||
return false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MoveFarTo(data.pos))
|
if (MoveFarTo(data.pos))
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -110,9 +110,42 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
// stones" that aren't on the actual route.
|
// stones" that aren't on the actual route.
|
||||||
bool tryNodes = (dis >= nodeFirstDis && sPlayerbotAIConfig.enableTravelNodes);
|
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)
|
||||||
|
{
|
||||||
|
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
|
// If a node plan is already active, ride it. The plan executor
|
||||||
// owns its own per-step transitions.
|
// owns its own per-step transitions.
|
||||||
if (tryNodes && botAI->rpgInfo.HasActiveTravelPlan())
|
if (tryNodes && !forceMmapOverNodes && !bothExhausted && botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
return UpdateTravelPlan();
|
return UpdateTravelPlan();
|
||||||
|
|
||||||
// 40-step chained mmap probe (cmangos getPathFromPath, ported in
|
// 40-step chained mmap probe (cmangos getPathFromPath, ported in
|
||||||
@ -125,10 +158,13 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
std::vector<WorldPosition> probe = botPos.getPathTo(dest, bot);
|
||||||
bool probeReachesDest = dest.isPathTo(probe, sPlayerbotAIConfig.spellDistance);
|
bool probeReachesDest = dest.isPathTo(probe, sPlayerbotAIConfig.spellDistance);
|
||||||
|
|
||||||
if (tryNodes && !probeReachesDest)
|
bool wantNodes = (tryNodes && !forceMmapOverNodes && !bothExhausted)
|
||||||
|
&& (!probeReachesDest || forceNodesOverMmap);
|
||||||
|
if (wantNodes)
|
||||||
{
|
{
|
||||||
// Long-distance move and mmap couldn't get within spellDistance
|
// Long-distance move and either mmap couldn't get within
|
||||||
// of the destination — commit to the travel-node graph
|
// spellDistance OR we're forcing nodes after 3 failed mmap
|
||||||
|
// loops — commit to the travel-node graph
|
||||||
// (cmangos TravelNode.cpp:1907 buildPath branch).
|
// (cmangos TravelNode.cpp:1907 buildPath branch).
|
||||||
StartTravelPlan(dest);
|
StartTravelPlan(dest);
|
||||||
if (botAI->rpgInfo.HasActiveTravelPlan())
|
if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
@ -138,6 +174,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
// (TravelPlan:walk/segment/...) continue from the executor.
|
// (TravelPlan:walk/segment/...) continue from the executor.
|
||||||
EmitDebugMove("MoveFar:nodetravel",
|
EmitDebugMove("MoveFar:nodetravel",
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
|
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/true);
|
||||||
return UpdateTravelPlan();
|
return UpdateTravelPlan();
|
||||||
}
|
}
|
||||||
// else: graph returned no plan — fall through to mmap best-effort
|
// else: graph returned no plan — fall through to mmap best-effort
|
||||||
@ -145,7 +182,8 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
else if (botAI->rpgInfo.HasActiveTravelPlan())
|
||||||
{
|
{
|
||||||
// mmap probe is now close enough OR we crossed below the
|
// mmap probe is now close enough OR we crossed below the
|
||||||
// node-first threshold — drop any leftover plan from a prior tick.
|
// node-first threshold OR we're forcing mmap — drop any
|
||||||
|
// leftover plan from a prior tick.
|
||||||
botAI->rpgInfo.ClearTravel();
|
botAI->rpgInfo.ClearTravel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +193,10 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
// endpoint to MoveTo and let the motion master plan its own
|
// endpoint to MoveTo and let the motion master plan its own
|
||||||
// spline. Functionally equivalent across multiple ticks
|
// spline. Functionally equivalent across multiple ticks
|
||||||
// (incremental progress).
|
// (incremental progress).
|
||||||
if (!probe.empty())
|
// Skip when both routing strategies have failed 3 times each —
|
||||||
|
// the probe is deterministic so it'd just lead back to the same
|
||||||
|
// dead end. Fall through to spline at the dest.
|
||||||
|
if (!probe.empty() && !bothExhausted)
|
||||||
{
|
{
|
||||||
WorldPosition stepDest = probe.back();
|
WorldPosition stepDest = probe.back();
|
||||||
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
|
float endDistToDest = dest.GetExactDist(stepDest.GetPositionX(),
|
||||||
@ -164,6 +205,7 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
{
|
{
|
||||||
EmitDebugMove("MoveFar:mmap",
|
EmitDebugMove("MoveFar:mmap",
|
||||||
stepDest.GetPositionX(), stepDest.GetPositionY(), stepDest.GetPositionZ());
|
stepDest.GetPositionX(), stepDest.GetPositionY(), stepDest.GetPositionZ());
|
||||||
|
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/false);
|
||||||
return MoveTo(bot->GetMapId(), stepDest.GetPositionX(), stepDest.GetPositionY(),
|
return MoveTo(bot->GetMapId(), stepDest.GetPositionX(), stepDest.GetPositionY(),
|
||||||
stepDest.GetPositionZ(), false, false, false, true);
|
stepDest.GetPositionZ(), false, false, false, true);
|
||||||
}
|
}
|
||||||
@ -171,10 +213,11 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
|
|||||||
|
|
||||||
// cmangos MovementActions.cpp:720 — empty / non-progressing path
|
// cmangos MovementActions.cpp:720 — empty / non-progressing path
|
||||||
// falls back to dispatching the destination as a single waypoint.
|
// falls back to dispatching the destination as a single waypoint.
|
||||||
// Best-effort spline; stuck-recovery teleport (above) takes over
|
// Best-effort spline; UnstuckAction (5/10 min) is the eventual
|
||||||
// if this oscillates.
|
// catch if this loops forever.
|
||||||
EmitDebugMove("MoveFar:spline",
|
EmitDebugMove("MoveFar:spline",
|
||||||
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ());
|
||||||
|
botAI->rpgInfo.RecordMoveFarAttempt(dest, /*wasNodeTravel=*/false);
|
||||||
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
return MoveTo(dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
|
||||||
false, false, false, true);
|
false, false, false, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,37 @@ void NewRpgInfo::Reset()
|
|||||||
data = Idle{};
|
data = Idle{};
|
||||||
startT = getMSTime();
|
startT = getMSTime();
|
||||||
ClearTravel();
|
ClearTravel();
|
||||||
|
recentMoveFarAttempts.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewRpgInfo::RecordMoveFarAttempt(WorldPosition const& dest, bool wasNodeTravel)
|
||||||
|
{
|
||||||
|
if (recentMoveFarAttempts.size() >= 3)
|
||||||
|
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()
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
#ifndef _PLAYERBOT_NEWRPGINFO_H
|
#ifndef _PLAYERBOT_NEWRPGINFO_H
|
||||||
#define _PLAYERBOT_NEWRPGINFO_H
|
#define _PLAYERBOT_NEWRPGINFO_H
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
#include "Define.h"
|
#include "Define.h"
|
||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
#include "ObjectMgr.h"
|
#include "ObjectMgr.h"
|
||||||
@ -81,6 +83,23 @@ 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. Cmangos doesn't do this — they wait 5+ minutes
|
||||||
|
// for UnstuckAction. We're more aggressive here for UX.
|
||||||
|
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