Skip to content

fix(nav): map-pinned navmesh patches — the carve's blind spots stop being bot traps - #16

Open
Xerialen wants to merge 7 commits into
qw-ctf:mainfrom
Xerialen:dm3-westshelf-navpatch
Open

fix(nav): map-pinned navmesh patches — the carve's blind spots stop being bot traps#16
Xerialen wants to merge 7 commits into
qw-ctf:mainfrom
Xerialen:dm3-westshelf-navpatch

Conversation

@Xerialen

@Xerialen Xerialen commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What this fixes

dm3 has a machinery shelf west of SNG (x −920..−845, y −48, standing height z 88) that the
column carve cannot see: the surface is ~12 u wide in y, sitting between the y=−64 sample row
(inside the wall) and the y=−32 one (out over the drop). plant_cell's doc already names it as
the worked example of the class. During normal 4on4 play bots climb onto it in pairs, localize
through nearest() to the floor 104 u below, plan routes that are fiction from where they
actually stand, and wedge until the round ends — one bot was carrying quad while stuck face-first
in the lip.

Measured on current main (cc5fa8ea, 600 s pacifist T2 on dm3, six runs): the shelf's trap line
fired 13/0/0/20/94/53 times per run — episodic, but when the big episodes hit, per-bot standstill
rises to 27.0–33.5 s against a 10.6–19.0 s range in the other runs, and the map's stall total
jumps (238/234 in those two runs, against a six-run average of 199.8 — 170.5 averaging only the
runs the trap missed entirely).

What the change is

plant_cell / plant_drop were built for exactly this failure class, but only exist as
control-channel verbs — nothing applies them in production, so every restart forgets the shelf.
This PR adds the missing wiring, deliberately as data, not behavior:

  • nav_patch.rs — a short, reviewable table of hand-verified plants per map, applied once
    when a nav build finishes. One entry: dm3 west-shelf, 4 cells along the shelf + 4 drops off
    its north lip (the south face is solid; drops that way fail classify_grounded, which is how
    the open side was found).
  • Fail-closed and transactional: every mutation goes through the build's own validators
    (plant_cell refuses non-standable spots, plant_drop accepts only a drop the build would
    itself emit), each planted cell must snap to the standing height measured on the shipped BSP
    (88.03125 ± 0.5 — a local geometry precondition: it catches the floor moving, the link
    validators catch the surroundings changing, but it is not a whole-BSP fingerprint), and each
    patch mutates a clone of the graph that only replaces the live one when every step validated.
    Each patch prints one unambiguous console line: applied (...) / skipped (already meshed) /
    failed (<why>). A skipped or failed patch leaves the graph bit-for-bit what the build
    produced.
  • rtx_nav_patch cvar, default on; 0 → build only, byte-identical graph to main.
  • testsuite/tools/trap_repro.py — the paired on/off acceptance drill: teleport a puppet bot
    onto the surface, goto the floor, watch. Exit code is the verdict, so both arms are
    scriptable.

If the carve ever learns to see the shelf, the patch reports skipped (already meshed) and
becomes a no-op — it cannot conflict with a future proper fix.

Evidence (branch head build 841a64e0, build==deployed digest-verified on the rig)

Ruler Patch on (this PR) Patch off / main (was)
Recovery drill, 40 s window, same binary 0/8 stuck, escape median 0.63 s (span 0.51–1.00) 4/4 stuck, still on/above the shelf when the window closes (three trials grounded at z=88, one mid-hop at z=124), 5–8 displacement stalls per trial on the floor walk links
T2 ×4, trap-line firings 0 / 0 / 0 / 0 13/0/0/20/94/53 across six runs on main
T2 per-bot standstill 8.4–22.0 s 27.0–33.5 s in the two big-episode runs (10.6–19.0 otherwise)
T2 displacement per run 25.8 60.7
T2 total stalls per run (avg) 185.0 199.8
New hotspots none — largest per-link increase is +4.1/run on 34342 (the known ring chain-entry family), within its episodic spread

The server log's status line is captured as evidence too:
rtx: navpatch west-shelf: applied (4 cells, 4 drops). Ingress was caught on demo: the bots
climb up by themselves (pairwise, via the machinery's lower steps), which is why carving the
surface away or fencing it off would not work — the navmesh has to know the surface instead.

The path question — for you to decide

Two honest ways to kill this class of trap:

  • A. Pin patches per map (this PR). Small, fail-closed, per-surface, zero risk to any other
    map or route; the cost is a hand-maintained table and the admission that the carve stays blind.
  • B. Teach the carve to see narrow tops (sub-GRID sampling or seeded probes on brush tops).
    The root fix, generalizes to every map — but it changes cell/link populations globally, so every
    route and every tuned cost on every map is in play, and it needs its own fence campaign.

This PR is deliberately path A and does not foreclose B: if B ever lands, every patch self-skips.
There are more instances of the class waiting (issue #9's SNG shelf is the same disease), so if
you'd rather go straight to B, say so and this stays a rig-side tool + the drill.

Checklist (all run on the final branch head, build==deployed digest 841a64e0)

  • cargo test --workspace --release: 786 passed, 0 failed (was 785 — the patch-table test is new)
  • cargo fmt --check clean
  • Drill paired, same binary: rtx_nav_patch 10/8 stuck, escape median 0.63 s (0.51–1.00);
    rtx_nav_patch 0 + map reload → 4/4 stuck with the baseline stall pattern (displacement
    on the floor walk links; three trials end grounded at z=88, one mid-hop at z=124)
  • Graph parity with the gate off: 5977 cells / 50228 links — identical counts to main;
    gate on: 5981 / 50238
  • T1: same 4 PASS (cell_503_194 10/10, cell_724_503 10/10, ring_to_ratop PASS (4/5 arrivals this run, threshold met),
    sng_mega 10/10), 0 verdict flips vs the cc5fa8e column. (The suite's overall T1
    verdict is FAIL by design — 16 of 21 drills are known-FAIL baselines; the fence is the four
    PASS drills and zero flips.)
  • T2 ×4: trap line 0/0/0/0 (was 13/0/0/20/94/53 across six runs on main), per-bot
    standstill 8.4–22.0 s (big-episode runs were 33.5/27.0), displacement 25.8/run (was 60.7),
    totals avg 185.0 (was 199.8)
  • Console line captured from the run's journal into the evidence bundle:
    rtx: navpatch west-shelf: applied (4 cells, 4 drops)

Authored with Claude Fable 5; adversarially reviewed over four rounds by GPT-5.6 (sol), which independently re-derived every number above from the rig's raw evidence.

Xerialen and others added 7 commits August 3, 2026 08:51
…p being bot traps

The column carve samples one column per GRID step of XY, so a standable
surface narrower than the pitch and out of phase with it gets no cell —
plant_cell's own doc names dm3's machinery shelf west of SNG as the worked
example. Bots climb onto that shelf in pairs during normal play, localize
through nearest() to the floor 104 units below, plan routes that are
fiction from where they stand, and wedge until the round ends: 0-94 stall
firings per 600 s T2 run on upstream main, with per-bot standstill doubling
in the runs where it hits.

plant_cell/plant_drop were built for exactly this, but existed only as
control-channel verbs — nothing applied them in production, so every
restart forgot the shelf. This wires the missing production path:

- nav_patch: a reviewable table of hand-verified plants per map (one entry:
  dm3 west-shelf, 4 cells + 4 north drops), applied when a build finishes.
  Fail-closed: every mutation goes through the build's own validators, each
  cell must snap to the exact standing height measured on the shipped BSP
  (z 88.03125 +/- 0.5 - an edited map misses the fingerprint and the patch
  refuses), and each patch prints one console line: applied / skipped /
  failed.
- cvar rtx_nav_patch (default on); 0 -> build only, byte-identical to the
  pre-patch graph.
- testsuite/tools/trap_repro.py: the paired on/off acceptance drill.
  Teleport a puppet onto the surface, goto the floor, watch. Measured on
  the jumpcost rig, same binary: patch off 4/4 stuck over 40 s with 5-8
  displacement stalls each; cells+drops present 0/8 stuck, median escape
  0.8 s. Ecological 600 s T2 with the plants live: trap-line firings 0
  (was 94/53 in the two runs that hit it), per-bot standstill 18.7 s
  (was 33.5/27.0), no new hotspot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he whole topology

Sol's review (gpt-5.6-sol, high) found the failure path unsound: a patch
that failed mid-apply left planted cells behind with reachability/LOD
tables still describing the pre-plant graph - nearest() could localize a
bot to a cell reachable() had never heard of. Each patch now mutates a
clone of the graph (NavGraph gained derive(Clone) for exactly this), which
replaces the live graph - derived tables rebuilt - only when every step
validated. A Failed patch publishes the build's graph bit-for-bit.

AlreadyMeshed also required only the cells to exist, so a future carve
that finds the surface but not the way off would have skipped the patch
and kept the trap. The skip verdict now requires every cell and every
drop; missing drops get planted (plant_drop does not deduplicate, so
existing equivalents are kept, not doubled) and the counts report new
topology only.

Softer honest wording for the snap check (a local geometry precondition,
not a BSP fingerprint), and trap_repro.py restores the rig cvars in a
finally block so an aborted run cannot leave telemetry/pacifist/bot_count
flipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NavGraph's derive(Clone) (the transactional nav_patch apply) needs every
member type to be Clone; SideTable, Reach, Lod, Gate and Plat were not.
All are plain owned Vecs/ids - no shared state, no handles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Portal, AbsEdge and PortalReach are the remaining members the NavGraph
clone needs; all plain owned data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… restore

Sol round 2: the pre-check against the aim point could disagree with
plant_cell's own dedup (which runs against the snapped position), so an
aim point sitting further off the floor could report Applied where the
graph gained nothing. Newness is now the cell count growing across the
call - the mutation itself, not a prediction of it.

trap_repro.py preps the rig inside the same try as the run (a failure on
the second set still restores the first) and restores each cvar
independently. Docstring and table comments now carry the final branch
build's measured numbers in their measured order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sol round 3: the docstring still quoted the previous build's drill numbers.
Python-only change; the game binary is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant