Skip to content

Bots follow a partial chase path as far as its areas reach - #2165

Merged
sunzenshen merged 5 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-chase-partial-path-pr
Oct 17, 2026
Merged

sunzenshen merged 5 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-chase-partial-path-pr

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Description

Background

In bot matches with smoke grenades, bots chasing an enemy they could not fully reach stood still for seconds at a time. NEO's path cost refuses smoke and other hazard areas on purpose, so that paths stop at their edge; when that left no complete route to the chased enemy, the chase threw the partial path away and the bot waited out the retry timer (10 s for a target 2000 u away). This PR has the bot walk the partial path to the edge of what it can reach instead, and roughly halves FAIL_NO_PATH_EXISTS failures on every map measured.

Navmesh PRs that rely on this

None of the open navmesh PRs needs this. It is a general chase fix that acts wherever a hazard or a gap in the mesh cuts a chase short, on every map and navmesh.

What changed and why

When Path::Compute cannot reach a chase target, ChasePath::RefreshPath now keeps the partial
path it built instead of treating it as a failure, and bounds how far the bot follows it:

  • a partial path that crosses no nav area before its last leg is still a failure (the bot already
    stands in the last area it can reach);
  • the bot walks the path's last leg only while the ground three quarters of a hull width (24 u) ahead is
    still the last reachable area, then waits there on the usual range-scaled retry;
  • if the path follower drops the path on that leg, the bot waits the same way instead of
    rebuilding the same path on the next tick.

Nothing changes when the path compute succeeds, and nothing outside ChasePath is touched. The
third commit removes the sv_neo_bot_path_partial_chase_enable cvar the earlier commits added: the
PR implements the behavior directly. The last two commits name the retry rate BackOff shares
with RefreshPath and correct a comment.

The defect

When Path::Compute cannot reach the goal area it returns false, but it has still built a valid
partial path. RefreshPath throws that away: it fires OnMoveToFailure(FAIL_NO_PATH_EXISTS),
invalidates the path and blocks re-pathing for 0.005 s per unit of range (10 s for a target
2000 u away), while the bot stands still.

NEO's own path cost relies on partial paths: neo_bot_path_cost.cpp refuses hazard areas under
DEFAULT_ROUTE "to partially path up to boundary of hazard area".

Why the bounds

A partial path is the chain of areas the search reached plus one straight leg from the last of
them to the target itself
(Compute appends the goal position when includeGoalIfPathFails).
That leg runs through whatever stopped the search. The first commit on this PR accepted a partial
path covering 90 % of the range, but it measured the distance to that appended target position, so
it passed 1,775 of 1,781 partial paths. Following the whole path had bots walk the leg into walls
and back into the smoke the path had avoided. The two bounds above stop the bot at the edge of the
reachable mesh instead. A time-limited attempt at the leg was also measured; it performed the same
and needed more state, so the edge check was kept.

With #2174

#2174 moves the chase of the enemy ghost carrier into CNEOBotCtgEnemyChase, which still drives a
ChasePath through CNEOBotPathUpdateChase, so this change applies to that chase as it does to
CNEOBotAttack and the escorts. The two PRs touch different files and merge in either order.
CNEOBotCtgEnemyChase::OnResume invalidates the chase path; ChasePath::Invalidate leaves the
retry timer alone, so a bot waiting at the edge of its reachable mesh keeps waiting after a
suspension, as it does after a stock failure.

Measurement

Headless bot matches on master f426c222b against master + this PR (Release, both arms the same
build type), 30 bots, smoke grenades thrown freely (sv_neo_bot_grenade_polite_smoke 0) to raise
hazard exposure, one round a match, every XP rank tier rotated across the matches. CTG and JGR
maps ran once per pinned ghost or juggernaut spawn: oilstain (5 spawns), transit (5), saitama (17),
yard (3); bullet (TDM) and apparatus (DM) ran 14 matches each. 58 matches an arm, paired by spawn,
0 asserts, every match ended on its own.

per match, 58 pairs master this PR ratio [95 % interval]
FAIL_NO_PATH_EXISTS 98.0 55.7 x0.57 [0.48, 0.66]
OnStuck 57.0 64.7 x1.14 [0.98, 1.30]
OnStuck while chasing (Attack, ghost-carrier chase) 15.9 17.6 x1.11 [0.92, 1.36]
FAIL_FELL_OFF 10.2 11.1 x1.10 [0.88, 1.34]
kills 99.2 100.1 +0.9 [-1.5, +3.4]
rounds won by capture (27 CTG matches) 4 8

FAIL_NO_PATH_EXISTS is lower on all six maps (x0.33 to x0.74). What remains is the two failures
this PR keeps: a bot already at the edge of its reachable mesh, and a bot that has walked to that
edge (about one edge stop in ten partial paths followed).

By XP tier, stuck episodes x1.06 to x1.12 and kills -3.8 (Sergeant) to +2.6 (Private) a match;
no tier stands out.

Per map, three tails stand out, all on few pairs: saitama's stuck episodes x1.43 [1.00, 2.07]
(17 pairs), transit's stucks while chasing 1.6 -> 8.0 a match (x5.00 [1.07, 43], 5 pairs) and
yard's kills -11.0 [-13.0, -7.0] (3 pairs). Few of those stucks follow a partial path: of the
stucks while chasing with this PR, 18 of 86 on saitama and 2 of 40 on transit came within 10 s
after the same bot accepted a partial path or stopped at its edge (121 of 1019 over all maps);
the rest were on complete paths.

OnStuck leans up without leaving its interval. The repeating spot is bullet's smoke corridor
(x ≈ -1024): bots stick there in both arms, mostly while retreating from the hazard (104 of 154
events on master, 130 of 206 with this PR), so a bot that walks to the smoke's edge instead of
standing still meets that existing retreat problem more often. A bot that stops at the edge is
stuck within 5 s afterwards in 2 of 489 edge stops.

Risk

A bot may now walk toward a target it cannot fully reach instead of standing still; it stops at the
edge of the last area it can reach and retries on the stock timer. Most chasing stucks happen on
complete paths and come from map geometry, the same with or without this change.

Toolchain

  • Windows MSVC VS2022
  • Linux GCC Distro Native (Linux Mint 22.3, GCC 13.3.0)

Linked Issues

  • None.

@sunzenshen
sunzenshen force-pushed the bot-chase-partial-path-pr branch 2 times, most recently from 4991b14 to 8f71472 Compare September 26, 2026 12:06
@sunzenshen sunzenshen changed the title Chase a partial path that already arrives instead of failing Follow a partial chase path as far as its areas reach Sep 27, 2026
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 1, 2026
@sunzenshen
sunzenshen force-pushed the bot-chase-partial-path-pr branch from dcb3c61 to 70b3fda Compare October 3, 2026 17:50
@sunzenshen
sunzenshen force-pushed the bot-chase-partial-path-pr branch from 70b3fda to 8fb5d74 Compare October 12, 2026 03:56
@sunzenshen sunzenshen changed the title Follow a partial chase path as far as its areas reach Bots follow a partial chase path as far as its areas reach Oct 15, 2026
@sunzenshen
sunzenshen requested a review from sunmachine October 15, 2026 04:01
@sunzenshen sunzenshen added the Bots Related to bot players label Oct 15, 2026
@sunzenshen
sunzenshen marked this pull request as ready for review October 15, 2026 04:01
ChasePath::RefreshPath treats every failed Path::Compute as
FAIL_NO_PATH_EXISTS: it fires OnMoveToFailure, invalidates the path and
blocks repathing for 0.005 s per unit of range. But a failed Compute still
leaves a valid PARTIAL_PATH ending in the reachable area closest to the
target, and the bot's own path cost asks for exactly that: neo_bot_path_cost
refuses hazard areas under DEFAULT_ROUTE "to partially path up to boundary
of hazard area". Over 584 measured chase failures every one carried such a
path, the median ended beside the target (141 u short) and the blackout ran
a median 6.3 s with the bot standing still.

Count a partial path that already covers 90 % of the straight-line range
to the target as a path. One that ends well short still fails, so the
range-scaled throttle keeps guarding the genuinely unreachable case.

Measured on six maps (oilstain, transit, bullet, yard, apparatus, saitama),
49 pinned matches per arm, paired per pin:
  FAIL_NO_PATH_EXISTS  11.63 -> 0.08 a match  x0.12 [0.10, 0.14], 47 down / 0 up
  other move failures   2.92 -> 1.71
  OnStuck              69.7  -> 68.1          x1.12 [0.92, 1.37] (no change)
  kills                13.1  -> 13.6          x1.05 [0.95, 1.15]
  rounds won by capture 0.39 -> 0.47
It lowered FAIL_NO_PATH_EXISTS on every map and moved nothing else.
A partial chase path runs through the areas the search reached and then
straight to the subject, through whatever stopped the search: a hazard
area refused under DEFAULT_ROUTE, a blocked area, a climb the class
cannot make. The previous commit's 90 % gate measured the distance to
that appended subject position, so it passed practically every partial
path, and the bot walked the unreachable last leg into walls and back
into the smoke the path had avoided.

Follow a partial chase path regardless of distance, with two bounds:
- refuse one that crosses no area before its last leg (the bot already
  stands in the last area it can reach), as a failed search does;
- walk the last leg only while the ground a hull width ahead is still
  the last reachable area, then wait there on the range-scaled retry.
  If the follower drops the path on the leg, wait the same way instead
  of rebuilding it on the next tick.

sv_neo_bot_path_partial_chase_enable 0 restores the stock behaviour.

Measured with bots on six maps, 24 matches an arm, against following
the whole partial path: hazard re-entries 2.17 -> 0.42 a match, stuck
on the last leg 0.62 -> 0, stuck on a partial path 0.42 a match against
~8 on full chase paths. Against stock on all 29 ntre maps: OnStuck,
falls and kills unchanged, FAIL_NO_PATH_EXISTS down on 25 maps.
The partial chase path is the behavior the change proposes, so it is
implemented directly instead of behind sv_neo_bot_path_partial_chase_enable.
The stock behavior stays in history for comparison.
BackOff repeated RefreshPath's 0.005 s per unit of range as a bare
literal. Name it CHASE_RETRY_SECONDS_PER_UNIT and say where the rate
comes from, so a reader can keep the two in step without touching the
stock failure branch.
The look-ahead is 24 u and a hull is 32 u wide (HalfHumanWidth 16), so
it is three quarters of a hull width, not about one.
@sunzenshen
sunzenshen force-pushed the bot-chase-partial-path-pr branch from cdd3aaf to 1b24371 Compare October 15, 2026 04:08

@sunmachine sunmachine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@sunzenshen
sunzenshen merged commit ee65f9b into NeotokyoRebuild:master Oct 17, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bots Related to bot players

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants