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(
86 GameTick,
87 debug_render_path_with_particles.in_set(PathfinderSystems),
88 )
89 .add_systems(PreUpdate, add_default_pathfinder.in_set(PathfinderSystems))
90 .add_systems(
91 Update,
92 (
93 goto_listener,
94 handle_tasks,
95 stop_pathfinding_on_world_change,
96 path_found_listener,
97 handle_stop_pathfinding_event,
98 )
99 .chain()
100 .before(MoveEventsSystems)
101 .before(InventorySystems)
102 .in_set(PathfinderSystems),
103 )
104 .add_plugins(DefaultPathfinderExecutionPlugin);
105 }
106}
107
108#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
109pub struct PathfinderSystems;
110
111#[derive(Clone, Component, Default)]
113#[non_exhaustive]
114pub struct Pathfinder {
115 pub goal: Option<Arc<dyn Goal>>,
116 pub opts: Option<PathfinderOpts>,
117 pub is_calculating: bool,
118 pub goto_id: Arc<AtomicUsize>,
119}
120
121#[derive(Clone, Component)]
124pub struct ExecutingPath {
125 pub path: VecDeque<astar::Edge<BlockPos, moves::MoveData>>,
126 pub queued_path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
127 pub last_reached_node: BlockPos,
128 pub ticks_since_last_node_reached: usize,
131 pub is_path_partial: bool,
132}
133
134#[derive(Clone, Debug, Message)]
135#[non_exhaustive]
136pub struct PathFoundEvent {
137 pub entity: Entity,
138 pub start: BlockPos,
139 pub path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
140 pub is_partial: bool,
141 pub successors_fn: SuccessorsFn,
142 pub allow_mining: bool,
143}
144
145#[allow(clippy::type_complexity)]
146pub fn add_default_pathfinder(
147 mut commands: Commands,
148 mut query: Query<Entity, (Without<Pathfinder>, With<LocalEntity>, With<Player>)>,
149) {
150 for entity in &mut query {
151 commands.entity(entity).insert(Pathfinder::default());
152 }
153}
154
155pub trait PathfinderClientExt {
156 fn goto(&self, goal: impl Goal + 'static) -> impl Future<Output = ()>;
169 fn goto_with_opts(
184 &self,
185 goal: impl Goal + 'static,
186 opts: PathfinderOpts,
187 ) -> impl Future<Output = ()>;
188 fn start_goto(&self, goal: impl Goal + 'static);
198 fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts);
204 fn stop_pathfinding(&self);
212 fn force_stop_pathfinding(&self);
215 fn wait_until_goto_target_reached(&self) -> impl Future<Output = ()>;
217 fn is_goto_target_reached(&self) -> bool;
220 fn is_executing_path(&self) -> bool;
225 fn is_calculating_path(&self) -> bool;
230}
231
232impl PathfinderClientExt for Client {
233 async fn goto(&self, goal: impl Goal + 'static) {
234 self.goto_with_opts(goal, PathfinderOpts::new()).await;
235 }
236 async fn goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) {
237 self.start_goto_with_opts(goal, opts);
238 self.wait_until_goto_target_reached().await;
239 }
240 fn start_goto(&self, goal: impl Goal + 'static) {
241 self.start_goto_with_opts(goal, PathfinderOpts::new());
242 }
243 fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) {
244 self.ecs
245 .write()
246 .write_message(GotoEvent::new(self.entity, goal, opts));
247 }
248 fn stop_pathfinding(&self) {
249 self.ecs.write().write_message(StopPathfindingEvent {
250 entity: self.entity,
251 force: false,
252 });
253 }
254 fn force_stop_pathfinding(&self) {
255 self.ecs.write().write_message(StopPathfindingEvent {
256 entity: self.entity,
257 force: true,
258 });
259 }
260
261 async fn wait_until_goto_target_reached(&self) {
262 self.wait_updates(1).await;
265
266 let mut tick_broadcaster = self.get_tick_broadcaster();
267 while !self.is_goto_target_reached() {
268 match tick_broadcaster.recv().await {
270 Ok(_) => (),
271 Err(RecvError::Closed) => return,
272 Err(err) => warn!("{err}"),
273 };
274 }
275 }
276 fn is_goto_target_reached(&self) -> bool {
277 self.get_component::<Pathfinder>()
278 .is_none_or(|p| p.goal.is_none() && !p.is_calculating)
279 }
280 fn is_executing_path(&self) -> bool {
281 self.get_component::<ExecutingPath>().is_some()
282 }
283 fn is_calculating_path(&self) -> bool {
284 self.get_component::<Pathfinder>()
285 .is_some_and(|p| p.is_calculating)
286 }
287}
288
289#[derive(Component)]
290pub struct ComputePath(Task<Option<PathFoundEvent>>);
291
292#[allow(clippy::type_complexity)]
293pub fn goto_listener(
294 mut commands: Commands,
295 mut events: MessageReader<GotoEvent>,
296 mut query: Query<(
297 &mut Pathfinder,
298 Option<&mut ExecutingPath>,
299 &Position,
300 &WorldName,
301 &Inventory,
302 Option<&CustomPathfinderState>,
303 )>,
304 worlds: Res<Worlds>,
305) {
306 let thread_pool = AsyncComputeTaskPool::get();
307
308 for event in events.read() {
309 let Ok((mut pathfinder, executing_path, position, world_name, inventory, custom_state)) =
310 query.get_mut(event.entity)
311 else {
312 warn!("got goto event for an entity that can't pathfind");
313 continue;
314 };
315
316 if env!("OPT_LEVEL") == "0" {
318 static WARNED: AtomicBool = AtomicBool::new(false);
319 if !WARNED.swap(true, atomic::Ordering::Relaxed) {
320 warn!(
321 "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."
322 )
323 }
324 }
325
326 let cur_pos = player_pos_to_block_pos(**position);
327
328 if event.goal.success(cur_pos) {
329 pathfinder.goal = None;
331 pathfinder.opts = None;
332 pathfinder.is_calculating = false;
333 debug!("already at goal, not pathfinding");
334 continue;
335 }
336
337 pathfinder.goal = Some(event.goal.clone());
339 pathfinder.opts = Some(event.opts.clone());
340 pathfinder.is_calculating = true;
341
342 let start = if let Some(mut executing_path) = executing_path
343 && { !executing_path.path.is_empty() }
344 {
345 let executing_path_limit = 50;
348 executing_path.path.truncate(executing_path_limit);
350
351 executing_path
352 .path
353 .back()
354 .expect("path was just checked to not be empty")
355 .movement
356 .target
357 } else {
358 cur_pos
359 };
360
361 if start == cur_pos {
362 info!("got goto {:?}, starting from {start:?}", event.goal);
363 } else {
364 info!(
365 "got goto {:?}, starting from {start:?} (currently at {cur_pos:?})",
366 event.goal,
367 );
368 }
369
370 let world_lock = worlds
371 .get(world_name)
372 .expect("Entity tried to pathfind but the entity isn't in a valid world");
373
374 let goal = event.goal.clone();
375 let entity = event.entity;
376
377 let goto_id_atomic = pathfinder.goto_id.clone();
378
379 let allow_mining = event.opts.allow_mining;
380 let mining_cache = MiningCache::new(if allow_mining {
381 Some(inventory.inventory_menu.clone())
382 } else {
383 None
384 });
385
386 let custom_state = custom_state.cloned().unwrap_or_default();
387 let opts = event.opts.clone();
388 let task = thread_pool.spawn(async move {
389 calculate_path(CalculatePathCtx {
390 entity,
391 start,
392 goal,
393 world_lock,
394 goto_id_atomic,
395 mining_cache,
396 custom_state,
397 opts,
398 })
399 });
400
401 commands.entity(event.entity).insert(ComputePath(task));
402 }
403}
404
405#[inline]
411pub fn player_pos_to_block_pos(position: Vec3) -> BlockPos {
412 BlockPos::from(position.up(0.5))
414}
415
416pub struct CalculatePathCtx {
417 pub entity: Entity,
418 pub start: BlockPos,
419 pub goal: Arc<dyn Goal>,
420 pub world_lock: Arc<RwLock<azalea_world::World>>,
421 pub goto_id_atomic: Arc<AtomicUsize>,
422 pub mining_cache: MiningCache,
423 pub custom_state: CustomPathfinderState,
424
425 pub opts: PathfinderOpts,
426}
427
428pub fn calculate_path(ctx: CalculatePathCtx) -> Option<PathFoundEvent> {
437 debug!("start: {:?}", ctx.start);
438
439 let goto_id = ctx.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1;
440
441 let origin = ctx.start;
442 let cached_world = CachedWorld::new(ctx.world_lock, origin);
443 let successors = |pos: RelBlockPos| {
444 call_successors_fn(
445 &cached_world,
446 &ctx.mining_cache,
447 &ctx.custom_state.0.read(),
448 ctx.opts.successors_fn,
449 pos,
450 )
451 };
452
453 let start_time = Instant::now();
454
455 let astar::Path {
456 movements,
457 is_partial,
458 cost,
459 } = a_star(
460 RelBlockPos::get_origin(origin),
461 |n| ctx.goal.heuristic(n.apply(origin)),
462 successors,
463 |n| ctx.goal.success(n.apply(origin)),
464 ctx.opts.min_timeout,
465 ctx.opts.max_timeout,
466 );
467 let end_time = Instant::now();
468 debug!("partial: {is_partial:?}, cost: {cost}");
469 let duration = end_time - start_time;
470 if is_partial {
471 if movements.is_empty() {
472 info!("Pathfinder took {duration:?} (empty path)");
473 } else {
474 info!("Pathfinder took {duration:?} (incomplete path)");
475 }
476 thread::sleep(Duration::from_millis(100));
478 } else {
479 info!("Pathfinder took {duration:?}");
480 }
481
482 debug!("Path:");
483 for movement in &movements {
484 debug!(" {}", movement.target.apply(origin));
485 }
486
487 let path = movements.into_iter().collect::<VecDeque<_>>();
488
489 let goto_id_now = ctx.goto_id_atomic.load(atomic::Ordering::SeqCst);
490 if goto_id != goto_id_now {
491 warn!("finished calculating a path, but it's outdated");
493 return None;
494 }
495
496 if path.is_empty() && is_partial {
497 debug!("this path is empty, we might be stuck :(");
498 }
499
500 let mut mapped_path = VecDeque::with_capacity(path.len());
501 let mut current_position = RelBlockPos::get_origin(origin);
502 for movement in path {
503 let mut found_edge = None;
504 for edge in successors(current_position) {
505 if edge.movement.target == movement.target {
506 found_edge = Some(edge);
507 break;
508 }
509 }
510
511 let found_edge = found_edge.expect(
512 "path should always still be possible because we're using the same world cache",
513 );
514 current_position = found_edge.movement.target;
515
516 mapped_path.push_back(Edge {
519 movement: astar::Movement {
520 target: movement.target.apply(origin),
521 data: movement.data,
522 },
523 cost: found_edge.cost,
524 });
525 }
526
527 Some(PathFoundEvent {
528 entity: ctx.entity,
529 start: ctx.start,
530 path: Some(mapped_path),
531 is_partial,
532 successors_fn: ctx.opts.successors_fn,
533 allow_mining: ctx.opts.allow_mining,
534 })
535}
536
537pub fn handle_tasks(
539 mut commands: Commands,
540 mut transform_tasks: Query<(Entity, &mut ComputePath)>,
541 mut path_found_events: MessageWriter<PathFoundEvent>,
542) {
543 for (entity, mut task) in &mut transform_tasks {
544 if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
545 if let Some(path_found_event) = optional_path_found_event {
546 path_found_events.write(path_found_event);
547 }
548
549 commands.entity(entity).remove::<ComputePath>();
551 }
552 }
553}
554
555#[allow(clippy::type_complexity)]
557pub fn path_found_listener(
558 mut events: MessageReader<PathFoundEvent>,
559 mut query: Query<(
560 &mut Pathfinder,
561 Option<&mut ExecutingPath>,
562 &WorldName,
563 &Inventory,
564 Option<&CustomPathfinderState>,
565 )>,
566 worlds: Res<Worlds>,
567 mut commands: Commands,
568) {
569 for event in events.read() {
570 let Ok((mut pathfinder, executing_path, world_name, inventory, custom_state)) =
571 query.get_mut(event.entity)
572 else {
573 debug!("got path found event for an entity that can't pathfind");
574 continue;
575 };
576 if let Some(path) = &event.path {
577 if let Some(mut executing_path) = executing_path {
578 let mut new_path = VecDeque::new();
579
580 if let Some(last_node_of_current_path) = executing_path.path.back() {
583 let world_lock = worlds
584 .get(world_name)
585 .expect("Entity tried to pathfind but the entity isn't in a valid world");
586 let origin = event.start;
587 let successors_fn: moves::SuccessorsFn = event.successors_fn;
588 let cached_world = CachedWorld::new(world_lock, origin);
589 let mining_cache = MiningCache::new(if event.allow_mining {
590 Some(inventory.inventory_menu.clone())
591 } else {
592 None
593 });
594 let custom_state = custom_state.cloned().unwrap_or_default();
595 let custom_state_ref = custom_state.0.read();
596 let successors = |pos: RelBlockPos| {
597 call_successors_fn(
598 &cached_world,
599 &mining_cache,
600 &custom_state_ref,
601 successors_fn,
602 pos,
603 )
604 };
605
606 if let Some(first_node_of_new_path) = path.front() {
607 let last_target_of_current_path = RelBlockPos::from_origin(
608 origin,
609 last_node_of_current_path.movement.target,
610 );
611 let first_target_of_new_path = RelBlockPos::from_origin(
612 origin,
613 first_node_of_new_path.movement.target,
614 );
615
616 if successors(last_target_of_current_path)
617 .iter()
618 .any(|edge| edge.movement.target == first_target_of_new_path)
619 {
620 debug!("combining old and new paths");
621 debug!(
622 "old path: {:?}",
623 executing_path.path.iter().collect::<Vec<_>>()
624 );
625 debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
626 new_path.extend(executing_path.path.iter().cloned());
627 }
628 } else {
629 new_path.extend(executing_path.path.iter().cloned());
630 }
631 }
632
633 new_path.extend(path.to_owned());
634
635 debug!(
636 "set queued path to {:?}",
637 new_path.iter().take(10).collect::<Vec<_>>()
638 );
639 executing_path.queued_path = Some(new_path);
640 executing_path.is_path_partial = event.is_partial;
641 } else if path.is_empty() {
642 debug!("calculated path is empty, so didn't add ExecutingPath");
643 if !pathfinder.opts.as_ref().is_some_and(|o| o.retry_on_no_path) {
644 debug!("retry_on_no_path is set to false, removing goal");
645 pathfinder.goal = None;
646 }
647 } else {
648 commands.entity(event.entity).insert(ExecutingPath {
649 path: path.to_owned(),
650 queued_path: None,
651 last_reached_node: event.start,
652 ticks_since_last_node_reached: 0,
653 is_path_partial: event.is_partial,
654 });
655 debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
656 debug!("partial: {}", event.is_partial);
657 }
658 } else {
659 error!("No path found");
660 if let Some(mut executing_path) = executing_path {
661 executing_path.queued_path = Some(VecDeque::new());
663 } else {
664 }
666 }
667 pathfinder.is_calculating = false;
668 }
669}
670
671#[derive(Message)]
672pub struct StopPathfindingEvent {
673 pub entity: Entity,
674 pub force: bool,
680}
681
682pub fn handle_stop_pathfinding_event(
683 mut events: MessageReader<StopPathfindingEvent>,
684 mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
685 mut walk_events: MessageWriter<StartWalkEvent>,
686 mut commands: Commands,
687) {
688 for event in events.read() {
689 commands.entity(event.entity).remove::<ComputePath>();
691
692 let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
693 continue;
694 };
695 pathfinder.goal = None;
696 if event.force {
697 executing_path.path.clear();
698 executing_path.queued_path = None;
699 } else {
700 executing_path.queued_path = Some(VecDeque::new());
702 executing_path.is_path_partial = false;
704 }
705
706 if executing_path.path.is_empty() {
707 walk_events.write(StartWalkEvent {
708 entity: event.entity,
709 direction: WalkDirection::None,
710 });
711 commands.entity(event.entity).remove::<ExecutingPath>();
712 }
713 }
714}
715
716pub fn stop_pathfinding_on_world_change(
717 mut query: Query<(Entity, &mut ExecutingPath), Changed<WorldName>>,
718 mut stop_pathfinding_events: MessageWriter<StopPathfindingEvent>,
719) {
720 for (entity, mut executing_path) in &mut query {
721 if !executing_path.path.is_empty() {
722 debug!("world changed, clearing path");
723 executing_path.path.clear();
724 stop_pathfinding_events.write(StopPathfindingEvent {
725 entity,
726 force: true,
727 });
728 }
729 }
730}
731
732pub fn call_successors_fn(
733 cached_world: &CachedWorld,
734 mining_cache: &MiningCache,
735 custom_state: &CustomPathfinderStateRef,
736 successors_fn: SuccessorsFn,
737 pos: RelBlockPos,
738) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>> {
739 let mut edges = Vec::with_capacity(16);
740 let mut ctx = MovesCtx {
741 edges: &mut edges,
742 world: cached_world,
743 mining_cache,
744 custom_state,
745 };
746 successors_fn(&mut ctx, pos);
747 edges
748}