Skip to content

fix(nav): a graph rebuild resumes walks in progress instead of snapping them (#60) - #66

Open
leonardomag31-lab wants to merge 3 commits into
mainfrom
fix/rebuild-snap-60
Open

leonardomag31-lab wants to merge 3 commits into
mainfrom
fix/rebuild-snap-60

Conversation

@leonardomag31-lab

@leonardomag31-lab leonardomag31-lab commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #60

What

A graph rebuild drops every live route and re-issues the saved Navigating orders, each from the node nearest the walker (at or below it). Partway along a segment that pick is often wrong:

  • climbing, it's the node behind the walker, which walks it back down;
  • descending toward its destination, it's the destination itself, and the #207 same-node branch puts the walker straight onto it in one frame (the 16–26 px jump @alexandrecalvao measured on stairs and on the stern MovementLink).

Fix, all in src/nav/controller.zig:

  • snapshotSegments remembers each walker's current waypoint before the rebuild (State.resume_at, kept across reset, cleared after the re-issue);
  • the re-issue (navigateWith, private) starts from that waypoint while a node still sits there (reissueFromNode, 1 px tolerance), else falls back to the old nearest-node pick;
  • a re-issued walker on its destination's node walks the rest (walksToSameNode) instead of the #207 snap.

The public navigate is unchanged, #207 snap included. speedOf / withinDirectArrival pulled out of two copies.

Test

  • Two new State-level tests: a climber partway up a stair resumes to the node ahead, not the one behind; a re-issue walks the last 40 px, a fresh navigate still snaps. Each fails when the fix is undone (no snapshot / ignore the waypoint / re-issue snaps).

  • zig build test 15/15, zig build spec 71/71.

  • Flying Platform test, re-measured 2026-10-19 (details in the comment below): two plain walkers (no Worker, so nothing else commands them) at 40 px/s, one ping-ponging over the stair, one over the stern steps (MovementLink), while a node is added or removed every 0.4 s. 180 s, 444 rebuilds:

    stair stern steps
    4.2.0: turn-backs / jumps / walks completed 886 / 0 / 0 886 / 0 / 0
    this branch 0 / 0 / 30 0 / 0 / 31

    (The first run's 24 leftover stair turn-backs were the test harness: wander took the walker over during a deferred re-issue. Fixed in the re-measurement.)

Heads-up

#65 also edits src/nav/controller.zig; whichever lands second will need a small rebase.

…ng them (#60)

A rebuild drops every live route and re-issues the saved orders, each
from the node nearest the walker (at or below it). Partway along a
segment that pick is often wrong: on a climb it's the node behind the
walker, which walks it back down; on a descent toward its destination
it's the destination itself, and the #207 same-node branch put the
walker straight onto it in one frame (a 16-26 px jump on stairs and on
the stern MovementLink).

Before a rebuild, remember the waypoint each walker was heading to; the
re-issue starts from it while a node still sits there, and a walker on
the destination's node walks the rest instead of snapping. A fresh
navigate is unchanged, #207 snap included.

The two decisions are pulled out (`reissueFromNode`, `walksToSameNode`)
and covered by State-level tests; each fails when the fix is undone.

Closes #60
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a0966df8-b7ab-4099-8fe5-23d3c3c1a4e4
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

apotema commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Reviewed head 639ebbc03bceea7f9f15849256d47fa27ec98256. One P2 finding to address before merging:

P2 — Revalidate graph-derived final segments on subsequent rebuilds.

The new same-node reissue branch in src/nav/controller.zig:598–611 stores the remaining graph segment in st.direct_moves, while persisting Navigating.mode = .graph. However:

  • snapshotSegments snapshots only st.pf.active, so it does not capture this final segment.
  • State.reset preserves direct_moves across a graph rebuild.
  • rehydrateNavigating skips any entity already present in direct_moves, before inspecting its persisted navigation mode.
  • advanceDirectMoves continues toward the stored coordinates and eventually calls settleArrival without validating that destination against the rebuilt graph.

Reproduction scenario: a rebuild catches a descending walker on its final segment, creating the new direct-move entry; before arrival, another rebuild removes or moves its destination node. The walker retains the obsolete target instead of reissuing or failing its graph order, and can report arrival at the old location.

Please distinguish graph-derived final segments from genuine moveTo orders and invalidate/reissue the former when the graph changes. Preserve genuine direct moves. Add coverage for a second rebuild while the final segment is active, including destination removal/movement and arrival/failure behavior.

Validation: the unchanged PR passes locally: 15/15 unit tests and 71/71 specs. A temporary State-level regression seeded the exact direct-move entry created by the new branch, ran snapshotSegments and reset, rebuilt without the destination, and failed because the entry remained. This reproduces the retained stale state; I did not run a full game-level arrival reproduction. The temporary test was removed and the original working branch restored. No changes pushed.

Current CI is green. After integrating with #65, the combined controller changes also need regression coverage for rebuild continuity and prepared endpoint publication.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resumed final legs bypass graph tracking, and consecutive rebuilds can erase unconsumed waypoint hints.

2 open issues
What changed in this PR

Addresses #60 by preserving in-progress navigation across graph rebuilds without changing fresh navigate behavior.

Changes:

  • Saves current waypoints for reissued routes.
  • Walks remaining same-node distances instead of snapping.
  • Extracts shared helpers and adds regression tests.
File Description
src/​nav/​controller.zig Adds waypoint snapshots, reissue handling, shared helpers, and tests.

Review effort: Balanced

Comment thread src/nav/controller.zig Outdated
Comment on lines +598 to +601
st.direct_moves.put(st.allocator, entity_id, .{
.target = node_pos,
.speed = speedOf(game, entity),
}) catch return .{ .deferred = .pending_alloc_failed };

@leonardomag31-lab leonardomag31-lab Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 510d723: the leg is now a one-waypoint graph route registered as pending, so later rebuilds re-validate it and it shows in pendingEntities/getMovementPath again.

Comment thread src/nav/controller.zig Outdated
/// An allocation failure only loses the hint: those walkers fall
/// back to the nearest-node re-issue.
fn snapshotSegments(st: *State) void {
st.resume_at.clearRetainingCapacity();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a35967b: snapshotSegments keeps earlier hints (live routes overwrite theirs); rehydrateNavigating still clears them. Test added.

@leonardomag31-lab

Copy link
Copy Markdown
Collaborator Author

Updated numbers (re-measured 2026-10-19, as promised in the description). @apotema

The 24 stair turn-backs in the first run were the test harness: the colony's wander script took the test walker over while its order was deferred mid-rebuild. The walkers are now plain entities (no Worker), so nothing else can command them, and a walker stopped short of its leg is re-sent to the same end, never the other.

Setup: Flying Platform main (b12d04d9) with the pathfinder vendored in-tree, the demo ship's geometry, no crew. Two walkers at 40 px/s ping-pong (1) deck (594, 0) ↔ first floor (644, 93) over the stair at x 697, and (2) stern deck (-50, 36) ↔ (176, 0) over the stern MovementLink. A movement node is added or removed every 0.4 s, each one a graph rebuild. 180 s each, ~100 fps, ~18,050 frames, 444 rebuilds. A turn-back is the walker's direction flipping by more than ~160° between frames; a jump is more than 5 px in one frame (the waypoint snap is ~2 px).

stair stern steps
4.2.0 turn-backs 886 886
this branch turn-backs 0 0
4.2.0 jumps 0 0
this branch jumps 0 0
4.2.0 walks completed 0 0
this branch walks completed 30 31

On 4.2.0 every rebuild sent both walkers back (two turn-backs per rebuild), so in 3 min neither ever reached the end of a walk. With this branch they walk through the rebuilds. (4.2.0's 0 jumps doesn't say much: its walkers never got close to a destination, where the old jump happened; the second unit test covers that case.)

The harness and scene are throwaway, not committed.

leonardomag31-lab and others added 2 commits October 20, 2026 09:32
The same-node re-issue (#60) walked the rest of the last segment as a
direct move while the order stayed `.graph`. `snapshotSegments` didn't
see it, `reset` kept it and `rehydrateNavigating` skipped it, so a
second rebuild that removed or moved the destination left the walker
heading to the old spot and reporting arrival there. It also dropped
out of `pendingEntities`/`getMovementPath`.

Walk it as a one-waypoint graph route instead (identical endpoints are
supported), so a later rebuild drops and re-issues it like any graph
walk; genuine `moveTo` orders are untouched. Route start + pending
registration move into `startRoute`, shared with `navigate`.

Review on #66.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`snapshotSegments` cleared `resume_at` before filling it from the live
routes. Two rebuilds can land before the next `advance` (an explicit
one, then a node-count change): the first drops every route, so the
second snapshot finds none and wiped the only saved hints, and a
mid-stair climber resumed toward the node behind it — the #60
turn-back. Keep earlier hints (live routes overwrite theirs);
`rehydrateNavigating` still clears them after the re-issue.

Review on #66.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@leonardomag31-lab

Copy link
Copy Markdown
Collaborator Author

@apotema thanks, both fixed. New head is a35967b.

P2: graph-derived final segments (510d723)
The same-node re-issue no longer uses direct_moves. It now walks a one-waypoint graph route (pf.navigate(id, n, n); the self-path is supported) and is registered as pending. That makes it an ordinary graph walk: snapshotSegments sees it, reset drops it, and rehydrateNavigating re-issues it against the new graph, which re-routes it or fails it. Genuine moveTo orders are untouched. It also comes back into pendingEntities/getMovementPath, which covers Copilot's deconstruction-drain point. Route start + pending registration are pulled into startRoute, which navigate now shares.

New coverage:

  • controller.zig, State level: a re-issued last leg is in pf.active + pending, not direct_moves. A second snapshot + reset drops it from all three. With the destination node gone, its hint no longer resolves and the re-issue falls back to the nearest node.
  • controller_v4_test.zig, engine level:
    • Arrival: a one-waypoint route 40 px short walks the rest (never more than the 2 px waypoint snap per frame) and fires onArrived with the goal node.
    • Failure: removing its node fires onPathInvalidated.

The full navigate → settle flow isn't covered here, since this repo doesn't mock the game.

Copilot: hints lost on a second rebuild (a35967b)
snapshotSegments no longer clears resume_at first. Earlier hints survive a rebuild that finds no live routes, a live route overwrites its walker's hint, and rehydrateNavigating still clears them after the re-issue. The new test fails (crashes on the missing hint) with the clear put back.

zig build test 17/17, zig build spec 73/73. I haven't re-run the Flying Platform stair/stern measurement on the new head.

Not done yet: the regression coverage you asked for after integrating with #65. That goes with whichever of the two lands second.

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.

fix(nav): a graph rebuild snaps a mid-segment walker to its destination

3 participants