Skip to content

Bots skip weapons they cannot reach when scavenging - #2186

Merged
sunzenshen merged 1 commit into
NeotokyoRebuild:masterfrom
sunzenshen:bot-seek-weapon-reach
Oct 3, 2026
Merged

sunzenshen merged 1 commit into
NeotokyoRebuild:masterfrom
sunzenshen:bot-seek-weapon-reach

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Description

Bots skip weapons they cannot reach when scavenging

CNEOBotSeekWeapon checks that its path ends within 150 u of the weapon, but that check never fails for a weapon lying off the nav mesh. When no nav area near the weapon can be reached, Path::Compute builds a partial path to the closest area and, with includeGoalIfPathFails at its default, appends the weapon's own position as the last segment. The path therefore always "ends at the weapon", and the bot walks the last leg in a straight line from the mesh's edge to the weapon, over whatever lies between. FindNearestPrimaryWeapon's visibility check is skipped for a weapon that has no nav area, so nothing else filters these weapons out. Giving up after 10 s, OnStuck and OnMoveToFailure also leave the weapon unmarked, so the next scavenge, a second later for a bot without primary ammo, picks the same weapon again.

ntre_saitama_ctg shows it every time the ghost spawns by the fire pit. A dead bot's weapon lands on the pit floor, 160 u below the grate rim; the next bot to scavenge sprints off the rim towards it and dies in the pit's trigger_hurt, dropping another weapon there. Maps with ladders tend to have such drops.

A weapon is now a scavenging target only if it lies on the nav mesh and the bot has a full path to it. Changes, in neo_bot_seek_weapon.cpp and neo_bot_path_compute.cpp:

  1. Only weapons on the nav mesh. FindNearestPrimaryWeapon takes a weapon only if it lies over its nearest nav area (found with brush line of sight), or at most half a player's width (16 u) past the area's edge, which is how far generated areas stop short of walls; and at most 32 u above the area's surface (a desk, a low crate) or 8 u below it (an area's plane can pass above a stairway's treads). The visibility check uses that area.
  2. Only a full path, and it stays on the mesh. SeekWeapon follows only a path that reaches that area, and it ends at the point on the area nearest the weapon; a bot there still touches a weapon lying against a wall. The path is checked when the behavior starts (before a ghost carrier drops the ghost for the weapon), at every repath (the weapon may have been knocked off the mesh meanwhile) and whenever the behavior picks another weapon.
  3. CNEOBotPathCompute returns the full path or nothing when asked to. Its comment says so for includeGoalIfPathFails false, but it returned any valid path, and Path::Compute leaves a partial path valid. Path::Compute's own result is the only reliable signal: when the bot's own area is the closest one it reaches, it builds a trivial path straight at the goal and reports it as COMPLETE_PATH. No caller passed false before this change.
  4. Remember a weapon the bot failed to reach. Giving up, a failed path, OnStuck and OnMoveToFailure add the weapon to the ignore cache the behavior already keeps for its "path does not get close enough" check. Both callers already own such a cache and reset it when their behavior starts (the tactical monitor's once a life).

Evidence

Upstream master (at #2168) plus the unwanted-ladder release half of the draft #2176, with harness logging added and a test-only switch for this change. 30 bots, default weapon damage, 8 matches at a time; each run is paired with a stock run of the same map, ghost spawn and repeat, back to back in the same batch. CTG: 2 rounds of 3 minutes with the ghost pinned at one ghost spawn; TDM: one 5-minute round. All 306 matches reached match end with 0 asserts and no crash. "Scavenging stuck" is OnStuck plus FAIL_STUCK move failures inside the weapon-scavenging behavior; "other stuck" is the rest, without the round-start spawn crowd. Intervals are 95 % bootstrap intervals of the paired ratio.

ntre_saitama_ctg, 46 pairs: the reworked navmesh from the companion neoAssets PR at every ghost spawn (34) and at spawn 9 (6), and master's navmesh at spawn 9 (6):

a match stock this change
fire-pit deaths, ghost at spawn 9 (14 pairs) 2.57 (36 in 14 matches) 0
fire-pit deaths, the other 16 spawns (32 pairs) 0 0
scavenging stuck 0.50 0.04
"fell off the path" move failures 1.33 0.17
other stuck, reworked navmesh (40 pairs) 20.8 28.4 (x1.34 [1.01, 1.79])
other stuck, master's navmesh, spawn 9 (6 pairs) 55.8 55.2 (x0.91 [0.49, 1.77])
FAIL_NO_PATH_EXISTS 3.1 4.2
kills 50.6 49.5

An earlier version of this change took weapons up to 40 u off the mesh and let the path end at the weapon itself. It still lost two bots in the pit in the same 14 spawn-9 matches, both scavenging a weapon lying just past the rim.

The rise in other stuck (x1.27 [0.98, 1.67] over all 46 pairs; the earlier version measured x1.21 [1.00, 1.49] over 109 pairs) comes from where the bots go instead. Most of it is in the map's west ladder tower (x1.53 [0.89, 2.55] there, x1.21 [0.85, 1.72] elsewhere): a descent of its lower ladder ends back on the top floor about 95 % of the time, and the bot then walks its floor-level path into a wall until the stuck monitor fires; one such bot can log dozens of stuck events in a round. Bots no longer busy with, or killed by, unreachable weapons spend more of the round on objectives and walk into that trap more often. The ladder-descent half of the draft #2176 targets it.

Other maps, 56 pairs (5 ghost spawns each, 2 runs; ntre_bullet_tdm 6 TDM matches), on the navmeshes of their companion or planned neoAssets PRs:

map pairs scavenging stuck, stock -> this other stuck, this / stock
ntre_tarmac_ctg 10 2.3 -> 0.0 x0.42 [0.17, 1.08]
ntre_transit_ctg 10 1.0 -> 0.0 x0.81 [0.43, 1.31]
ntre_subsurface_ctg 10 0.2 -> 0.0 x0.82 [0.42, 1.35]
ntre_marketa_ctg 10 0.1 -> 0.0 x1.19 [0.78, 2.00]
ntre_bullet_tdm 6 6.8 -> 3.0 x1.24 [0.67, 2.16]
ntre_rogue_ctg 10 3.0 -> 0.2 x1.30 [0.53, 2.69]
pooled 56 1.91 -> 0.36 x0.88 [0.64, 1.19], 28 down / 28 up

Pooled over those maps: FAIL_NO_PATH_EXISTS 10.1 -> 5.4 a match, "fell off the path" 2.0 -> 0.7, kills 65.8 -> 65.8, deaths to hurt volumes and falls 8 -> 8 in 56 matches (none while scavenging: rogue's canal water and tank, bullet's track). Bots start scavenging 17 % less often and pick up 11 % fewer weapons (17.8 -> 15.9 a match); the weapons they no longer go for lie off the mesh. On ntre_saitama_ctg most of those were seen only while still falling after a death (and taken once they landed), 0.3 a match lay in the pit, and about 0.5 a match lay 16-40 u past an area's edge.

Cost

One nearest-area search with line-of-sight traces per candidate weapon that would otherwise win, during a scavenge search (at most once a second per bot, and only while a bot looks for a weapon), and the same search at each repath of the behavior (every 1-2 s while it runs).

Toolchain

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

Linked Issues

@sunzenshen
sunzenshen force-pushed the bot-seek-weapon-reach branch 2 times, most recently from 00c5635 to dbf883c Compare October 1, 2026 05:49
@sunzenshen
sunzenshen marked this pull request as ready for review October 1, 2026 05:50
@sunzenshen sunzenshen added the Bots Related to bot players label Oct 1, 2026
@sunzenshen
sunzenshen requested a review from sunmachine October 1, 2026 05:50
@sunzenshen sunzenshen changed the title Bots skip weapons they cannot reach Bots skip weapons they cannot reach when scavenging Oct 1, 2026
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 1, 2026
SeekWeapon checks that its path ends within 150 u of the weapon, but
Path::Compute appends the goal position to a path that failed to
reach it, so the check always passes for a weapon lying off the nav
mesh: the bot walks the last leg straight at the weapon, over
whatever lies between. On ntre_saitama_ctg that leg runs off the fire
pit's grate rim into its trigger_hurt, and each death drops another
weapon for the next scavenger. Giving up, a failed path, OnStuck and
OnMoveToFailure also left the weapon unmarked, so the next scavenge
picked it again.

FindNearestPrimaryWeapon now skips a weapon with no nav area within
40 u of it (brush line of sight), or more than 72 u above / 32 u
below that area. A weapon the bot failed to reach goes into the
ignore cache the behavior already keeps.

Seek only weapons on the mesh with a full path

The first change still let a weapon near the nav mesh draw a bot: the
path to it could stop at the mesh's edge and end with a straight walk
to the weapon, and a weapon on an area with no links in (a pit floor
cut off from the mesh) gets that walk from the area beside it.

FindNearestPrimaryWeapon now takes a weapon only if it lies over a nav
area or at most half a player's width past its edge (generated areas
stop that short of walls), at most 32 u above the area's surface or
8 u below it. SeekWeapon follows only a full path to that area, ending
at the point on it nearest the weapon, so the bot never leaves the
mesh for a weapon; a bot at the area's edge still touches one lying
against the wall. This is checked when the behavior starts (before a
ghost carrier drops the ghost), on every repath and whenever it picks
a new weapon. A weapon without such a path goes into the ignore cache.

CNEOBotPathCompute now does what its comment says: with
includeGoalIfPathFails false it returns the full path or nothing.
No caller passed false before.
@sunzenshen
sunzenshen force-pushed the bot-seek-weapon-reach branch from dbf883c to ab3ae94 Compare October 3, 2026 17:51

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

Took a bit, but it LGTM

@sunzenshen
sunzenshen merged commit e6eec40 into NeotokyoRebuild:master Oct 3, 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