Skip to content

Bots skip crouch gaps their hull cannot fit - #2199

Open
sunzenshen wants to merge 5 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-support-duck-lane
Open

sunzenshen wants to merge 5 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-support-duck-lane

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

Support and Juggernaut bots skip crouch gaps their ducked hull does not fit

A navmesh marks an area NAV_MESH_CROUCH when a standing player does not fit there but a crouching one does, measured with the stock crouch height, HumanCrouchHeight (55 u). NT;RE's classes duck to different heights: Recon 46 u and Assault 48 u fit under that, but Support ducks to 59 u and the Juggernaut to 75 u. CNEOBotPathCost treats every crouch area as open to every class, so a Support plans through a duct it cannot enter, presses into it until the stuck monitor fires, and the next path often sends it the same way.

ntre_yard_jgr hits this every match: the low gaps beside its prop staircases (around (120, -1361) and (-310, -749)) are crouch areas Support's ducked hull does not fit. On upstream master Supports get stuck at the first of them about once a match (1.1), and on our research build, which carries our other open bot PRs, 2.6 times.

Navmesh PRs that rely on this

None of the open navmesh PRs needs this. The crouch gaps it closes are on master's navmeshes too (see "Evidence"), so it acts without any navmesh PR. NeotokyoRebuild/neoAssets#154 (ntre_yard_jgr) lists the two crawlspaces under "Known, not changed" as this PR's case, and NeotokyoRebuild/neoAssets#165 (ntre_sentinel_jgr) its north culvert pipe, where the Juggernaut's stuck went 1.00 -> 0.56 a match with this PR (see "Risk").

What this PR changes

With this PR, a bot whose ducked hull is taller than HumanCrouchHeight (Support, Juggernaut) refuses a level step (a rise within a step, 18 u) into or out of a crouch area when no lane across the portal holds its ducked hull:

  • one lane per 4 u of portal width, spread from edge to edge, each from 2 u inside one area to 2 u inside the other, over the higher of the two floors;
  • the hull is swept with its underside a step above the floor, so it ignores a lip or slope under 18 u, as the bot steps over it;
  • world and static props only, so each portal's answer is kept until the level changes or the navmesh's area count changes (nav_load, nav editing); moving props are not part of it.

One clear lane keeps the portal open. Climbs and drops into crouch areas are not tested. Recon and Assault plan exactly as before: the path cost reads the bot's ducked hull once, and for them the rule is one bool per step. Two files, neo_bot_path_cost.{cpp,h}, +193.

Why the test runs only at crouch areas

The traces run only for a step that touches a NAV_MESH_CROUCH area, which is where the mesh says a standing player does not fit, and each portal is traced once a map. The test belongs to path planning rather than to the bot's surroundings: A* has to know that a gap anywhere on the route is closed when it plans, or the plan goes through the gap and the bot finds out only when it arrives. So it does not wait until the bot is near the gap.

Low headroom that the mesh does not mark as crouch (a pipe, a beam or a seat box overhead, which nav_generate's brush-only crouch test does not see) is a different case and affects every class: there the bot has to duck as it walks. That is #2235, separate from this PR.

How to test

  1. map ntre_yard_jgr (its navmesh on neoAssets master has the gaps), then neo_bot_quota 12, and join spectator.
  2. Watch the low gaps beside the prop staircases, near (120, -1361) and (-310, -749): Support bots, and a bot in the Juggernaut suit, route around them, while Recon and Assault still duck through. A build without this PR shows Supports walking into the gap at (120, -1361) and sticking there, about once a match with 30 bots.

Evidence

Current upstream, October 14. The PR's base (upstream a497c98, with #2198) against this PR's head, both with harness logging only (kill, stuck and position lines), on our research navmeshes (yard's has the same gaps as master's, see below). A spawn sweep: every ghost or juggernaut spawn of 13 maps once, ntre_yard_jgr's three juggernaut spawns 4 times and ntre_isolation_ctg's 8 times (they have the gaps this PR is about), ntre_apparatus_dm twice; 1 round, every player held at one XP rank tier, rotated across the spawns. 109 pairs, 218 of 218 matches reached match end with 0 asserts.

a match base this PR
Support stuck at the yard gap at (120, -1361), 12 pairs 3.50 0.17 (x0.45 [0.23, 0.79], 5 pairs down / 0 up)
Support stuck at the yard gap at (-310, -749) 0.58 0
Support stuck, yard 20.1 12.3 (x0.49 [0.24, 1.06], 8 / 1)
Support stuck, all 13 maps 4.61 3.31 (x0.85 [0.68, 1.06], 39 / 24)
Support no-path failures, all 13 5.48 5.57 (x0.97 [0.81, 1.16])
kills, all 13 44.0 44.1 (x1.02 [0.99, 1.05])

Recon and Assault stuck read higher on yard (11.1 -> 26.7 and 12.6 -> 17.5 a match, x1.81 [0.81, 3.68] and x1.31 [0.58, 3.07]) and on some single maps (decom, rogue, dawn Assault, a few events a match). The rule never runs for them (the build logged no portal test for their heights). On yard the extra events sit at spots both arms share: a wall row at (640 to 1115, -3160) on the way to area 55 (12 bots stuck there over the base arm's 12 matches, 15 with this PR, in longer episodes; 10 of this PR's events there had a player as the blocker, 0 in the base arm) and the ladder at (-629, -652). Supports were not there more often (501 and 449 position samples), but Recon and Assault were there 2.7 times as long in this PR's matches (402 and 1096 samples), and their stuck there scales with that time. We found no spot that only this PR's arm has, and no way for the rule to move Recon or Assault, so we read it as how the 12 juggernaut matches played out; the many-map sweep has Recon x1.13 [0.89, 1.44] and Assault x1.14 [0.88, 1.45]. Elsewhere the single-map rises are single bots at a different area each match.

The tables below are from before October 11, when test matches still started XP at zero, so their numbers are not directly comparable with the one above.

All bot matches: 30 bots, default weapon damage (1.485); both arms of each pair run back to back in the same 8-wide batch. Every match reached match end with 0 asserts. "Stuck" is OnStuck plus FAIL_STUCK move failures, without the round-start spawn crowd and weapon pickups. Class is read from the stuck bot's hull. Ratios are paired B/A with a bootstrap 95 % interval.

This branch on ntre_yard_jgr and ntre_isolation_ctg. Upstream master (at #2180) against upstream master with this PR, both with harness logging only (kill, stuck and position lines); the maps' research navmeshes. The gaps this PR closes on those maps are the same crouch areas, with the same ids and portals, on the navmeshes in neoAssets master (yard from #121): one match of this branch on each of those navmeshes refused the same gaps, plus one more on yard and three portals into one more crouch area on isolation. So the effect does not wait on a navmesh PR. 24 pairs of 5-minute JGR matches on yard (its three juggernaut spawns pinned in turn) and 16 pairs of CTG matches (2 rounds of 3 minutes) on isolation; 80 of 80 matches reached match end with 0 asserts.

a match master this PR
Support stuck at the yard gap at (120, -1361) 1.12 0 (x0.73 [0.52, 0.94], 4 pairs down / 0 up)
Support stuck at the yard gap at (-310, -749) 0.12 0
Support stuck, yard 15.8 13.1 (x0.90 [0.56, 1.43])
stuck, yard 59.0 44.0 (x0.76 [0.58, 0.97])
Support stuck, isolation 4.8 3.6 (x0.97 [0.49, 1.90])
stuck, isolation 13.8 11.1 (x0.76 [0.41, 1.40])
Support no-path failures, both maps 4.6 4.0 (x0.91 [0.70, 1.17])
kills, both maps 78.3 77.3 (x0.98 [0.93, 1.03])

Supports spent 10.2 position samples a match (about one a second) on the areas of the yard gaps this closes on master, and 0.8 with this PR. Master's yard stuck is about three times the research build's below, which carries our other open bot PRs. Some of yard's remaining Support stuck moved to ladders: Support stuck while approaching or on a ladder went 0.9 -> 1.8 a match (the rest 15.1 -> 11.3), all of it on an upper floor of the research navmesh that has one of the closed gaps; master's yard navmesh has neither that floor nor ladders. The Juggernaut had one such episode at a ladder there (0 -> 0.5 a match). On isolation the gap closes from one side only: the crouch area behind the hut's low gap (21922) also connects to a neighbor (21923) whose portal has a clear lane, so a Support that comes in that way still sticks inside it, in both arms (20 events in one master match, 13 in two with this PR).

Every crouch link, 28 maps. On our bot research build (upstream master plus our open bot PRs), research navmeshes, the change switched by a test-only cvar. A link walker (bots cross one nav link at a time from a standing start) walked all 2569 crouch links of the 28 ntre_ maps that have one, three times with each class, with and without this change:

stock this PR
Support class-links failing 144 116
Support failing trials 399 359
Support links refused that it failed - 21
Support links refused that it crossed - 1
Recon / Assault planning changes - none

The one refused crossing is a 16 u step down under a pipe on ntre_apparatus_dm whose way back up Support fails.

13 maps, 240 pairs, research build (as the walker; ntre_yard_jgr 48 pairs, isolation, apparatus, decom, skyline, ridgeline, rogue, dawn, pissalley, saitama, tarmac, sentinel CTG and JGR; 8 to 48 pairs a map, CTG and JGR from up to 4 pinned spawns):

a match stock this PR
Support stuck at the yard gap at (120, -1361), 48 pairs 2.62 0.27 (x0.56 [0.41, 0.77], 17 pairs down / 3 up)
Support stuck at the yard gap at (-310, -749), 48 pairs 0.85 0.17 (x0.87 [0.69, 1.07])
Support stuck, yard and isolation (64 pairs) 7.2 4.5 (x0.72 [0.52, 1.00])
Support stuck, the other 11 (176 pairs) 2.7 2.8 (x1.05 [0.89, 1.25])
stuck, the other 11 13.3 12.9 (x1.02 [0.89, 1.15])
stuck, all 13 14.6 13.5 (x0.97 [0.87, 1.09])
Support no-path failures, all 13 0.38 0.35 (x0.98 [0.92, 1.05])
kills, all 13 71.1 71.8 (x1.01 [0.99, 1.03])

On the other 11 maps Supports rarely reach a gap this closes in ordinary matches (0 to 1 position samples a match on those areas, against 16 to 19 on yard and 11 on isolation), so they test that nothing else changes. Three of them rose in a first 8-pair batch (decom, ridgeline Support, rogue); 16 fresh pairs each did not repeat it, and no Support stood at a closed gap there in either arm.

Cost

Path computation only, and only for Support and Juggernaut on level crouch portals: the first test of a portal sweeps up to one lane per 4 u of portal width (two short traces and three hull sweeps each, stopping at the first clear lane); later tests read the per-map cache. Recon and Assault pay one bool per step.

Measured with a test-only timer on the October 14 sweep (wall clock, 8 servers at once at 10x speed, so an upper bound): 29 to 271 portal tests a match (Support and Juggernaut heights), 217 to 2448 traces, 0.7 to 4.9 ms in a whole match. Most of it falls in one tick, the bots' first plans after they spawn, which is before the round starts: at most 3.4 ms (ntre_yard_jgr), elsewhere 0.5 to 1.8 ms. After that the cache answers.

Risk

  • Low headroom that the mesh does not mark as crouch is not covered (see "Why the test runs only at crouch areas"); every class still walks into it upright, as today.
  • If every route to a goal goes through a gap the hull does not fit, Support now gets no path where it used to walk into the gap and stick. Support no-path failures did not change in the matches above.
  • The test is static geometry at the portal. Where something else stops the bot (a displacement floor on ntre_subsurface_ctg, a physics prop, a brush entity such as func_brush, a low beam deeper inside a crouch area than the lanes reach), the lanes read clear and the portal stays open, as today.
  • The Juggernaut's 75 u ducked hull fits almost no crouch portal, so it now plans around crouch areas it could not cross anyway. On ntre_sentinel_jgr its stuck went 1.00 -> 0.56 a match (16 pairs, research build).

Ordering

Adds an early return in CNEOBotPathCost::operator(); #2170 (the NAV_MESH_AVOID cost, draft) changes the same function on other lines. Independent of the per-map navmesh PRs.

Approaches tried and dropped

  • Filling the cache during freeze time. The first plans already fall before the round starts, so it would add code for no change in play.
  • One verdict per portal for both directions. Halves the first-plan cost, which is already a few milliseconds before the round, and changes the lanes the link walker measured.
  • Letting a Support leave the crouch area it stands in by a closed side. For a bot that entered a duct by its open side; the October 14 sweep had no such case, and exempting the bot's own area reopened the yard gap from its mouth.
  • One ducked hull at the portal's center, on the floor. A lip or slope under the hull, a nav plane a few units off the floor, or an off-center opening made it close crouch portals Supports do cross (639 on the walker); stuck rose x1.39 [1.06, 1.83] on six maps without crouch trouble.
  • Climbs and drops too. Adds deck edges Support never stands on (it cannot climb 43.6 u) and vent climbs worth about 0.1 stuck a match, and refuses 8 crossings Support makes.
  • Support's standing hull on every portal: closes 3 to 8 % of all portals a map, 90 % of them climbs and drops.
  • Removing or re-marking the areas in the meshes. A crouch area that Support does not fit is still a route for Recon and Assault; the mesh has no per-class height.

Toolchain

  • Linux GCC Distro Native (Linux Mint 22.3, GCC 13.3.0)

Linked Issues

sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 4, 2026
The per-map reset compared string_t pointers, and the engine keeps
gpGlobals->mapname in a fixed buffer, so it never fired: after a
changelevel the old map's verdicts would apply to the new map's area
ids. A small game system now clears the cache in
LevelShutdownPostEntity, as in NeotokyoRebuild#2199. Research
cvars unchanged.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 7, 2026
The per-map reset compared string_t pointers, and the engine keeps
gpGlobals->mapname in a fixed buffer, so it never fired: after a
changelevel the old map's verdicts would apply to the new map's area
ids. A small game system now clears the cache in
LevelShutdownPostEntity, as in NeotokyoRebuild#2199. Research
cvars unchanged.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 8, 2026
The per-map reset compared string_t pointers, and the engine keeps
gpGlobals->mapname in a fixed buffer, so it never fired: after a
changelevel the old map's verdicts would apply to the new map's area
ids. A small game system now clears the cache in
LevelShutdownPostEntity, as in NeotokyoRebuild#2199. Research
cvars unchanged.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 8, 2026
The per-map reset compared string_t pointers, and the engine keeps
gpGlobals->mapname in a fixed buffer, so it never fired: after a
changelevel the old map's verdicts would apply to the new map's area
ids. A small game system now clears the cache in
LevelShutdownPostEntity, as in NeotokyoRebuild#2199. Research
cvars unchanged.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 8, 2026
The per-map reset compared string_t pointers, and the engine keeps
gpGlobals->mapname in a fixed buffer, so it never fired: after a
changelevel the old map's verdicts would apply to the new map's area
ids. A small game system now clears the cache in
LevelShutdownPostEntity, as in NeotokyoRebuild#2199. Research
cvars unchanged.
@sunzenshen
sunzenshen force-pushed the bot-support-duck-lane branch from d42472a to 6644c55 Compare October 12, 2026 03:55
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 13, 2026
The per-map reset compared string_t pointers, and the engine keeps
gpGlobals->mapname in a fixed buffer, so it never fired: after a
changelevel the old map's verdicts would apply to the new map's area
ids. A small game system now clears the cache in
LevelShutdownPostEntity, as in NeotokyoRebuild#2199. Research
cvars unchanged.
@sunzenshen
sunzenshen marked this pull request as ready for review October 15, 2026 04:10
A crouch nav area promises HumanCrouchHeight (55 u) of headroom, but
Support ducks to 59 u and the Juggernaut to 75 u. They planned through
crouch gaps their ducked hull does not fit and got stuck there.

For a bot whose ducked hull is taller than HumanCrouchHeight, the path
cost now refuses a level step into or out of a crouch area when no
lane across the portal holds that hull. There is one lane per 4 u of
portal width; the hull is swept a step above the floor against the
world and static props, and each verdict is kept until the level
changes. Climbs and drops are not tested. Recon and Assault plan as
before.
The duck lane rule derived the bot's ducked hull from its class and
model scale on every crouch edge the search expanded. The path cost now
reads it once in its constructor and tests a single bool first, so Recon
and Assault pay one check per edge and never read area attributes for
this rule. The level-step test uses the class's m_stepHeight, as the
rest of operator() does.
The verdicts are keyed by area id and were cleared only at level
shutdown. nav_load, nav_generate_incremental and the nav editing
commands (split, merge, delete) renumber areas inside a running level,
so old verdicts would apply to new areas. Clear the cache when the area
count it was filled on changes. Players never hit this; a nav editing
session with bots can.
The lane sweep comment said it raises the hull the way CGameMovement
steps. StepMove lifts the whole hull, while the lane sweep keeps the top
at the ducked height and ignores the lowest step of the hull, so say
what it does instead. Break comment lines at clause boundaries, and name
the key struct with the _t suffix used for plain structs.
@sunzenshen
sunzenshen force-pushed the bot-support-duck-lane branch from 8170a50 to f94a437 Compare October 15, 2026 04:10
@sunzenshen
sunzenshen requested a review from sunmachine October 15, 2026 04:10
Resolve conflict in neo_bot_path_cost.cpp:
- Keep duck lane hull cache code (PR NeotokyoRebuild#2199)
- Adopt upstream's hurry-hazard multiplier logic (m_bInAHurry)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant