From 0e1e6789e6bb1bba57a2b0ee9daa9387a88b9cb5 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 1 Oct 2026 19:24:00 -0600 Subject: [PATCH 1/4] Skip roam goals no route reaches A roaming bot picks random spawn points, gunfire, wander points and, for the juggernaut, random juggernaut spawns. When no route reaches the goal and the search got no nearer to it than the bot's own area, Path::Compute falls back to a straight line labeled COMPLETE_PATH, so the "complete path only" picks took it and the bot walked into the wall between it and the goal. The wander pick took any path, and a partial path ends by walking straight at the unreachable goal too. TryPathToRoamGoal now serves every random roam pick: a complete path that is not the two-segment fallback. When none of the ten wander points has a route (a pocket, or hazards all round the bot), the last one's path stands instead of leaving the bot with none. --- .../bot/behavior/neo_bot_jgr_juggernaut.cpp | 3 +- .../bot/behavior/neo_bot_seek_and_destroy.cpp | 31 ++++++++++++++++--- .../bot/behavior/neo_bot_seek_and_destroy.h | 1 + 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_jgr_juggernaut.cpp b/src/game/server/neo/bot/behavior/neo_bot_jgr_juggernaut.cpp index e8545d6c0c..a9c7817207 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_jgr_juggernaut.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_jgr_juggernaut.cpp @@ -2,7 +2,6 @@ #include "neo_player.h" #include "neo_gamerules.h" #include "bot/neo_bot.h" -#include "bot/neo_bot_path_compute.h" #include "bot/behavior/neo_bot_jgr_juggernaut.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "weapon_balc.h" @@ -70,7 +69,7 @@ void CNEOBotJgrJuggernaut::RecomputeSeekPath( CNEOBot *me ) m_vGoalPos = pTargetSpawn->GetAbsOrigin(); m_bGoingToTargetEntity = false; - if (CNEOBotPathCompute(me, m_path, m_vGoalPos, DEFAULT_ROUTE) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH) + if (TryPathToRoamGoal(me, m_vGoalPos)) { return; } diff --git a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp index 8945695c6c..5bbb991346 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp @@ -106,8 +106,7 @@ bool CNEOBotSeekAndDestroy::TryPathToCombatSound( CNEOBot *me ) m_combatSoundCommitTimer.Start( sv_neo_bot_seek_and_destroy_combat_sound_commit_time.GetFloat() ); } - if ( CNEOBotPathCompute( me, m_path, m_vCombatSoundSpot, DEFAULT_ROUTE ) - && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + if ( TryPathToRoamGoal( me, m_vCombatSoundSpot ) ) { m_vGoalPos = m_vCombatSoundSpot; m_bGoingToTargetEntity = false; @@ -128,6 +127,27 @@ bool CNEOBotSeekAndDestroy::TryPathToCombatSound( CNEOBot *me ) } +//--------------------------------------------------------------------------------------------- +// When no route reaches the goal and the search got no nearer to it than the bot's own area, Path::Compute +// falls back to a straight line, labeled COMPLETE_PATH: two segments in two areas. A real route has three or more. +static bool IsStraightLineFallback( const Path &path ) +{ + const Path::Segment *pFirst = path.FirstSegment(); + const Path::Segment *pLast = path.LastSegment(); + return pFirst && path.NextSegment( pFirst ) == pLast && pFirst->area != pLast->area; +} + + +//--------------------------------------------------------------------------------------------- +// Roam goals are picked at random, so skip one that no route reaches instead of walking at it. A refused +// attempt stays in m_path: the wander loop's last resort keeps it. +bool CNEOBotSeekAndDestroy::TryPathToRoamGoal( CNEOBot *me, const Vector &vGoal ) +{ + return CNEOBotPathCompute( me, m_path, vGoal, DEFAULT_ROUTE ) + && m_path.GetResult() == Path::COMPLETE_PATH && !IsStraightLineFallback( m_path ); +} + + //--------------------------------------------------------------------------------------------- CNEOBotSeekAndDestroy::CNEOBotSeekAndDestroy( float duration ) { @@ -592,7 +612,7 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) m_hTargetEntity = pSpawns[RandomInt( 0, pSpawns.Size() - 1 )]; m_bGoingToTargetEntity = true; m_vGoalPos = m_hTargetEntity->WorldSpaceCenter(); - if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, DEFAULT_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + if ( TryPathToRoamGoal( me, m_vGoalPos ) ) return; } } @@ -604,11 +624,12 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) Vector vWanderPoint = TheNavAreas[RandomInt( 0, TheNavAreas.Size() - 1 )]->GetCenter(); m_vGoalPos = vWanderPoint; - if ( CNEOBotPathCompute( me, m_path, vWanderPoint, DEFAULT_ROUTE ) ) + if ( TryPathToRoamGoal( me, vWanderPoint ) ) return; } - m_path.Invalidate(); + // No wander point has a route either (a pocket, or hazards all round): keep the last one's partial or + // straight-line path rather than stand still } diff --git a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.h b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.h index a442d82f53..585322de63 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.h +++ b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.h @@ -36,6 +36,7 @@ class CNEOBotSeekAndDestroy : public Action< CNEOBot > virtual void RecomputeSeekPath( CNEOBot *me ); bool TryPathToCombatSound( CNEOBot *me ); + bool TryPathToRoamGoal( CNEOBot *me, const Vector &vGoal ); virtual bool IsSeekGoalAnObjective() const { return false; } PathFollower m_path; From d43c219a484767f5a013a085066490ba1c60179b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 1 Oct 2026 20:52:01 -0600 Subject: [PATCH 2/4] Re-pick cover when no route reaches it A retreating bot picks cover once, from the areas it can walk or drop to from its last known area. When it drops off a ledge after the pick, often because the retreat began as it stepped off one, nothing leads back up to that cover. Update re-paths to it after every stuck or move failure without looking at the result, so the bot follows Path::Compute's straight-line fallback into the ledge face about once a second while a threat stays in view. When the path to cover is not a route, pick cover again from where the bot is. The test is the one the roam picks use, moved to neo_bot_path_compute as CNEOBotPathReachesGoal: a complete path that is not the two-segment fallback. --- .../neo/bot/behavior/neo_bot_retreat_to_cover.cpp | 13 +++++++++++++ .../neo/bot/behavior/neo_bot_seek_and_destroy.cpp | 14 +------------- src/game/server/neo/bot/neo_bot_path_compute.cpp | 15 +++++++++++++++ src/game/server/neo/bot/neo_bot_path_compute.h | 4 ++++ 4 files changed, 33 insertions(+), 13 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..46bac50795 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 @@ -354,6 +354,19 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv if ( !m_path.IsValid() ) { CNEOBotPathCompute( me, m_path, m_coverArea->GetCenter(), RETREAT_ROUTE ); + + // Cover is chosen where the retreat starts; after a drop off a ledge no route + // may reach it any more, so choose again from here + if ( !CNEOBotPathReachesGoal( m_path ) ) + { + m_coverArea = FindCoverArea( me ); + if ( m_coverArea == NULL ) + { + return Done( "My cover is out of reach, and there is no other cover available!" ); + } + + CNEOBotPathCompute( me, m_path, m_coverArea->GetCenter(), RETREAT_ROUTE ); + } } m_path.Update( me ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp index 5bbb991346..3f0e7ea161 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp @@ -127,24 +127,12 @@ bool CNEOBotSeekAndDestroy::TryPathToCombatSound( CNEOBot *me ) } -//--------------------------------------------------------------------------------------------- -// When no route reaches the goal and the search got no nearer to it than the bot's own area, Path::Compute -// falls back to a straight line, labeled COMPLETE_PATH: two segments in two areas. A real route has three or more. -static bool IsStraightLineFallback( const Path &path ) -{ - const Path::Segment *pFirst = path.FirstSegment(); - const Path::Segment *pLast = path.LastSegment(); - return pFirst && path.NextSegment( pFirst ) == pLast && pFirst->area != pLast->area; -} - - //--------------------------------------------------------------------------------------------- // Roam goals are picked at random, so skip one that no route reaches instead of walking at it. A refused // attempt stays in m_path: the wander loop's last resort keeps it. bool CNEOBotSeekAndDestroy::TryPathToRoamGoal( CNEOBot *me, const Vector &vGoal ) { - return CNEOBotPathCompute( me, m_path, vGoal, DEFAULT_ROUTE ) - && m_path.GetResult() == Path::COMPLETE_PATH && !IsStraightLineFallback( m_path ); + return CNEOBotPathCompute( me, m_path, vGoal, DEFAULT_ROUTE ) && CNEOBotPathReachesGoal( m_path ); } 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..48408ea205 100644 --- a/src/game/server/neo/bot/neo_bot_path_compute.cpp +++ b/src/game/server/neo/bot/neo_bot_path_compute.cpp @@ -69,6 +69,21 @@ bool CNEOBotPathCompute(CNEOBot* bot, PathFollower& path, const Vector& goal, Ro return false; } +// When no route reaches the goal and the search got no nearer to it than the bot's own area, +// Path::Compute falls back to a straight line labeled COMPLETE_PATH: two segments in two areas +bool CNEOBotPathReachesGoal(const PathFollower& path) +{ + if (!path.IsValid() || path.GetResult() != Path::COMPLETE_PATH) + { + return false; + } + + const Path::Segment* pFirst = path.FirstSegment(); + const Path::Segment* pLast = path.LastSegment(); + const bool bStraightLineFallback = (path.NextSegment(pFirst) == pLast) && (pFirst->area != pLast->area); + return !bStraightLineFallback; +} + bool CNEOBotPathUpdateChase(CNEOBot* bot, ChasePath& path, CBaseEntity* subject, RouteType route, Vector* pPredictedSubjectPos) { CNEOBotPathCost cost_with_reservations(bot, route); diff --git a/src/game/server/neo/bot/neo_bot_path_compute.h b/src/game/server/neo/bot/neo_bot_path_compute.h index 5fea6b7c47..61bb8be2db 100644 --- a/src/game/server/neo/bot/neo_bot_path_compute.h +++ b/src/game/server/neo/bot/neo_bot_path_compute.h @@ -18,6 +18,10 @@ bool CNEOBotPathCompute bool requireGoalArea = false ); +// True if the path reaches its goal: complete, and not the straight-line fallback +// Path::Compute builds when no route reaches the goal +bool CNEOBotPathReachesGoal(const PathFollower& path); + bool CNEOBotPathUpdateChase ( CNEOBot *bot, From 12b56cdfc46ff537ecab800b16b5d9271d106b9f Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 19:07:42 -0600 Subject: [PATCH 3/4] Stop retreating when the same cover is picked again A re-pick that lands on the cover that just failed would path to it again, every tick, without moving. End the retreat instead, as when no other cover is left. --- src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 46bac50795..7388fa3b9f 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 @@ -359,8 +359,9 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv // may reach it any more, so choose again from here if ( !CNEOBotPathReachesGoal( m_path ) ) { + CNavArea *pUnreachableArea = m_coverArea; m_coverArea = FindCoverArea( me ); - if ( m_coverArea == NULL ) + if ( m_coverArea == NULL || m_coverArea == pUnreachableArea ) { return Done( "My cover is out of reach, and there is no other cover available!" ); } From b7f5b83d85c523e2f1ced93cfdfc010f2f46a4de Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 14 Oct 2026 19:08:42 -0600 Subject: [PATCH 4/4] Note the trivial path the fallback check misreads A goal in the bot's own area also yields a two-segment path. When the areas nearest the bot and the goal differ, as with stacked or overlapping areas, CNEOBotPathReachesGoal reads it as the straight-line fallback: a roam pick is skipped, or a retreat re-picks its cover once. A cheaper discriminator needs a Path change, so say so at the check. --- src/game/server/neo/bot/neo_bot_path_compute.cpp | 3 +++ 1 file changed, 3 insertions(+) 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 48408ea205..66330d70ee 100644 --- a/src/game/server/neo/bot/neo_bot_path_compute.cpp +++ b/src/game/server/neo/bot/neo_bot_path_compute.cpp @@ -80,6 +80,9 @@ bool CNEOBotPathReachesGoal(const PathFollower& path) const Path::Segment* pFirst = path.FirstSegment(); const Path::Segment* pLast = path.LastSegment(); + + // A goal inside the bot's own area also gives a two-segment path, + // so where the areas nearest the bot and the goal differ (stacked or overlapping areas) it reads as the fallback const bool bStraightLineFallback = (path.NextSegment(pFirst) == pLast) && (pFirst->area != pLast->area); return !bStraightLineFallback; }