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, 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 let cur_pos = player_pos_to_block_pos(**position);
310
311 if event.goal.success(cur_pos) {
312 pathfinder.goal = None;
314 pathfinder.opts = None;
315 pathfinder.is_calculating = false;
316 debug!("already at goal, not pathfinding");
317 continue;
318 }
319
320 pathfinder.goal = Some(event.goal.clone());
322 pathfinder.opts = Some(event.opts.clone());
323 pathfinder.is_calculating = true;
324
325 let start = if let Some(mut executing_path) = executing_path
326 && { !executing_path.path.is_empty() }
327 {
328 let executing_path_limit = 50;
331 executing_path.path.truncate(executing_path_limit);
333
334 executing_path
335 .path
336 .back()
337 .expect("path was just checked to not be empty")
338 .movement
339 .target
340 } else {
341 cur_pos
342 };
343
344 if start == cur_pos {
345 info!("got goto {:?}, starting from {start:?}", event.goal);
346 } else {
347 info!(
348 "got goto {:?}, starting from {start:?} (currently at {cur_pos:?})",
349 event.goal,
350 );
351 }
352
353 let world_lock = worlds
354 .get(world_name)
355 .expect("Entity tried to pathfind but the entity isn't in a valid world");
356
357 let goal = event.goal.clone();
358 let entity = event.entity;
359
360 let goto_id_atomic = pathfinder.goto_id.clone();
361
362 let allow_mining = event.opts.allow_mining;
363 let mining_cache = MiningCache::new(if allow_mining {
364 Some(inventory.inventory_menu.clone())
365 } else {
366 None
367 });
368
369 let custom_state = custom_state.cloned().unwrap_or_default();
370 let opts = event.opts.clone();
371 let task = thread_pool.spawn(async move {
372 calculate_path(CalculatePathCtx {
373 entity,
374 start,
375 goal,
376 world_lock,
377 goto_id_atomic,
378 mining_cache,
379 custom_state,
380 opts,
381 })
382 });
383
384 commands.entity(event.entity).insert(ComputePath(task));
385 }
386}
387
388#[inline]
394pub fn player_pos_to_block_pos(position: Vec3) -> BlockPos {
395 BlockPos::from(position.up(0.5))
397}
398
399pub struct CalculatePathCtx {
400 pub entity: Entity,
401 pub start: BlockPos,
402 pub goal: Arc<dyn Goal>,
403 pub world_lock: Arc<RwLock<azalea_world::World>>,
404 pub goto_id_atomic: Arc<AtomicUsize>,
405 pub mining_cache: MiningCache,
406 pub custom_state: CustomPathfinderState,
407
408 pub opts: PathfinderOpts,
409}
410
411pub fn calculate_path(ctx: CalculatePathCtx) -> Option<PathFoundEvent> {
420 debug!("start: {:?}", ctx.start);
421
422 let goto_id = ctx.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1;
423
424 let origin = ctx.start;
425 let cached_world = CachedWorld::new(ctx.world_lock, origin);
426 let successors = |pos: RelBlockPos| {
427 call_successors_fn(
428 &cached_world,
429 &ctx.mining_cache,
430 &ctx.custom_state.0.read(),
431 ctx.opts.successors_fn,
432 pos,
433 )
434 };
435
436 let start_time = Instant::now();
437
438 let astar::Path {
439 movements,
440 is_partial,
441 cost,
442 } = a_star(
443 RelBlockPos::get_origin(origin),
444 |n| ctx.goal.heuristic(n.apply(origin)),
445 successors,
446 |n| ctx.goal.success(n.apply(origin)),
447 ctx.opts.min_timeout,
448 ctx.opts.max_timeout,
449 );
450 let end_time = Instant::now();
451 debug!("partial: {is_partial:?}, cost: {cost}");
452 let duration = end_time - start_time;
453 if is_partial {
454 if movements.is_empty() {
455 info!("Pathfinder took {duration:?} (empty path)");
456 } else {
457 info!("Pathfinder took {duration:?} (incomplete path)");
458 }
459 thread::sleep(Duration::from_millis(100));
461 } else {
462 info!("Pathfinder took {duration:?}");
463 }
464
465 debug!("Path:");
466 for movement in &movements {
467 debug!(" {}", movement.target.apply(origin));
468 }
469
470 let path = movements.into_iter().collect::<VecDeque<_>>();
471
472 let goto_id_now = ctx.goto_id_atomic.load(atomic::Ordering::SeqCst);
473 if goto_id != goto_id_now {
474 warn!("finished calculating a path, but it's outdated");
476 return None;
477 }
478
479 if path.is_empty() && is_partial {
480 debug!("this path is empty, we might be stuck :(");
481 }
482
483 let mut mapped_path = VecDeque::with_capacity(path.len());
484 let mut current_position = RelBlockPos::get_origin(origin);
485 for movement in path {
486 let mut found_edge = None;
487 for edge in successors(current_position) {
488 if edge.movement.target == movement.target {
489 found_edge = Some(edge);
490 break;
491 }
492 }
493
494 let found_edge = found_edge.expect(
495 "path should always still be possible because we're using the same world cache",
496 );
497 current_position = found_edge.movement.target;
498
499 mapped_path.push_back(Edge {
502 movement: astar::Movement {
503 target: movement.target.apply(origin),
504 data: movement.data,
505 },
506 cost: found_edge.cost,
507 });
508 }
509
510 Some(PathFoundEvent {
511 entity: ctx.entity,
512 start: ctx.start,
513 path: Some(mapped_path),
514 is_partial,
515 successors_fn: ctx.opts.successors_fn,
516 allow_mining: ctx.opts.allow_mining,
517 })
518}
519
520pub fn handle_tasks(
522 mut commands: Commands,
523 mut transform_tasks: Query<(Entity, &mut ComputePath)>,
524 mut path_found_events: MessageWriter<PathFoundEvent>,
525) {
526 for (entity, mut task) in &mut transform_tasks {
527 if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
528 if let Some(path_found_event) = optional_path_found_event {
529 path_found_events.write(path_found_event);
530 }
531
532 commands.entity(entity).remove::<ComputePath>();
534 }
535 }
536}
537
538#[allow(clippy::type_complexity)]
540pub fn path_found_listener(
541 mut events: MessageReader<PathFoundEvent>,
542 mut query: Query<(
543 &mut Pathfinder,
544 Option<&mut ExecutingPath>,
545 &WorldName,
546 &Inventory,
547 Option<&CustomPathfinderState>,
548 )>,
549 worlds: Res<Worlds>,
550 mut commands: Commands,
551) {
552 for event in events.read() {
553 let Ok((mut pathfinder, executing_path, world_name, inventory, custom_state)) =
554 query.get_mut(event.entity)
555 else {
556 debug!("got path found event for an entity that can't pathfind");
557 continue;
558 };
559 if let Some(path) = &event.path {
560 if let Some(mut executing_path) = executing_path {
561 let mut new_path = VecDeque::new();
562
563 if let Some(last_node_of_current_path) = executing_path.path.back() {
566 let world_lock = worlds
567 .get(world_name)
568 .expect("Entity tried to pathfind but the entity isn't in a valid world");
569 let origin = event.start;
570 let successors_fn: moves::SuccessorsFn = event.successors_fn;
571 let cached_world = CachedWorld::new(world_lock, origin);
572 let mining_cache = MiningCache::new(if event.allow_mining {
573 Some(inventory.inventory_menu.clone())
574 } else {
575 None
576 });
577 let custom_state = custom_state.cloned().unwrap_or_default();
578 let custom_state_ref = custom_state.0.read();
579 let successors = |pos: RelBlockPos| {
580 call_successors_fn(
581 &cached_world,
582 &mining_cache,
583 &custom_state_ref,
584 successors_fn,
585 pos,
586 )
587 };
588
589 if let Some(first_node_of_new_path) = path.front() {
590 let last_target_of_current_path = RelBlockPos::from_origin(
591 origin,
592 last_node_of_current_path.movement.target,
593 );
594 let first_target_of_new_path = RelBlockPos::from_origin(
595 origin,
596 first_node_of_new_path.movement.target,
597 );
598
599 if successors(last_target_of_current_path)
600 .iter()
601 .any(|edge| edge.movement.target == first_target_of_new_path)
602 {
603 debug!("combining old and new paths");
604 debug!(
605 "old path: {:?}",
606 executing_path.path.iter().collect::<Vec<_>>()
607 );
608 debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
609 new_path.extend(executing_path.path.iter().cloned());
610 }
611 } else {
612 new_path.extend(executing_path.path.iter().cloned());
613 }
614 }
615
616 new_path.extend(path.to_owned());
617
618 debug!(
619 "set queued path to {:?}",
620 new_path.iter().take(10).collect::<Vec<_>>()
621 );
622 executing_path.queued_path = Some(new_path);
623 executing_path.is_path_partial = event.is_partial;
624 } else if path.is_empty() {
625 debug!("calculated path is empty, so didn't add ExecutingPath");
626 if !pathfinder.opts.as_ref().is_some_and(|o| o.retry_on_no_path) {
627 debug!("retry_on_no_path is set to false, removing goal");
628 pathfinder.goal = None;
629 }
630 } else {
631 commands.entity(event.entity).insert(ExecutingPath {
632 path: path.to_owned(),
633 queued_path: None,
634 last_reached_node: event.start,
635 ticks_since_last_node_reached: 0,
636 is_path_partial: event.is_partial,
637 });
638 debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
639 debug!("partial: {}", event.is_partial);
640 }
641 } else {
642 error!("No path found");
643 if let Some(mut executing_path) = executing_path {
644 executing_path.queued_path = Some(VecDeque::new());
646 } else {
647 }
649 }
650 pathfinder.is_calculating = false;
651 }
652}
653
654#[derive(Message)]
655pub struct StopPathfindingEvent {
656 pub entity: Entity,
657 pub force: bool,
663}
664
665pub fn handle_stop_pathfinding_event(
666 mut events: MessageReader<StopPathfindingEvent>,
667 mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
668 mut walk_events: MessageWriter<StartWalkEvent>,
669 mut commands: Commands,
670) {
671 for event in events.read() {
672 commands.entity(event.entity).remove::<ComputePath>();
674
675 let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
676 continue;
677 };
678 pathfinder.goal = None;
679 if event.force {
680 executing_path.path.clear();
681 executing_path.queued_path = None;
682 } else {
683 executing_path.queued_path = Some(VecDeque::new());
685 executing_path.is_path_partial = false;
687 }
688
689 if executing_path.path.is_empty() {
690 walk_events.write(StartWalkEvent {
691 entity: event.entity,
692 direction: WalkDirection::None,
693 });
694 commands.entity(event.entity).remove::<ExecutingPath>();
695 }
696 }
697}
698
699pub fn stop_pathfinding_on_world_change(
700 mut query: Query<(Entity, &mut ExecutingPath), Changed<WorldName>>,
701 mut stop_pathfinding_events: MessageWriter<StopPathfindingEvent>,
702) {
703 for (entity, mut executing_path) in &mut query {
704 if !executing_path.path.is_empty() {
705 debug!("world changed, clearing path");
706 executing_path.path.clear();
707 stop_pathfinding_events.write(StopPathfindingEvent {
708 entity,
709 force: true,
710 });
711 }
712 }
713}
714
715pub fn call_successors_fn(
716 cached_world: &CachedWorld,
717 mining_cache: &MiningCache,
718 custom_state: &CustomPathfinderStateRef,
719 successors_fn: SuccessorsFn,
720 pos: RelBlockPos,
721) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>> {
722 let mut edges = Vec::with_capacity(16);
723 let mut ctx = MovesCtx {
724 edges: &mut edges,
725 world: cached_world,
726 mining_cache,
727 custom_state,
728 };
729 successors_fn(&mut ctx, pos);
730 edges
731}