Skip to main content

azalea/pathfinder/
mod.rs

1//! A pathfinding plugin to make bots able to traverse the world.
2//!
3//! For the new functions on `Client` that the pathfinder adds, see
4//! [`PathfinderClientExt`].
5//!
6//! Note that the pathfinder is highly optimized, but it will be very slow if
7//! it's not compiled with optimizations enabled.
8//!
9//! For smoother and more realistic path execution, also see
10//! [`SimulationPathfinderExecutionPlugin`].
11//!
12//! Much of the pathfinder's code is based on [Baritone](https://github.com/cabaletta/baritone). <3
13//!
14//! [`SimulationPathfinderExecutionPlugin`]: execute::simulation::SimulationPathfinderExecutionPlugin
15
16pub mod astar;
17pub mod costs;
18pub mod custom_state;
19pub mod debug;
20pub mod execute;
21pub mod goals;
22mod goto_event;
23pub mod mining;
24pub mod moves;
25pub mod positions;
26pub mod simulation;
27#[cfg(test)]
28mod tests;
29pub mod world;
30
31use std::{
32    collections::VecDeque,
33    sync::{
34        Arc,
35        atomic::{self, AtomicBool, AtomicUsize},
36    },
37    thread,
38    time::{Duration, Instant},
39};
40
41use astar::Edge;
42use azalea_client::{StartWalkEvent, inventory::InventorySystems, movement::MoveEventsSystems};
43use azalea_core::{
44    position::{BlockPos, Vec3},
45    tick::GameTick,
46};
47use azalea_entity::{LocalEntity, Position, inventory::Inventory, metadata::Player};
48use azalea_world::{WorldName, Worlds};
49use bevy_app::{PreUpdate, Update};
50use bevy_ecs::prelude::*;
51use bevy_tasks::{AsyncComputeTaskPool, Task};
52use custom_state::{CustomPathfinderState, CustomPathfinderStateRef};
53use futures_lite::future;
54pub use goto_event::{GotoEvent, PathfinderOpts};
55use parking_lot::RwLock;
56use positions::RelBlockPos;
57use tokio::sync::broadcast::error::RecvError;
58use tracing::{debug, error, info, warn};
59
60use self::{
61    debug::debug_render_path_with_particles, goals::Goal, mining::MiningCache, moves::SuccessorsFn,
62};
63use crate::{
64    Client, WalkDirection,
65    app::{App, Plugin},
66    ecs::{
67        component::Component,
68        entity::Entity,
69        query::{With, Without},
70        system::{Commands, Query, Res},
71    },
72    pathfinder::{
73        astar::a_star, execute::DefaultPathfinderExecutionPlugin, moves::MovesCtx,
74        world::CachedWorld,
75    },
76};
77
78#[derive(Clone, Default)]
79pub struct PathfinderPlugin;
80impl Plugin for PathfinderPlugin {
81    fn build(&self, app: &mut App) {
82        app.add_message::<GotoEvent>()
83            .add_message::<PathFoundEvent>()
84            .add_message::<StopPathfindingEvent>()
85            .add_systems(
86                GameTick,
87                debug_render_path_with_particles.in_set(PathfinderSystems),
88            )
89            .add_systems(PreUpdate, add_default_pathfinder.in_set(PathfinderSystems))
90            .add_systems(
91                Update,
92                (
93                    goto_listener,
94                    handle_tasks,
95                    stop_pathfinding_on_world_change,
96                    path_found_listener,
97                    handle_stop_pathfinding_event,
98                )
99                    .chain()
100                    .before(MoveEventsSystems)
101                    .before(InventorySystems)
102                    .in_set(PathfinderSystems),
103            )
104            .add_plugins(DefaultPathfinderExecutionPlugin);
105    }
106}
107
108#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
109pub struct PathfinderSystems;
110
111/// A component that makes this client able to pathfind.
112#[derive(Clone, Component, Default)]
113#[non_exhaustive]
114pub struct Pathfinder {
115    pub goal: Option<Arc<dyn Goal>>,
116    pub opts: Option<PathfinderOpts>,
117    pub is_calculating: bool,
118    pub goto_id: Arc<AtomicUsize>,
119}
120
121/// A component that's present on clients that are actively following a
122/// pathfinder path.
123#[derive(Clone, Component)]
124pub struct ExecutingPath {
125    pub path: VecDeque<astar::Edge<BlockPos, moves::MoveData>>,
126    pub queued_path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
127    pub last_reached_node: BlockPos,
128    // count ticks instead of using real time to make our timeouts more consistent, in case we lag
129    // and our ticks take a while
130    pub ticks_since_last_node_reached: usize,
131    pub is_path_partial: bool,
132}
133
134#[derive(Clone, Debug, Message)]
135#[non_exhaustive]
136pub struct PathFoundEvent {
137    pub entity: Entity,
138    pub start: BlockPos,
139    pub path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
140    pub is_partial: bool,
141    pub successors_fn: SuccessorsFn,
142    pub allow_mining: bool,
143}
144
145#[allow(clippy::type_complexity)]
146pub fn add_default_pathfinder(
147    mut commands: Commands,
148    mut query: Query<Entity, (Without<Pathfinder>, With<LocalEntity>, With<Player>)>,
149) {
150    for entity in &mut query {
151        commands.entity(entity).insert(Pathfinder::default());
152    }
153}
154
155pub trait PathfinderClientExt {
156    /// Pathfind to the given goal and wait until either the target is reached
157    /// or the pathfinding is canceled.
158    ///
159    /// You can use [`Self::start_goto`] instead if you don't want to wait.
160    ///
161    /// ```
162    /// # use azalea::prelude::*;
163    /// # use azalea::{BlockPos, pathfinder::goals::BlockPosGoal};
164    /// # async fn example(bot: &Client) {
165    /// bot.goto(BlockPosGoal(BlockPos::new(0, 70, 0))).await;
166    /// # }
167    /// ```
168    fn goto(&self, goal: impl Goal + 'static) -> impl Future<Output = ()>;
169    /// Same as [`Self::goto`], but allows you to set custom options for
170    /// pathfinding, including disabling mining and setting custom moves.
171    ///
172    /// ```
173    /// # use azalea::prelude::*;
174    /// # use azalea::{BlockPos, pathfinder::{goals::BlockPosGoal, PathfinderOpts}};
175    /// # async fn example(bot: &Client) {
176    /// bot.goto_with_opts(
177    ///     BlockPosGoal(BlockPos::new(0, 70, 0)),
178    ///     PathfinderOpts::new().allow_mining(false),
179    /// )
180    /// .await;
181    /// # }
182    /// ```
183    fn goto_with_opts(
184        &self,
185        goal: impl Goal + 'static,
186        opts: PathfinderOpts,
187    ) -> impl Future<Output = ()>;
188    /// Start pathfinding to a given goal.
189    ///
190    /// ```
191    /// # use azalea::prelude::*;
192    /// # use azalea::{BlockPos, pathfinder::goals::BlockPosGoal};
193    /// # fn example(bot: &Client) {
194    /// bot.start_goto(BlockPosGoal(BlockPos::new(0, 70, 0)));
195    /// # }
196    /// ```
197    fn start_goto(&self, goal: impl Goal + 'static);
198    /// Same as [`Self::start_goto`], but allows you to set custom
199    /// options for pathfinding, including disabling mining and setting custom
200    /// moves.
201    ///
202    /// Also see [`Self::goto_with_opts`].
203    fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts);
204    /// Stop calculating a path, and stop moving once the current movement is
205    /// finished.
206    ///
207    /// This behavior exists to prevent the bot from taking damage if
208    /// `stop_pathfinding` was called while executing a parkour jump, but if
209    /// it's undesirable then you may want to consider using
210    /// [`Self::force_stop_pathfinding`] instead.
211    fn stop_pathfinding(&self);
212    /// Stop calculating a path and stop executing the current movement
213    /// immediately.
214    fn force_stop_pathfinding(&self);
215    /// Waits forever until the bot no longer has a pathfinder goal.
216    fn wait_until_goto_target_reached(&self) -> impl Future<Output = ()>;
217    /// Returns true if the pathfinder has no active goal and isn't calculating
218    /// a path.
219    fn is_goto_target_reached(&self) -> bool;
220    /// Whether the pathfinder is currently following a path.
221    ///
222    /// Also see [`Self::is_calculating_path`] and
223    /// [`Self::is_goto_target_reached`].
224    fn is_executing_path(&self) -> bool;
225    /// Whether the pathfinder is currently calculating a path.
226    ///
227    /// Also see [`Self::is_executing_path`] and
228    /// [`Self::is_goto_target_reached`].
229    fn is_calculating_path(&self) -> bool;
230}
231
232impl PathfinderClientExt for Client {
233    async fn goto(&self, goal: impl Goal + 'static) {
234        self.goto_with_opts(goal, PathfinderOpts::new()).await;
235    }
236    async fn goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) {
237        self.start_goto_with_opts(goal, opts);
238        self.wait_until_goto_target_reached().await;
239    }
240    fn start_goto(&self, goal: impl Goal + 'static) {
241        self.start_goto_with_opts(goal, PathfinderOpts::new());
242    }
243    fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) {
244        self.ecs
245            .write()
246            .write_message(GotoEvent::new(self.entity, goal, opts));
247    }
248    fn stop_pathfinding(&self) {
249        self.ecs.write().write_message(StopPathfindingEvent {
250            entity: self.entity,
251            force: false,
252        });
253    }
254    fn force_stop_pathfinding(&self) {
255        self.ecs.write().write_message(StopPathfindingEvent {
256            entity: self.entity,
257            force: true,
258        });
259    }
260
261    async fn wait_until_goto_target_reached(&self) {
262        // we do this to make sure the event got handled before we start checking
263        // is_goto_target_reached
264        self.wait_updates(1).await;
265
266        let mut tick_broadcaster = self.get_tick_broadcaster();
267        while !self.is_goto_target_reached() {
268            // check every tick
269            match tick_broadcaster.recv().await {
270                Ok(_) => (),
271                Err(RecvError::Closed) => return,
272                Err(err) => warn!("{err}"),
273            };
274        }
275    }
276    fn is_goto_target_reached(&self) -> bool {
277        self.get_component::<Pathfinder>()
278            .is_none_or(|p| p.goal.is_none() && !p.is_calculating)
279    }
280    fn is_executing_path(&self) -> bool {
281        self.get_component::<ExecutingPath>().is_some()
282    }
283    fn is_calculating_path(&self) -> bool {
284        self.get_component::<Pathfinder>()
285            .is_some_and(|p| p.is_calculating)
286    }
287}
288
289#[derive(Component)]
290pub struct ComputePath(Task<Option<PathFoundEvent>>);
291
292#[allow(clippy::type_complexity)]
293pub fn goto_listener(
294    mut commands: Commands,
295    mut events: MessageReader<GotoEvent>,
296    mut query: Query<(
297        &mut Pathfinder,
298        Option<&mut ExecutingPath>,
299        &Position,
300        &WorldName,
301        &Inventory,
302        Option<&CustomPathfinderState>,
303    )>,
304    worlds: Res<Worlds>,
305) {
306    let thread_pool = AsyncComputeTaskPool::get();
307
308    for event in events.read() {
309        let Ok((mut pathfinder, executing_path, position, world_name, inventory, custom_state)) =
310            query.get_mut(event.entity)
311        else {
312            warn!("got goto event for an entity that can't pathfind");
313            continue;
314        };
315
316        // this env variable is set from the build.rs
317        if env!("OPT_LEVEL") == "0" {
318            static WARNED: AtomicBool = AtomicBool::new(false);
319            if !WARNED.swap(true, atomic::Ordering::Relaxed) {
320                warn!(
321                    "Azalea was compiled with no optimizations, which may result in significantly reduced pathfinding performance. Consider following the steps at https://azalea.matdoes.dev/azalea/#optimization for faster performance in debug mode."
322                )
323            }
324        }
325
326        let cur_pos = player_pos_to_block_pos(**position);
327
328        if event.goal.success(cur_pos) {
329            // we're already at the goal, nothing to do
330            pathfinder.goal = None;
331            pathfinder.opts = None;
332            pathfinder.is_calculating = false;
333            debug!("already at goal, not pathfinding");
334            continue;
335        }
336
337        // we store the goal so it can be recalculated later if necessary
338        pathfinder.goal = Some(event.goal.clone());
339        pathfinder.opts = Some(event.opts.clone());
340        pathfinder.is_calculating = true;
341
342        let start = if let Some(mut executing_path) = executing_path
343            && { !executing_path.path.is_empty() }
344        {
345            // if we're currently pathfinding and got a goto event, start a little ahead
346
347            let executing_path_limit = 50;
348            // truncate the executing path so we can cleanly combine the two paths later
349            executing_path.path.truncate(executing_path_limit);
350
351            executing_path
352                .path
353                .back()
354                .expect("path was just checked to not be empty")
355                .movement
356                .target
357        } else {
358            cur_pos
359        };
360
361        if start == cur_pos {
362            info!("got goto {:?}, starting from {start:?}", event.goal);
363        } else {
364            info!(
365                "got goto {:?}, starting from {start:?} (currently at {cur_pos:?})",
366                event.goal,
367            );
368        }
369
370        let world_lock = worlds
371            .get(world_name)
372            .expect("Entity tried to pathfind but the entity isn't in a valid world");
373
374        let goal = event.goal.clone();
375        let entity = event.entity;
376
377        let goto_id_atomic = pathfinder.goto_id.clone();
378
379        let allow_mining = event.opts.allow_mining;
380        let mining_cache = MiningCache::new(if allow_mining {
381            Some(inventory.inventory_menu.clone())
382        } else {
383            None
384        });
385
386        let custom_state = custom_state.cloned().unwrap_or_default();
387        let opts = event.opts.clone();
388        let task = thread_pool.spawn(async move {
389            calculate_path(CalculatePathCtx {
390                entity,
391                start,
392                goal,
393                world_lock,
394                goto_id_atomic,
395                mining_cache,
396                custom_state,
397                opts,
398            })
399        });
400
401        commands.entity(event.entity).insert(ComputePath(task));
402    }
403}
404
405/// Convert a player position to a block position, used internally in the
406/// pathfinder.
407///
408/// This is almost the same as `BlockPos::from(position)`, except that non-full
409/// blocks are handled correctly.
410#[inline]
411pub fn player_pos_to_block_pos(position: Vec3) -> BlockPos {
412    // 0.5 to account for non-full blocks
413    BlockPos::from(position.up(0.5))
414}
415
416pub struct CalculatePathCtx {
417    pub entity: Entity,
418    pub start: BlockPos,
419    pub goal: Arc<dyn Goal>,
420    pub world_lock: Arc<RwLock<azalea_world::World>>,
421    pub goto_id_atomic: Arc<AtomicUsize>,
422    pub mining_cache: MiningCache,
423    pub custom_state: CustomPathfinderState,
424
425    pub opts: PathfinderOpts,
426}
427
428/// Calculate the [`PathFoundEvent`] for the given pathfinder options.
429///
430/// You usually want to just use [`PathfinderClientExt::goto`] or send a
431/// [`GotoEvent`] instead of calling this directly.
432///
433/// You are expected to immediately send the `PathFoundEvent` you received after
434/// calling this function. `None` will be returned if the pathfinding was
435/// interrupted by another path calculation.
436pub fn calculate_path(ctx: CalculatePathCtx) -> Option<PathFoundEvent> {
437    debug!("start: {:?}", ctx.start);
438
439    let goto_id = ctx.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1;
440
441    let origin = ctx.start;
442    let cached_world = CachedWorld::new(ctx.world_lock, origin);
443    let successors = |pos: RelBlockPos| {
444        call_successors_fn(
445            &cached_world,
446            &ctx.mining_cache,
447            &ctx.custom_state.0.read(),
448            ctx.opts.successors_fn,
449            pos,
450        )
451    };
452
453    let start_time = Instant::now();
454
455    let astar::Path {
456        movements,
457        is_partial,
458        cost,
459    } = a_star(
460        RelBlockPos::get_origin(origin),
461        |n| ctx.goal.heuristic(n.apply(origin)),
462        successors,
463        |n| ctx.goal.success(n.apply(origin)),
464        ctx.opts.min_timeout,
465        ctx.opts.max_timeout,
466    );
467    let end_time = Instant::now();
468    debug!("partial: {is_partial:?}, cost: {cost}");
469    let duration = end_time - start_time;
470    if is_partial {
471        if movements.is_empty() {
472            info!("Pathfinder took {duration:?} (empty path)");
473        } else {
474            info!("Pathfinder took {duration:?} (incomplete path)");
475        }
476        // wait a bit so it's not a busy loop
477        thread::sleep(Duration::from_millis(100));
478    } else {
479        info!("Pathfinder took {duration:?}");
480    }
481
482    debug!("Path:");
483    for movement in &movements {
484        debug!("  {}", movement.target.apply(origin));
485    }
486
487    let path = movements.into_iter().collect::<VecDeque<_>>();
488
489    let goto_id_now = ctx.goto_id_atomic.load(atomic::Ordering::SeqCst);
490    if goto_id != goto_id_now {
491        // we must've done another goto while calculating this path, so throw it away
492        warn!("finished calculating a path, but it's outdated");
493        return None;
494    }
495
496    if path.is_empty() && is_partial {
497        debug!("this path is empty, we might be stuck :(");
498    }
499
500    let mut mapped_path = VecDeque::with_capacity(path.len());
501    let mut current_position = RelBlockPos::get_origin(origin);
502    for movement in path {
503        let mut found_edge = None;
504        for edge in successors(current_position) {
505            if edge.movement.target == movement.target {
506                found_edge = Some(edge);
507                break;
508            }
509        }
510
511        let found_edge = found_edge.expect(
512            "path should always still be possible because we're using the same world cache",
513        );
514        current_position = found_edge.movement.target;
515
516        // we don't just clone the found_edge because we're using BlockPos instead of
517        // RelBlockPos as the target type
518        mapped_path.push_back(Edge {
519            movement: astar::Movement {
520                target: movement.target.apply(origin),
521                data: movement.data,
522            },
523            cost: found_edge.cost,
524        });
525    }
526
527    Some(PathFoundEvent {
528        entity: ctx.entity,
529        start: ctx.start,
530        path: Some(mapped_path),
531        is_partial,
532        successors_fn: ctx.opts.successors_fn,
533        allow_mining: ctx.opts.allow_mining,
534    })
535}
536
537// poll the tasks and send the PathFoundEvent if they're done
538pub fn handle_tasks(
539    mut commands: Commands,
540    mut transform_tasks: Query<(Entity, &mut ComputePath)>,
541    mut path_found_events: MessageWriter<PathFoundEvent>,
542) {
543    for (entity, mut task) in &mut transform_tasks {
544        if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
545            if let Some(path_found_event) = optional_path_found_event {
546                path_found_events.write(path_found_event);
547            }
548
549            // Task is complete, so remove task component from entity
550            commands.entity(entity).remove::<ComputePath>();
551        }
552    }
553}
554
555// set the path for the target entity when we get the PathFoundEvent
556#[allow(clippy::type_complexity)]
557pub fn path_found_listener(
558    mut events: MessageReader<PathFoundEvent>,
559    mut query: Query<(
560        &mut Pathfinder,
561        Option<&mut ExecutingPath>,
562        &WorldName,
563        &Inventory,
564        Option<&CustomPathfinderState>,
565    )>,
566    worlds: Res<Worlds>,
567    mut commands: Commands,
568) {
569    for event in events.read() {
570        let Ok((mut pathfinder, executing_path, world_name, inventory, custom_state)) =
571            query.get_mut(event.entity)
572        else {
573            debug!("got path found event for an entity that can't pathfind");
574            continue;
575        };
576        if let Some(path) = &event.path {
577            if let Some(mut executing_path) = executing_path {
578                let mut new_path = VecDeque::new();
579
580                // combine the old and new paths if the first node of the new path is a
581                // successor of the last node of the old path
582                if let Some(last_node_of_current_path) = executing_path.path.back() {
583                    let world_lock = worlds
584                        .get(world_name)
585                        .expect("Entity tried to pathfind but the entity isn't in a valid world");
586                    let origin = event.start;
587                    let successors_fn: moves::SuccessorsFn = event.successors_fn;
588                    let cached_world = CachedWorld::new(world_lock, origin);
589                    let mining_cache = MiningCache::new(if event.allow_mining {
590                        Some(inventory.inventory_menu.clone())
591                    } else {
592                        None
593                    });
594                    let custom_state = custom_state.cloned().unwrap_or_default();
595                    let custom_state_ref = custom_state.0.read();
596                    let successors = |pos: RelBlockPos| {
597                        call_successors_fn(
598                            &cached_world,
599                            &mining_cache,
600                            &custom_state_ref,
601                            successors_fn,
602                            pos,
603                        )
604                    };
605
606                    if let Some(first_node_of_new_path) = path.front() {
607                        let last_target_of_current_path = RelBlockPos::from_origin(
608                            origin,
609                            last_node_of_current_path.movement.target,
610                        );
611                        let first_target_of_new_path = RelBlockPos::from_origin(
612                            origin,
613                            first_node_of_new_path.movement.target,
614                        );
615
616                        if successors(last_target_of_current_path)
617                            .iter()
618                            .any(|edge| edge.movement.target == first_target_of_new_path)
619                        {
620                            debug!("combining old and new paths");
621                            debug!(
622                                "old path: {:?}",
623                                executing_path.path.iter().collect::<Vec<_>>()
624                            );
625                            debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
626                            new_path.extend(executing_path.path.iter().cloned());
627                        }
628                    } else {
629                        new_path.extend(executing_path.path.iter().cloned());
630                    }
631                }
632
633                new_path.extend(path.to_owned());
634
635                debug!(
636                    "set queued path to {:?}",
637                    new_path.iter().take(10).collect::<Vec<_>>()
638                );
639                executing_path.queued_path = Some(new_path);
640                executing_path.is_path_partial = event.is_partial;
641            } else if path.is_empty() {
642                debug!("calculated path is empty, so didn't add ExecutingPath");
643                if !pathfinder.opts.as_ref().is_some_and(|o| o.retry_on_no_path) {
644                    debug!("retry_on_no_path is set to false, removing goal");
645                    pathfinder.goal = None;
646                }
647            } else {
648                commands.entity(event.entity).insert(ExecutingPath {
649                    path: path.to_owned(),
650                    queued_path: None,
651                    last_reached_node: event.start,
652                    ticks_since_last_node_reached: 0,
653                    is_path_partial: event.is_partial,
654                });
655                debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
656                debug!("partial: {}", event.is_partial);
657            }
658        } else {
659            error!("No path found");
660            if let Some(mut executing_path) = executing_path {
661                // set the queued path so we don't stop in the middle of a move
662                executing_path.queued_path = Some(VecDeque::new());
663            } else {
664                // wasn't executing a path, don't need to do anything
665            }
666        }
667        pathfinder.is_calculating = false;
668    }
669}
670
671#[derive(Message)]
672pub struct StopPathfindingEvent {
673    pub entity: Entity,
674    /// Whether we should stop moving immediately without waiting for the
675    /// current movement to finish.
676    ///
677    /// This is usually set to false, since it might cause the bot to fall if it
678    /// was in the middle of parkouring.
679    pub force: bool,
680}
681
682pub fn handle_stop_pathfinding_event(
683    mut events: MessageReader<StopPathfindingEvent>,
684    mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
685    mut walk_events: MessageWriter<StartWalkEvent>,
686    mut commands: Commands,
687) {
688    for event in events.read() {
689        // stop computing any path that's being computed
690        commands.entity(event.entity).remove::<ComputePath>();
691
692        let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
693            continue;
694        };
695        pathfinder.goal = None;
696        if event.force {
697            executing_path.path.clear();
698            executing_path.queued_path = None;
699        } else {
700            // switch to an empty path as soon as it can
701            executing_path.queued_path = Some(VecDeque::new());
702            // make sure it doesn't recalculate
703            executing_path.is_path_partial = false;
704        }
705
706        if executing_path.path.is_empty() {
707            walk_events.write(StartWalkEvent {
708                entity: event.entity,
709                direction: WalkDirection::None,
710            });
711            commands.entity(event.entity).remove::<ExecutingPath>();
712        }
713    }
714}
715
716pub fn stop_pathfinding_on_world_change(
717    mut query: Query<(Entity, &mut ExecutingPath), Changed<WorldName>>,
718    mut stop_pathfinding_events: MessageWriter<StopPathfindingEvent>,
719) {
720    for (entity, mut executing_path) in &mut query {
721        if !executing_path.path.is_empty() {
722            debug!("world changed, clearing path");
723            executing_path.path.clear();
724            stop_pathfinding_events.write(StopPathfindingEvent {
725                entity,
726                force: true,
727            });
728        }
729    }
730}
731
732pub fn call_successors_fn(
733    cached_world: &CachedWorld,
734    mining_cache: &MiningCache,
735    custom_state: &CustomPathfinderStateRef,
736    successors_fn: SuccessorsFn,
737    pos: RelBlockPos,
738) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>> {
739    let mut edges = Vec::with_capacity(16);
740    let mut ctx = MovesCtx {
741        edges: &mut edges,
742        world: cached_world,
743        mining_cache,
744        custom_state,
745    };
746    successors_fn(&mut ctx, pos);
747    edges
748}