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