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::atomic::{self, AtomicUsize};
18use std::sync::Arc;
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::metadata::Player;
30use azalea_entity::LocalEntity;
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
45use self::debug::debug_render_path_with_particles;
46pub use self::debug::PathfinderDebugParticles;
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::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 + Send + Sync>>,
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 + Send + Sync>,
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 + Send + Sync + 'static);
184 fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static);
185 fn stop_pathfinding(&self);
186}
187
188impl PathfinderClientExt for azalea_client::Client {
189 fn goto(&self, goal: impl Goal + Send + Sync + 'static) {
199 self.ecs.lock().send_event(GotoEvent {
200 entity: self.entity,
201 goal: Arc::new(goal),
202 successors_fn: moves::default_move,
203 allow_mining: true,
204 min_timeout: PathfinderTimeout::Time(Duration::from_secs(1)),
205 max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)),
206 });
207 }
208
209 fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static) {
212 self.ecs.lock().send_event(GotoEvent {
213 entity: self.entity,
214 goal: Arc::new(goal),
215 successors_fn: moves::default_move,
216 allow_mining: false,
217 min_timeout: PathfinderTimeout::Time(Duration::from_secs(1)),
218 max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)),
219 });
220 }
221
222 fn stop_pathfinding(&self) {
223 self.ecs.lock().send_event(StopPathfindingEvent {
224 entity: self.entity,
225 force: false,
226 });
227 }
228}
229
230#[derive(Component)]
231pub struct ComputePath(Task<Option<PathFoundEvent>>);
232
233pub fn goto_listener(
234 mut commands: Commands,
235 mut events: EventReader<GotoEvent>,
236 mut query: Query<(
237 &mut Pathfinder,
238 Option<&ExecutingPath>,
239 &Position,
240 &InstanceName,
241 &Inventory,
242 )>,
243 instance_container: Res<InstanceContainer>,
244) {
245 let thread_pool = AsyncComputeTaskPool::get();
246
247 for event in events.read() {
248 let Ok((mut pathfinder, executing_path, position, instance_name, inventory)) =
249 query.get_mut(event.entity)
250 else {
251 warn!("got goto event for an entity that can't pathfind");
252 continue;
253 };
254
255 if event.goal.success(BlockPos::from(position)) {
256 pathfinder.goal = None;
258 pathfinder.successors_fn = None;
259 pathfinder.is_calculating = false;
260 debug!("already at goal, not pathfinding");
261 continue;
262 }
263
264 pathfinder.goal = Some(event.goal.clone());
266 pathfinder.successors_fn = Some(event.successors_fn);
267 pathfinder.is_calculating = true;
268 pathfinder.allow_mining = event.allow_mining;
269 pathfinder.min_timeout = Some(event.min_timeout);
270 pathfinder.max_timeout = Some(event.max_timeout);
271
272 let start = if let Some(executing_path) = executing_path
273 && let Some(final_node) = executing_path.path.back()
274 {
275 executing_path.path.get(50).unwrap_or(final_node).target
277 } else {
278 BlockPos::from(position)
279 };
280
281 if start == BlockPos::from(position) {
282 info!("got goto {:?}, starting from {start:?}", event.goal);
283 } else {
284 info!(
285 "got goto {:?}, starting from {start:?} (currently at {:?})",
286 event.goal,
287 BlockPos::from(position)
288 );
289 }
290
291 let successors_fn: moves::SuccessorsFn = event.successors_fn;
292
293 let world_lock = instance_container
294 .get(instance_name)
295 .expect("Entity tried to pathfind but the entity isn't in a valid world");
296
297 let goal = event.goal.clone();
298 let entity = event.entity;
299
300 let goto_id_atomic = pathfinder.goto_id.clone();
301
302 let allow_mining = event.allow_mining;
303 let mining_cache = MiningCache::new(if allow_mining {
304 Some(inventory.inventory_menu.clone())
305 } else {
306 None
307 });
308
309 let min_timeout = event.min_timeout;
310 let max_timeout = event.max_timeout;
311
312 let task = thread_pool.spawn(async move {
313 calculate_path(CalculatePathOpts {
314 entity,
315 start,
316 goal,
317 successors_fn,
318 world_lock,
319 goto_id_atomic,
320 allow_mining,
321 mining_cache,
322 min_timeout,
323 max_timeout,
324 })
325 });
326
327 commands.entity(event.entity).insert(ComputePath(task));
328 }
329}
330
331pub struct CalculatePathOpts {
332 pub entity: Entity,
333 pub start: BlockPos,
334 pub goal: Arc<dyn Goal + Send + Sync>,
335 pub successors_fn: SuccessorsFn,
336 pub world_lock: Arc<RwLock<azalea_world::Instance>>,
337 pub goto_id_atomic: Arc<AtomicUsize>,
338 pub allow_mining: bool,
339 pub mining_cache: MiningCache,
340 pub min_timeout: PathfinderTimeout,
342 pub max_timeout: PathfinderTimeout,
343}
344
345pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
354 debug!("start: {:?}", opts.start);
355
356 let goto_id = opts.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1;
357
358 let origin = opts.start;
359 let cached_world = CachedWorld::new(opts.world_lock, origin);
360 let successors = |pos: RelBlockPos| {
361 call_successors_fn(&cached_world, &opts.mining_cache, opts.successors_fn, pos)
362 };
363
364 let start_time = Instant::now();
365
366 let astar::Path {
367 movements,
368 is_partial,
369 } = a_star(
370 RelBlockPos::get_origin(origin),
371 |n| opts.goal.heuristic(n.apply(origin)),
372 successors,
373 |n| opts.goal.success(n.apply(origin)),
374 opts.min_timeout,
375 opts.max_timeout,
376 );
377 let end_time = Instant::now();
378 debug!("partial: {is_partial:?}");
379 let duration = end_time - start_time;
380 if is_partial {
381 if movements.is_empty() {
382 info!("Pathfinder took {duration:?} (empty path)");
383 } else {
384 info!("Pathfinder took {duration:?} (incomplete path)");
385 }
386 thread::sleep(Duration::from_millis(100));
388 } else {
389 info!("Pathfinder took {duration:?}");
390 }
391
392 debug!("Path:");
393 for movement in &movements {
394 debug!(" {}", movement.target.apply(origin));
395 }
396
397 let path = movements.into_iter().collect::<VecDeque<_>>();
398
399 let goto_id_now = opts.goto_id_atomic.load(atomic::Ordering::SeqCst);
400 if goto_id != goto_id_now {
401 warn!("finished calculating a path, but it's outdated");
403 return None;
404 }
405
406 if path.is_empty() && is_partial {
407 debug!("this path is empty, we might be stuck :(");
408 }
409
410 let mapped_path = path
412 .into_iter()
413 .map(|movement| astar::Movement {
414 target: movement.target.apply(origin),
415 data: movement.data,
416 })
417 .collect();
418
419 Some(PathFoundEvent {
420 entity: opts.entity,
421 start: opts.start,
422 path: Some(mapped_path),
423 is_partial,
424 successors_fn: opts.successors_fn,
425 allow_mining: opts.allow_mining,
426 })
427}
428
429pub fn handle_tasks(
431 mut commands: Commands,
432 mut transform_tasks: Query<(Entity, &mut ComputePath)>,
433 mut path_found_events: EventWriter<PathFoundEvent>,
434) {
435 for (entity, mut task) in &mut transform_tasks {
436 if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
437 if let Some(path_found_event) = optional_path_found_event {
438 path_found_events.send(path_found_event);
439 }
440
441 commands.entity(entity).remove::<ComputePath>();
443 }
444 }
445}
446
447pub fn path_found_listener(
449 mut events: EventReader<PathFoundEvent>,
450 mut query: Query<(
451 &mut Pathfinder,
452 Option<&mut ExecutingPath>,
453 &InstanceName,
454 &Inventory,
455 )>,
456 instance_container: Res<InstanceContainer>,
457 mut commands: Commands,
458) {
459 for event in events.read() {
460 let (mut pathfinder, executing_path, instance_name, inventory) = query
461 .get_mut(event.entity)
462 .expect("Path found for an entity that doesn't have a pathfinder");
463 if let Some(path) = &event.path {
464 if let Some(mut executing_path) = executing_path {
465 let mut new_path = VecDeque::new();
466
467 if let Some(last_node_of_current_path) = executing_path.path.back() {
470 let world_lock = instance_container
471 .get(instance_name)
472 .expect("Entity tried to pathfind but the entity isn't in a valid world");
473 let origin = event.start;
474 let successors_fn: moves::SuccessorsFn = event.successors_fn;
475 let cached_world = CachedWorld::new(world_lock, origin);
476 let mining_cache = MiningCache::new(if event.allow_mining {
477 Some(inventory.inventory_menu.clone())
478 } else {
479 None
480 });
481 let successors = |pos: RelBlockPos| {
482 call_successors_fn(&cached_world, &mining_cache, successors_fn, pos)
483 };
484
485 if let Some(first_node_of_new_path) = path.front() {
486 let last_target_of_current_path =
487 RelBlockPos::from_origin(origin, last_node_of_current_path.target);
488 let first_target_of_new_path =
489 RelBlockPos::from_origin(origin, first_node_of_new_path.target);
490
491 if successors(last_target_of_current_path)
492 .iter()
493 .any(|edge| edge.movement.target == first_target_of_new_path)
494 {
495 debug!("combining old and new paths");
496 debug!(
497 "old path: {:?}",
498 executing_path.path.iter().collect::<Vec<_>>()
499 );
500 debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
501 new_path.extend(executing_path.path.iter().cloned());
502 }
503 } else {
504 new_path.extend(executing_path.path.iter().cloned());
505 }
506 }
507
508 new_path.extend(path.to_owned());
509
510 debug!(
511 "set queued path to {:?}",
512 new_path.iter().take(10).collect::<Vec<_>>()
513 );
514 executing_path.queued_path = Some(new_path);
515 executing_path.is_path_partial = event.is_partial;
516 } else if path.is_empty() {
517 debug!("calculated path is empty, so didn't add ExecutingPath");
518 } else {
519 commands.entity(event.entity).insert(ExecutingPath {
520 path: path.to_owned(),
521 queued_path: None,
522 last_reached_node: event.start,
523 last_node_reached_at: Instant::now(),
524 is_path_partial: event.is_partial,
525 });
526 debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
527 debug!("partial: {}", event.is_partial);
528 }
529 } else {
530 error!("No path found");
531 if let Some(mut executing_path) = executing_path {
532 executing_path.queued_path = Some(VecDeque::new());
534 } else {
535 }
537 }
538 pathfinder.is_calculating = false;
539 }
540}
541
542#[allow(clippy::type_complexity)]
543pub fn timeout_movement(
544 mut query: Query<(
545 Entity,
546 &mut Pathfinder,
547 &mut ExecutingPath,
548 &Position,
549 Option<&Mining>,
550 &InstanceName,
551 &Inventory,
552 )>,
553 instance_container: Res<InstanceContainer>,
554) {
555 for (entity, mut pathfinder, mut executing_path, position, mining, instance_name, inventory) in
556 &mut query
557 {
558 if let Some(mining) = mining {
560 if mining.pos.distance_squared_to(&BlockPos::from(position)) < 6_i32.pow(2) {
562 executing_path.last_node_reached_at = Instant::now();
565 continue;
566 }
567 }
568
569 if executing_path.last_node_reached_at.elapsed() > Duration::from_secs(2)
570 && !pathfinder.is_calculating
571 && !executing_path.path.is_empty()
572 {
573 warn!("pathfinder timeout, trying to patch path");
574 executing_path.queued_path = None;
575 executing_path.last_reached_node = BlockPos::from(position);
576
577 let world_lock = instance_container
578 .get(instance_name)
579 .expect("Entity tried to pathfind but the entity isn't in a valid world");
580 let successors_fn: moves::SuccessorsFn = pathfinder.successors_fn.unwrap();
581
582 patch_path(
586 0..=cmp::min(20, executing_path.path.len() - 1),
587 &mut executing_path,
588 &mut pathfinder,
589 inventory,
590 entity,
591 successors_fn,
592 world_lock,
593 );
594 executing_path.last_node_reached_at = Instant::now();
596 }
597 }
598}
599
600pub fn check_node_reached(
601 mut query: Query<(
602 Entity,
603 &mut Pathfinder,
604 &mut ExecutingPath,
605 &Position,
606 &Physics,
607 )>,
608 mut walk_events: EventWriter<StartWalkEvent>,
609 mut commands: Commands,
610) {
611 for (entity, mut pathfinder, mut executing_path, position, physics) in &mut query {
612 'skip: loop {
613 for (i, movement) in executing_path
618 .path
619 .clone()
620 .into_iter()
621 .enumerate()
622 .take(20)
623 .rev()
624 {
625 let is_reached_ctx = IsReachedCtx {
626 target: movement.target,
627 start: executing_path.last_reached_node,
628 position: **position,
629 physics,
630 };
631 let extra_strict_if_last = if i == executing_path.path.len() - 1 {
632 let x_difference_from_center = position.x - (movement.target.x as f64 + 0.5);
633 let z_difference_from_center = position.z - (movement.target.z as f64 + 0.5);
634 physics.on_ground()
636 && BlockPos::from(position) == movement.target
637 && (x_difference_from_center + physics.velocity.x).abs() < 0.2
640 && (z_difference_from_center + physics.velocity.z).abs() < 0.2
641 } else {
642 true
643 };
644 if (movement.data.is_reached)(is_reached_ctx) && extra_strict_if_last {
645 executing_path.path = executing_path.path.split_off(i + 1);
646 executing_path.last_reached_node = movement.target;
647 executing_path.last_node_reached_at = Instant::now();
648 trace!("reached node {}", movement.target);
649
650 if let Some(new_path) = executing_path.queued_path.take() {
651 debug!(
652 "swapped path to {:?}",
653 new_path.iter().take(10).collect::<Vec<_>>()
654 );
655 executing_path.path = new_path;
656
657 if executing_path.path.is_empty() {
658 info!("the path we just swapped to was empty, so reached end of path");
659 walk_events.send(StartWalkEvent {
660 entity,
661 direction: WalkDirection::None,
662 });
663 commands.entity(entity).remove::<ExecutingPath>();
664 break;
665 }
666
667 continue 'skip;
669 }
670
671 if executing_path.path.is_empty() {
672 debug!("pathfinder path is now empty");
673 walk_events.send(StartWalkEvent {
674 entity,
675 direction: WalkDirection::None,
676 });
677 commands.entity(entity).remove::<ExecutingPath>();
678 if let Some(goal) = pathfinder.goal.clone() {
679 if goal.success(movement.target) {
680 info!("goal was reached!");
681 pathfinder.goal = None;
682 pathfinder.successors_fn = None;
683 }
684 }
685 }
686
687 break;
688 }
689 }
690 break;
691 }
692 }
693}
694
695pub fn check_for_path_obstruction(
696 mut query: Query<(
697 Entity,
698 &mut Pathfinder,
699 &mut ExecutingPath,
700 &InstanceName,
701 &Inventory,
702 )>,
703 instance_container: Res<InstanceContainer>,
704) {
705 for (entity, mut pathfinder, mut executing_path, instance_name, inventory) in &mut query {
706 let Some(successors_fn) = pathfinder.successors_fn else {
707 continue;
708 };
709
710 let world_lock = instance_container
711 .get(instance_name)
712 .expect("Entity tried to pathfind but the entity isn't in a valid world");
713
714 let origin = executing_path.last_reached_node;
716 let cached_world = CachedWorld::new(world_lock, origin);
717 let mining_cache = MiningCache::new(if pathfinder.allow_mining {
718 Some(inventory.inventory_menu.clone())
719 } else {
720 None
721 });
722 let successors =
723 |pos: RelBlockPos| call_successors_fn(&cached_world, &mining_cache, successors_fn, pos);
724
725 if let Some(obstructed_index) = check_path_obstructed(
726 origin,
727 RelBlockPos::from_origin(origin, executing_path.last_reached_node),
728 &executing_path.path,
729 successors,
730 ) {
731 warn!(
732 "path obstructed at index {obstructed_index} (starting at {:?}, path: {:?})",
733 executing_path.last_reached_node, executing_path.path
734 );
735 if obstructed_index + 5 > executing_path.path.len() {
738 debug!(
739 "obstruction is near the end of the path, truncating and marking path as partial"
740 );
741 executing_path.path.truncate(obstructed_index);
742 executing_path.is_path_partial = true;
743 continue;
744 }
745
746 let Some(successors_fn) = pathfinder.successors_fn else {
747 error!("got PatchExecutingPathEvent but the bot has no successors_fn");
748 continue;
749 };
750
751 let world_lock = instance_container
752 .get(instance_name)
753 .expect("Entity tried to pathfind but the entity isn't in a valid world");
754
755 let patch_end_index = cmp::min(obstructed_index + 20, executing_path.path.len() - 1);
757
758 patch_path(
759 obstructed_index..=patch_end_index,
760 &mut executing_path,
761 &mut pathfinder,
762 inventory,
763 entity,
764 successors_fn,
765 world_lock,
766 );
767 }
768 }
769}
770
771fn patch_path(
777 patch_nodes: RangeInclusive<usize>,
778 executing_path: &mut ExecutingPath,
779 pathfinder: &mut Pathfinder,
780 inventory: &Inventory,
781 entity: Entity,
782 successors_fn: SuccessorsFn,
783 world_lock: Arc<RwLock<azalea_world::Instance>>,
784) {
785 let patch_start = if *patch_nodes.start() == 0 {
786 executing_path.last_reached_node
787 } else {
788 executing_path.path[*patch_nodes.start() - 1].target
789 };
790
791 let patch_end = executing_path.path[*patch_nodes.end()].target;
792
793 let goal = Arc::new(BlockPosGoal(patch_end));
796
797 let goto_id_atomic = pathfinder.goto_id.clone();
798
799 let allow_mining = pathfinder.allow_mining;
800 let mining_cache = MiningCache::new(if allow_mining {
801 Some(inventory.inventory_menu.clone())
802 } else {
803 None
804 });
805
806 let path_found_event = calculate_path(CalculatePathOpts {
808 entity,
809 start: patch_start,
810 goal,
811 successors_fn,
812 world_lock,
813 goto_id_atomic,
814 allow_mining,
815 mining_cache,
816 min_timeout: PathfinderTimeout::Nodes(10_000),
817 max_timeout: PathfinderTimeout::Nodes(10_000),
818 });
819
820 pathfinder.is_calculating = false;
822
823 debug!("obstruction patch: {path_found_event:?}");
824
825 let mut new_path = VecDeque::new();
826 if *patch_nodes.start() > 0 {
827 new_path.extend(
828 executing_path
829 .path
830 .iter()
831 .take(*patch_nodes.start())
832 .cloned(),
833 );
834 }
835
836 let mut is_patch_complete = false;
837 if let Some(path_found_event) = path_found_event {
838 if let Some(found_path_patch) = path_found_event.path {
839 if !found_path_patch.is_empty() {
840 new_path.extend(found_path_patch);
841
842 if !path_found_event.is_partial {
843 new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
844 is_patch_complete = true;
845 debug!("the patch is not partial :)");
846 } else {
847 debug!("the patch is partial, throwing away rest of path :(");
848 }
849 }
850 }
851 } else {
852 }
854
855 executing_path.path = new_path;
856 if !is_patch_complete {
857 executing_path.is_path_partial = true;
858 }
859}
860
861pub fn recalculate_near_end_of_path(
862 mut query: Query<(Entity, &mut Pathfinder, &mut ExecutingPath)>,
863 mut walk_events: EventWriter<StartWalkEvent>,
864 mut goto_events: EventWriter<GotoEvent>,
865 mut commands: Commands,
866) {
867 for (entity, mut pathfinder, mut executing_path) in &mut query {
868 let Some(successors_fn) = pathfinder.successors_fn else {
869 continue;
870 };
871
872 if (executing_path.path.len() == 50 || executing_path.path.len() < 5)
874 && !pathfinder.is_calculating
875 && executing_path.is_path_partial
876 {
877 if let Some(goal) = pathfinder.goal.as_ref().cloned() {
878 debug!("Recalculating path because it's empty or ends soon");
879 debug!(
880 "recalculate_near_end_of_path executing_path.is_path_partial: {}",
881 executing_path.is_path_partial
882 );
883 goto_events.send(GotoEvent {
884 entity,
885 goal,
886 successors_fn,
887 allow_mining: pathfinder.allow_mining,
888 min_timeout: if executing_path.path.len() == 50 {
889 PathfinderTimeout::Time(Duration::from_secs(5))
892 } else {
893 PathfinderTimeout::Time(Duration::from_secs(1))
894 },
895 max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
896 });
897 pathfinder.is_calculating = true;
898
899 if executing_path.path.is_empty() {
900 if let Some(new_path) = executing_path.queued_path.take() {
901 executing_path.path = new_path;
902 if executing_path.path.is_empty() {
903 info!("the path we just swapped to was empty, so reached end of path");
904 walk_events.send(StartWalkEvent {
905 entity,
906 direction: WalkDirection::None,
907 });
908 commands.entity(entity).remove::<ExecutingPath>();
909 break;
910 }
911 } else {
912 walk_events.send(StartWalkEvent {
913 entity,
914 direction: WalkDirection::None,
915 });
916 commands.entity(entity).remove::<ExecutingPath>();
917 }
918 }
919 } else if executing_path.path.is_empty() {
920 walk_events.send(StartWalkEvent {
922 entity,
923 direction: WalkDirection::None,
924 });
925 }
926 }
927 }
928}
929
930#[allow(clippy::type_complexity)]
931pub fn tick_execute_path(
932 mut query: Query<(
933 Entity,
934 &mut ExecutingPath,
935 &Position,
936 &Physics,
937 Option<&Mining>,
938 &InstanceHolder,
939 &Inventory,
940 )>,
941 mut look_at_events: EventWriter<LookAtEvent>,
942 mut sprint_events: EventWriter<StartSprintEvent>,
943 mut walk_events: EventWriter<StartWalkEvent>,
944 mut jump_events: EventWriter<JumpEvent>,
945 mut start_mining_events: EventWriter<StartMiningBlockEvent>,
946 mut set_selected_hotbar_slot_events: EventWriter<SetSelectedHotbarSlotEvent>,
947) {
948 for (entity, executing_path, position, physics, mining, instance_holder, inventory_component) in
949 &mut query
950 {
951 if let Some(movement) = executing_path.path.front() {
952 let ctx = ExecuteCtx {
953 entity,
954 target: movement.target,
955 position: **position,
956 start: executing_path.last_reached_node,
957 physics,
958 is_currently_mining: mining.is_some(),
959 instance: instance_holder.instance.clone(),
960 menu: inventory_component.inventory_menu.clone(),
961
962 look_at_events: &mut look_at_events,
963 sprint_events: &mut sprint_events,
964 walk_events: &mut walk_events,
965 jump_events: &mut jump_events,
966 start_mining_events: &mut start_mining_events,
967 set_selected_hotbar_slot_events: &mut set_selected_hotbar_slot_events,
968 };
969 trace!(
970 "executing move, position: {}, last_reached_node: {}",
971 **position,
972 executing_path.last_reached_node
973 );
974 (movement.data.execute)(ctx);
975 }
976 }
977}
978
979pub fn recalculate_if_has_goal_but_no_path(
980 mut query: Query<(Entity, &mut Pathfinder), Without<ExecutingPath>>,
981 mut goto_events: EventWriter<GotoEvent>,
982) {
983 for (entity, mut pathfinder) in &mut query {
984 if pathfinder.goal.is_some() && !pathfinder.is_calculating {
985 if let Some(goal) = pathfinder.goal.as_ref().cloned() {
986 debug!("Recalculating path because it has a goal but no ExecutingPath");
987 goto_events.send(GotoEvent {
988 entity,
989 goal,
990 successors_fn: pathfinder.successors_fn.unwrap(),
991 allow_mining: pathfinder.allow_mining,
992 min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"),
993 max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
994 });
995 pathfinder.is_calculating = true;
996 }
997 }
998 }
999}
1000
1001#[derive(Event)]
1002pub struct StopPathfindingEvent {
1003 pub entity: Entity,
1004 pub force: bool,
1008}
1009
1010pub fn handle_stop_pathfinding_event(
1011 mut events: EventReader<StopPathfindingEvent>,
1012 mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
1013 mut walk_events: EventWriter<StartWalkEvent>,
1014 mut commands: Commands,
1015) {
1016 for event in events.read() {
1017 commands.entity(event.entity).remove::<ComputePath>();
1019
1020 let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
1021 continue;
1022 };
1023 pathfinder.goal = None;
1024 if event.force {
1025 executing_path.path.clear();
1026 executing_path.queued_path = None;
1027 } else {
1028 executing_path.queued_path = Some(VecDeque::new());
1030 executing_path.is_path_partial = false;
1032 }
1033
1034 if executing_path.path.is_empty() {
1035 walk_events.send(StartWalkEvent {
1036 entity: event.entity,
1037 direction: WalkDirection::None,
1038 });
1039 commands.entity(event.entity).remove::<ExecutingPath>();
1040 }
1041 }
1042}
1043
1044pub fn stop_pathfinding_on_instance_change(
1045 mut query: Query<(Entity, &mut ExecutingPath), Changed<InstanceName>>,
1046 mut stop_pathfinding_events: EventWriter<StopPathfindingEvent>,
1047) {
1048 for (entity, mut executing_path) in &mut query {
1049 if !executing_path.path.is_empty() {
1050 debug!("instance changed, clearing path");
1051 executing_path.path.clear();
1052 stop_pathfinding_events.send(StopPathfindingEvent {
1053 entity,
1054 force: true,
1055 });
1056 }
1057 }
1058}
1059
1060pub fn check_path_obstructed<SuccessorsFn>(
1063 origin: BlockPos,
1064 mut current_position: RelBlockPos,
1065 path: &VecDeque<astar::Movement<BlockPos, moves::MoveData>>,
1066 successors_fn: SuccessorsFn,
1067) -> Option<usize>
1068where
1069 SuccessorsFn: Fn(RelBlockPos) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>>,
1070{
1071 for (i, movement) in path.iter().enumerate() {
1072 let movement_target = RelBlockPos::from_origin(origin, movement.target);
1073
1074 let mut found_obstruction = false;
1075 for edge in successors_fn(current_position) {
1076 if edge.movement.target == movement_target {
1077 current_position = movement_target;
1078 found_obstruction = false;
1079 break;
1080 } else {
1081 found_obstruction = true;
1082 }
1083 }
1084 if found_obstruction {
1085 return Some(i);
1086 }
1087 }
1088
1089 None
1090}
1091
1092pub fn call_successors_fn(
1093 cached_world: &CachedWorld,
1094 mining_cache: &MiningCache,
1095 successors_fn: SuccessorsFn,
1096 pos: RelBlockPos,
1097) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>> {
1098 let mut edges = Vec::with_capacity(16);
1099 let mut ctx = PathfinderCtx {
1100 edges: &mut edges,
1101 world: cached_world,
1102 mining_cache,
1103 };
1104 successors_fn(&mut ctx, pos);
1105 edges
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 use std::{
1111 collections::HashSet,
1112 sync::Arc,
1113 time::{Duration, Instant},
1114 };
1115
1116 use azalea_core::position::{BlockPos, ChunkPos, Vec3};
1117 use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage};
1118
1119 use super::{
1120 astar::PathfinderTimeout,
1121 goals::BlockPosGoal,
1122 moves,
1123 simulation::{SimulatedPlayerBundle, Simulation},
1124 GotoEvent,
1125 };
1126
1127 fn setup_blockposgoal_simulation(
1128 partial_chunks: &mut PartialChunkStorage,
1129 start_pos: BlockPos,
1130 end_pos: BlockPos,
1131 solid_blocks: Vec<BlockPos>,
1132 ) -> Simulation {
1133 let mut simulation = setup_simulation_world(partial_chunks, start_pos, solid_blocks);
1134
1135 simulation.app.world_mut().send_event(GotoEvent {
1143 entity: simulation.entity,
1144 goal: Arc::new(BlockPosGoal(end_pos)),
1145 successors_fn: moves::default_move,
1146 allow_mining: false,
1147 min_timeout: PathfinderTimeout::Nodes(1_000_000),
1148 max_timeout: PathfinderTimeout::Nodes(5_000_000),
1149 });
1150 simulation
1151 }
1152
1153 fn setup_simulation_world(
1154 partial_chunks: &mut PartialChunkStorage,
1155 start_pos: BlockPos,
1156 solid_blocks: Vec<BlockPos>,
1157 ) -> Simulation {
1158 let mut chunk_positions = HashSet::new();
1159 for block_pos in &solid_blocks {
1160 chunk_positions.insert(ChunkPos::from(block_pos));
1161 }
1162
1163 let mut chunks = ChunkStorage::default();
1164 for chunk_pos in chunk_positions {
1165 partial_chunks.set(&chunk_pos, Some(Chunk::default()), &mut chunks);
1166 }
1167 for block_pos in solid_blocks {
1168 chunks.set_block_state(&block_pos, azalea_registry::Block::Stone.into());
1169 }
1170 let player = SimulatedPlayerBundle::new(Vec3::new(
1171 start_pos.x as f64 + 0.5,
1172 start_pos.y as f64,
1173 start_pos.z as f64 + 0.5,
1174 ));
1175 Simulation::new(chunks, player)
1176 }
1177
1178 pub fn assert_simulation_reaches(simulation: &mut Simulation, ticks: usize, end_pos: BlockPos) {
1179 wait_until_bot_starts_moving(simulation);
1180 for _ in 0..ticks {
1181 simulation.tick();
1182 }
1183 assert_eq!(BlockPos::from(simulation.position()), end_pos);
1184 }
1185
1186 pub fn wait_until_bot_starts_moving(simulation: &mut Simulation) {
1187 let start_pos = simulation.position();
1188 let start_time = Instant::now();
1189 while simulation.position() == start_pos
1190 && !simulation.is_mining()
1191 && start_time.elapsed() < Duration::from_millis(500)
1192 {
1193 simulation.tick();
1194 std::thread::yield_now();
1195 }
1196 }
1197
1198 #[test]
1199 fn test_simple_forward() {
1200 let mut partial_chunks = PartialChunkStorage::default();
1201 let mut simulation = setup_blockposgoal_simulation(
1202 &mut partial_chunks,
1203 BlockPos::new(0, 71, 0),
1204 BlockPos::new(0, 71, 1),
1205 vec![BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 1)],
1206 );
1207 assert_simulation_reaches(&mut simulation, 20, BlockPos::new(0, 71, 1));
1208 }
1209
1210 #[test]
1211 fn test_double_diagonal_with_walls() {
1212 let mut partial_chunks = PartialChunkStorage::default();
1213 let mut simulation = setup_blockposgoal_simulation(
1214 &mut partial_chunks,
1215 BlockPos::new(0, 71, 0),
1216 BlockPos::new(2, 71, 2),
1217 vec![
1218 BlockPos::new(0, 70, 0),
1219 BlockPos::new(1, 70, 1),
1220 BlockPos::new(2, 70, 2),
1221 BlockPos::new(1, 72, 0),
1222 BlockPos::new(2, 72, 1),
1223 ],
1224 );
1225 assert_simulation_reaches(&mut simulation, 30, BlockPos::new(2, 71, 2));
1226 }
1227
1228 #[test]
1229 fn test_jump_with_sideways_momentum() {
1230 let mut partial_chunks = PartialChunkStorage::default();
1231 let mut simulation = setup_blockposgoal_simulation(
1232 &mut partial_chunks,
1233 BlockPos::new(0, 71, 3),
1234 BlockPos::new(5, 76, 0),
1235 vec![
1236 BlockPos::new(0, 70, 3),
1237 BlockPos::new(0, 70, 2),
1238 BlockPos::new(0, 70, 1),
1239 BlockPos::new(0, 70, 0),
1240 BlockPos::new(1, 71, 0),
1241 BlockPos::new(2, 72, 0),
1242 BlockPos::new(3, 73, 0),
1243 BlockPos::new(4, 74, 0),
1244 BlockPos::new(5, 75, 0),
1245 ],
1246 );
1247 assert_simulation_reaches(&mut simulation, 120, BlockPos::new(5, 76, 0));
1248 }
1249
1250 #[test]
1251 fn test_parkour_2_block_gap() {
1252 let mut partial_chunks = PartialChunkStorage::default();
1253 let mut simulation = setup_blockposgoal_simulation(
1254 &mut partial_chunks,
1255 BlockPos::new(0, 71, 0),
1256 BlockPos::new(0, 71, 3),
1257 vec![BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 3)],
1258 );
1259 assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 71, 3));
1260 }
1261
1262 #[test]
1263 fn test_descend_and_parkour_2_block_gap() {
1264 let mut partial_chunks = PartialChunkStorage::default();
1265 let mut simulation = setup_blockposgoal_simulation(
1266 &mut partial_chunks,
1267 BlockPos::new(0, 71, 0),
1268 BlockPos::new(3, 67, 4),
1269 vec![
1270 BlockPos::new(0, 70, 0),
1271 BlockPos::new(0, 69, 1),
1272 BlockPos::new(0, 68, 2),
1273 BlockPos::new(0, 67, 3),
1274 BlockPos::new(0, 66, 4),
1275 BlockPos::new(3, 66, 4),
1276 ],
1277 );
1278 assert_simulation_reaches(&mut simulation, 100, BlockPos::new(3, 67, 4));
1279 }
1280
1281 #[test]
1282 fn test_small_descend_and_parkour_2_block_gap() {
1283 let mut partial_chunks = PartialChunkStorage::default();
1284 let mut simulation = setup_blockposgoal_simulation(
1285 &mut partial_chunks,
1286 BlockPos::new(0, 71, 0),
1287 BlockPos::new(0, 70, 5),
1288 vec![
1289 BlockPos::new(0, 70, 0),
1290 BlockPos::new(0, 70, 1),
1291 BlockPos::new(0, 69, 2),
1292 BlockPos::new(0, 69, 5),
1293 ],
1294 );
1295 assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 70, 5));
1296 }
1297
1298 #[test]
1299 fn test_quickly_descend() {
1300 let mut partial_chunks = PartialChunkStorage::default();
1301 let mut simulation = setup_blockposgoal_simulation(
1302 &mut partial_chunks,
1303 BlockPos::new(0, 71, 0),
1304 BlockPos::new(0, 68, 3),
1305 vec![
1306 BlockPos::new(0, 70, 0),
1307 BlockPos::new(0, 69, 1),
1308 BlockPos::new(0, 68, 2),
1309 BlockPos::new(0, 67, 3),
1310 ],
1311 );
1312 assert_simulation_reaches(&mut simulation, 60, BlockPos::new(0, 68, 3));
1313 }
1314
1315 #[test]
1316 fn test_2_gap_ascend_thrice() {
1317 let mut partial_chunks = PartialChunkStorage::default();
1318 let mut simulation = setup_blockposgoal_simulation(
1319 &mut partial_chunks,
1320 BlockPos::new(0, 71, 0),
1321 BlockPos::new(3, 74, 0),
1322 vec![
1323 BlockPos::new(0, 70, 0),
1324 BlockPos::new(0, 71, 3),
1325 BlockPos::new(3, 72, 3),
1326 BlockPos::new(3, 73, 0),
1327 ],
1328 );
1329 assert_simulation_reaches(&mut simulation, 60, BlockPos::new(3, 74, 0));
1330 }
1331
1332 #[test]
1333 fn test_consecutive_3_gap_parkour() {
1334 let mut partial_chunks = PartialChunkStorage::default();
1335 let mut simulation = setup_blockposgoal_simulation(
1336 &mut partial_chunks,
1337 BlockPos::new(0, 71, 0),
1338 BlockPos::new(4, 71, 12),
1339 vec![
1340 BlockPos::new(0, 70, 0),
1341 BlockPos::new(0, 70, 4),
1342 BlockPos::new(0, 70, 8),
1343 BlockPos::new(0, 70, 12),
1344 BlockPos::new(4, 70, 12),
1345 ],
1346 );
1347 assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 71, 12));
1348 }
1349
1350 #[test]
1351 fn test_jumps_with_more_sideways_momentum() {
1352 let mut partial_chunks = PartialChunkStorage::default();
1353 let mut simulation = setup_blockposgoal_simulation(
1354 &mut partial_chunks,
1355 BlockPos::new(0, 71, 0),
1356 BlockPos::new(4, 74, 9),
1357 vec![
1358 BlockPos::new(0, 70, 0),
1359 BlockPos::new(0, 70, 1),
1360 BlockPos::new(0, 70, 2),
1361 BlockPos::new(0, 71, 3),
1362 BlockPos::new(0, 72, 6),
1363 BlockPos::new(0, 73, 9),
1364 BlockPos::new(2, 73, 9),
1366 BlockPos::new(4, 73, 9),
1367 ],
1368 );
1369 assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 74, 9));
1370 }
1371}