Skip to content

ntre_engage_ctg: navmesh rework - #152

Merged
sunzenshen merged 7 commits into
NeotokyoRebuild:masterfrom
sunzenshen:navmesh-engage-release
Oct 14, 2026
Merged

sunzenshen merged 7 commits into
NeotokyoRebuild:masterfrom
sunzenshen:navmesh-engage-release

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

ntre_engage_ctg: navmesh rework

One file, neo/maps/nav/ntre_engage_ctg.nav. It was measured on upstream master plus the draft NeotokyoRebuild/neo#2176 (then only its unwanted-ladder release, now its changes 1 and 7) and NeotokyoRebuild/neo#2186 ("Bots skip weapons they cannot reach", merged since), with harness logging added. The balcony staircase from #135 is kept, with each flight of stairs made one area and its two lower turns widened (commits 4 and 5). ntre_disengage_ctg shares most of this layout and gets its own PR (#153).

Dependencies and status

Needs (worse than master, or broken, without it):

  • None. No open bot code PR has to be reviewed or merged before this mesh: bots rarely use this map's two nav ladders, the mesh's gains (the roof walkway, the staircase, the car) do not involve them, and it measures better than master with More tweaks to bot ladder approaches, climbs and dismounts neo#2176 and without it (see "How it was tested").

Helped by (measured better with it; the mesh does not wait on it):

  • None measured.

Pending before review:

  • Nothing: ready for review.

At a glance

master this PR
nav areas 1950 1414
file size 3,343,660 bytes 1,881,901 bytes (-44 %)
areas the engine blocks at every round start 22 0
areas no spawn can reach 110 in 26 pockets 102 in 23 pockets, all left by dropping (see below)
nav ladders 2 2, both ends linked both ways, as on master
bot stuck events a match (see below) 13.3 4.9
bot stuck episodes a round at the #135 staircase 0.044 0.012

Seven commits:

  1. Rework - the mesh itself.
  2. Id alignment with master - ids only, so an id-keyed diff against master reads sensibly.
  3. The roof walkway's drop over its parapet - one walk link cut (see below).
  4. One area per flight of stairs - the navmesh edits for engage (staircase) #135 staircase's 24 step strips become 4 sloped areas.
  5. The staircase's two lower turns - two landings split so the path turns clear of the stringer ends.
  6. Review fixes - the cuts at the mech, the car and the truck finished, one-way walk links, and climbs no bot makes, from an outside review (section 6).
  7. Blocked links - drop backs restored, and areas no player hull fits in removed, from the engine's hull audit of commit 6 (section 6, last part).

1. What changed

  • Sliver areas merged into their neighbors on the same floor, props carved out of standing room, and areas the engine blocks at round start (a prop or brush over their center) cut away. On master the engine blocks 22 areas at every round start; here none.
  • Walk links a player hull cannot cross are cut: past the truck near (0,1300), the wrecked mech near (850,1750) and the car near (1010,90). At the mech and the car this commit cut one direction of each link; commit 6 cuts the other.
  • The roof walkway west of the rooftop room (x 1250..1400, y 200..350, z 144) is a channel with 35 u parapets on its west, north and south sides. Master had two drops from it to the street 250 u below, one over the south parapet (12598 -> 728) and one over the north parapet (12600 -> 1426). A bot following either walks into the parapet. The south one was master's largest stuck spot. Both are cut (the north one in commit 3); the walkway is left through the rooftop room and the navmesh edits for engage (staircase) #135 staircase.
  • Ghost spawn 6's deck near (133,-145): its pocket had exits and no entry; a deck strip near (190,60) gives it one.
  • The navmesh edits for engage (staircase) #135 staircase is carried over unchanged in this commit: same 40 areas, same links, same ids. Commits 4 and 5 rework it.
  • Pockets no spawn can reach and nothing links into are removed.
  • Re-analyzed in game after the edits, so the per-area visibility lists bots use for what they can see are current.

2. Id alignment

1441 of the 1443 areas carry the id of the master area they match, including every area of the #135 staircase. Both ladders keep their ids. Areas are matched only within 64 u of height. Geometry, connections, attributes and the analysis data are unchanged by this step (verified topology-identical). After commits 4 and 5, 1421 of the 1426 areas carry master's ids; the other five are two areas of the rework and the three landing pieces of commit 5, numbered above master's highest id.

3. The roof walkway

With the staircase in place, bots reach the rooftop room (ghost spawn 2) both ways, and the north drop became the walkway's shortest way down. Measured against the same mesh without this cut (24 pairs): stuck events on the walkway, the room and ladder 2's top 11.8 -> 0 a match (0.47 -> 0 bot episodes a round; master 2.2 a match, 0.27 episodes a round). Bots leaving the room now walk down the staircase; commits 4 and 5 make that walk reliable.

4. One area per flight of stairs

#135 meshes each of the staircase's four flights as six strips, one per tread: 5-6 u deep on 8 u treads, flat, with gaps of 1-6 u between strips and up to 13 u between a flight and its landings. nav_generate never meshes stairs that way: it grows sloped areas over the ground samples that lie within 5 u of a plane, so a flight becomes one or a few areas, and the engine marks such an area NAV_MESH_STAIRS when it loads the mesh (CNavArea::TestStairs). A flat strip never gets that mark, so the path follower handled the staircase as 24 separate steps, with uncovered gaps between them.

Each flight is now one sloped area from its lower landing's edge to its upper landing's edge, level across the flight, its low edge at the lower landing's height and its high edge at the upper landing's: each meets its landings without a step. The plane stays within 13 u of every tread (the treads rise 10.3-11.7 u at a time). Each keeps its bottom step's id: 12730, 12737, 12748, 12749. The landings, the railing-top strips and every link into or out of a flight are unchanged; only the step-to-step links inside a flight are gone. The engine marks all four STAIRS at load. 24 areas become 4.

  • stuck at the staircase (over all three comparison batches): master 0.046 bot episodes a round, the mesh before this commit 0.176, after it 0.038 (stuck events a match 0.42 / 2.18 / 0.46)
  • stuck events a match against the mesh before this commit: x0.89 [0.63, 1.25], 48 pairs

5. The staircase's two lower turns

The middle landing (z -9) and the lower west landing (z -85) each join two flights that run side by side. At the inner corner of each turn the end of one flight's stringer stands up to 9 u over the landing, and the links put the path's turn within half a hull of it: 12588 -> 12587 crosses at the landing's west edge, and 9572, the lower landing's south part, met flight B 8 u off the flight's middle. The few bots that still stalled after commit 4 stalled there. The upper west landing already turns 40 u clear of its stringers and had no stalls.

  • Middle landing: 12588 and 12587 split at x 1640, and the west halves not linked to each other, so the turn is made east of x 1640.
  • Lower west landing: 12723 split at x 1525; its east part (new 12766) meets only flight A, 9572 now spans flight B's full width and meets only flight B, and the two connect through 12723 west of x 1525.

Stuck at the staircase: 0.012 bot episodes a round (master 0.044 in the same batches, commit 4 alone 0.038); none at the middle landing in 251 rounds. Against commit 4 alone, with and without #2176 (80 pairs): stuck events a match x1.00 [0.73, 1.38], FAIL_NO_PATH_EXISTS failures x0.82 [0.70, 0.95].

6. Review fixes

  • The cuts at the wrecked mech, the car and the truck, finished. Commit 1 cut the direction bots had been stuck in at the mech and the car and left the other (7031 -> 11341, 7032 -> 172, 7187 -> 7185); master already had the truck's 6251 -> 2684 one way only. The engine's hull sweep finds each pair blocked in both directions, so the remaining direction is cut too. A fourth link the review counted at the car, 7357 -> 843, is clear in both directions and gets its missing direction back.
  • Other one-way walk links. An earlier pass that cut walk links over railings and props cut one direction of some pairs and kept the other, so a bot following the kept direction walked into the same obstacle. Each pair was swept by the engine with a player hull in both directions:
    • blocked both ways, now cut both ways: seven pairs, at the handrails of the stairwell near (2750,300) (6) and a street lamp (1)
    • clear both ways, the missing direction restored: eight pairs, among them 7357 -> 843 and two beside panel vans (5780 / 1620, 10598 / 732)
  • Climbs no bot makes. Eight vehicle roofs and an awning near (-280,330) (39 areas) were reached only by walk climbs of 74-88 u, above every class's limit (a Recon plans climbs under 68 u). Those ten climbs are removed; the areas stay as pockets bots can drop from, like the other roof pockets this mesh keeps. In the map geometry a chain of crouch jumps of 40 u or less over bumper and hood reaches each of the vehicle roofs; bots are not given that route here.
  • Blocked links (commit 7). The engine's hull audit of commit 6 (a player hull stood on every area and swept across every link, in the server), and a link check against master:
    • three climbs of 19-32 u had lost their drop back; the climb reads clear on every lane, so the drop back is restored. A fourth led onto a 61 x 9 u strip on the wall trim beside the walkway stairs near (1322,370), where no hull stands and which the climb reached through a wall (no lane clear): the strip is deleted
    • areas no player hull stands in, every stand sample blocked, are deleted: squares inside the spider tank, the taxi, two street lamps, a scooter and a car, two corner squares near (1850,630), and 7187 inside the car by the parking meters near (1010,62) (bots heading into it stuck: 20 stuck events in 48 matches on commit 6, 2 after)
    • six pairs blocked both ways that bots never cross are cut both ways: a parking meter, a scaffold strip, a stair handrail, the spider tank's legs (2), a vehicle barrier
    • kept: the two squares inside the wrecked mech near (838,1750), which bots cross by jumping over its legs (13 crossings in 24 matches, no stuck event), and four links the audit reads blocked on displacement slopes, which bots cross; the recycle bin near (2450,812), a physics prop, left as it stands at map load for the bot code's runtime prop avoidance

Every area keeps its id.

How it was tested

  • Reachability:
    • every spawn can reach every other spawn
    • every objective is reachable
    • 102 areas no spawn can reach, in 23 pockets, all of which lead back into the mesh: ledge, roof and vehicle-roof pockets bots can only drop from (master has 110 in 26 pockets). 39 of them were reached before commit 6 only by climbs of 74-88 u, which no class plans
    • no area that can be entered and not left, for any class, counting only the climbs each class plans (Recon under 68 u, Assault 48 u, Support 43 u)
    • no walk link between two areas at the same level runs one way, and no climb a class plans lacks its drop back
    • the engine hull audit of this mesh: of 2510 pairs of areas at the same height, 3 read blocked both ways, the mech squares kept above; one by a physics prop (BLOCKED_LINKS map=ntre_engage_ctg level_pairs=2510 oneway=0 keep=0 cut=0 area=3 check=4 entity=1)
    • the engine blocks no area at round start (master: 22)
  • Falls. The map has no trigger_hurt; its two env_fire do 1 damage a second. No drop kills a bot at full health (the deepest drop link is 340 u, 9 damage). No fall marks were needed.
  • Master against this mesh, same bot code. Byte-identical copies of the map, one carrying each .nav, run back to back. CTG, 2 rounds of 3 minutes with the ghost pinned at each of the 8 ghost spawns in turn, 12 runs each (two batches of 6), 30 bots, default weapon damage, 8 matches at a time. Every match reached match end with 0 asserts and no crash (1127 matches on engage and disengage in all, probes and the other revisions included). The stuck count is OnStuck plus FAIL_STUCK move failures, excluding the spawn crush at round start and the weapon-scavenging behavior.
    • stuck events 13.3 -> 4.9 a match (paired x0.38, 95 % interval [0.29, 0.49], 70 of 96 pairs down, 19 up)
    • without #2176 (the unwanted-ladder release switched off in both arms, so upstream ladder code; 4 runs a spawn): 12.2 -> 4.3 (x0.52 [0.32, 0.87], 23 of 32 pairs down, 8 up); the roof walkway 0.33 -> 0 bot episodes a round and the staircase 0.037 -> 0, as with #2176
    • FAIL_NO_PATH_EXISTS move failures 17.5 -> 15.0 a match (x0.86 [0.73, 1.00]); about three quarters of them on both meshes are bots attacking toward an area the bot code marks hazardous, which the path cost refuses; "fell off the path" failures 0.06 -> 0.02
    • kills 49.6 -> 49.1 a match (x0.99 [0.97, 1.01]); ghost captures 19 of 252 rounds -> 25 of 251 (rounds on this map mostly end by elimination); team kill ratio (Jinrai / NSF enemy kills) 0.96 -> 0.98; deaths to the world 1 -> 0 in 96 matches
    • bots still see and shoot across the mesh: 99 % of kills are between bots in different nav areas (master 99 %), 95 % in non-adjacent areas (master 97 %); median kill distance 974 u (master 1001 u)
    • at the spots: the roof walkway, the rooftop room and ladder 2's top 3.0 -> 0 stuck events a match (0.31 -> 0 bot episodes a round); the navmesh edits for engage (staircase) #135 staircase 0.48 -> 0.06 (0.044 -> 0.012); the car near (1010,90) 0.6 -> 0.4
  • The review fixes (commit 6) against commit 5, one batch. The same twin setup with both revisions, the ghost pinned at each of the 8 ghost spawns, 3 matches each, 30 bots, default weapon damage, 8 matches at a time, on the same bot code. The fixes change no route a bot plans except at the restored and cut links, so the test checks that they cost nothing:
    • stuck events 3.9 -> 3.4 a match (paired x0.98, 95 % interval [0.60, 1.62], 11 of 24 pairs down, 10 up)
    • stuck events at the ends of the edited links 13 -> 9 in 24 matches (at the car 10 -> 7, at the mech 3 -> 0)
    • FAIL_NO_PATH_EXISTS move failures 17.6 -> 16.1 a match; kills 48.8 -> 48.5 a match; ghost captures 8 of 64 rounds -> 6 of 67; 99 % of kills between different nav areas on both, 97 / 95 % between non-adjacent ones
    • no fall deaths, no asserts, no crashes in 48 matches
  • Commit 7 against commit 6, two batches. The same twin setup, the ghost pinned at each of the 8 ghost spawns, 6 matches each, 30 bots, default weapon damage, 8 matches at a time, on the same bot code:
    • stuck events 3.5 -> 3.7 a match (paired x1.09, 95 % interval [0.76, 1.55], 17 of 48 pairs down, 22 up); at the edited links and areas 22 -> 2 in 48 matches
    • FAIL_NO_PATH_EXISTS move failures 13.8 -> 16.7 a match: bots attacking toward an area the bot code marks hazardous, spread over the map, none toward a deleted area; kills 48.8 -> 49.2 a match; ghost captures 9 -> 10 of 96 rounds; 99 % of kills between different nav areas on both, 95 / 97 % between non-adjacent ones
    • no fall deaths, no asserts, no crashes in 96 matches

How the pinned-ghost probes work

The offline checks show that the mesh links an area to a spawn. They do not show that a bot will walk there. To test one spot in game, the ghost is placed on it and bots are sent to get it.

  • The ghost is placed on the spot by a server cvar. A harness-only neo patch, used on test builds and not part of this PR or any neo PR, adds sv_neo_ghost_spawn_pos "x y z", which replaces the position of the round's ghost spawn. The map file is not edited.
  • A spot counts as reached when a bot stands within 40 u of it, or picks the ghost up there rather than on the floor below, where a ghost that rolls off a narrow landing ends up. A wider radius would count a bot on a level above or below.
  • Master's mesh runs beside this one on a byte-identical copy of the .bsp under a second name that loads its own .nav, so the same pin runs on both meshes in one batch.

On this map

Both ends of both ladders were probed (every landing at ladder 1's top), plus the four landings of the #135 staircase, 3 matches each, 12 bots, at a tenth of weapon damage, on the same build, on this mesh before commit 3 (which only removes a drop off the roof); ladder 2's bottom got 6 more matches on each mesh, commit 3 included. Commits 4 and 5 leave the probed landings where they were (12587 and 12723 are split by commit 5, and both pieces stay linked to the mesh).

probe master this PR
ladder 1 bottom 3 of 3 3 of 3
ladder 1 top (all landings) 8 of 9 6 of 6
ladder 2 bottom 7 of 9 7 of 9 (6 of 6 with commit 3)
ladder 2 top 3 of 3 3 of 3
staircase landings (4) 12 of 12 12 of 12

Bots rarely climb either ladder in normal play (0 to 0.5 climbs a match on both meshes). Ghost spawn 6 (see "Known, not changed") was probed the same way, 9 matches each: picked up in 8 of 9 on master and 6 of 9 here, always on the floor under the deck, after 90-580 s on both.

Known, not changed

  • A wall corner near (1750,2715): bots coming south into area 219 and turning west into 330 cut the corner and press into it, 8 bot episodes in 251 rounds (master 0 of 252). The areas and links there are the same as master's.
  • Ghost spawn 6 (133,-145): the ghost falls off the tilted deck to the floor under it, and bots take 150-250 s to pick it up, on master as on this mesh (both mesh that spot the same way). That is bot code for a ghost bots cannot walk onto, not the mesh.

One file. Against master: 1950 -> 1443 areas, 3,343,660 -> 1,911,403
bytes; areas the engine blocks at every round start 22 -> 0; areas
no spawn can reach 110 -> 63.

- Sliver areas merged into their neighbors on the same floor, props
  carved out of standing room, and areas the engine blocks at round
  start (a prop or brush over their center) cut away.
- Walk links a player hull cannot cross are cut: past the truck near
  (0,1300), the wrecked mech near (850,1750) and the car near
  (1010,90).
- The drop from the roof near (1350,275) to the street 250 u below
  (12598 -> 728) is cut. It was master's largest stuck spot: bots
  heading for it pressed into the roof's edge.
- Ghost spawn 6's deck near (133,-145): its pocket had exits and no
  entry; a deck strip gives it one.
- The balcony staircase from NeotokyoRebuild#135 is kept as merged.
- Pockets no spawn can reach and nothing links into are removed.
- Re-analyzed in game after the edits.
Renumbers area and ladder ids only, so an id-keyed diff against
master reads sensibly: 1441 of the 1443 areas and both ladders carry
the id of the master area they match, all of NeotokyoRebuild#135's staircase among
them. Areas are matched only within 64 u of height. Geometry,
connections, attributes and the analyzed visibility data are
unchanged (verified topology-identical).
The walkway at z 144 west of the rooftop room (12598 / 12600 /
12610 / 12611, x 1250..1400, y 200..350) has 35 u parapets on its
west, north and south sides. 12600 -> 1426 was a 252 u drop to the
street over the north parapet: bots following it walked into the
parapet. With NeotokyoRebuild#135's staircase it had become the shortest way off
the roof. Cut; the walkway is left through the rooftop room and the
staircase. Master's twin of it on the south side (12598 -> 728) was
already cut. Ids unchanged. Re-analyzed in game after the edit.
NeotokyoRebuild#135 meshes each of the balcony staircase's four flights as six
5-6 u strips, one per 8 u tread, with gaps of up to 13 u between
them and the landings. Each flight is now one sloped area from its
lower landing's edge to its upper landing's edge, its low and high
edges at the landings' heights, as nav_generate meshes a flight;
the engine marks the four STAIRS at load. 24 areas become 4, each
keeping its bottom step's id (12730, 12737, 12748, 12749). The
landings, the railing-top strips and every link into or out of a
flight are unchanged. Re-analyzed in game after the edit.
The middle and lower west landings each join two flights that run
side by side, and the end of one flight's stringer stands up to 9 u
over the landing at the inner corner of the turn. The links put the
path's turn within half a hull of it, and bots turning there pressed
into it. The upper west landing already turns 40 u clear. The middle
landing (12588 / 12587) is split at x 1640 and its west halves are
not linked to each other, so the turn is made east of x 1640. The
lower west landing is split at x 1525: its east part beside flight
A (new 12766) and 9572 beside flight B are not linked to each other
and meet through 12723 west of x 1525; 9572 now spans flight B's
full width, so a bot steps onto the flight at its middle. Re-analyzed
in game after the edit.
An outside review of this mesh found walk links that run one way
only, among them the reverse halves of the cuts at the wrecked mech,
the car and the truck, and roof pockets entered only by climbs no bot
plans. Each one-way pair was swept with a player hull in both
directions by the engine's hull audit.

- Finish the cuts at the wrecked mech (7031 / 11341, 7032 / 172), the
  car (7187 / 7185) and the truck (6251 / 2684): the hull is blocked
  both ways, and the reverse direction was still linked.
- One-way links left by an earlier railing cut: cut both ways the
  seven pairs blocked both ways (interior stair handrails, a street
  lamp); restore the missing direction of eight pairs that are clear
  both ways, among them 7357 -> 843 beside the car and two panel vans.
- Remove the ten climbs of 74-88 u that were the only way onto eight
  vehicle roofs and an awning (39 areas). No class plans a climb that
  high, so no route changes; the areas stay as drop-only pockets, like
  the other roof pockets in this mesh.

Every area keeps its id. Re-analyzed in game (same graph, fresh
visibility).
The engine's hull audit (a player hull stood on every area and swept
across every link, in the server) of the previous revision, and
navconnect's link check against master:

- Restore the drop back of three climbs of 19-32 u whose drop had
  been cut: the climb reads clear on every lane.
- Delete 12630, a 61 x 9 u strip on the wall trim beside the walkway
  stairs at (1322, 370): no hull stands on it, and its only way in
  was a 26 u climb blocked on every lane.
- Delete areas no player hull stands in, every stand sample blocked:
  squares inside the spider tank, the taxi, the car by the parking
  meters, two street lamps, a scooter and a car, and two corner
  squares behind a wall at (1850, 630).
- Cut six pairs blocked both ways that bots never cross: a parking
  meter, a scaffold strip, a stair handrail, the spider tank's legs,
  a vehicle barrier.

Kept: the squares inside the wrecked mech near (838, 1750), which
bots cross by jumping over its legs without sticking, and links the
audit reads blocked on displacement slopes, which bots cross; the
recycle bin near (2450, 812), a physics prop, left as at map load.

Existing areas keep their ids. Re-analyzed in game (same graph,
fresh visibility).
@sunzenshen

Copy link
Copy Markdown
Contributor Author

Known issue:

These kinds of gap jumps are immensely difficult for the bots to make, so these ended up getting cut because bots can get into a failure loop:

image

Maybe it's not the most resilient solution, but this would need dedicated attention as a special bot case, that I think should be outside the scope of reshaping the nav areas.

@sunzenshen
sunzenshen marked this pull request as ready for review October 9, 2026 05:26
@sunzenshen
sunzenshen requested a review from sunmachine October 9, 2026 05:26
@sunzenshen
sunzenshen merged commit 4b0c79f into NeotokyoRebuild:master Oct 14, 2026
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.

2 participants