azalea/pathfinder/
mod.rs

1//! A pathfinding plugin to make bots able to traverse the world.
2//!
3//! Much of this code is based on [Baritone](https://github.com/cabaletta/baritone).
4
5pub mod astar;
6pub mod costs;
7mod debug;
8pub mod goals;
9pub mod mining;
10pub mod moves;
11pub mod rel_block_pos;
12pub mod simulation;
13pub mod world;
14
15use std::collections::VecDeque;
16use std::ops::RangeInclusive;
17use std::sync::atomic::{self, AtomicUsize};
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20use std::{cmp, thread};
21
22use astar::PathfinderTimeout;
23use azalea_client::inventory::{Inventory, InventorySet, SetSelectedHotbarSlotEvent};
24use azalea_client::mining::{Mining, StartMiningBlockEvent};
25use azalea_client::movement::MoveEventsSet;
26use azalea_client::{InstanceHolder, StartSprintEvent, StartWalkEvent};
27use azalea_core::position::BlockPos;
28use azalea_core::tick::GameTick;
29use azalea_entity::metadata::Player;
30use azalea_entity::LocalEntity;
31use azalea_entity::{Physics, Position};
32use azalea_physics::PhysicsSet;
33use azalea_world::{InstanceContainer, InstanceName};
34use bevy_app::{PreUpdate, Update};
35use bevy_ecs::prelude::Event;
36use bevy_ecs::query::Changed;
37use bevy_ecs::schedule::IntoSystemConfigs;
38use bevy_tasks::{AsyncComputeTaskPool, Task};
39use futures_lite::future;
40use goals::BlockPosGoal;
41use parking_lot::RwLock;
42use rel_block_pos::RelBlockPos;
43use tracing::{debug, error, info, trace, warn};
44
45use self::debug::debug_render_path_with_particles;
46pub use self::debug::PathfinderDebugParticles;
47use self::goals::Goal;
48use self::mining::MiningCache;
49use self::moves::{ExecuteCtx, IsReachedCtx, SuccessorsFn};
50use crate::app::{App, Plugin};
51use crate::bot::{JumpEvent, LookAtEvent};
52use crate::ecs::{
53    component::Component,
54    entity::Entity,
55    event::{EventReader, EventWriter},
56    query::{With, Without},
57    system::{Commands, Query, Res},
58};
59use crate::pathfinder::{astar::a_star, moves::PathfinderCtx, world::CachedWorld};
60use crate::WalkDirection;
61
62#[derive(Clone, Default)]
63pub struct PathfinderPlugin;
64impl Plugin for PathfinderPlugin {
65    fn build(&self, app: &mut App) {
66        app.add_event::<GotoEvent>()
67            .add_event::<PathFoundEvent>()
68            .add_event::<StopPathfindingEvent>()
69            .add_systems(
70                // putting systems in the GameTick schedule makes them run every Minecraft tick
71                // (every 50 milliseconds).
72                GameTick,
73                (
74                    timeout_movement,
75                    check_for_path_obstruction,
76                    check_node_reached,
77                    tick_execute_path,
78                    debug_render_path_with_particles,
79                    recalculate_near_end_of_path,
80                    recalculate_if_has_goal_but_no_path,
81                )
82                    .chain()
83                    .after(PhysicsSet)
84                    .after(azalea_client::movement::send_position),
85            )
86            .add_systems(PreUpdate, add_default_pathfinder)
87            .add_systems(
88                Update,
89                (
90                    goto_listener,
91                    handle_tasks,
92                    stop_pathfinding_on_instance_change,
93                    path_found_listener,
94                    handle_stop_pathfinding_event,
95                )
96                    .chain()
97                    .before(MoveEventsSet)
98                    .before(InventorySet),
99            );
100    }
101}
102
103/// A component that makes this client able to pathfind.
104#[derive(Component, Default, Clone)]
105pub struct Pathfinder {
106    pub goal: Option<Arc<dyn Goal + Send + Sync>>,
107    pub successors_fn: Option<SuccessorsFn>,
108    pub is_calculating: bool,
109    pub allow_mining: bool,
110
111    pub min_timeout: Option<PathfinderTimeout>,
112    pub max_timeout: Option<PathfinderTimeout>,
113
114    pub goto_id: Arc<AtomicUsize>,
115}
116
117/// A component that's present on clients that are actively following a
118/// pathfinder path.
119#[derive(Component, Clone)]
120pub struct ExecutingPath {
121    pub path: VecDeque<astar::Movement<BlockPos, moves::MoveData>>,
122    pub queued_path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
123    pub last_reached_node: BlockPos,
124    pub last_node_reached_at: Instant,
125    pub is_path_partial: bool,
126}
127
128/// Send this event to start pathfinding to the given goal.
129///
130/// Also see [`PathfinderClientExt::goto`].
131///
132/// This event is read by [`goto_listener`].
133#[derive(Event)]
134pub struct GotoEvent {
135    /// The local bot entity that will do the pathfinding and execute the path.
136    pub entity: Entity,
137    pub goal: Arc<dyn Goal + Send + Sync>,
138    /// The function that's used for checking what moves are possible. Usually
139    /// `pathfinder::moves::default_move`
140    pub successors_fn: SuccessorsFn,
141
142    /// Whether the bot is allowed to break blocks while pathfinding.
143    pub allow_mining: bool,
144
145    /// The minimum amount of time that should pass before the A* pathfinder
146    /// function can return a timeout. It may take up to [`Self::max_timeout`]
147    /// if it can't immediately find a usable path.
148    ///
149    /// A good default value for this is
150    /// `PathfinderTimeout::Time(Duration::from_secs(1))`.
151    ///
152    /// Also see [`PathfinderTimeout::Nodes`]
153    pub min_timeout: PathfinderTimeout,
154    /// The absolute maximum amount of time that the pathfinder function can
155    /// take to find a path. If it takes this long, it means no usable path was
156    /// found (so it might be impossible).
157    ///
158    /// A good default value for this is
159    /// `PathfinderTimeout::Time(Duration::from_secs(5))`.
160    pub max_timeout: PathfinderTimeout,
161}
162#[derive(Event, Clone, Debug)]
163pub struct PathFoundEvent {
164    pub entity: Entity,
165    pub start: BlockPos,
166    pub path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
167    pub is_partial: bool,
168    pub successors_fn: SuccessorsFn,
169    pub allow_mining: bool,
170}
171
172#[allow(clippy::type_complexity)]
173pub fn add_default_pathfinder(
174    mut commands: Commands,
175    mut query: Query<Entity, (Without<Pathfinder>, With<LocalEntity>, With<Player>)>,
176) {
177    for entity in &mut query {
178        commands.entity(entity).insert(Pathfinder::default());
179    }
180}
181
182pub trait PathfinderClientExt {
183    fn goto(&self, goal: impl Goal + Send + Sync + 'static);
184    fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static);
185    fn stop_pathfinding(&self);
186}
187
188impl PathfinderClientExt for azalea_client::Client {
189    /// Start pathfinding to a given goal.
190    ///
191    /// ```
192    /// # use azalea::prelude::*;
193    /// # use azalea::{BlockPos, pathfinder::goals::BlockPosGoal};
194    /// # fn example(bot: &Client) {
195    /// bot.goto(BlockPosGoal(BlockPos::new(0, 70, 0)));
196    /// # }
197    /// ```
198    fn goto(&self, goal: impl Goal + Send + Sync + 'static) {
199        self.ecs.lock().send_event(GotoEvent {
200            entity: self.entity,
201            goal: Arc::new(goal),
202            successors_fn: moves::default_move,
203            allow_mining: true,
204            min_timeout: PathfinderTimeout::Time(Duration::from_secs(1)),
205            max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)),
206        });
207    }
208
209    /// Same as [`goto`](Self::goto). but the bot won't break any blocks while
210    /// executing the path.
211    fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static) {
212        self.ecs.lock().send_event(GotoEvent {
213            entity: self.entity,
214            goal: Arc::new(goal),
215            successors_fn: moves::default_move,
216            allow_mining: false,
217            min_timeout: PathfinderTimeout::Time(Duration::from_secs(1)),
218            max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)),
219        });
220    }
221
222    fn stop_pathfinding(&self) {
223        self.ecs.lock().send_event(StopPathfindingEvent {
224            entity: self.entity,
225            force: false,
226        });
227    }
228}
229
230#[derive(Component)]
231pub struct ComputePath(Task<Option<PathFoundEvent>>);
232
233pub fn goto_listener(
234    mut commands: Commands,
235    mut events: EventReader<GotoEvent>,
236    mut query: Query<(
237        &mut Pathfinder,
238        Option<&ExecutingPath>,
239        &Position,
240        &InstanceName,
241        &Inventory,
242    )>,
243    instance_container: Res<InstanceContainer>,
244) {
245    let thread_pool = AsyncComputeTaskPool::get();
246
247    for event in events.read() {
248        let Ok((mut pathfinder, executing_path, position, instance_name, inventory)) =
249            query.get_mut(event.entity)
250        else {
251            warn!("got goto event for an entity that can't pathfind");
252            continue;
253        };
254
255        if event.goal.success(BlockPos::from(position)) {
256            // we're already at the goal, nothing to do
257            pathfinder.goal = None;
258            pathfinder.successors_fn = None;
259            pathfinder.is_calculating = false;
260            debug!("already at goal, not pathfinding");
261            continue;
262        }
263
264        // we store the goal so it can be recalculated later if necessary
265        pathfinder.goal = Some(event.goal.clone());
266        pathfinder.successors_fn = Some(event.successors_fn);
267        pathfinder.is_calculating = true;
268        pathfinder.allow_mining = event.allow_mining;
269        pathfinder.min_timeout = Some(event.min_timeout);
270        pathfinder.max_timeout = Some(event.max_timeout);
271
272        let start = if let Some(executing_path) = executing_path
273            && let Some(final_node) = executing_path.path.back()
274        {
275            // if we're currently pathfinding and got a goto event, start a little ahead
276            executing_path.path.get(50).unwrap_or(final_node).target
277        } else {
278            BlockPos::from(position)
279        };
280
281        if start == BlockPos::from(position) {
282            info!("got goto {:?}, starting from {start:?}", event.goal);
283        } else {
284            info!(
285                "got goto {:?}, starting from {start:?} (currently at {:?})",
286                event.goal,
287                BlockPos::from(position)
288            );
289        }
290
291        let successors_fn: moves::SuccessorsFn = event.successors_fn;
292
293        let world_lock = instance_container
294            .get(instance_name)
295            .expect("Entity tried to pathfind but the entity isn't in a valid world");
296
297        let goal = event.goal.clone();
298        let entity = event.entity;
299
300        let goto_id_atomic = pathfinder.goto_id.clone();
301
302        let allow_mining = event.allow_mining;
303        let mining_cache = MiningCache::new(if allow_mining {
304            Some(inventory.inventory_menu.clone())
305        } else {
306            None
307        });
308
309        let min_timeout = event.min_timeout;
310        let max_timeout = event.max_timeout;
311
312        let task = thread_pool.spawn(async move {
313            calculate_path(CalculatePathOpts {
314                entity,
315                start,
316                goal,
317                successors_fn,
318                world_lock,
319                goto_id_atomic,
320                allow_mining,
321                mining_cache,
322                min_timeout,
323                max_timeout,
324            })
325        });
326
327        commands.entity(event.entity).insert(ComputePath(task));
328    }
329}
330
331pub struct CalculatePathOpts {
332    pub entity: Entity,
333    pub start: BlockPos,
334    pub goal: Arc<dyn Goal + Send + Sync>,
335    pub successors_fn: SuccessorsFn,
336    pub world_lock: Arc<RwLock<azalea_world::Instance>>,
337    pub goto_id_atomic: Arc<AtomicUsize>,
338    pub allow_mining: bool,
339    pub mining_cache: MiningCache,
340    /// Also see [`GotoEvent::min_timeout`].
341    pub min_timeout: PathfinderTimeout,
342    pub max_timeout: PathfinderTimeout,
343}
344
345/// Calculate the [`PathFoundEvent`] for the given pathfinder options.
346///
347/// You usually want to just use [`PathfinderClientExt::goto`] or send a
348/// [`GotoEvent`] instead of calling this directly.
349///
350/// You are expected to immediately send the `PathFoundEvent` you received after
351/// calling this function. `None` will be returned if the pathfinding was
352/// interrupted by another path calculation.
353pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
354    debug!("start: {:?}", opts.start);
355
356    let goto_id = opts.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1;
357
358    let origin = opts.start;
359    let cached_world = CachedWorld::new(opts.world_lock, origin);
360    let successors = |pos: RelBlockPos| {
361        call_successors_fn(&cached_world, &opts.mining_cache, opts.successors_fn, pos)
362    };
363
364    let start_time = Instant::now();
365
366    let astar::Path {
367        movements,
368        is_partial,
369    } = a_star(
370        RelBlockPos::get_origin(origin),
371        |n| opts.goal.heuristic(n.apply(origin)),
372        successors,
373        |n| opts.goal.success(n.apply(origin)),
374        opts.min_timeout,
375        opts.max_timeout,
376    );
377    let end_time = Instant::now();
378    debug!("partial: {is_partial:?}");
379    let duration = end_time - start_time;
380    if is_partial {
381        if movements.is_empty() {
382            info!("Pathfinder took {duration:?} (empty path)");
383        } else {
384            info!("Pathfinder took {duration:?} (incomplete path)");
385        }
386        // wait a bit so it's not a busy loop
387        thread::sleep(Duration::from_millis(100));
388    } else {
389        info!("Pathfinder took {duration:?}");
390    }
391
392    debug!("Path:");
393    for movement in &movements {
394        debug!("  {}", movement.target.apply(origin));
395    }
396
397    let path = movements.into_iter().collect::<VecDeque<_>>();
398
399    let goto_id_now = opts.goto_id_atomic.load(atomic::Ordering::SeqCst);
400    if goto_id != goto_id_now {
401        // we must've done another goto while calculating this path, so throw it away
402        warn!("finished calculating a path, but it's outdated");
403        return None;
404    }
405
406    if path.is_empty() && is_partial {
407        debug!("this path is empty, we might be stuck :(");
408    }
409
410    // replace the RelBlockPos types with BlockPos
411    let mapped_path = path
412        .into_iter()
413        .map(|movement| astar::Movement {
414            target: movement.target.apply(origin),
415            data: movement.data,
416        })
417        .collect();
418
419    Some(PathFoundEvent {
420        entity: opts.entity,
421        start: opts.start,
422        path: Some(mapped_path),
423        is_partial,
424        successors_fn: opts.successors_fn,
425        allow_mining: opts.allow_mining,
426    })
427}
428
429// poll the tasks and send the PathFoundEvent if they're done
430pub fn handle_tasks(
431    mut commands: Commands,
432    mut transform_tasks: Query<(Entity, &mut ComputePath)>,
433    mut path_found_events: EventWriter<PathFoundEvent>,
434) {
435    for (entity, mut task) in &mut transform_tasks {
436        if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
437            if let Some(path_found_event) = optional_path_found_event {
438                path_found_events.send(path_found_event);
439            }
440
441            // Task is complete, so remove task component from entity
442            commands.entity(entity).remove::<ComputePath>();
443        }
444    }
445}
446
447// set the path for the target entity when we get the PathFoundEvent
448pub fn path_found_listener(
449    mut events: EventReader<PathFoundEvent>,
450    mut query: Query<(
451        &mut Pathfinder,
452        Option<&mut ExecutingPath>,
453        &InstanceName,
454        &Inventory,
455    )>,
456    instance_container: Res<InstanceContainer>,
457    mut commands: Commands,
458) {
459    for event in events.read() {
460        let (mut pathfinder, executing_path, instance_name, inventory) = query
461            .get_mut(event.entity)
462            .expect("Path found for an entity that doesn't have a pathfinder");
463        if let Some(path) = &event.path {
464            if let Some(mut executing_path) = executing_path {
465                let mut new_path = VecDeque::new();
466
467                // combine the old and new paths if the first node of the new path is a
468                // successor of the last node of the old path
469                if let Some(last_node_of_current_path) = executing_path.path.back() {
470                    let world_lock = instance_container
471                        .get(instance_name)
472                        .expect("Entity tried to pathfind but the entity isn't in a valid world");
473                    let origin = event.start;
474                    let successors_fn: moves::SuccessorsFn = event.successors_fn;
475                    let cached_world = CachedWorld::new(world_lock, origin);
476                    let mining_cache = MiningCache::new(if event.allow_mining {
477                        Some(inventory.inventory_menu.clone())
478                    } else {
479                        None
480                    });
481                    let successors = |pos: RelBlockPos| {
482                        call_successors_fn(&cached_world, &mining_cache, successors_fn, pos)
483                    };
484
485                    if let Some(first_node_of_new_path) = path.front() {
486                        let last_target_of_current_path =
487                            RelBlockPos::from_origin(origin, last_node_of_current_path.target);
488                        let first_target_of_new_path =
489                            RelBlockPos::from_origin(origin, first_node_of_new_path.target);
490
491                        if successors(last_target_of_current_path)
492                            .iter()
493                            .any(|edge| edge.movement.target == first_target_of_new_path)
494                        {
495                            debug!("combining old and new paths");
496                            debug!(
497                                "old path: {:?}",
498                                executing_path.path.iter().collect::<Vec<_>>()
499                            );
500                            debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
501                            new_path.extend(executing_path.path.iter().cloned());
502                        }
503                    } else {
504                        new_path.extend(executing_path.path.iter().cloned());
505                    }
506                }
507
508                new_path.extend(path.to_owned());
509
510                debug!(
511                    "set queued path to {:?}",
512                    new_path.iter().take(10).collect::<Vec<_>>()
513                );
514                executing_path.queued_path = Some(new_path);
515                executing_path.is_path_partial = event.is_partial;
516            } else if path.is_empty() {
517                debug!("calculated path is empty, so didn't add ExecutingPath");
518            } else {
519                commands.entity(event.entity).insert(ExecutingPath {
520                    path: path.to_owned(),
521                    queued_path: None,
522                    last_reached_node: event.start,
523                    last_node_reached_at: Instant::now(),
524                    is_path_partial: event.is_partial,
525                });
526                debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
527                debug!("partial: {}", event.is_partial);
528            }
529        } else {
530            error!("No path found");
531            if let Some(mut executing_path) = executing_path {
532                // set the queued path so we don't stop in the middle of a move
533                executing_path.queued_path = Some(VecDeque::new());
534            } else {
535                // wasn't executing a path, don't need to do anything
536            }
537        }
538        pathfinder.is_calculating = false;
539    }
540}
541
542#[allow(clippy::type_complexity)]
543pub fn timeout_movement(
544    mut query: Query<(
545        Entity,
546        &mut Pathfinder,
547        &mut ExecutingPath,
548        &Position,
549        Option<&Mining>,
550        &InstanceName,
551        &Inventory,
552    )>,
553    instance_container: Res<InstanceContainer>,
554) {
555    for (entity, mut pathfinder, mut executing_path, position, mining, instance_name, inventory) in
556        &mut query
557    {
558        // don't timeout if we're mining
559        if let Some(mining) = mining {
560            // also make sure we're close enough to the block that's being mined
561            if mining.pos.distance_squared_to(&BlockPos::from(position)) < 6_i32.pow(2) {
562                // also reset the last_node_reached_at so we don't timeout after we finish
563                // mining
564                executing_path.last_node_reached_at = Instant::now();
565                continue;
566            }
567        }
568
569        if executing_path.last_node_reached_at.elapsed() > Duration::from_secs(2)
570            && !pathfinder.is_calculating
571            && !executing_path.path.is_empty()
572        {
573            warn!("pathfinder timeout, trying to patch path");
574            executing_path.queued_path = None;
575            executing_path.last_reached_node = BlockPos::from(position);
576
577            let world_lock = instance_container
578                .get(instance_name)
579                .expect("Entity tried to pathfind but the entity isn't in a valid world");
580            let successors_fn: moves::SuccessorsFn = pathfinder.successors_fn.unwrap();
581
582            // try to fix the path without recalculating everything.
583            // (though, it'll still get fully recalculated by `recalculate_near_end_of_path`
584            // if the new path is too short)
585            patch_path(
586                0..=cmp::min(20, executing_path.path.len() - 1),
587                &mut executing_path,
588                &mut pathfinder,
589                inventory,
590                entity,
591                successors_fn,
592                world_lock,
593            );
594            // reset last_node_reached_at so we don't immediately try to patch again
595            executing_path.last_node_reached_at = Instant::now();
596        }
597    }
598}
599
600pub fn check_node_reached(
601    mut query: Query<(
602        Entity,
603        &mut Pathfinder,
604        &mut ExecutingPath,
605        &Position,
606        &Physics,
607    )>,
608    mut walk_events: EventWriter<StartWalkEvent>,
609    mut commands: Commands,
610) {
611    for (entity, mut pathfinder, mut executing_path, position, physics) in &mut query {
612        'skip: loop {
613            // we check if the goal was reached *before* actually executing the movement so
614            // we don't unnecessarily execute a movement when it wasn't necessary
615
616            // see if we already reached any future nodes and can skip ahead
617            for (i, movement) in executing_path
618                .path
619                .clone()
620                .into_iter()
621                .enumerate()
622                .take(20)
623                .rev()
624            {
625                let is_reached_ctx = IsReachedCtx {
626                    target: movement.target,
627                    start: executing_path.last_reached_node,
628                    position: **position,
629                    physics,
630                };
631                let extra_strict_if_last = if i == executing_path.path.len() - 1 {
632                    let x_difference_from_center = position.x - (movement.target.x as f64 + 0.5);
633                    let z_difference_from_center = position.z - (movement.target.z as f64 + 0.5);
634                    // this is to make sure we don't fall off immediately after finishing the path
635                    physics.on_ground()
636                    && BlockPos::from(position) == movement.target
637                    // adding the delta like this isn't a perfect solution but it helps to make
638                    // sure we don't keep going if our delta is high
639                    && (x_difference_from_center + physics.velocity.x).abs() < 0.2
640                    && (z_difference_from_center + physics.velocity.z).abs() < 0.2
641                } else {
642                    true
643                };
644                if (movement.data.is_reached)(is_reached_ctx) && extra_strict_if_last {
645                    executing_path.path = executing_path.path.split_off(i + 1);
646                    executing_path.last_reached_node = movement.target;
647                    executing_path.last_node_reached_at = Instant::now();
648                    trace!("reached node {}", movement.target);
649
650                    if let Some(new_path) = executing_path.queued_path.take() {
651                        debug!(
652                            "swapped path to {:?}",
653                            new_path.iter().take(10).collect::<Vec<_>>()
654                        );
655                        executing_path.path = new_path;
656
657                        if executing_path.path.is_empty() {
658                            info!("the path we just swapped to was empty, so reached end of path");
659                            walk_events.send(StartWalkEvent {
660                                entity,
661                                direction: WalkDirection::None,
662                            });
663                            commands.entity(entity).remove::<ExecutingPath>();
664                            break;
665                        }
666
667                        // run the function again since we just swapped
668                        continue 'skip;
669                    }
670
671                    if executing_path.path.is_empty() {
672                        debug!("pathfinder path is now empty");
673                        walk_events.send(StartWalkEvent {
674                            entity,
675                            direction: WalkDirection::None,
676                        });
677                        commands.entity(entity).remove::<ExecutingPath>();
678                        if let Some(goal) = pathfinder.goal.clone() {
679                            if goal.success(movement.target) {
680                                info!("goal was reached!");
681                                pathfinder.goal = None;
682                                pathfinder.successors_fn = None;
683                            }
684                        }
685                    }
686
687                    break;
688                }
689            }
690            break;
691        }
692    }
693}
694
695pub fn check_for_path_obstruction(
696    mut query: Query<(
697        Entity,
698        &mut Pathfinder,
699        &mut ExecutingPath,
700        &InstanceName,
701        &Inventory,
702    )>,
703    instance_container: Res<InstanceContainer>,
704) {
705    for (entity, mut pathfinder, mut executing_path, instance_name, inventory) in &mut query {
706        let Some(successors_fn) = pathfinder.successors_fn else {
707            continue;
708        };
709
710        let world_lock = instance_container
711            .get(instance_name)
712            .expect("Entity tried to pathfind but the entity isn't in a valid world");
713
714        // obstruction check (the path we're executing isn't possible anymore)
715        let origin = executing_path.last_reached_node;
716        let cached_world = CachedWorld::new(world_lock, origin);
717        let mining_cache = MiningCache::new(if pathfinder.allow_mining {
718            Some(inventory.inventory_menu.clone())
719        } else {
720            None
721        });
722        let successors =
723            |pos: RelBlockPos| call_successors_fn(&cached_world, &mining_cache, successors_fn, pos);
724
725        if let Some(obstructed_index) = check_path_obstructed(
726            origin,
727            RelBlockPos::from_origin(origin, executing_path.last_reached_node),
728            &executing_path.path,
729            successors,
730        ) {
731            warn!(
732                "path obstructed at index {obstructed_index} (starting at {:?}, path: {:?})",
733                executing_path.last_reached_node, executing_path.path
734            );
735            // if it's near the end, don't bother recalculating a patch, just truncate and
736            // mark it as partial
737            if obstructed_index + 5 > executing_path.path.len() {
738                debug!(
739                    "obstruction is near the end of the path, truncating and marking path as partial"
740                );
741                executing_path.path.truncate(obstructed_index);
742                executing_path.is_path_partial = true;
743                continue;
744            }
745
746            let Some(successors_fn) = pathfinder.successors_fn else {
747                error!("got PatchExecutingPathEvent but the bot has no successors_fn");
748                continue;
749            };
750
751            let world_lock = instance_container
752                .get(instance_name)
753                .expect("Entity tried to pathfind but the entity isn't in a valid world");
754
755            // patch up to 20 nodes
756            let patch_end_index = cmp::min(obstructed_index + 20, executing_path.path.len() - 1);
757
758            patch_path(
759                obstructed_index..=patch_end_index,
760                &mut executing_path,
761                &mut pathfinder,
762                inventory,
763                entity,
764                successors_fn,
765                world_lock,
766            );
767        }
768    }
769}
770
771/// update the given [`ExecutingPath`] to recalculate the path of the nodes in
772/// the given index range.
773///
774/// You should avoid making the range too large, since the timeout for the A*
775/// calculation is very low. About 20 nodes is a good amount.
776fn patch_path(
777    patch_nodes: RangeInclusive<usize>,
778    executing_path: &mut ExecutingPath,
779    pathfinder: &mut Pathfinder,
780    inventory: &Inventory,
781    entity: Entity,
782    successors_fn: SuccessorsFn,
783    world_lock: Arc<RwLock<azalea_world::Instance>>,
784) {
785    let patch_start = if *patch_nodes.start() == 0 {
786        executing_path.last_reached_node
787    } else {
788        executing_path.path[*patch_nodes.start() - 1].target
789    };
790
791    let patch_end = executing_path.path[*patch_nodes.end()].target;
792
793    // this doesn't override the main goal, it's just the goal for this A*
794    // calculation
795    let goal = Arc::new(BlockPosGoal(patch_end));
796
797    let goto_id_atomic = pathfinder.goto_id.clone();
798
799    let allow_mining = pathfinder.allow_mining;
800    let mining_cache = MiningCache::new(if allow_mining {
801        Some(inventory.inventory_menu.clone())
802    } else {
803        None
804    });
805
806    // the timeout is small enough that this doesn't need to be async
807    let path_found_event = calculate_path(CalculatePathOpts {
808        entity,
809        start: patch_start,
810        goal,
811        successors_fn,
812        world_lock,
813        goto_id_atomic,
814        allow_mining,
815        mining_cache,
816        min_timeout: PathfinderTimeout::Nodes(10_000),
817        max_timeout: PathfinderTimeout::Nodes(10_000),
818    });
819
820    // this is necessary in case we interrupted another ongoing path calculation
821    pathfinder.is_calculating = false;
822
823    debug!("obstruction patch: {path_found_event:?}");
824
825    let mut new_path = VecDeque::new();
826    if *patch_nodes.start() > 0 {
827        new_path.extend(
828            executing_path
829                .path
830                .iter()
831                .take(*patch_nodes.start())
832                .cloned(),
833        );
834    }
835
836    let mut is_patch_complete = false;
837    if let Some(path_found_event) = path_found_event {
838        if let Some(found_path_patch) = path_found_event.path {
839            if !found_path_patch.is_empty() {
840                new_path.extend(found_path_patch);
841
842                if !path_found_event.is_partial {
843                    new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
844                    is_patch_complete = true;
845                    debug!("the patch is not partial :)");
846                } else {
847                    debug!("the patch is partial, throwing away rest of path :(");
848                }
849            }
850        }
851    } else {
852        // no path found, rip
853    }
854
855    executing_path.path = new_path;
856    if !is_patch_complete {
857        executing_path.is_path_partial = true;
858    }
859}
860
861pub fn recalculate_near_end_of_path(
862    mut query: Query<(Entity, &mut Pathfinder, &mut ExecutingPath)>,
863    mut walk_events: EventWriter<StartWalkEvent>,
864    mut goto_events: EventWriter<GotoEvent>,
865    mut commands: Commands,
866) {
867    for (entity, mut pathfinder, mut executing_path) in &mut query {
868        let Some(successors_fn) = pathfinder.successors_fn else {
869            continue;
870        };
871
872        // start recalculating if the path ends soon
873        if (executing_path.path.len() == 50 || executing_path.path.len() < 5)
874            && !pathfinder.is_calculating
875            && executing_path.is_path_partial
876        {
877            if let Some(goal) = pathfinder.goal.as_ref().cloned() {
878                debug!("Recalculating path because it's empty or ends soon");
879                debug!(
880                    "recalculate_near_end_of_path executing_path.is_path_partial: {}",
881                    executing_path.is_path_partial
882                );
883                goto_events.send(GotoEvent {
884                    entity,
885                    goal,
886                    successors_fn,
887                    allow_mining: pathfinder.allow_mining,
888                    min_timeout: if executing_path.path.len() == 50 {
889                        // we have quite some time until the node is reached, soooo we might as well
890                        // burn some cpu cycles to get a good path
891                        PathfinderTimeout::Time(Duration::from_secs(5))
892                    } else {
893                        PathfinderTimeout::Time(Duration::from_secs(1))
894                    },
895                    max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
896                });
897                pathfinder.is_calculating = true;
898
899                if executing_path.path.is_empty() {
900                    if let Some(new_path) = executing_path.queued_path.take() {
901                        executing_path.path = new_path;
902                        if executing_path.path.is_empty() {
903                            info!("the path we just swapped to was empty, so reached end of path");
904                            walk_events.send(StartWalkEvent {
905                                entity,
906                                direction: WalkDirection::None,
907                            });
908                            commands.entity(entity).remove::<ExecutingPath>();
909                            break;
910                        }
911                    } else {
912                        walk_events.send(StartWalkEvent {
913                            entity,
914                            direction: WalkDirection::None,
915                        });
916                        commands.entity(entity).remove::<ExecutingPath>();
917                    }
918                }
919            } else if executing_path.path.is_empty() {
920                // idk when this can happen but stop moving just in case
921                walk_events.send(StartWalkEvent {
922                    entity,
923                    direction: WalkDirection::None,
924                });
925            }
926        }
927    }
928}
929
930#[allow(clippy::type_complexity)]
931pub fn tick_execute_path(
932    mut query: Query<(
933        Entity,
934        &mut ExecutingPath,
935        &Position,
936        &Physics,
937        Option<&Mining>,
938        &InstanceHolder,
939        &Inventory,
940    )>,
941    mut look_at_events: EventWriter<LookAtEvent>,
942    mut sprint_events: EventWriter<StartSprintEvent>,
943    mut walk_events: EventWriter<StartWalkEvent>,
944    mut jump_events: EventWriter<JumpEvent>,
945    mut start_mining_events: EventWriter<StartMiningBlockEvent>,
946    mut set_selected_hotbar_slot_events: EventWriter<SetSelectedHotbarSlotEvent>,
947) {
948    for (entity, executing_path, position, physics, mining, instance_holder, inventory_component) in
949        &mut query
950    {
951        if let Some(movement) = executing_path.path.front() {
952            let ctx = ExecuteCtx {
953                entity,
954                target: movement.target,
955                position: **position,
956                start: executing_path.last_reached_node,
957                physics,
958                is_currently_mining: mining.is_some(),
959                instance: instance_holder.instance.clone(),
960                menu: inventory_component.inventory_menu.clone(),
961
962                look_at_events: &mut look_at_events,
963                sprint_events: &mut sprint_events,
964                walk_events: &mut walk_events,
965                jump_events: &mut jump_events,
966                start_mining_events: &mut start_mining_events,
967                set_selected_hotbar_slot_events: &mut set_selected_hotbar_slot_events,
968            };
969            trace!(
970                "executing move, position: {}, last_reached_node: {}",
971                **position,
972                executing_path.last_reached_node
973            );
974            (movement.data.execute)(ctx);
975        }
976    }
977}
978
979pub fn recalculate_if_has_goal_but_no_path(
980    mut query: Query<(Entity, &mut Pathfinder), Without<ExecutingPath>>,
981    mut goto_events: EventWriter<GotoEvent>,
982) {
983    for (entity, mut pathfinder) in &mut query {
984        if pathfinder.goal.is_some() && !pathfinder.is_calculating {
985            if let Some(goal) = pathfinder.goal.as_ref().cloned() {
986                debug!("Recalculating path because it has a goal but no ExecutingPath");
987                goto_events.send(GotoEvent {
988                    entity,
989                    goal,
990                    successors_fn: pathfinder.successors_fn.unwrap(),
991                    allow_mining: pathfinder.allow_mining,
992                    min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"),
993                    max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
994                });
995                pathfinder.is_calculating = true;
996            }
997        }
998    }
999}
1000
1001#[derive(Event)]
1002pub struct StopPathfindingEvent {
1003    pub entity: Entity,
1004    /// If false, then let the current movement finish before stopping. If true,
1005    /// then stop moving immediately. This might cause the bot to fall if it was
1006    /// in the middle of parkouring.
1007    pub force: bool,
1008}
1009
1010pub fn handle_stop_pathfinding_event(
1011    mut events: EventReader<StopPathfindingEvent>,
1012    mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
1013    mut walk_events: EventWriter<StartWalkEvent>,
1014    mut commands: Commands,
1015) {
1016    for event in events.read() {
1017        // stop computing any path that's being computed
1018        commands.entity(event.entity).remove::<ComputePath>();
1019
1020        let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
1021            continue;
1022        };
1023        pathfinder.goal = None;
1024        if event.force {
1025            executing_path.path.clear();
1026            executing_path.queued_path = None;
1027        } else {
1028            // switch to an empty path as soon as it can
1029            executing_path.queued_path = Some(VecDeque::new());
1030            // make sure it doesn't recalculate
1031            executing_path.is_path_partial = false;
1032        }
1033
1034        if executing_path.path.is_empty() {
1035            walk_events.send(StartWalkEvent {
1036                entity: event.entity,
1037                direction: WalkDirection::None,
1038            });
1039            commands.entity(event.entity).remove::<ExecutingPath>();
1040        }
1041    }
1042}
1043
1044pub fn stop_pathfinding_on_instance_change(
1045    mut query: Query<(Entity, &mut ExecutingPath), Changed<InstanceName>>,
1046    mut stop_pathfinding_events: EventWriter<StopPathfindingEvent>,
1047) {
1048    for (entity, mut executing_path) in &mut query {
1049        if !executing_path.path.is_empty() {
1050            debug!("instance changed, clearing path");
1051            executing_path.path.clear();
1052            stop_pathfinding_events.send(StopPathfindingEvent {
1053                entity,
1054                force: true,
1055            });
1056        }
1057    }
1058}
1059
1060/// Checks whether the path has been obstructed, and returns Some(index) if it
1061/// has been. The index is of the first obstructed node.
1062pub fn check_path_obstructed<SuccessorsFn>(
1063    origin: BlockPos,
1064    mut current_position: RelBlockPos,
1065    path: &VecDeque<astar::Movement<BlockPos, moves::MoveData>>,
1066    successors_fn: SuccessorsFn,
1067) -> Option<usize>
1068where
1069    SuccessorsFn: Fn(RelBlockPos) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>>,
1070{
1071    for (i, movement) in path.iter().enumerate() {
1072        let movement_target = RelBlockPos::from_origin(origin, movement.target);
1073
1074        let mut found_obstruction = false;
1075        for edge in successors_fn(current_position) {
1076            if edge.movement.target == movement_target {
1077                current_position = movement_target;
1078                found_obstruction = false;
1079                break;
1080            } else {
1081                found_obstruction = true;
1082            }
1083        }
1084        if found_obstruction {
1085            return Some(i);
1086        }
1087    }
1088
1089    None
1090}
1091
1092pub fn call_successors_fn(
1093    cached_world: &CachedWorld,
1094    mining_cache: &MiningCache,
1095    successors_fn: SuccessorsFn,
1096    pos: RelBlockPos,
1097) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>> {
1098    let mut edges = Vec::with_capacity(16);
1099    let mut ctx = PathfinderCtx {
1100        edges: &mut edges,
1101        world: cached_world,
1102        mining_cache,
1103    };
1104    successors_fn(&mut ctx, pos);
1105    edges
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    use std::{
1111        collections::HashSet,
1112        sync::Arc,
1113        time::{Duration, Instant},
1114    };
1115
1116    use azalea_core::position::{BlockPos, ChunkPos, Vec3};
1117    use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage};
1118
1119    use super::{
1120        astar::PathfinderTimeout,
1121        goals::BlockPosGoal,
1122        moves,
1123        simulation::{SimulatedPlayerBundle, Simulation},
1124        GotoEvent,
1125    };
1126
1127    fn setup_blockposgoal_simulation(
1128        partial_chunks: &mut PartialChunkStorage,
1129        start_pos: BlockPos,
1130        end_pos: BlockPos,
1131        solid_blocks: Vec<BlockPos>,
1132    ) -> Simulation {
1133        let mut simulation = setup_simulation_world(partial_chunks, start_pos, solid_blocks);
1134
1135        // you can uncomment this while debugging tests to get trace logs
1136        // simulation.app.add_plugins(bevy_log::LogPlugin {
1137        //     level: bevy_log::Level::TRACE,
1138        //     filter: "".to_string(),
1139        //     ..Default::default()
1140        // });
1141
1142        simulation.app.world_mut().send_event(GotoEvent {
1143            entity: simulation.entity,
1144            goal: Arc::new(BlockPosGoal(end_pos)),
1145            successors_fn: moves::default_move,
1146            allow_mining: false,
1147            min_timeout: PathfinderTimeout::Nodes(1_000_000),
1148            max_timeout: PathfinderTimeout::Nodes(5_000_000),
1149        });
1150        simulation
1151    }
1152
1153    fn setup_simulation_world(
1154        partial_chunks: &mut PartialChunkStorage,
1155        start_pos: BlockPos,
1156        solid_blocks: Vec<BlockPos>,
1157    ) -> Simulation {
1158        let mut chunk_positions = HashSet::new();
1159        for block_pos in &solid_blocks {
1160            chunk_positions.insert(ChunkPos::from(block_pos));
1161        }
1162
1163        let mut chunks = ChunkStorage::default();
1164        for chunk_pos in chunk_positions {
1165            partial_chunks.set(&chunk_pos, Some(Chunk::default()), &mut chunks);
1166        }
1167        for block_pos in solid_blocks {
1168            chunks.set_block_state(&block_pos, azalea_registry::Block::Stone.into());
1169        }
1170        let player = SimulatedPlayerBundle::new(Vec3::new(
1171            start_pos.x as f64 + 0.5,
1172            start_pos.y as f64,
1173            start_pos.z as f64 + 0.5,
1174        ));
1175        Simulation::new(chunks, player)
1176    }
1177
1178    pub fn assert_simulation_reaches(simulation: &mut Simulation, ticks: usize, end_pos: BlockPos) {
1179        wait_until_bot_starts_moving(simulation);
1180        for _ in 0..ticks {
1181            simulation.tick();
1182        }
1183        assert_eq!(BlockPos::from(simulation.position()), end_pos);
1184    }
1185
1186    pub fn wait_until_bot_starts_moving(simulation: &mut Simulation) {
1187        let start_pos = simulation.position();
1188        let start_time = Instant::now();
1189        while simulation.position() == start_pos
1190            && !simulation.is_mining()
1191            && start_time.elapsed() < Duration::from_millis(500)
1192        {
1193            simulation.tick();
1194            std::thread::yield_now();
1195        }
1196    }
1197
1198    #[test]
1199    fn test_simple_forward() {
1200        let mut partial_chunks = PartialChunkStorage::default();
1201        let mut simulation = setup_blockposgoal_simulation(
1202            &mut partial_chunks,
1203            BlockPos::new(0, 71, 0),
1204            BlockPos::new(0, 71, 1),
1205            vec![BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 1)],
1206        );
1207        assert_simulation_reaches(&mut simulation, 20, BlockPos::new(0, 71, 1));
1208    }
1209
1210    #[test]
1211    fn test_double_diagonal_with_walls() {
1212        let mut partial_chunks = PartialChunkStorage::default();
1213        let mut simulation = setup_blockposgoal_simulation(
1214            &mut partial_chunks,
1215            BlockPos::new(0, 71, 0),
1216            BlockPos::new(2, 71, 2),
1217            vec![
1218                BlockPos::new(0, 70, 0),
1219                BlockPos::new(1, 70, 1),
1220                BlockPos::new(2, 70, 2),
1221                BlockPos::new(1, 72, 0),
1222                BlockPos::new(2, 72, 1),
1223            ],
1224        );
1225        assert_simulation_reaches(&mut simulation, 30, BlockPos::new(2, 71, 2));
1226    }
1227
1228    #[test]
1229    fn test_jump_with_sideways_momentum() {
1230        let mut partial_chunks = PartialChunkStorage::default();
1231        let mut simulation = setup_blockposgoal_simulation(
1232            &mut partial_chunks,
1233            BlockPos::new(0, 71, 3),
1234            BlockPos::new(5, 76, 0),
1235            vec![
1236                BlockPos::new(0, 70, 3),
1237                BlockPos::new(0, 70, 2),
1238                BlockPos::new(0, 70, 1),
1239                BlockPos::new(0, 70, 0),
1240                BlockPos::new(1, 71, 0),
1241                BlockPos::new(2, 72, 0),
1242                BlockPos::new(3, 73, 0),
1243                BlockPos::new(4, 74, 0),
1244                BlockPos::new(5, 75, 0),
1245            ],
1246        );
1247        assert_simulation_reaches(&mut simulation, 120, BlockPos::new(5, 76, 0));
1248    }
1249
1250    #[test]
1251    fn test_parkour_2_block_gap() {
1252        let mut partial_chunks = PartialChunkStorage::default();
1253        let mut simulation = setup_blockposgoal_simulation(
1254            &mut partial_chunks,
1255            BlockPos::new(0, 71, 0),
1256            BlockPos::new(0, 71, 3),
1257            vec![BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 3)],
1258        );
1259        assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 71, 3));
1260    }
1261
1262    #[test]
1263    fn test_descend_and_parkour_2_block_gap() {
1264        let mut partial_chunks = PartialChunkStorage::default();
1265        let mut simulation = setup_blockposgoal_simulation(
1266            &mut partial_chunks,
1267            BlockPos::new(0, 71, 0),
1268            BlockPos::new(3, 67, 4),
1269            vec![
1270                BlockPos::new(0, 70, 0),
1271                BlockPos::new(0, 69, 1),
1272                BlockPos::new(0, 68, 2),
1273                BlockPos::new(0, 67, 3),
1274                BlockPos::new(0, 66, 4),
1275                BlockPos::new(3, 66, 4),
1276            ],
1277        );
1278        assert_simulation_reaches(&mut simulation, 100, BlockPos::new(3, 67, 4));
1279    }
1280
1281    #[test]
1282    fn test_small_descend_and_parkour_2_block_gap() {
1283        let mut partial_chunks = PartialChunkStorage::default();
1284        let mut simulation = setup_blockposgoal_simulation(
1285            &mut partial_chunks,
1286            BlockPos::new(0, 71, 0),
1287            BlockPos::new(0, 70, 5),
1288            vec![
1289                BlockPos::new(0, 70, 0),
1290                BlockPos::new(0, 70, 1),
1291                BlockPos::new(0, 69, 2),
1292                BlockPos::new(0, 69, 5),
1293            ],
1294        );
1295        assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 70, 5));
1296    }
1297
1298    #[test]
1299    fn test_quickly_descend() {
1300        let mut partial_chunks = PartialChunkStorage::default();
1301        let mut simulation = setup_blockposgoal_simulation(
1302            &mut partial_chunks,
1303            BlockPos::new(0, 71, 0),
1304            BlockPos::new(0, 68, 3),
1305            vec![
1306                BlockPos::new(0, 70, 0),
1307                BlockPos::new(0, 69, 1),
1308                BlockPos::new(0, 68, 2),
1309                BlockPos::new(0, 67, 3),
1310            ],
1311        );
1312        assert_simulation_reaches(&mut simulation, 60, BlockPos::new(0, 68, 3));
1313    }
1314
1315    #[test]
1316    fn test_2_gap_ascend_thrice() {
1317        let mut partial_chunks = PartialChunkStorage::default();
1318        let mut simulation = setup_blockposgoal_simulation(
1319            &mut partial_chunks,
1320            BlockPos::new(0, 71, 0),
1321            BlockPos::new(3, 74, 0),
1322            vec![
1323                BlockPos::new(0, 70, 0),
1324                BlockPos::new(0, 71, 3),
1325                BlockPos::new(3, 72, 3),
1326                BlockPos::new(3, 73, 0),
1327            ],
1328        );
1329        assert_simulation_reaches(&mut simulation, 60, BlockPos::new(3, 74, 0));
1330    }
1331
1332    #[test]
1333    fn test_consecutive_3_gap_parkour() {
1334        let mut partial_chunks = PartialChunkStorage::default();
1335        let mut simulation = setup_blockposgoal_simulation(
1336            &mut partial_chunks,
1337            BlockPos::new(0, 71, 0),
1338            BlockPos::new(4, 71, 12),
1339            vec![
1340                BlockPos::new(0, 70, 0),
1341                BlockPos::new(0, 70, 4),
1342                BlockPos::new(0, 70, 8),
1343                BlockPos::new(0, 70, 12),
1344                BlockPos::new(4, 70, 12),
1345            ],
1346        );
1347        assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 71, 12));
1348    }
1349
1350    #[test]
1351    fn test_jumps_with_more_sideways_momentum() {
1352        let mut partial_chunks = PartialChunkStorage::default();
1353        let mut simulation = setup_blockposgoal_simulation(
1354            &mut partial_chunks,
1355            BlockPos::new(0, 71, 0),
1356            BlockPos::new(4, 74, 9),
1357            vec![
1358                BlockPos::new(0, 70, 0),
1359                BlockPos::new(0, 70, 1),
1360                BlockPos::new(0, 70, 2),
1361                BlockPos::new(0, 71, 3),
1362                BlockPos::new(0, 72, 6),
1363                BlockPos::new(0, 73, 9),
1364                // this is the point where the bot might fall if it has too much momentum
1365                BlockPos::new(2, 73, 9),
1366                BlockPos::new(4, 73, 9),
1367            ],
1368        );
1369        assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 74, 9));
1370    }
1371}