From 1c8bf054b96f94a15847d5e5bc202367598d1c08 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 4 Oct 2026 21:30:00 -0600 Subject: [PATCH 1/3] Re-check a skipped-to goal when off the path The path follower skips ahead to a goal that is clear from where the bot is. A bot pushed off its path keeps steering at that goal from wherever it ends up, and when the straight line from there runs into a wall or a prop it wedges until the stuck monitor fires. On Avoid()'s 0.5 s cadence, when the bot stands on neither the goal's area nor the one before it, run the stock IsPotentiallyTraversable check to the goal. If it is blocked, back the goal up to the nearest earlier ground waypoint that is walkable from the feet; when that waypoint is within the goal tolerance, or none is walkable, invalidate the path so the bot re-paths. A re-path can lead back into the same prop, so after an action a goal near the one acted on, or feet near where the bot stood, is held for 3 s, and after two actions it is left to the stuck monitor. --- .../server/NextBot/Path/NextBotPathFollow.cpp | 105 ++++++++++++++++++ .../server/NextBot/Path/NextBotPathFollow.h | 6 + 2 files changed, 111 insertions(+) diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.cpp b/src/game/server/NextBot/Path/NextBotPathFollow.cpp index da57ce9281..b7ec3e0e6e 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -34,6 +34,16 @@ ConVar NextBotDebugClimbing( "nb_debug_climbing", "0", FCVAR_CHEAT ); #ifdef NEO // How far from its feet a bot that landed looks for a nav area, the range UpdateLastKnownArea() searches static const float LANDING_NAV_AREA_RANGE = 50.0f; + +// A re-path from the goal recheck can lead back into the same prop, so once the recheck has acted, +// a goal within this range of the one it acted on, +static constexpr float RECHECK_SAME_GOAL_RANGE = 16.0f; +// or feet within this range of where the bot stood (a re-path can alternate between waypoints a few units apart), +static constexpr float RECHECK_SAME_FEET_RANGE = 32.0f; +// is left alone this long after each action, +static constexpr float RECHECK_HOLD_TIME = 3.0f; +// and after this many actions is left to the stuck monitor +static constexpr int RECHECK_MAX_ACTIONS = 2; #endif @@ -57,6 +67,11 @@ PathFollower::PathFollower( void ) #ifdef NEO m_wasOnGround = true; + + m_recheckGoalPos = vec3_invalid; + m_recheckFeetPos = vec3_invalid; + m_recheckCount = 0; + m_recheckHoldTimer.Invalidate(); #endif } @@ -657,6 +672,88 @@ static bool IsNearNavMesh( INextBot *bot ) { return TheNavMesh->GetNearestNavArea( bot->GetEntity(), GETNAVAREA_CHECK_GROUND | GETNAVAREA_CHECK_LOS, LANDING_NAV_AREA_RANGE ) != NULL; } + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the goal an off-path bot should steer at, or NULL to re-path. The path may have skipped ahead + * to a goal that was clear from where the bot was then, but a bot pushed off its path keeps steering at it + * from wherever it ends up, and the straight line from there can run into a wall or a prop + */ +static const Path::Segment *FindWalkableGoal( INextBot *bot, const Path *path, const Path::Segment *goal, float goalTolerance ) +{ + ILocomotion *mover = bot->GetLocomotionInterface(); + if ( goal->type != Path::ON_GROUND || goal->ladder || !mover->IsOnGround() || mover->IsClimbingOrJumping() ) + { + return goal; + } + + // on the goal's area or the one before it, the bot is still on the line the path planned + const CNavArea *myArea = bot->GetEntity()->GetLastKnownArea(); + const Path::Segment *prior = path->PriorSegment( goal ); + if ( !myArea || myArea == goal->area || ( prior && myArea == prior->area ) ) + { + return goal; + } + + const Vector &feet = mover->GetFeet(); + if ( mover->IsPotentiallyTraversable( feet, goal->pos ) ) + { + return goal; + } + + // back up to the nearest earlier waypoint still walkable from here, but not back over a ladder, climb or drop + for ( const Path::Segment *seg = prior; seg && seg->type == Path::ON_GROUND && !seg->ladder; seg = path->PriorSegment( seg ) ) + { + if ( !mover->IsPotentiallyTraversable( feet, seg->pos ) ) + { + continue; + } + + // a waypoint the bot already stands at would hand it straight back the blocked goal, so re-path instead + const Vector2D toSeg = ( seg->pos - feet ).AsVector2D(); + return toSeg.IsLengthLessThan( goalTolerance ) ? NULL : seg; + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check that an off-path bot can still walk to its goal, and back the goal up or re-path when it cannot. + * Return false if the path was invalidated for a re-path + */ +bool PathFollower::RecheckGoal( INextBot *bot ) +{ + const Vector &feet = bot->GetLocomotionInterface()->GetFeet(); + const bool isRepeat = ( m_goal->pos - m_recheckGoalPos ).IsLengthLessThan( RECHECK_SAME_GOAL_RANGE ) || + ( feet - m_recheckFeetPos ).IsLengthLessThan( RECHECK_SAME_FEET_RANGE ); + if ( isRepeat && ( m_recheckCount >= RECHECK_MAX_ACTIONS || !m_recheckHoldTimer.IsElapsed() ) ) + { + return true; + } + + const Segment *walkableGoal = FindWalkableGoal( bot, this, m_goal, m_goalTolerance ); + if ( walkableGoal == m_goal ) + { + return true; + } + + m_recheckCount = isRepeat ? m_recheckCount + 1 : 1; + m_recheckGoalPos = m_goal->pos; + m_recheckFeetPos = feet; + m_recheckHoldTimer.Start( RECHECK_HOLD_TIME ); + + if ( !walkableGoal ) + { + Invalidate(); + return false; + } + + m_goal = walkableGoal; + return true; +} #endif @@ -867,6 +964,14 @@ void PathFollower::Update( INextBot *bot ) } } +#ifdef NEO + // on Avoid()'s cadence, an off-path bot whose goal is blocked backs it up or re-paths + if ( m_avoidTimer.IsElapsed() && !RecheckGoal( bot ) ) + { + return; + } +#endif + Vector goalPos = m_goal->pos; diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.h b/src/game/server/NextBot/Path/NextBotPathFollow.h index c421f82852..91b67e661c 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.h +++ b/src/game/server/NextBot/Path/NextBotPathFollow.h @@ -91,6 +91,12 @@ class PathFollower : public Path bool HasFallenBelowPath( INextBot *bot ) const; // return true if we stand off the path, more than a step below it bool m_wasOnGround; CNEOBotPathObstacles m_pathObstacles; // looks ahead for props to steer around and breakables in the way + + bool RecheckGoal( INextBot *bot ); // an off-path bot whose goal is blocked backs it up or re-paths + Vector m_recheckGoalPos; // the goal the recheck last acted on, + Vector m_recheckFeetPos; // where the bot stood then, + int m_recheckCount; // how many times in a row it acted there, + CountdownTimer m_recheckHoldTimer; // and when it may act there again #endif float m_goalTolerance; From 22125f85236f7ae36bfc236f54ad8872591cf158 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 19:08:32 -0600 Subject: [PATCH 2/3] Leave the goal re-check off while detouring a prop #2197's prop detour sets the path goal to the waypoint past the props and steers around them to reach it, so the straight line from the bot to that goal runs through the prop on purpose. The re-check would see it as blocked and back the goal up or re-path, undoing the detour. --- src/game/server/NextBot/Path/NextBotPathFollow.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.cpp b/src/game/server/NextBot/Path/NextBotPathFollow.cpp index e0f91c3849..c4e4c5a845 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -974,8 +974,9 @@ void PathFollower::Update( INextBot *bot ) } #ifdef NEO - // on Avoid()'s cadence, an off-path bot whose goal is blocked backs it up or re-paths - if ( m_avoidTimer.IsElapsed() && !RecheckGoal( bot ) ) + // on Avoid()'s cadence, an off-path bot whose goal is blocked backs it up or re-paths, + // except while it detours around a prop: the detour's goal lies past the prop on purpose + if ( !isDetouring && m_avoidTimer.IsElapsed() && !RecheckGoal( bot ) ) { return; } From 2b6812ef539f062c6ea3f1d78032921c43d3d516 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 19:09:32 -0600 Subject: [PATCH 3/3] Align a header comment --- src/game/server/NextBot/Path/NextBotPathFollow.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.h b/src/game/server/NextBot/Path/NextBotPathFollow.h index f35b54d2d8..fb2793ec3f 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.h +++ b/src/game/server/NextBot/Path/NextBotPathFollow.h @@ -98,7 +98,7 @@ class PathFollower : public Path bool RecheckGoal( INextBot *bot ); // an off-path bot whose goal is blocked backs it up or re-paths Vector m_recheckGoalPos; // the goal the recheck last acted on, Vector m_recheckFeetPos; // where the bot stood then, - int m_recheckCount; // how many times in a row it acted there, + int m_recheckCount; // how many times in a row it acted there, CountdownTimer m_recheckHoldTimer; // and when it may act there again #endif