From 86828c307af257d2eceec83dcf490c60b66f7b76 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 9 Oct 2026 02:20:53 -0600 Subject: [PATCH] Refuse a path whose last leg climbs past a jump A bot told to reach a goal high above it, such as cover on a ramp or walkway overhead, can be handed a path whose last leg is a straight line up to the goal. The path follower raises FAIL_FELL_OFF ("goal is high above us") on every tick the bot stands below it, the behavior computes the same line again, and the bot stands there until it is shot or the behavior changes. Path::Compute builds such a leg in two ways. When no area the search reaches is nearer the goal than the bot's own, it falls back to BuildTrivialPath, a two-segment partial path, and CNEOBotPathCompute accepts any partial path. When the goal has no nav area of its own (GetNearestNavArea finds none with its line-of-sight check), the search stops at the first area that "contains" the goal, and CNavArea::Contains only compares against areas whose extent overlaps its own height range. A floor area directly below the goal therefore contains it: the search ends there, reports the goal reached, and builds a straight line up to the goal from that area. So every path's last leg is judged, complete or not: when the goal sits more than the bot's jump height (GetMaxJumpHeight) above the floor of the area the leg starts from, and no planned jump-up, gap jump, drop or ladder leads into it, the path is unusable. The compute then falls through to the reservation-free pass and reports no path. Attack already drops its cover area and chases on a failed compute; retreat to cover now picks another cover area, or ends when the pick repeats, instead of ignoring the result. On ntre_apparatus_dm, area 2067 below the ramp area 7687, every one of 547 FAIL_FELL_OFF events in 48 matches had this two-segment path. --- .../bot/behavior/neo_bot_retreat_to_cover.cpp | 13 +++++-- .../server/neo/bot/neo_bot_path_compute.cpp | 36 ++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index 218b61624f..e95668a1c1 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -351,9 +351,18 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv m_waitInCoverTimer.Reset(); - if ( !m_path.IsValid() ) + if ( !m_path.IsValid() && !CNEOBotPathCompute( me, m_path, m_coverArea->GetCenter(), RETREAT_ROUTE ) ) { - CNEOBotPathCompute( me, m_path, m_coverArea->GetCenter(), RETREAT_ROUTE ); + // cover we have no path to is no cover: pick another spot, or stop retreating when the pick repeats + CNavArea *pUnreachableArea = m_coverArea; + m_coverArea = FindCoverArea( me ); + + if ( m_coverArea == NULL || m_coverArea == pUnreachableArea ) + { + return Done( "No path to my cover" ); + } + + return Continue(); } m_path.Update( me ); diff --git a/src/game/server/neo/bot/neo_bot_path_compute.cpp b/src/game/server/neo/bot/neo_bot_path_compute.cpp index ff4e1457c3..2fb7d84db2 100644 --- a/src/game/server/neo/bot/neo_bot_path_compute.cpp +++ b/src/game/server/neo/bot/neo_bot_path_compute.cpp @@ -34,10 +34,38 @@ static void CNEOBotReservePath(CNEOBot* me, PathFollower& path) } } +// The last leg walks from the last area the path reached to the goal position, +// and the path follower takes a goal more than a jump above that area's floor as a fall (FAIL_FELL_OFF) +static bool IsGoalLegTooHigh(CNEOBot* bot, const PathFollower& path) +{ + const Path::Segment* goalSeg = path.LastSegment(); + const Path::Segment* legStart = path.PriorSegment(goalSeg); + if (!legStart || !legStart->area) + { + return false; + } + + // a planned jump-up, gap jump, drop or ladder carries the bot along this leg + if (legStart->type != Path::ON_GROUND || goalSeg->type != Path::ON_GROUND) + { + return false; + } + + const float rise = goalSeg->pos.z - legStart->area->GetZ(goalSeg->pos); + return rise > bot->GetLocomotionInterface()->GetMaxJumpHeight(); +} + // A path that stops short of the goal is only wanted if the caller asked for partial paths -static bool IsPathUsable(const PathFollower& path, bool reachedGoal, bool includeGoalIfPathFails) +static bool IsPathUsable(CNEOBot* bot, const PathFollower& path, bool reachedGoal, bool includeGoalIfPathFails) { - return path.IsValid() && (reachedGoal || includeGoalIfPathFails); + // Path::Compute can take an area below the goal for the goal's own, as CNavArea::Contains skips areas outside its height range, + // so a complete path's last leg is checked too + if (!path.IsValid() || IsGoalLegTooHigh(bot, path)) + { + return false; + } + + return reachedGoal || includeGoalIfPathFails; } // By default, assumes that we would prefer to have partial paths even if we can't make a full path @@ -49,7 +77,7 @@ bool CNEOBotPathCompute(CNEOBot* bot, PathFollower& path, const Vector& goal, Ro CNEOBotPathCost cost_with_reservations(bot, route); bool reachedGoal = path.Compute(bot, goal, cost_with_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea); - if (IsPathUsable(path, reachedGoal, includeGoalIfPathFails)) + if (IsPathUsable(bot, path, reachedGoal, includeGoalIfPathFails)) { CNEOBotReservePath(bot, path); return true; @@ -58,7 +86,7 @@ bool CNEOBotPathCompute(CNEOBot* bot, PathFollower& path, const Vector& goal, Ro CNEOBotPathCost cost_without_reservations(bot, FASTEST_ROUTE); cost_without_reservations.m_bIgnoreReservations = true; reachedGoal = path.Compute(bot, goal, cost_without_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea); - if (IsPathUsable(path, reachedGoal, includeGoalIfPathFails)) + if (IsPathUsable(bot, path, reachedGoal, includeGoalIfPathFails)) { CNEOBotReservePath(bot, path); return true;