fix(nav): map-pinned navmesh patches — the carve's blind spots stop being bot traps - #16
Open
Xerialen wants to merge 7 commits into
Open
fix(nav): map-pinned navmesh patches — the carve's blind spots stop being bot traps#16Xerialen wants to merge 7 commits into
Xerialen wants to merge 7 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 asthe 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 theyactually 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 linefired 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_dropwere built for exactly this failure class, but only exist ascontrol-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 oncewhen a nav build finishes. One entry:
dm3 west-shelf, 4 cells along the shelf + 4 drops offits north lip (the south face is solid; drops that way fail
classify_grounded, which is howthe open side was found).
(
plant_cellrefuses non-standable spots,plant_dropaccepts only a drop the build woulditself 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 buildproduced.
rtx_nav_patchcvar, default on;0→ build only, byte-identical graph to main.testsuite/tools/trap_repro.py— the paired on/off acceptance drill: teleport a puppet botonto the surface,
gotothe floor, watch. Exit code is the verdict, so both arms arescriptable.
If the carve ever learns to see the shelf, the patch reports
skipped (already meshed)andbecomes a no-op — it cannot conflict with a future proper fix.
Evidence (branch head build
841a64e0, build==deployed digest-verified on the rig)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 botsclimb 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:
map or route; the cost is a hand-maintained table and the admission that the carve stays blind.
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 --checkcleanrtx_nav_patch 1→ 0/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 (displacementon the floor walk links; three trials end grounded at z=88, one mid-hop at z=124)
gate on: 5981 / 50238
cell_503_19410/10,cell_724_50310/10,ring_to_ratopPASS (4/5 arrivals this run, threshold met),sng_mega10/10), 0 verdict flips vs the cc5fa8e column. (The suite's overall T1verdict is FAIL by design — 16 of 21 drills are known-FAIL baselines; the fence is the four
PASS drills and zero flips.)
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)
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.