From de18672c46fe89c4b30bb433f2caf8dfef25905d Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 3 Oct 2026 19:01:31 -0600 Subject: [PATCH 1/5] Jump a gap link down instead of dropping into it Path::ComputePathDetails marks every walk link onto an area more than a step lower as a drop, and pushes the drop point forward from the ledge until a hull column below it is clear. When the two areas do not touch, that column is over the gap, so the bot falls into it. The gap-jump pass that follows only looks at ground segments, so it never turns such a link into a jump. Leave a link down to an area more than 38 u away in 2D (1.9 of NEO's 20 u generation steps: the gap-jump pass's own separation test, now a named constant) on the ground. The gap-jump pass then makes it a run-up and a jump for the landing; where the drop is more than twice the gap it adds no jump and the bot walks off the ledge toward the landing. A path recomputed in mid-air starts from the take-off area, so its first goal is the take-off point behind the bot, which it cannot reach once it lands lower, and it walks back into the gap. Move a gap-jump goal on to its landing once the bot is jumping the gap or has reached the landing's area. --- src/game/server/NextBot/Path/NextBotPath.cpp | 28 +++++++++++++++++++ .../server/NextBot/Path/NextBotPathFollow.cpp | 13 +++++++++ 2 files changed, 41 insertions(+) diff --git a/src/game/server/NextBot/Path/NextBotPath.cpp b/src/game/server/NextBot/Path/NextBotPath.cpp index 02ced4b052..0d9093203e 100644 --- a/src/game/server/NextBot/Path/NextBotPath.cpp +++ b/src/game/server/NextBot/Path/NextBotPath.cpp @@ -23,6 +23,24 @@ ConVar NextBotPathDrawIncrement( "nb_path_draw_inc", "100", FCVAR_CHEAT ); ConVar NextBotPathDrawSegmentCount( "nb_path_draw_segment_count", "100", FCVAR_CHEAT ); ConVar NextBotPathSegmentInfluenceRadius( "nb_path_segment_influence_radius", "100", FCVAR_CHEAT ); +#ifdef NEO +// areas farther apart than this in 2D do not touch, so a link between them crosses a gap +static const float GapSeparationTolerance = 1.9f * GenerationStepSize; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the link from one area to the next crosses a gap, measured as the gap-jump pass measures it + */ +static bool IsLinkAcrossGap( const CNavArea *from, const Vector &fromPos, const CNavArea *to ) +{ + Vector closeTo, closeFrom; + to->GetClosestPointOnArea( fromPos, &closeTo ); + from->GetClosestPointOnArea( closeTo, &closeFrom ); + + return ( closeFrom - closeTo ).AsVector2D().IsLengthGreaterThan( GapSeparationTolerance ); +} +#endif + //-------------------------------------------------------------------------------------------------------------- Path::Path( void ) { @@ -104,7 +122,13 @@ bool Path::ComputePathDetails( INextBot *bot, const Vector &start ) float expectedHeightDrop = -DotProduct( alongPath, groundNormal ); +#ifdef NEO + // a drop marked on a link across a gap would send the bot straight off the edge into the gap, + // so the link stays on the ground: the bot heads for the landing, with a running jump when the gap-jump pass below adds one + if ( expectedHeightDrop > mover->GetStepHeight() && !IsLinkAcrossGap( from->area, from->pos, to->area ) ) +#else if ( expectedHeightDrop > mover->GetStepHeight() ) +#endif { // NOTE: We can't know this is a drop-down yet, because of subtle interactions // between nav area links and "portals" and "area crossings" @@ -281,7 +305,11 @@ bool Path::ComputePathDetails( INextBot *bot, const Vector &start ) } +#ifdef NEO + const float separationTolerance = GapSeparationTolerance; +#else const float separationTolerance = 1.9f * GenerationStepSize; +#endif if ( (closeFrom - closeTo).AsVector2D().IsLengthGreaterThan( separationTolerance ) && ( closeTo - closeFrom ).AsVector2D().IsLengthGreaterThan( 0.5f * fabs( closeTo.z - closeFrom.z ) ) ) { // areas are disjoint and mostly level - add gap jump target diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.cpp b/src/game/server/NextBot/Path/NextBotPathFollow.cpp index 216f9a10e0..d08c29ca58 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -473,6 +473,19 @@ bool PathFollower::CheckProgress( INextBot *bot ) { ILocomotion *mover = bot->GetLocomotionInterface(); +#ifdef NEO + // a path recomputed in mid-air starts from the area the bot jumped from, so its goal is the take-off behind it: + // once the bot is jumping the gap or has reached the landing's area, head for the landing instead + if ( m_goal && m_goal->type == JUMP_OVER_GAP ) + { + const Path::Segment *landing = NextSegment( m_goal ); + if ( landing && ( mover->IsJumpingAcrossGap() || bot->GetEntity()->GetLastKnownArea() == landing->area ) ) + { + m_goal = landing; + } + } +#endif + // skip nearby goal points that are redundant to smooth path following motion const Path::Segment *pSkipToGoal = NULL; if ( m_minLookAheadRange > 0.0f ) From 63a4a8d87ecac6734539058e10e2d795e8ca17fc Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 9 Oct 2026 01:43:14 -0600 Subject: [PATCH 2/5] Name both outcomes of a gap link left on the ground The comment above the drop test named the jump the gap-jump pass adds, but not the other outcome: on a link whose drop is more than twice the gap, the pass adds no jump and the bot walks off the ledge toward the landing. Name both, so a reader of the drop test does not have to work out the pass's level test. --- src/game/server/NextBot/Path/NextBotPath.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/server/NextBot/Path/NextBotPath.cpp b/src/game/server/NextBot/Path/NextBotPath.cpp index 0d9093203e..9d39807f3f 100644 --- a/src/game/server/NextBot/Path/NextBotPath.cpp +++ b/src/game/server/NextBot/Path/NextBotPath.cpp @@ -123,8 +123,8 @@ bool Path::ComputePathDetails( INextBot *bot, const Vector &start ) float expectedHeightDrop = -DotProduct( alongPath, groundNormal ); #ifdef NEO - // a drop marked on a link across a gap would send the bot straight off the edge into the gap, - // so the link stays on the ground: the bot heads for the landing, with a running jump when the gap-jump pass below adds one + // a drop on a link across a gap would send the bot into the gap, so leave the link on the ground: + // the gap-jump pass below adds a jump, or where the drop is more than twice the gap the bot walks off toward the landing if ( expectedHeightDrop > mover->GetStepHeight() && !IsLinkAcrossGap( from->area, from->pos, to->area ) ) #else if ( expectedHeightDrop > mover->GetStepHeight() ) From 980f31d9b02292b8ee28b62998a8825a0f241f62 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 9 Oct 2026 01:43:44 -0600 Subject: [PATCH 3/5] Drop a null check CheckProgress does not need CheckProgress runs only from Update, after Update has returned on a NULL goal, and the stock code below the gap-jump hand-off reads the goal unguarded through IsAtGoal, so the extra m_goal test on the hand-off guards nothing the rest of the function does not already assume. --- src/game/server/NextBot/Path/NextBotPathFollow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.cpp b/src/game/server/NextBot/Path/NextBotPathFollow.cpp index d08c29ca58..363ae93a9e 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -476,7 +476,7 @@ bool PathFollower::CheckProgress( INextBot *bot ) #ifdef NEO // a path recomputed in mid-air starts from the area the bot jumped from, so its goal is the take-off behind it: // once the bot is jumping the gap or has reached the landing's area, head for the landing instead - if ( m_goal && m_goal->type == JUMP_OVER_GAP ) + if ( m_goal->type == JUMP_OVER_GAP ) { const Path::Segment *landing = NextSegment( m_goal ); if ( landing && ( mover->IsJumpingAcrossGap() || bot->GetEntity()->GetLastKnownArea() == landing->area ) ) From ca746e6e0ff08ffc35ba00c232cfc78111f9251f Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 9 Oct 2026 01:44:00 -0600 Subject: [PATCH 4/5] Say where the gap separation constant comes from Name the constant's value in units (38 u: 1.9 of NEO's 20 u generation steps) and that it is the gap-jump pass's stock separation test, so a reader can tell it is inherited, not tuned. Say in the helper's comment that the pass repeats the measure from a path position that earlier insertions may have moved, so the two tests share a constant, not a point: a link one test calls a gap and the other does not ends in a walk-off toward the landing, not a drop into the gap. --- src/game/server/NextBot/Path/NextBotPath.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/game/server/NextBot/Path/NextBotPath.cpp b/src/game/server/NextBot/Path/NextBotPath.cpp index 9d39807f3f..713f2ef952 100644 --- a/src/game/server/NextBot/Path/NextBotPath.cpp +++ b/src/game/server/NextBot/Path/NextBotPath.cpp @@ -24,12 +24,14 @@ ConVar NextBotPathDrawSegmentCount( "nb_path_draw_segment_count", "100", FCVAR_C ConVar NextBotPathSegmentInfluenceRadius( "nb_path_segment_influence_radius", "100", FCVAR_CHEAT ); #ifdef NEO -// areas farther apart than this in 2D do not touch, so a link between them crosses a gap +// areas farther apart than this in 2D (38 u with NEO's 20 u generation step) do not touch, so a link between them crosses a gap: +// the gap-jump pass's own separation test, unchanged from stock, now shared with the drop test static const float GapSeparationTolerance = 1.9f * GenerationStepSize; //-------------------------------------------------------------------------------------------------------------- /** - * Return true if the link from one area to the next crosses a gap, measured as the gap-jump pass measures it + * Return true if the link from one area to the next crosses a gap, measured as the gap-jump pass measures it. + * The pass repeats this measure for its landing geometry, from a path position it may have moved, so the two agree by constant, not by point */ static bool IsLinkAcrossGap( const CNavArea *from, const Vector &fromPos, const CNavArea *to ) { From e7987b39767b063ff36727ed1f6645c8941b6952 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 9 Oct 2026 02:57:00 -0600 Subject: [PATCH 5/5] Name both conditions of the gap-jump hand-off The comment gave one cause, a path recomputed in mid-air, for two conditions. Say what each covers: a bot still in the jump, and a bot already standing on the landing's area when the re-path lands. --- src/game/server/NextBot/Path/NextBotPathFollow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/server/NextBot/Path/NextBotPathFollow.cpp b/src/game/server/NextBot/Path/NextBotPathFollow.cpp index 363ae93a9e..0ab0186e15 100644 --- a/src/game/server/NextBot/Path/NextBotPathFollow.cpp +++ b/src/game/server/NextBot/Path/NextBotPathFollow.cpp @@ -475,7 +475,7 @@ bool PathFollower::CheckProgress( INextBot *bot ) #ifdef NEO // a path recomputed in mid-air starts from the area the bot jumped from, so its goal is the take-off behind it: - // once the bot is jumping the gap or has reached the landing's area, head for the landing instead + // while the bot is still jumping the gap, or once it stands on the landing's area, head for the landing instead if ( m_goal->type == JUMP_OVER_GAP ) { const Path::Segment *landing = NextSegment( m_goal );