1pub 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 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#[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#[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#[derive(Event)]
134pub struct GotoEvent {
135 pub entity: Entity,
137 pub goal: Arc<dyn Goal>,
138 pub successors_fn: SuccessorsFn,
141
142 pub allow_mining: bool,
144
145 pub min_timeout: PathfinderTimeout,
154 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 async fn goto(&self, goal: impl Goal + 'static) {
203 self.start_goto(goal);
204 self.wait_until_goto_target_reached().await;
205 }
206
207 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 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 async fn wait_until_goto_target_reached(&self) {
249 self.wait_one_update().await;
252
253 let mut tick_broadcaster = self.get_tick_broadcaster();
254 while !self.is_goto_target_reached() {
255 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 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 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 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 pub min_timeout: PathfinderTimeout,
380 pub max_timeout: PathfinderTimeout,
381}
382
383pub 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 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 warn!("finished calculating a path, but it's outdated");
441 return None;
442 }
443
444 if path.is_empty() && is_partial {
445 debug!("this path is empty, we might be stuck :(");
446 }
447
448 let 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
467pub 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 commands.entity(entity).remove::<ComputePath>();
481 }
482 }
483}
484
485pub 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 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 executing_path.queued_path = Some(VecDeque::new());
572 } else {
573 }
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 if let Some(mining) = mining {
598 if mining.pos.distance_squared_to(&BlockPos::from(position)) < 6_i32.pow(2) {
600 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 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 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 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 physics.on_ground()
674 && BlockPos::from(position) == movement.target
675 && (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 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 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 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 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
809fn 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 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 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 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 }
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 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 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 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 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 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 executing_path.queued_path = Some(VecDeque::new());
1075 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
1105pub 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 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 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}