azalea/pathfinder/
mod.rs

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