1pub mod astar;
17pub mod costs;
18pub mod custom_state;
19pub mod debug;
20pub mod execute;
21pub mod goals;
22mod goto_event;
23pub mod mining;
24pub mod moves;
25pub mod positions;
26pub mod simulation;
27#[cfg(test)]
28mod tests;
29pub mod world;
30
31use std::{
32 collections::VecDeque,
33 sync::{
34 Arc,
35 atomic::{self, AtomicBool, AtomicUsize},
36 },
37 thread,
38 time::{Duration, Instant},
39};
40
41use astar::Edge;
42use azalea_client::{StartWalkEvent, inventory::InventorySystems, movement::MoveEventsSystems};
43use azalea_core::{
44 position::{BlockPos, Vec3},
45 tick::GameTick,
46};
47use azalea_entity::{LocalEntity, Position, inventory::Inventory, metadata::Player};
48use azalea_world::{WorldName, Worlds};
49use bevy_app::{PreUpdate, Update};
50use bevy_ecs::prelude::*;
51use bevy_tasks::{AsyncComputeTaskPool, Task};
52use custom_state::{CustomPathfinderState, CustomPathfinderStateRef};
53use futures_lite::future;
54pub use goto_event::{GotoEvent, PathfinderOpts};
55use parking_lot::RwLock;
56use positions::RelBlockPos;
57use tokio::sync::broadcast::error::RecvError;
58use tracing::{debug, error, info, warn};
59
60use self::{
61 debug::debug_render_path_with_particles, goals::Goal, mining::MiningCache, moves::SuccessorsFn,
62};
63use crate::{
64 Client, WalkDirection,
65 app::{App, Plugin},
66 ecs::{
67 component::Component,
68 entity::Entity,
69 query::{With, Without},
70 system::{Commands, Query, Res},
71 },
72 pathfinder::{
73 astar::a_star, execute::DefaultPathfinderExecutionPlugin, moves::MovesCtx,
74 world::CachedWorld,
75 },
76};
77
78#[derive(Clone, Default)]
79pub struct PathfinderPlugin;
80impl Plugin for PathfinderPlugin {
81 fn build(&self, app: &mut App) {
82 app.add_message::<GotoEvent>()
83 .add_message::<PathFoundEvent>()
84 .add_message::<StopPathfindingEvent>()
85 .add_systems(GameTick, debug_render_path_with_particles)
86 .add_systems(PreUpdate, add_default_pathfinder)
87 .add_systems(
88 Update,
89 (
90 goto_listener,
91 handle_tasks,
92 stop_pathfinding_on_world_change,
93 path_found_listener,
94 handle_stop_pathfinding_event,
95 )
96 .chain()
97 .before(MoveEventsSystems)
98 .before(InventorySystems),
99 )
100 .add_plugins(DefaultPathfinderExecutionPlugin);
101 }
102}
103
104#[derive(Clone, Component, Default)]
106#[non_exhaustive]
107pub struct Pathfinder {
108 pub goal: Option<Arc<dyn Goal>>,
109 pub opts: Option<PathfinderOpts>,
110 pub is_calculating: bool,
111 pub goto_id: Arc<AtomicUsize>,
112}
113
114#[derive(Clone, Component)]
117pub struct ExecutingPath {
118 pub path: VecDeque<astar::Edge<BlockPos, moves::MoveData>>,
119 pub queued_path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
120 pub last_reached_node: BlockPos,
121 pub ticks_since_last_node_reached: usize,
124 pub is_path_partial: bool,
125}
126
127#[derive(Clone, Debug, Message)]
128#[non_exhaustive]
129pub struct PathFoundEvent {
130 pub entity: Entity,
131 pub start: BlockPos,
132 pub path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
133 pub is_partial: bool,
134 pub successors_fn: SuccessorsFn,
135 pub allow_mining: bool,
136}
137
138#[allow(clippy::type_complexity)]
139pub fn add_default_pathfinder(
140 mut commands: Commands,
141 mut query: Query<Entity, (Without<Pathfinder>, With<LocalEntity>, With<Player>)>,
142) {
143 for entity in &mut query {
144 commands.entity(entity).insert(Pathfinder::default());
145 }
146}
147
148pub trait PathfinderClientExt {
149 fn goto(&self, goal: impl Goal + 'static) -> impl Future<Output = ()>;
162 fn goto_with_opts(
177 &self,
178 goal: impl Goal + 'static,
179 opts: PathfinderOpts,
180 ) -> impl Future<Output = ()>;
181 fn start_goto(&self, goal: impl Goal + 'static);
191 fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts);
197 fn stop_pathfinding(&self);
205 fn force_stop_pathfinding(&self);
208 fn wait_until_goto_target_reached(&self) -> impl Future<Output = ()>;
210 fn is_goto_target_reached(&self) -> bool;
213 fn is_executing_path(&self) -> bool;
218 fn is_calculating_path(&self) -> bool;
223}
224
225impl PathfinderClientExt for Client {
226 async fn goto(&self, goal: impl Goal + 'static) {
227 self.goto_with_opts(goal, PathfinderOpts::new()).await;
228 }
229 async fn goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) {
230 self.start_goto_with_opts(goal, opts);
231 self.wait_until_goto_target_reached().await;
232 }
233 fn start_goto(&self, goal: impl Goal + 'static) {
234 self.start_goto_with_opts(goal, PathfinderOpts::new());
235 }
236 fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) {
237 self.ecs
238 .write()
239 .write_message(GotoEvent::new(self.entity, goal, opts));
240 }
241 fn stop_pathfinding(&self) {
242 self.ecs.write().write_message(StopPathfindingEvent {
243 entity: self.entity,
244 force: false,
245 });
246 }
247 fn force_stop_pathfinding(&self) {
248 self.ecs.write().write_message(StopPathfindingEvent {
249 entity: self.entity,
250 force: true,
251 });
252 }
253
254 async fn wait_until_goto_target_reached(&self) {
255 self.wait_updates(1).await;
258
259 let mut tick_broadcaster = self.get_tick_broadcaster();
260 while !self.is_goto_target_reached() {
261 match tick_broadcaster.recv().await {
263 Ok(_) => (),
264 Err(RecvError::Closed) => return,
265 Err(err) => warn!("{err}"),
266 };
267 }
268 }
269 fn is_goto_target_reached(&self) -> bool {
270 self.get_component::<Pathfinder>()
271 .is_none_or(|p| p.goal.is_none() && !p.is_calculating)
272 }
273 fn is_executing_path(&self) -> bool {
274 self.get_component::<ExecutingPath>().is_some()
275 }
276 fn is_calculating_path(&self) -> bool {
277 self.get_component::<Pathfinder>()
278 .is_some_and(|p| p.is_calculating)
279 }
280}
281
282#[derive(Component)]
283pub struct ComputePath(Task<Option<PathFoundEvent>>);
284
285#[allow(clippy::type_complexity)]
286pub fn goto_listener(
287 mut commands: Commands,
288 mut events: MessageReader<GotoEvent>,
289 mut query: Query<(
290 &mut Pathfinder,
291 Option<&mut ExecutingPath>,
292 &Position,
293 &WorldName,
294 &Inventory,
295 Option<&CustomPathfinderState>,
296 )>,
297 worlds: Res<Worlds>,
298) {
299 let thread_pool = AsyncComputeTaskPool::get();
300
301 for event in events.read() {
302 let Ok((mut pathfinder, executing_path, position, world_name, inventory, custom_state)) =
303 query.get_mut(event.entity)
304 else {
305 warn!("got goto event for an entity that can't pathfind");
306 continue;
307 };
308
309 if env!("OPT_LEVEL") == "0" {
311 static WARNED: AtomicBool = AtomicBool::new(false);
312 if !WARNED.swap(true, atomic::Ordering::Relaxed) {
313 warn!(
314 "Azalea was compiled with no optimizations, which may result in significantly reduced pathfinding performance. Consider following the steps at https://azalea.matdoes.dev/azalea/#optimization for faster performance in debug mode."
315 )
316 }
317 }
318
319 let cur_pos = player_pos_to_block_pos(**position);
320
321 if event.goal.success(cur_pos) {
322 pathfinder.goal = None;
324 pathfinder.opts = None;
325 pathfinder.is_calculating = false;
326 debug!("already at goal, not pathfinding");
327 continue;
328 }
329
330 pathfinder.goal = Some(event.goal.clone());
332 pathfinder.opts = Some(event.opts.clone());
333 pathfinder.is_calculating = true;
334
335 let start = if let Some(mut executing_path) = executing_path
336 && { !executing_path.path.is_empty() }
337 {
338 let executing_path_limit = 50;
341 executing_path.path.truncate(executing_path_limit);
343
344 executing_path
345 .path
346 .back()
347 .expect("path was just checked to not be empty")
348 .movement
349 .target
350 } else {
351 cur_pos
352 };
353
354 if start == cur_pos {
355 info!("got goto {:?}, starting from {start:?}", event.goal);
356 } else {
357 info!(
358 "got goto {:?}, starting from {start:?} (currently at {cur_pos:?})",
359 event.goal,
360 );
361 }
362
363 let world_lock = worlds
364 .get(world_name)
365 .expect("Entity tried to pathfind but the entity isn't in a valid world");
366
367 let goal = event.goal.clone();
368 let entity = event.entity;
369
370 let goto_id_atomic = pathfinder.goto_id.clone();
371
372 let allow_mining = event.opts.allow_mining;
373 let mining_cache = MiningCache::new(if allow_mining {
374 Some(inventory.inventory_menu.clone())
375 } else {
376 None
377 });
378
379 let custom_state = custom_state.cloned().unwrap_or_default();
380 let opts = event.opts.clone();
381 let task = thread_pool.spawn(async move {
382 calculate_path(CalculatePathCtx {
383 entity,
384 start,
385 goal,
386 world_lock,
387 goto_id_atomic,
388 mining_cache,
389 custom_state,
390 opts,
391 })
392 });
393
394 commands.entity(event.entity).insert(ComputePath(task));
395 }
396}
397
398#[inline]
404pub fn player_pos_to_block_pos(position: Vec3) -> BlockPos {
405 BlockPos::from(position.up(0.5))
407}
408
409pub struct CalculatePathCtx {
410 pub entity: Entity,
411 pub start: BlockPos,
412 pub goal: Arc<dyn Goal>,
413 pub world_lock: Arc<RwLock<azalea_world::World>>,
414 pub goto_id_atomic: Arc<AtomicUsize>,
415 pub mining_cache: MiningCache,
416 pub custom_state: CustomPathfinderState,
417
418 pub opts: PathfinderOpts,
419}
420
421pub fn calculate_path(ctx: CalculatePathCtx) -> Option<PathFoundEvent> {
430 debug!("start: {:?}", ctx.start);
431
432 let goto_id = ctx.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1;
433
434 let origin = ctx.start;
435 let cached_world = CachedWorld::new(ctx.world_lock, origin);
436 let successors = |pos: RelBlockPos| {
437 call_successors_fn(
438 &cached_world,
439 &ctx.mining_cache,
440 &ctx.custom_state.0.read(),
441 ctx.opts.successors_fn,
442 pos,
443 )
444 };
445
446 let start_time = Instant::now();
447
448 let astar::Path {
449 movements,
450 is_partial,
451 cost,
452 } = a_star(
453 RelBlockPos::get_origin(origin),
454 |n| ctx.goal.heuristic(n.apply(origin)),
455 successors,
456 |n| ctx.goal.success(n.apply(origin)),
457 ctx.opts.min_timeout,
458 ctx.opts.max_timeout,
459 );
460 let end_time = Instant::now();
461 debug!("partial: {is_partial:?}, cost: {cost}");
462 let duration = end_time - start_time;
463 if is_partial {
464 if movements.is_empty() {
465 info!("Pathfinder took {duration:?} (empty path)");
466 } else {
467 info!("Pathfinder took {duration:?} (incomplete path)");
468 }
469 thread::sleep(Duration::from_millis(100));
471 } else {
472 info!("Pathfinder took {duration:?}");
473 }
474
475 debug!("Path:");
476 for movement in &movements {
477 debug!(" {}", movement.target.apply(origin));
478 }
479
480 let path = movements.into_iter().collect::<VecDeque<_>>();
481
482 let goto_id_now = ctx.goto_id_atomic.load(atomic::Ordering::SeqCst);
483 if goto_id != goto_id_now {
484 warn!("finished calculating a path, but it's outdated");
486 return None;
487 }
488
489 if path.is_empty() && is_partial {
490 debug!("this path is empty, we might be stuck :(");
491 }
492
493 let mut mapped_path = VecDeque::with_capacity(path.len());
494 let mut current_position = RelBlockPos::get_origin(origin);
495 for movement in path {
496 let mut found_edge = None;
497 for edge in successors(current_position) {
498 if edge.movement.target == movement.target {
499 found_edge = Some(edge);
500 break;
501 }
502 }
503
504 let found_edge = found_edge.expect(
505 "path should always still be possible because we're using the same world cache",
506 );
507 current_position = found_edge.movement.target;
508
509 mapped_path.push_back(Edge {
512 movement: astar::Movement {
513 target: movement.target.apply(origin),
514 data: movement.data,
515 },
516 cost: found_edge.cost,
517 });
518 }
519
520 Some(PathFoundEvent {
521 entity: ctx.entity,
522 start: ctx.start,
523 path: Some(mapped_path),
524 is_partial,
525 successors_fn: ctx.opts.successors_fn,
526 allow_mining: ctx.opts.allow_mining,
527 })
528}
529
530pub fn handle_tasks(
532 mut commands: Commands,
533 mut transform_tasks: Query<(Entity, &mut ComputePath)>,
534 mut path_found_events: MessageWriter<PathFoundEvent>,
535) {
536 for (entity, mut task) in &mut transform_tasks {
537 if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
538 if let Some(path_found_event) = optional_path_found_event {
539 path_found_events.write(path_found_event);
540 }
541
542 commands.entity(entity).remove::<ComputePath>();
544 }
545 }
546}
547
548#[allow(clippy::type_complexity)]
550pub fn path_found_listener(
551 mut events: MessageReader<PathFoundEvent>,
552 mut query: Query<(
553 &mut Pathfinder,
554 Option<&mut ExecutingPath>,
555 &WorldName,
556 &Inventory,
557 Option<&CustomPathfinderState>,
558 )>,
559 worlds: Res<Worlds>,
560 mut commands: Commands,
561) {
562 for event in events.read() {
563 let Ok((mut pathfinder, executing_path, world_name, inventory, custom_state)) =
564 query.get_mut(event.entity)
565 else {
566 debug!("got path found event for an entity that can't pathfind");
567 continue;
568 };
569 if let Some(path) = &event.path {
570 if let Some(mut executing_path) = executing_path {
571 let mut new_path = VecDeque::new();
572
573 if let Some(last_node_of_current_path) = executing_path.path.back() {
576 let world_lock = worlds
577 .get(world_name)
578 .expect("Entity tried to pathfind but the entity isn't in a valid world");
579 let origin = event.start;
580 let successors_fn: moves::SuccessorsFn = event.successors_fn;
581 let cached_world = CachedWorld::new(world_lock, origin);
582 let mining_cache = MiningCache::new(if event.allow_mining {
583 Some(inventory.inventory_menu.clone())
584 } else {
585 None
586 });
587 let custom_state = custom_state.cloned().unwrap_or_default();
588 let custom_state_ref = custom_state.0.read();
589 let successors = |pos: RelBlockPos| {
590 call_successors_fn(
591 &cached_world,
592 &mining_cache,
593 &custom_state_ref,
594 successors_fn,
595 pos,
596 )
597 };
598
599 if let Some(first_node_of_new_path) = path.front() {
600 let last_target_of_current_path = RelBlockPos::from_origin(
601 origin,
602 last_node_of_current_path.movement.target,
603 );
604 let first_target_of_new_path = RelBlockPos::from_origin(
605 origin,
606 first_node_of_new_path.movement.target,
607 );
608
609 if successors(last_target_of_current_path)
610 .iter()
611 .any(|edge| edge.movement.target == first_target_of_new_path)
612 {
613 debug!("combining old and new paths");
614 debug!(
615 "old path: {:?}",
616 executing_path.path.iter().collect::<Vec<_>>()
617 );
618 debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
619 new_path.extend(executing_path.path.iter().cloned());
620 }
621 } else {
622 new_path.extend(executing_path.path.iter().cloned());
623 }
624 }
625
626 new_path.extend(path.to_owned());
627
628 debug!(
629 "set queued path to {:?}",
630 new_path.iter().take(10).collect::<Vec<_>>()
631 );
632 executing_path.queued_path = Some(new_path);
633 executing_path.is_path_partial = event.is_partial;
634 } else if path.is_empty() {
635 debug!("calculated path is empty, so didn't add ExecutingPath");
636 if !pathfinder.opts.as_ref().is_some_and(|o| o.retry_on_no_path) {
637 debug!("retry_on_no_path is set to false, removing goal");
638 pathfinder.goal = None;
639 }
640 } else {
641 commands.entity(event.entity).insert(ExecutingPath {
642 path: path.to_owned(),
643 queued_path: None,
644 last_reached_node: event.start,
645 ticks_since_last_node_reached: 0,
646 is_path_partial: event.is_partial,
647 });
648 debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
649 debug!("partial: {}", event.is_partial);
650 }
651 } else {
652 error!("No path found");
653 if let Some(mut executing_path) = executing_path {
654 executing_path.queued_path = Some(VecDeque::new());
656 } else {
657 }
659 }
660 pathfinder.is_calculating = false;
661 }
662}
663
664#[derive(Message)]
665pub struct StopPathfindingEvent {
666 pub entity: Entity,
667 pub force: bool,
673}
674
675pub fn handle_stop_pathfinding_event(
676 mut events: MessageReader<StopPathfindingEvent>,
677 mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
678 mut walk_events: MessageWriter<StartWalkEvent>,
679 mut commands: Commands,
680) {
681 for event in events.read() {
682 commands.entity(event.entity).remove::<ComputePath>();
684
685 let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
686 continue;
687 };
688 pathfinder.goal = None;
689 if event.force {
690 executing_path.path.clear();
691 executing_path.queued_path = None;
692 } else {
693 executing_path.queued_path = Some(VecDeque::new());
695 executing_path.is_path_partial = false;
697 }
698
699 if executing_path.path.is_empty() {
700 walk_events.write(StartWalkEvent {
701 entity: event.entity,
702 direction: WalkDirection::None,
703 });
704 commands.entity(event.entity).remove::<ExecutingPath>();
705 }
706 }
707}
708
709pub fn stop_pathfinding_on_world_change(
710 mut query: Query<(Entity, &mut ExecutingPath), Changed<WorldName>>,
711 mut stop_pathfinding_events: MessageWriter<StopPathfindingEvent>,
712) {
713 for (entity, mut executing_path) in &mut query {
714 if !executing_path.path.is_empty() {
715 debug!("world changed, clearing path");
716 executing_path.path.clear();
717 stop_pathfinding_events.write(StopPathfindingEvent {
718 entity,
719 force: true,
720 });
721 }
722 }
723}
724
725pub fn call_successors_fn(
726 cached_world: &CachedWorld,
727 mining_cache: &MiningCache,
728 custom_state: &CustomPathfinderStateRef,
729 successors_fn: SuccessorsFn,
730 pos: RelBlockPos,
731) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>> {
732 let mut edges = Vec::with_capacity(16);
733 let mut ctx = MovesCtx {
734 edges: &mut edges,
735 world: cached_world,
736 mining_cache,
737 custom_state,
738 };
739 successors_fn(&mut ctx, pos);
740 edges
741}