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