Skip to content

fix(pathfinding): prevent corrupted and stale mob waypoints#2519

Draft
RedstoneGithub wants to merge 4 commits into
Pumpkin-MC:masterfrom
RedstoneGithub:fix/mob-navigation
Draft

fix(pathfinding): prevent corrupted and stale mob waypoints#2519
RedstoneGithub wants to merge 4 commits into
Pumpkin-MC:masterfrom
RedstoneGithub:fix/mob-navigation

Conversation

@RedstoneGithub

@RedstoneGithub RedstoneGithub commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Marking as draft, looking for feedback.

What Changed

  • Updated navigator A* node relaxation so an existing node’s predecessor is replaced only when the newly discovered route has a strictly lower g cost.

  • Added Vanilla-style directional waypoint skipping. When the current waypoint is behind the mob relative to the following waypoint, navigation advances past the stale waypoint instead of turning back toward it. This is disabled for fire-adjacent, damage-adjacent, and walkable-door nodes.

Added regression coverage for:

  • Unseen, lower-cost, equal-cost, and higher-cost node relaxation.
  • Rejecting a worse route to a node already recorded in the closed set.
  • Skipping stale current waypoints at both the start and later in a path.
  • Preserving danger-adjacent waypoints instead of shortcutting through them.

Why

After creating #2442, I realized when a mob would hit the player, it would turn away and walk a few blocks backward before pathing to the player again. The mobs would also unnecessarily spin at times. I noticed two reasons that caused this, one was incorrect A* behavior, the second was fixed by adding waypoint skipping (where possible, Vanilla also does this).

  • Once a node left the open heap, the navigator could treat it as unseen and overwrite its came_from predecessor with a worse route. This could create cycles during reconstruction and produce routes beginning away from the mob.
  • A valid newly calculated path can begin with a waypoint the mob has already moved past. Without directional skipping, the mob turns back toward that stale waypoint before continuing. This was easily noticeable when spawning a large number of zombies and having them aggro a player, some would spin around.

Impact

Mob navigation paths retain an acyclic predecessor chain back to their start node.

While following valid paths, mobs can advance past stale waypoints behind them, matching Vanilla ground-navigation behavior and preventing the post-attack backward correction/spin.

Known Limitations

This does not attempt full parity with Vanilla for navigation. It builds on top of Pumpkin's existing nav behavior. I am looking for people to test pathing in-game to ensure there are no regressions.

Validation

Passed locally:

  • cargo fmt
  • cargo check
  • cargo clippy
  • cargo test

AI was used to help with this PR.

Copilot AI review requested due to automatic review settings July 25, 2026 03:50

Copilot AI 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.

Pull request overview

Updates the navigator’s A* relaxation logic to avoid overwriting a node’s predecessor with a higher/equal-cost route, preventing cycles during path reconstruction (which can manifest as mobs briefly pathing “backwards” after attacks).

Changes:

  • Introduces helper functions to centralize “should relax” (strictly lower g) and to fetch a node’s known g from either open or closed sets.
  • Updates neighbor relaxation to consider both open and closed sets and to reopen a closed node only for strictly lower-cost paths.
  • Adds unit tests covering the relaxation predicate and a closed-set lookup scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +484 to +499

#[test]
fn does_not_reopen_closed_node_with_worse_cost() {
let closed_node = Node {
g: 2.0,
..Default::default()
};

let mut closed_set = HashMap::new();
closed_set.insert(closed_node.pos.0, closed_node);

let known_g = known_node_g(&BinaryHeap::new(), &closed_set, &closed_node);
assert_eq!(known_g, Some(2.0));
assert!(!should_relax_node(4.0, known_g));
}
}
Copilot AI review requested due to automatic review settings July 25, 2026 19:07

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

pumpkin/src/entity/ai/pathfinder/mod.rs:433

  • After skipping a waypoint via should_target_next_node_in_direction, tick returns immediately without recomputing yaw/movement for the newly advanced next node. That can leave the mob targeting the old (now-behind) node for one tick, causing brief backtracking/jitter and undermining the intent of skipping the behind waypoint. Advance the path index but continue through the rest of tick so control outputs are computed for the updated next node in the same tick.
            let current_pos = entity.entity.pos.load();
            let navigation_pos =
                Vector3::new(current_pos.x, (current_pos.y + 0.5).floor(), current_pos.z);
            if should_target_next_node_in_direction(path, navigation_pos) {
                path.advance();
                self.current_goal = Some(goal);
                return;
            }

Comment thread pumpkin/src/entity/ai/pathfinder/mod.rs
Copilot AI review requested due to automatic review settings July 25, 2026 19:12

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

pumpkin/src/entity/ai/pathfinder/mod.rs:430

  • This adds a new path-following heuristic that advances the path when the next waypoint is "behind" the mob. The PR description and title focus on A* node relaxation / closed-set reopening, so this behavioral change looks out of scope and makes it harder to evaluate risk and intent. Either update the PR description to explicitly cover this new behavior (and why it’s needed), or move this change into a separate PR.
            let current_pos = entity.entity.pos.load();
            let navigation_pos =
                Vector3::new(current_pos.x, (current_pos.y + 0.5).floor(), current_pos.z);
            if should_target_next_node_in_direction(path, navigation_pos) {
                path.advance();

Comment on lines +136 to +144
let to_current = node_center(current).sub(&mob_pos);
let to_next = node_center(next).sub(&mob_pos);
let current_distance_sq =
to_current.x * to_current.x + to_current.y * to_current.y + to_current.z * to_current.z;
let next_distance_sq = to_next.x * to_next.x + to_next.y * to_next.y + to_next.z * to_next.z;

current_distance_sq < 4.0
&& (next_distance_sq < current_distance_sq || current_distance_sq < 0.5)
&& to_current.x * to_next.x + to_current.y * to_next.y + to_current.z * to_next.z < 0.0
@RedstoneGithub RedstoneGithub changed the title fix(navigator): reopen closed nodes only for lower-cost paths fix(pathfinding): prevent corrupted and stale mob waypoints Jul 25, 2026
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.

2 participants