1pub 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 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#[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#[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#[derive(Event)]
132pub struct GotoEvent {
133 pub entity: Entity,
135 pub goal: Arc<dyn Goal>,
136 pub successors_fn: SuccessorsFn,
139
140 pub allow_mining: bool,
142
143 pub min_timeout: PathfinderTimeout,
152 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 async fn goto(&self, goal: impl Goal + 'static) {
201 self.start_goto(goal);
202 self.wait_until_goto_target_reached().await;
203 }
204
205 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 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 async fn wait_until_goto_target_reached(&self) {
247 self.wait_one_update().await;
250
251 let mut tick_broadcaster = self.get_tick_broadcaster();
252 while !self.is_goto_target_reached() {
253 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 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 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 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 pub min_timeout: PathfinderTimeout,
387 pub max_timeout: PathfinderTimeout,
388}
389
390pub 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 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 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 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
492pub 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 commands.entity(entity).remove::<ComputePath>();
506 }
507 }
508}
509
510pub 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 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 executing_path.queued_path = Some(VecDeque::new());
601 } else {
602 }
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 if let Some(mining) = mining {
627 if mining.pos.distance_squared_to(&BlockPos::from(position)) < 6_i32.pow(2) {
629 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 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 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 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 physics.on_ground()
704 && BlockPos::from(position) == movement.target
705 && (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 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 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 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 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
839fn 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 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 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 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 }
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 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 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 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 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 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 executing_path.queued_path = Some(VecDeque::new());
1107 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
1137pub 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 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 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}