Repository navigation
Bots skip goals no route reaches - #2190
Draft
sunzenshen wants to merge 4 commits into
Draft
sunzenshen wants to merge 4 commits into
sunzenshen wants to merge 4 commits into
Conversation
sunzenshen
force-pushed
the
bot-roam-reject-unreachable
branch
from
October 3, 2026 17:52
a708bf5 to
6d8be8e
Compare
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.
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.
sunzenshen
force-pushed
the
bot-roam-reject-unreachable
branch
from
October 12, 2026 03:55
6d8be8e to
d43c219
Compare
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
When no route reaches a goal,
Path::Computestill returns a path that walks at it. Either it is a partial path with the goal appended after the nearest reached area (includeGoalIfPathFailsdefaults to true), or, when no area the search reached is nearer the goal than the bot's own,BuildTrivialPath: two segments, straight from the bot to the goal, labeledCOMPLETE_PATH. Two behaviors choose their goals at their own discretion and follow such paths into walls: roaming and retreating to cover. Both now accept a goal only ifCNEOBotPathReachesGoal(new, inneo_bot_path_compute) passes the path: valid,COMPLETE_PATH, and not the two-segment fallback. When it fails they choose another goal. A roam point or a cover spot can be swapped for another at no cost. Objective seeks (the ghost, the juggernaut item) and Attack's chase keep their own tests, because giving up on an objective or a target is a different decision.Three commits: the first covers the roam picks, the second covers RetreatToCover and moves the path test into
neo_bot_path_compute, where both use it, the third ends a retreat whose re-pick lands on the cover that just failed, and the fourth comments a case the path test misreads (below).Navmesh PRs that rely on this
None of the open navmesh PRs needs this. NeotokyoRebuild/neoAssets#154 (
ntre_yard_jgr) lists the wall under the attacker deck and the south-west courtyard's door under "Known, not changed"; this PR acts on both, and on master's yard navmesh as well (see "Evidence").1. Roaming bots skip goals no route reaches
When a bot has nothing better to do,
CNEOBotSeekAndDestroy::RecomputeSeekPathsends it to a random spawn point, and after ten failed picks to a random nav area;TryPathToCombatSoundsends it toward gunfire; the juggernaut patrols random juggernaut spawns. The spawn, gunfire and patrol picks take a goal only ifCNEOBotPathComputereturns a path whose result isCOMPLETE_PATH, so an unreachable goal is skipped. Two cases get through:COMPLETE_PATH, takes it and walks into whatever is in the way. When it gets stuck it re-picks, and may draw another unreachable goal.ntre_yard_jgrshows both in every match. Its spawn regions are one-way: bots can drop out of the attacker spawn (a deck at z 152) and the defender spawn room, but no stair or ladder leads back in. From the main floor every spawn point but one is unreachable. A bot in the courtyard under the deck is the nearest a route gets to the attacker spawns, so every attacker spawn it picks comes back as a straight line into the wall under the deck. On the master navmesh that wall is the map's largest stuck spot, about 30 stuck events a match.CNEOBotSeekAndDestroy::TryPathToRoamGoalnow serves every random roam pick: spawn points, gunfire, wander points and the juggernaut's patrol spawns. It takes a goal only ifCNEOBotPathReachesGoalpasses the path. A real route between two areas always has at least three segments (the area chain plus the goal), becausePath::Optimizedoes nothing andComputePathDetailsonly inserts segments. A goal inside the bot's own area also gives two segments; when the areas nearest the bot and the goal differ (stacked or overlapping areas), the test reads it as the fallback, so a roam pick is skipped or a retreat re-picks its cover once. A cheaper test would need a change toPath. When none of the ten wander points has a route, the bot keeps the last one's path as before, instead of standing still. That happens when the bot is in a pocket with no way out, or when every area around it is a live frag or smoke hazard, whichDEFAULT_ROUTErefuses.2. Retreating bots re-pick cover no route reaches
CNEOBotRetreatToCoverpicks cover once, inOnStart:FindCoverAreasearches the areas the bot can walk or drop to from its last known area and takes one of the nearest that the known threats cannot see.Updatethen paths to that cover whenever the path is invalid (the first update, and afterOnStuckorOnMoveToFailure, which only invalidate the path) and does not look at the result. Cover is chosen again only when the bot arrives and a threat can still see it.So when the bot drops off a ledge after the pick, the cover can be out of reach: the search allowed the drop on the way there, but nothing leads back up. The bot follows a partial path or the straight line into the ledge face, gets stuck, re-paths to the same cover and gets the same path, about once a second for as long as a threat stays in view.
The common way in is a retreat that starts while the bot is stepping off a ledge.
GetLastKnownArea()is still the area above, so the cover search runs from there. Onntre_yard_jgr(the reworked navmesh in NeotokyoRebuild/neoAssets#154), a south-west courtyard opens onto the main yard only through a door with a 102 u drop off a loading dock. A bot fighting its way out drops through the door and starts retreating in mid-air. It picks cover on the courtyard floor behind the dock, which it can no longer reach, and presses the dock face from the yard.Updatenow checks the path to cover when it computes it. IfCNEOBotPathReachesGoalfails, the bot picks cover again from where it is and paths there. If there is no cover at all, or the new pick is the cover that just failed, the retreat ends, as it already does inOnStartand when cover is exposed; without that check the bot would path to the same cover again every tick. A cover found from here is reachable over the navmesh links. If the path cost still refuses the way (the cover search accepts drops deeper than the death-drop height, the path cost does not), that path stands as before and the next stuck event re-picks. Retreats whose cover is reachable keep the cover they chose.Evidence
Re-measured on current master
Release dedicated servers on Linux, upstream master f426c22 without and with this PR's three commits, both with the same test-only logging; upstream master's navmeshes (neoAssets 467c146; yard is master's, not #154's); 30 bots, default weapon damage, 15 s freeze, XP rank held at one tier for all players and rotated across spawns; 1 round, the ghost or juggernaut pinned at each of its spawns once (apparatus and bullet: 4 matches); both arms of each pair in the same batch of 8. All 174 matches reached match end with 0 asserts and a clean exit. Counts are during play, before the round ends; an episode is one bot's stuck events less than 10 s apart.
FAIL_FELL_OFFntre_yard_jgrntre_terminal_jgrntre_sentinel_jgrntre_bullet_tdmntre_apparatus_dmntre_dawn_ctgntre_oilstain_ctgntre_pissalley_ctgntre_rogue_ctgntre_saitama_ctgntre_skyline_ctgntre_threadplate_ctgntre_transit_ctgPooled stuck events 14.2 -> 14.9 a match, x1.05 [0.76, 1.47],
FAIL_FELL_OFFx0.69 [0.46, 1.05],FAIL_NO_PATHx1.08 [0.78, 1.45], fall deaths x0.94 [0.63, 1.42]. Yard on master's navmesh: stuck events 74.0 -> 35.3 a match (x0.48 [0.29, 0.93]), most of it roaming (51.0 -> 19.7); itsFAIL_NO_PATHrose 19.7 -> 29.0, refused roam picks reported as no path. The CTG maps' upticks (pissalley, threadplate, saitama) are single bots: on threadplate one bot in one match held 41 stuck events at a wall while seeking the enemy in CTG, and saitama's top spot (-1280, 2176) is in both builds; neither is in the roam or cover code. The largest rises in stuck events are single bots outside that code too: onntre_bullet_tdm(44.2 -> 79.8 events a match, while its episodes fell x0.89 and stuck while retreating to cover fell 19.8 -> 14.2) one bot in one match held 148 stuck events at (-2240, -138) while attacking; onntre_rogue_ctg(6.6 -> 16.0, episodes x1.30 [0.59, 5.33], 5 pairs) two matches each had one bot stuck at one wall while going for the ghost or its carrier (ctgSeek,ctgEnemy), about 45 and 25 events. Kills by XP tier, paired difference a match: Corporal -4.6 (21 pairs), Lieutenant -2.0 (18), Private +0.7 (29), Sergeant +0.5 (19).ntre_apparatus_dmover 24 pairs: stuck episodes x0.93 [0.75, 1.15],FAIL_FELL_OFFx0.91 [0.72, 1.16], kills -4.2 [-10.0, +1.5] a match.At the south-west courtyard door on #154's reworked yard navmesh, 6 matches a build with
nb_debug behavior: on master, 5 bot episodes there, nearly all retreating to cover, the two longest 23.5 s (13 stuck events) and 14.3 s; with this PR, 4 times a retreating bot failed there once and left.Original measurement
Upstream master (at #2181) with each part behind a test-only switch, plus harness logging: a line per roam pick, per cover pick and per path to cover, and the usual stuck lines. The builds also carried #2176, #2185 and #2186, switched off in every match. Each part was measured with the other switched off, then both together on yard. 30 bots, default weapon damage, 8 wide; for each map, pin and repeat the two arms run back to back. JGR: 1 round of 5 minutes, the juggernaut pinned at each of its spawns in turn. CTG: 2 rounds of 3 minutes, the ghost pinned the same way. DM / TDM: 1 round of 5 minutes. "Stuck" is
OnStuckplusFAIL_STUCKmove failures, without the round-start spawn crowd and the weapon-scavenging behavior. An episode is one bot's stuck events less than 10 s apart. Ratios are paired by pin and repeat, as a geometric mean of (count + 1) with a bootstrap 95 % interval. Every match reached match end with 0 asserts and no crash.Roam picks (RetreatToCover stock)
ntre_yard_jgr, master navmesh and the reworked navmesh of NeotokyoRebuild/neoAssets#154, 18 pairs each:FAIL_FELL_OFFFAIL_NO_PATH_EXISTSThe "fell off" failures were bots on the fallback line dropping off the ledge below the deck. The stuck that remains on a straight line comes from RetreatToCover at the reworked navmesh's south-west courtyard door, which the second commit handles.
Thirteen maps in two batches, on the reworked navmeshes from the ntre research branch (yard: the one in neoAssets#154), 210 pairs: stuck 26.1 -> 23.5 a match, x0.89 [0.78, 1.02], 114 pairs down / 87 up; episodes x0.93 [0.86, 1.01]; kills x1.01 [0.98, 1.03]; fall deaths 539 -> 535 in 210 matches;
FAIL_NO_PATH_EXISTSx1.06 [0.92, 1.23]. Stuck while roaming (the SeekAndDestroy action) went from 7.1 to 5.3 a match. The change only acts where roam picks are unreachable, so most maps show no change. "Lines" is how often a stock bot took the straight-line fallback for a spawn pick, a match.ntre_yard_jgrntre_terminal_jgrntre_apparatus_dmntre_pissalley_ctgntre_threadplate_ctgntre_skyline_ctgntre_transit_ctgntre_bullet_tdmntre_rogue_ctgntre_oilstain_ctgntre_saitama_ctgntre_sentinel_jgrntre_dawn_ctgNo map's interval lies above 1 in either batch. The two highest are tails outside the roam code. On
ntre_dawn_ctg, 224 of the 578 stuck events are one bot escorting the ghost, held at one spot (2037, 3920) in one match. Onntre_sentinel_jgr, the rise is bots seeking the juggernaut at one pin, at a spot (88, 959) where stock bots stick too. Onntre_sentinel_jgrthe change does remove its straight-line stuck (2.8 -> 0 a match).Cover re-pick (roam picks stock)
ntre_yard_jgr, reworked navmesh (neoAssets#154) and master navmesh, 18 pairs each:With the change the reworked navmesh's door is at master's level. What is left there is roaming (0.50 a match, which the first commit addresses) and retreats on real routes. The re-pick happens on 0.39 retreats a match: rare, but each one would otherwise have been a bot pressing the dock face for as long as an enemy stayed in view. On the master navmesh it hardly acts (2 re-picks in 18 matches), so its kills difference is not this change.
Thirteen maps in two batches, as above, 210 pairs: stuck 25.1 -> 24.0 a match, x0.94 [0.83, 1.07], 107 pairs down / 96 up; episodes x0.95 [0.87, 1.03]; stuck while retreating to cover 3.40 -> 2.83; kills x1.00 [0.97, 1.03]; fall deaths 520 -> 480 in 210 matches;
FAIL_NO_PATH_EXISTSx1.02 [0.90, 1.17]. The case is rare: cover was re-picked 0.22 times a match, on three maps only. On the other ten the two arms ran the same code, so their spread is the noise of the method. Of the 5,038 stuck events with the change, 9 came within 30 s after the same bot re-picked its cover.ntre_apparatus_dmntre_yard_jgrntre_sentinel_jgrntre_skyline_ctgntre_pissalley_ctgntre_bullet_tdmntre_terminal_jgrntre_transit_ctgntre_oilstain_ctgntre_saitama_ctgntre_threadplate_ctgntre_dawn_ctgntre_rogue_ctgntre_apparatus_dmshows a second way into the loop. For cover on a stairway (area 7687),Path::Computeresolves the goal, that area's center, to the dead-end pocket under the stairs (area 2067): the path comes backCOMPLETE_PATHending in the pocket, and the bot walks in. TherePathFollowerfinds the goal 170 u overhead and reportsFAIL_FELL_OFF, and the re-path from the pocket is the straight-line fallback. Stock bots repeat that every 0.13 s for up to 4 s; with this change they choose cover again and walk out. None of the stuck events there came from a bot after its re-pick.Both commits
On yard with the roam-pick change on in both arms (12 pairs a navmesh), stuck at the reworked navmesh's door while retreating went from 2.42 to 0.42 a match, and from 1.83 to 0 on a straight line; kills 110.1 -> 109.5 (reworked), 106.9 -> 108.5 (master).
How the parts were chosen
On yard, an arm that refused the fallback only for spawn, gunfire and patrol picks cut the fallback stuck but not the stuck episodes: x0.79 [0.66, 0.95] events, x1.01 episodes, 36 pairs. Refused bots fell through to the wander loop and walked partial paths at other unreachable points, so stuck on real routes rose x1.13. Requiring a route for wander points as well fixed that (x0.58, episodes x0.70). Without the last resort, though, bots hemmed in by frag or smoke hazards or in a no-exit pocket stood still, about 2 % of bot time on yard. With the last resort no bot is left without a path, and the stuck figures are the same.
A broader version reported every straight-line fallback as
PARTIAL_PATHinsidePath::Compute. That changed every caller at once, including the objective seeks, and left the wander loop as it was, so on yard the refused bots walked the wander loop's partial paths into the same walls. On 10 maps it measured x1.09 [0.94, 1.26] (130 pairs, at 12 bots, where the fallback stuck was about 3 % of all stuck).Cost
Each roam pick makes the same
CNEOBotPathComputecall as before, plus a look at the first two path segments. A refused pick costs one more pick. On yard, roam picks went from 5,250 to 5,580 a match (+6 %). Over the thirteen maps they went from 1,290 to 1,520 a match (+18 %), mostly in the wander loop, which now goes on past points with no route. The last resort fires 4.9 times a match: 4.1 with hazards at or around the bot, 0.8 without. It reuses the path already computed.A path to cover gets the same look at its segments. Only when it fails does the bot run one more
FindCoverAreasearch and path computation: 0.22 times a match over the thirteen maps, 0.39 on yard's reworked navmesh. Paths to cover went from 273.5 to 277.1 a match.Toolchain
Linked Issues