Skip to content

Bots walk off a ledge they land on part way down a drop - #2201

Merged
sunzenshen merged 4 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-path-drop-land-pr
Oct 17, 2026
Merged

sunzenshen merged 4 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-path-drop-land-pr

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

Background

When a path drops off a ledge, Path::Compute puts a DROP_DOWN point at the top of the drop and the landing straight below it. If the bot lands on something part way down that has no nav area (a rim, a sill, a narrow ledge), the path follower's "goal is high above us" check sees the drop top more than a jump above it. It then tests whether the landing is reachable with IsPotentiallyTraversable from the feet to the landing. That trace runs diagonally down through the ledge the bot stands on, so it fails. The follower reports FAIL_FELL_OFF, invalidates the path, clears the stuck status and returns before steering. The behavior re-paths from the bot's last known area, which is still the area it dropped from, and gets the same drop again. The bot never moves: one FAIL_FELL_OFF a tick, and the stuck monitor is reset every tick, so OnStuck never fires either.

This was found on ntre_ghost_ctg, where bots that ran off the upper walkway into a vent shaft and landed on the 12 u rim beside the hole stood there, ducked, for the rest of the round (46-57 s). The loop can happen at any drop with a narrow ledge part way down, on any map, so it is fixed in the path follower rather than in one navmesh.

On the current neoAssets meshes no planned path drops onto such a ledge (the merged ghost mesh has no drop link from the walkway's west end), so the spawn sweep below shows no change on them, and the loop guard had nothing to act on either. The fix keeps the follower from looping when a mesh does plan such a drop, which any nav_generate pass or link edit can bring back; the replays below show the loop on a mesh revision that has it.

What this PR changes

  1. Steer on. When the goal is a DROP_DOWN top, the landing is below the feet and within 50 u of them in 2D (the bot is in the drop's column, on a ledge or still falling), and there is no nav area under the feet, the landing is tested level from the feet: can the bot step toward it? If it can, the drop top stays the goal and the bot steers at it, which walks it off the ledge, and it falls to the landing. It uses only the bot's own planned path.
  2. A ledge with a nav area re-paths as before. A bot that lands on a ledge wide enough to have a nav area of its own is left to the stock re-path, which plans from that area. Without this, the drop's top stands over such a ledge, and steering at it circled the bot on the spot until the stuck monitor fired (found in the measurement below, second commit).
  3. Loop guard. A fell-off re-path that leaves the bot where it was (3 within 2 s, feet less than 8 u apart) no longer clears the stuck status, so the stuck monitor can see a bot that sits off the mesh with a path it cannot follow, and OnStuck handling can act.

A bot whose landing is not clear at its own height, that is not over the drop's column, or that stands on a nav area gets the stock behavior. The change is in NextBotPathFollow.cpp / .h under #ifdef NEO, with the values as named constants.

How to test

The vent drop on ntre_ghost_ctg is no longer planned with the merged navmesh (NeotokyoRebuild/neoAssets#163 has no drop link from the walkway's west end), so the loop needs a mesh that plans a drop onto a rim. To see it, use a ghost mesh revision that keeps the walkway drop (area 4314 to 225), such as neo/maps/nav/ntre_ghost_ctg.nav at commit 2975aa4 on the bot-navmesh-generation-research branch of sunzenshen/neoAssets, map ntre_ghost_ctg, neo_bot_quota 30, join spectator, and watch the vent shaft at the west end of the upper walkway near (-1425, 920, 255). A bot that lands on the rim beside the hole (z 163) stays there without this PR (nb_debug PATH shows FAIL_FELL_OFF every tick); with it, it steps off within about a second and lands in the room below.

Risk

  • The level trace and the nav area lookup are made only where the stock check was about to make its own trace, so per-tick cost is unchanged in practice.
  • A bot that steers on and still cannot get off the ledge stays on the drop goal with its stuck status kept, so the stuck monitor deals with it as with any other stuck bot instead of the old silent loop.
  • The loop guard only withholds a ClearStuckStatus call. It never re-paths or moves the bot itself.

Merging with #2200

#2200 adds code at the same four places in these two files (the constants block, the constructor, the helpers after IsNearNavMesh, the header's NEO members). Whichever of the two merges second resolves those conflicts by keeping both sides; nothing else changes. A trial merge of both branches builds clean. The logic does not overlap: this PR acts inside the "goal is high above us" check on a DROP_DOWN goal, and #2200's re-check leaves any goal that is not an ordinary ground waypoint alone.

Results

All on this branch alone against master f426c222b (both with the harness's logging and match-end support only), Release builds, neoAssets master navmeshes, 30 bots, default weapon damage (1.485), 1 round, 15 s freeze, every player at one XP tier rotated over the spawns, 8 wide. Every match reached match end with 0 asserts; one Debug match on ntre_ghost_ctg had 0 asserts.

Replays at the ghost vent. A test-only harness puts a bot on the walkway (three start points 40-75 u east of the drop, small random jitter on each repeat) with a floor point in the room below as its goal, 3 classes, 24 repeats a start. On the merged ghost mesh the planned route does not use the drop (16-24 s routes, no rim stops in either arm). On the ghost mesh revision named in How to test, which keeps the walkway drop, used as a test rig (same .bsp):

trials stopped on a rim with FAIL_FELL_OFF reached the floor
master 216 15 201
this PR 216 0 216

The rim stops are on both rims (east x -1408, z 163; west x -1440, z 163-177), about 1-1.5 s after the start. With the PR every trial reached the floor (3.3-4.2 s median by start and class).

Spawn sweep, ntre_ghost_ctg (10 ghost spawns) and ntre_vtol_ctg (14), each spawn once, arms paired by spawn:

a match master this PR ratio [95 % CI]
stuck (OnStuck, FAIL_STUCK) 5.9 5.1 x0.86 [0.48, 1.45]
stuck episodes (one bot, 10 s) 1.9 1.6 x0.83 [0.50, 1.31]
FAIL_FELL_OFF 2.1 2.0 x0.96 [0.61, 1.43]
no path 1.8 1.1 x0.63 [0.27, 1.59]
kills 24.9 25.6 +0.7 [-0.3, +1.8]

The steer-on acted 30 times, mostly bots still falling down ordinary drops, and no stuck episode followed any of them. No bot in either arm looped on a ledge, so on master's meshes the change has nothing to fix in this sweep: it guards a loop that the navmesh decides whether to expose.

An earlier sweep of the first commit alone found one new spot: on ntre_ghost_ctg at (-850, 1570, 128), a bot dropping from the walkway landed on a 20 u ledge with its own nav area (4883), part way down to the floor. The level test passed, the drop top stood over the ledge, and the bot circled under it; OnStuck fired after 6.6 s and kept firing for 10 s more (2 bots in 10 matches). The second commit leaves a bot on a nav area to the stock re-path, and the sweep above has no stuck after a steer-on there.

Earlier evidence, on the harness research build

Measured on our research build (master plus other open bot PRs, #2200 on), with the change behind a test-only switch, on the research navmeshes of the time: the same replays gave 14 rim stops in 336 trials without the change and 0 in 216 with it; a survey of 29 ntre_ maps (212 matches an arm) went from 8.92 to 1.46 FAIL_FELL_OFF a match, 1659 of the 1892 without it from 3 bots on the ghost vent rims. On ntre_vtol_ctg's pit under the map the loop guard let the stuck monitor fire after about 6 s (3 bots in 14 matches); the merged vtol mesh had no bot fall in during the sweep above.

Approaches considered and dropped

  • A nav area on the rims. The rims beside the hole are 12 u wide; a 32 u hull standing on one is mostly over the hole, so no walkable area fits there.
  • NAV_MESH_AVOID on the walkway's west end. It would make fewer paths use that drop, but a bot that still drops there would loop as before, and the same loop can happen at any drop onto a narrow ledge.
  • Re-anchoring a fallen bot to the nav area below it. From the room's area the path runs along the room's floor, so the bot on the rim presses into the shaft wall instead of dropping. The bot needs to finish the drop it planned.

Toolchain

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

Linked Issues

A bot that lands on a ledge part way down a planned drop failed the
"goal is high above us" check every tick: its diagonal trace from the
feet to the landing hits the ledge's rim, so it reported a fall,
re-pathed to the same drop and had its stuck status cleared, and never
moved. Test the landing level from the feet instead, and keep steering
at the drop, which walks the bot off the rim.

Fell-off re-paths that leave the bot where it was (3 within 2 s, under
8 u apart) no longer clear its stuck status, so the stuck monitor can
act on a bot off the mesh with a path it cannot follow.
…embers)

Both this branch and NeotokyoRebuild#2176 add constants, constructor lines and header
members next to the same places in NextBotPathFollow; keep both.
A bot that drops onto a ledge wide enough to have a nav area of its
own, part way down a drop, also passed the level test: the drop's top
stands over that ledge, so steering at it circled the bot on the spot
until the stuck monitor fired (10 s on ntre_ghost_ctg at
(-850, 1570, 128), area 4883). Such a ledge is a place to plan from,
so keep the stock fell-off re-path there; the steer-on is for rims
and sills with no nav area, and for a bot still falling.
Fold the steer-on into the stock fell-off test as one keepDropGoal
condition instead of an empty if/else, pass the landing segment the
caller already has, word the helper's comment as the caller's
precondition, and say why the column range matters for a stuck bot.
IsFellOffLoop counts as well as tests, so call it CountFellOffRepath.
Constants are constexpr; header comment aligned.
@sunzenshen
sunzenshen marked this pull request as ready for review October 15, 2026 04:04
@sunzenshen
sunzenshen requested a review from sunmachine October 15, 2026 04:04
@sunzenshen sunzenshen added the Bots Related to bot players label Oct 15, 2026

@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.

Also good!

@sunzenshen
sunzenshen merged commit 2c8254e into NeotokyoRebuild:master Oct 17, 2026
10 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