fix(Core/Travel): Path-type bitmask, loop-breaker convergence, LaunchWalkSpline LOS-cull

This commit is contained in:
bash 2026-05-07 01:31:36 +02:00
parent 5077e01096
commit 83e9ad3a97
3 changed files with 23 additions and 36 deletions

View File

@ -3232,39 +3232,6 @@ bool MovementAction::LaunchWalkSpline(TravelPlan& state)
for (auto& pt : state.walkPoints)
bot->UpdateAllowedPositionZ(pt.x, pt.y, pt.z);
// Drop waypoints whose segment from the previous point crosses
// solid geometry. Z-snapping each point to ground is necessary
// but not sufficient — two ground-level waypoints A and B with a
// mountain between them produce a spline that linearly
// interpolates straight through the mountain. vmap LoS check on
// each segment catches that. We only drop the offending B
// (skipping it) — if A→C is also blocked, the loop drops C too,
// until either the path becomes contiguous or empties out.
if (Map* losMap = bot->GetMap())
{
uint32 const phaseMask = bot->GetPhaseMask();
for (size_t i = 1; i < state.walkPoints.size(); /* incremented in body */)
{
G3D::Vector3 const& a = state.walkPoints[i - 1];
G3D::Vector3 const& b = state.walkPoints[i];
// +2y on Z so the raycast starts/ends near the bot's
// chest level rather than ground (avoids false positives
// from sub-floor poly).
if (!losMap->isInLineOfSight(a.x, a.y, a.z + 2.0f, b.x, b.y, b.z + 2.0f,
phaseMask, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing))
{
state.walkPoints.erase(state.walkPoints.begin() + i);
continue;
}
++i;
}
if (state.walkPoints.size() < 2)
{
state.walkPoints.clear();
return true;
}
}
// Mount up
if (!bot->IsMounted() && !bot->IsInCombat() && bot->IsOutdoors() && bot->IsAlive())
botAI->DoSpecificAction("check mount state", Event(), true);

View File

@ -78,12 +78,25 @@ void NewRpgInfo::Reset()
data = Idle{};
startT = getMSTime();
ClearTravel();
recentMoveFarAttempts.clear();
// 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)
{
if (recentMoveFarAttempts.size() >= 3)
// 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;

View File

@ -739,7 +739,14 @@ std::vector<WorldPosition> WorldPosition::getPathStepFrom(WorldPosition startPos
if (tempCreature)
delete tempCreature;
if (type == PATHFIND_INCOMPLETE || type == PATHFIND_NORMAL)
// PathType is a bitmask (PathGenerator.h). Detour can return e.g.
// PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY_END (0x84) when the
// destination is a few yards off the nearest polygon — a strict
// `== PATHFIND_INCOMPLETE` check would reject the perfectly usable
// partial path and the chained probe would terminate empty on the
// very first call. PathGenerator's own internal code uses bitwise
// tests like `!(_type & PATHFIND_INCOMPLETE)`.
if (type & (PATHFIND_NORMAL | PATHFIND_INCOMPLETE))
return fromPointsArray(points);
return {};