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