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