1pub mod astar;
6pub mod costs;
7pub mod custom_state;
8pub mod debug;
9pub mod goals;
10mod goto_event;
11pub mod mining;
12pub mod moves;
13pub mod rel_block_pos;
14pub mod simulation;
15#[cfg(test)]
16mod tests;
17pub mod world;
18
19use std::{
20 cmp,
21 collections::VecDeque,
22 ops::RangeInclusive,
23 sync::{
24 Arc,
25 atomic::{self, AtomicUsize},
26 },
27 thread,
28 time::{Duration, Instant},
29};
30
31use astar::{Edge, PathfinderTimeout};
32use azalea_client::{
33 StartSprintEvent, StartWalkEvent,
34 inventory::{Inventory, InventorySet, SetSelectedHotbarSlotEvent},
35 local_player::InstanceHolder,
36 mining::{Mining, MiningSet, StartMiningBlockEvent},
37 movement::MoveEventsSet,
38};
39use azalea_core::{position::BlockPos, tick::GameTick};
40use azalea_entity::{LocalEntity, Physics, Position, metadata::Player};
41use azalea_physics::PhysicsSet;
42use azalea_world::{InstanceContainer, InstanceName};
43use bevy_app::{PreUpdate, Update};
44use bevy_ecs::prelude::*;
45use bevy_tasks::{AsyncComputeTaskPool, Task};
46use custom_state::{CustomPathfinderState, CustomPathfinderStateRef};
47use futures_lite::future;
48use goals::BlockPosGoal;
49pub use goto_event::GotoEvent;
50use parking_lot::RwLock;
51use rel_block_pos::RelBlockPos;
52use tokio::sync::broadcast::error::RecvError;
53use tracing::{debug, error, info, trace, warn};
54
55use self::{
56 debug::debug_render_path_with_particles,
57 goals::Goal,
58 mining::MiningCache,
59 moves::{ExecuteCtx, IsReachedCtx, SuccessorsFn},
60};
61use crate::{
62 BotClientExt, WalkDirection,
63 app::{App, Plugin},
64 bot::{JumpEvent, LookAtEvent},
65 ecs::{
66 component::Component,
67 entity::Entity,
68 event::{EventReader, EventWriter},
69 query::{With, Without},
70 system::{Commands, Query, Res},
71 },
72 pathfinder::{astar::a_star, moves::PathfinderCtx, world::CachedWorld},
73};
74
75#[derive(Clone, Default)]
76pub struct PathfinderPlugin;
77impl Plugin for PathfinderPlugin {
78 fn build(&self, app: &mut App) {
79 app.add_event::<GotoEvent>()
80 .add_event::<PathFoundEvent>()
81 .add_event::<StopPathfindingEvent>()
82 .add_systems(
83 GameTick,
86 (
87 timeout_movement,
88 check_for_path_obstruction,
89 check_node_reached,
90 tick_execute_path,
91 debug_render_path_with_particles,
92 recalculate_near_end_of_path,
93 recalculate_if_has_goal_but_no_path,
94 )
95 .chain()
96 .after(PhysicsSet)
97 .after(azalea_client::movement::send_position)
98 .after(MiningSet),
99 )
100 .add_systems(PreUpdate, add_default_pathfinder)
101 .add_systems(
102 Update,
103 (
104 goto_listener,
105 handle_tasks,
106 stop_pathfinding_on_instance_change,
107 path_found_listener,
108 handle_stop_pathfinding_event,
109 )
110 .chain()
111 .before(MoveEventsSet)
112 .before(InventorySet),
113 );
114 }
115}
116
117#[derive(Component, Default, Clone)]
119#[non_exhaustive]
120pub struct Pathfinder {
121 pub goal: Option<Arc<dyn Goal>>,
122 pub successors_fn: Option<SuccessorsFn>,
123 pub is_calculating: bool,
124 pub allow_mining: bool,
125 pub retry_on_no_path: bool,
126
127 pub min_timeout: Option<PathfinderTimeout>,
128 pub max_timeout: Option<PathfinderTimeout>,
129
130 pub goto_id: Arc<AtomicUsize>,
131}
132
133#[derive(Component, Clone)]
136pub struct ExecutingPath {
137 pub path: VecDeque<astar::Edge<BlockPos, moves::MoveData>>,
138 pub queued_path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
139 pub last_reached_node: BlockPos,
140 pub last_node_reached_at: Instant,
141 pub is_path_partial: bool,
142}
143
144#[derive(Event, Clone, Debug)]
145#[non_exhaustive]
146pub struct PathFoundEvent {
147 pub entity: Entity,
148 pub start: BlockPos,
149 pub path: Option<VecDeque<astar::Edge<BlockPos, moves::MoveData>>>,
150 pub is_partial: bool,
151 pub successors_fn: SuccessorsFn,
152 pub allow_mining: bool,
153}
154
155#[allow(clippy::type_complexity)]
156pub fn add_default_pathfinder(
157 mut commands: Commands,
158 mut query: Query<Entity, (Without<Pathfinder>, With<LocalEntity>, With<Player>)>,
159) {
160 for entity in &mut query {
161 commands.entity(entity).insert(Pathfinder::default());
162 }
163}
164
165pub trait PathfinderClientExt {
166 fn goto(&self, goal: impl Goal + 'static) -> impl Future<Output = ()>;
167 fn start_goto(&self, goal: impl Goal + 'static);
168 fn start_goto_without_mining(&self, goal: impl Goal + 'static);
169 fn stop_pathfinding(&self);
170 fn wait_until_goto_target_reached(&self) -> impl Future<Output = ()>;
171 fn is_goto_target_reached(&self) -> bool;
172}
173
174impl PathfinderClientExt for azalea_client::Client {
175 async fn goto(&self, goal: impl Goal + 'static) {
188 self.start_goto(goal);
189 self.wait_until_goto_target_reached().await;
190 }
191
192 fn start_goto(&self, goal: impl Goal + 'static) {
202 self.ecs
203 .lock()
204 .send_event(GotoEvent::new(self.entity, goal));
205 }
206
207 fn start_goto_without_mining(&self, goal: impl Goal + 'static) {
210 self.ecs
211 .lock()
212 .send_event(GotoEvent::new(self.entity, goal).with_allow_mining(false));
213 }
214
215 fn stop_pathfinding(&self) {
216 self.ecs.lock().send_event(StopPathfindingEvent {
217 entity: self.entity,
218 force: false,
219 });
220 }
221
222 async fn wait_until_goto_target_reached(&self) {
224 self.wait_updates(1).await;
227
228 let mut tick_broadcaster = self.get_tick_broadcaster();
229 while !self.is_goto_target_reached() {
230 match tick_broadcaster.recv().await {
232 Ok(_) => (),
233 Err(RecvError::Closed) => return,
234 Err(err) => warn!("{err}"),
235 };
236 }
237 }
238
239 fn is_goto_target_reached(&self) -> bool {
240 self.map_get_component::<Pathfinder, _>(|p| p.goal.is_none() && !p.is_calculating)
241 .unwrap_or(true)
242 }
243}
244
245#[derive(Component)]
246pub struct ComputePath(Task<Option<PathFoundEvent>>);
247
248#[allow(clippy::type_complexity)]
249pub fn goto_listener(
250 mut commands: Commands,
251 mut events: EventReader<GotoEvent>,
252 mut query: Query<(
253 &mut Pathfinder,
254 Option<&ExecutingPath>,
255 &Position,
256 &InstanceName,
257 &Inventory,
258 Option<&CustomPathfinderState>,
259 )>,
260 instance_container: Res<InstanceContainer>,
261) {
262 let thread_pool = AsyncComputeTaskPool::get();
263
264 for event in events.read() {
265 let Ok((mut pathfinder, executing_path, position, instance_name, inventory, custom_state)) =
266 query.get_mut(event.entity)
267 else {
268 warn!("got goto event for an entity that can't pathfind");
269 continue;
270 };
271
272 if event.goal.success(BlockPos::from(position)) {
273 pathfinder.goal = None;
275 pathfinder.successors_fn = None;
276 pathfinder.is_calculating = false;
277 debug!("already at goal, not pathfinding");
278 continue;
279 }
280
281 pathfinder.goal = Some(event.goal.clone());
283 pathfinder.successors_fn = Some(event.successors_fn);
284 pathfinder.is_calculating = true;
285 pathfinder.allow_mining = event.allow_mining;
286 pathfinder.min_timeout = Some(event.min_timeout);
287 pathfinder.max_timeout = Some(event.max_timeout);
288
289 let start = if let Some(executing_path) = executing_path
290 && let Some(final_node) = executing_path.path.back()
291 {
292 executing_path
294 .path
295 .get(50)
296 .unwrap_or(final_node)
297 .movement
298 .target
299 } else {
300 BlockPos::from(position)
301 };
302
303 if start == BlockPos::from(position) {
304 info!("got goto {:?}, starting from {start:?}", event.goal);
305 } else {
306 info!(
307 "got goto {:?}, starting from {start:?} (currently at {:?})",
308 event.goal,
309 BlockPos::from(position)
310 );
311 }
312
313 let successors_fn: moves::SuccessorsFn = event.successors_fn;
314
315 let world_lock = instance_container
316 .get(instance_name)
317 .expect("Entity tried to pathfind but the entity isn't in a valid world");
318
319 let goal = event.goal.clone();
320 let entity = event.entity;
321
322 let goto_id_atomic = pathfinder.goto_id.clone();
323
324 let allow_mining = event.allow_mining;
325 let retry_on_no_path = event.retry_on_no_path;
326 let mining_cache = MiningCache::new(if allow_mining {
327 Some(inventory.inventory_menu.clone())
328 } else {
329 None
330 });
331
332 let custom_state = custom_state.cloned().unwrap_or_default();
333
334 let min_timeout = event.min_timeout;
335 let max_timeout = event.max_timeout;
336
337 let task = thread_pool.spawn(async move {
338 calculate_path(CalculatePathOpts {
339 entity,
340 start,
341 goal,
342 successors_fn,
343 world_lock,
344 goto_id_atomic,
345 allow_mining,
346 mining_cache,
347 retry_on_no_path,
348 custom_state,
349 min_timeout,
350 max_timeout,
351 })
352 });
353
354 commands.entity(event.entity).insert(ComputePath(task));
355 }
356}
357
358pub struct CalculatePathOpts {
359 pub entity: Entity,
360 pub start: BlockPos,
361 pub goal: Arc<dyn Goal>,
362 pub successors_fn: SuccessorsFn,
363 pub world_lock: Arc<RwLock<azalea_world::Instance>>,
364 pub goto_id_atomic: Arc<AtomicUsize>,
365 pub allow_mining: bool,
366 pub mining_cache: MiningCache,
367 pub retry_on_no_path: bool,
369
370 pub min_timeout: PathfinderTimeout,
372 pub max_timeout: PathfinderTimeout,
373
374 pub custom_state: CustomPathfinderState,
375}
376
377pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> {
386 debug!("start: {:?}", opts.start);
387
388 let goto_id = opts.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1;
389
390 let origin = opts.start;
391 let cached_world = CachedWorld::new(opts.world_lock, origin);
392 let successors = |pos: RelBlockPos| {
393 call_successors_fn(
394 &cached_world,
395 &opts.mining_cache,
396 &opts.custom_state.0.read(),
397 opts.successors_fn,
398 pos,
399 )
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 mut mapped_path = VecDeque::with_capacity(path.len());
449 let mut current_position = RelBlockPos::get_origin(origin);
450 for movement in path {
451 let mut found_edge = None;
452 for edge in successors(current_position) {
453 if edge.movement.target == movement.target {
454 found_edge = Some(edge);
455 break;
456 }
457 }
458
459 let found_edge = found_edge.expect(
460 "path should always still be possible because we're using the same world cache",
461 );
462 current_position = found_edge.movement.target;
463
464 mapped_path.push_back(Edge {
467 movement: astar::Movement {
468 target: movement.target.apply(origin),
469 data: movement.data,
470 },
471 cost: found_edge.cost,
472 });
473 }
474
475 Some(PathFoundEvent {
476 entity: opts.entity,
477 start: opts.start,
478 path: Some(mapped_path),
479 is_partial,
480 successors_fn: opts.successors_fn,
481 allow_mining: opts.allow_mining,
482 })
483}
484
485pub fn handle_tasks(
487 mut commands: Commands,
488 mut transform_tasks: Query<(Entity, &mut ComputePath)>,
489 mut path_found_events: EventWriter<PathFoundEvent>,
490) {
491 for (entity, mut task) in &mut transform_tasks {
492 if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
493 if let Some(path_found_event) = optional_path_found_event {
494 path_found_events.write(path_found_event);
495 }
496
497 commands.entity(entity).remove::<ComputePath>();
499 }
500 }
501}
502
503#[allow(clippy::type_complexity)]
505pub fn path_found_listener(
506 mut events: EventReader<PathFoundEvent>,
507 mut query: Query<(
508 &mut Pathfinder,
509 Option<&mut ExecutingPath>,
510 &InstanceName,
511 &Inventory,
512 Option<&CustomPathfinderState>,
513 )>,
514 instance_container: Res<InstanceContainer>,
515 mut commands: Commands,
516) {
517 for event in events.read() {
518 let (mut pathfinder, executing_path, instance_name, inventory, custom_state) = query
519 .get_mut(event.entity)
520 .expect("Path found for an entity that doesn't have a pathfinder");
521 if let Some(path) = &event.path {
522 if let Some(mut executing_path) = executing_path {
523 let mut new_path = VecDeque::new();
524
525 if let Some(last_node_of_current_path) = executing_path.path.back() {
528 let world_lock = instance_container
529 .get(instance_name)
530 .expect("Entity tried to pathfind but the entity isn't in a valid world");
531 let origin = event.start;
532 let successors_fn: moves::SuccessorsFn = event.successors_fn;
533 let cached_world = CachedWorld::new(world_lock, origin);
534 let mining_cache = MiningCache::new(if event.allow_mining {
535 Some(inventory.inventory_menu.clone())
536 } else {
537 None
538 });
539 let custom_state = custom_state.cloned().unwrap_or_default();
540 let custom_state_ref = custom_state.0.read();
541 let successors = |pos: RelBlockPos| {
542 call_successors_fn(
543 &cached_world,
544 &mining_cache,
545 &custom_state_ref,
546 successors_fn,
547 pos,
548 )
549 };
550
551 if let Some(first_node_of_new_path) = path.front() {
552 let last_target_of_current_path = RelBlockPos::from_origin(
553 origin,
554 last_node_of_current_path.movement.target,
555 );
556 let first_target_of_new_path = RelBlockPos::from_origin(
557 origin,
558 first_node_of_new_path.movement.target,
559 );
560
561 if successors(last_target_of_current_path)
562 .iter()
563 .any(|edge| edge.movement.target == first_target_of_new_path)
564 {
565 debug!("combining old and new paths");
566 debug!(
567 "old path: {:?}",
568 executing_path.path.iter().collect::<Vec<_>>()
569 );
570 debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
571 new_path.extend(executing_path.path.iter().cloned());
572 }
573 } else {
574 new_path.extend(executing_path.path.iter().cloned());
575 }
576 }
577
578 new_path.extend(path.to_owned());
579
580 debug!(
581 "set queued path to {:?}",
582 new_path.iter().take(10).collect::<Vec<_>>()
583 );
584 executing_path.queued_path = Some(new_path);
585 executing_path.is_path_partial = event.is_partial;
586 } else if path.is_empty() {
587 debug!("calculated path is empty, so didn't add ExecutingPath");
588 if !pathfinder.retry_on_no_path {
589 debug!("retry_on_no_path is set to false, removing goal");
590 pathfinder.goal = None;
591 }
592 } else {
593 commands.entity(event.entity).insert(ExecutingPath {
594 path: path.to_owned(),
595 queued_path: None,
596 last_reached_node: event.start,
597 last_node_reached_at: Instant::now(),
598 is_path_partial: event.is_partial,
599 });
600 debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
601 debug!("partial: {}", event.is_partial);
602 }
603 } else {
604 error!("No path found");
605 if let Some(mut executing_path) = executing_path {
606 executing_path.queued_path = Some(VecDeque::new());
608 } else {
609 }
611 }
612 pathfinder.is_calculating = false;
613 }
614}
615
616#[allow(clippy::type_complexity)]
617pub fn timeout_movement(
618 mut query: Query<(
619 Entity,
620 &mut Pathfinder,
621 &mut ExecutingPath,
622 &Position,
623 Option<&Mining>,
624 &InstanceName,
625 &Inventory,
626 Option<&CustomPathfinderState>,
627 )>,
628 instance_container: Res<InstanceContainer>,
629) {
630 for (
631 entity,
632 mut pathfinder,
633 mut executing_path,
634 position,
635 mining,
636 instance_name,
637 inventory,
638 custom_state,
639 ) in &mut query
640 {
641 if let Some(mining) = mining {
643 if mining.pos.distance_squared_to(position.into()) < 6_i32.pow(2) {
645 executing_path.last_node_reached_at = Instant::now();
648 continue;
649 }
650 }
651
652 if executing_path.last_node_reached_at.elapsed() > Duration::from_secs(2)
653 && !pathfinder.is_calculating
654 && !executing_path.path.is_empty()
655 {
656 warn!("pathfinder timeout, trying to patch path");
657 executing_path.queued_path = None;
658 executing_path.last_reached_node = BlockPos::from(position);
659
660 let world_lock = instance_container
661 .get(instance_name)
662 .expect("Entity tried to pathfind but the entity isn't in a valid world");
663 let Some(successors_fn) = pathfinder.successors_fn else {
664 warn!(
665 "pathfinder was going to patch path because of timeout, but there was no successors_fn"
666 );
667 return;
668 };
669
670 let custom_state = custom_state.cloned().unwrap_or_default();
671
672 patch_path(
676 0..=cmp::min(20, executing_path.path.len() - 1),
677 &mut executing_path,
678 &mut pathfinder,
679 inventory,
680 entity,
681 successors_fn,
682 world_lock,
683 custom_state,
684 );
685 executing_path.last_node_reached_at = Instant::now();
687 }
688 }
689}
690
691pub fn check_node_reached(
692 mut query: Query<(
693 Entity,
694 &mut Pathfinder,
695 &mut ExecutingPath,
696 &Position,
697 &Physics,
698 )>,
699 mut walk_events: EventWriter<StartWalkEvent>,
700 mut commands: Commands,
701) {
702 for (entity, mut pathfinder, mut executing_path, position, physics) in &mut query {
703 'skip: loop {
704 for (i, edge) in executing_path
709 .path
710 .clone()
711 .into_iter()
712 .enumerate()
713 .take(20)
714 .rev()
715 {
716 let movement = edge.movement;
717 let is_reached_ctx = IsReachedCtx {
718 target: movement.target,
719 start: executing_path.last_reached_node,
720 position: **position,
721 physics,
722 };
723 let extra_strict_if_last = if i == executing_path.path.len() - 1 {
724 let x_difference_from_center = position.x - (movement.target.x as f64 + 0.5);
725 let z_difference_from_center = position.z - (movement.target.z as f64 + 0.5);
726 physics.on_ground()
728 && BlockPos::from(position.up(0.5)) == movement.target
730 && (x_difference_from_center + physics.velocity.x).abs() < 0.2
733 && (z_difference_from_center + physics.velocity.z).abs() < 0.2
734 } else {
735 true
736 };
737 if (movement.data.is_reached)(is_reached_ctx) && extra_strict_if_last {
738 executing_path.path = executing_path.path.split_off(i + 1);
739 executing_path.last_reached_node = movement.target;
740 executing_path.last_node_reached_at = Instant::now();
741 trace!("reached node {}", movement.target);
742
743 if let Some(new_path) = executing_path.queued_path.take() {
744 debug!(
745 "swapped path to {:?}",
746 new_path.iter().take(10).collect::<Vec<_>>()
747 );
748 executing_path.path = new_path;
749
750 if executing_path.path.is_empty() {
751 info!("the path we just swapped to was empty, so reached end of path");
752 walk_events.write(StartWalkEvent {
753 entity,
754 direction: WalkDirection::None,
755 });
756 commands.entity(entity).remove::<ExecutingPath>();
757 break;
758 }
759
760 continue 'skip;
762 }
763
764 if executing_path.path.is_empty() {
765 debug!("pathfinder path is now empty");
766 walk_events.write(StartWalkEvent {
767 entity,
768 direction: WalkDirection::None,
769 });
770 commands.entity(entity).remove::<ExecutingPath>();
771 if let Some(goal) = pathfinder.goal.clone()
772 && goal.success(movement.target)
773 {
774 info!("goal was reached!");
775 pathfinder.goal = None;
776 pathfinder.successors_fn = None;
777 }
778 }
779
780 break;
781 }
782 }
783 break;
784 }
785 }
786}
787
788#[allow(clippy::type_complexity)]
789pub fn check_for_path_obstruction(
790 mut query: Query<(
791 Entity,
792 &mut Pathfinder,
793 &mut ExecutingPath,
794 &InstanceName,
795 &Inventory,
796 Option<&CustomPathfinderState>,
797 )>,
798 instance_container: Res<InstanceContainer>,
799) {
800 for (entity, mut pathfinder, mut executing_path, instance_name, inventory, custom_state) in
801 &mut query
802 {
803 let Some(successors_fn) = pathfinder.successors_fn else {
804 continue;
805 };
806
807 let world_lock = instance_container
808 .get(instance_name)
809 .expect("Entity tried to pathfind but the entity isn't in a valid world");
810
811 let origin = executing_path.last_reached_node;
813 let cached_world = CachedWorld::new(world_lock, origin);
814 let mining_cache = MiningCache::new(if pathfinder.allow_mining {
815 Some(inventory.inventory_menu.clone())
816 } else {
817 None
818 });
819 let custom_state = custom_state.cloned().unwrap_or_default();
820 let custom_state_ref = custom_state.0.read();
821 let successors = |pos: RelBlockPos| {
822 call_successors_fn(
823 &cached_world,
824 &mining_cache,
825 &custom_state_ref,
826 successors_fn,
827 pos,
828 )
829 };
830
831 let Some(obstructed_index) = check_path_obstructed(
832 origin,
833 RelBlockPos::from_origin(origin, executing_path.last_reached_node),
834 &executing_path.path,
835 successors,
836 ) else {
837 continue;
838 };
839
840 drop(custom_state_ref);
841
842 warn!(
843 "path obstructed at index {obstructed_index} (starting at {:?})",
844 executing_path.last_reached_node,
845 );
846 debug!("obstructed path: {:?}", executing_path.path);
847 if obstructed_index + 5 > executing_path.path.len() {
850 debug!(
851 "obstruction is near the end of the path, truncating and marking path as partial"
852 );
853 executing_path.path.truncate(obstructed_index);
854 executing_path.is_path_partial = true;
855 continue;
856 }
857
858 let Some(successors_fn) = pathfinder.successors_fn else {
859 error!("got PatchExecutingPathEvent but the bot has no successors_fn");
860 continue;
861 };
862
863 let world_lock = instance_container
864 .get(instance_name)
865 .expect("Entity tried to pathfind but the entity isn't in a valid world");
866
867 let patch_end_index = cmp::min(obstructed_index + 20, executing_path.path.len() - 1);
869
870 patch_path(
871 obstructed_index..=patch_end_index,
872 &mut executing_path,
873 &mut pathfinder,
874 inventory,
875 entity,
876 successors_fn,
877 world_lock,
878 custom_state.clone(),
879 );
880 }
881}
882
883#[allow(clippy::too_many_arguments)]
889fn patch_path(
890 patch_nodes: RangeInclusive<usize>,
891 executing_path: &mut ExecutingPath,
892 pathfinder: &mut Pathfinder,
893 inventory: &Inventory,
894 entity: Entity,
895 successors_fn: SuccessorsFn,
896 world_lock: Arc<RwLock<azalea_world::Instance>>,
897 custom_state: CustomPathfinderState,
898) {
899 let patch_start = if *patch_nodes.start() == 0 {
900 executing_path.last_reached_node
901 } else {
902 executing_path.path[*patch_nodes.start() - 1]
903 .movement
904 .target
905 };
906
907 let patch_end = executing_path.path[*patch_nodes.end()].movement.target;
908
909 let goal = Arc::new(BlockPosGoal(patch_end));
912
913 let goto_id_atomic = pathfinder.goto_id.clone();
914 let allow_mining = pathfinder.allow_mining;
915 let retry_on_no_path = pathfinder.retry_on_no_path;
916
917 let mining_cache = MiningCache::new(if allow_mining {
918 Some(inventory.inventory_menu.clone())
919 } else {
920 None
921 });
922
923 let path_found_event = calculate_path(CalculatePathOpts {
925 entity,
926 start: patch_start,
927 goal,
928 successors_fn,
929 world_lock,
930 goto_id_atomic,
931 allow_mining,
932 mining_cache,
933 retry_on_no_path,
934
935 custom_state,
936 min_timeout: PathfinderTimeout::Nodes(10_000),
937 max_timeout: PathfinderTimeout::Nodes(10_000),
938 });
939
940 pathfinder.is_calculating = false;
942
943 debug!("obstruction patch: {path_found_event:?}");
944
945 let mut new_path = VecDeque::new();
946 if *patch_nodes.start() > 0 {
947 new_path.extend(
948 executing_path
949 .path
950 .iter()
951 .take(*patch_nodes.start())
952 .cloned(),
953 );
954 }
955
956 let mut is_patch_complete = false;
957 if let Some(path_found_event) = path_found_event {
958 if let Some(found_path_patch) = path_found_event.path
959 && !found_path_patch.is_empty()
960 {
961 new_path.extend(found_path_patch);
962
963 if !path_found_event.is_partial {
964 new_path.extend(executing_path.path.iter().skip(*patch_nodes.end()).cloned());
965 is_patch_complete = true;
966 debug!("the patch is not partial :)");
967 } else {
968 debug!("the patch is partial, throwing away rest of path :(");
969 }
970 }
971 } else {
972 }
974
975 executing_path.path = new_path;
976 if !is_patch_complete {
977 executing_path.is_path_partial = true;
978 }
979}
980
981pub fn recalculate_near_end_of_path(
982 mut query: Query<(Entity, &mut Pathfinder, &mut ExecutingPath)>,
983 mut walk_events: EventWriter<StartWalkEvent>,
984 mut goto_events: EventWriter<GotoEvent>,
985 mut commands: Commands,
986) {
987 for (entity, mut pathfinder, mut executing_path) in &mut query {
988 let Some(successors_fn) = pathfinder.successors_fn else {
989 continue;
990 };
991
992 if (executing_path.path.len() == 50 || executing_path.path.len() < 5)
994 && !pathfinder.is_calculating
995 && executing_path.is_path_partial
996 {
997 match pathfinder.goal.as_ref().cloned() {
998 Some(goal) => {
999 debug!("Recalculating path because it's empty or ends soon");
1000 debug!(
1001 "recalculate_near_end_of_path executing_path.is_path_partial: {}",
1002 executing_path.is_path_partial
1003 );
1004 goto_events.write(GotoEvent {
1005 entity,
1006 goal,
1007 successors_fn,
1008 allow_mining: pathfinder.allow_mining,
1009 retry_on_no_path: pathfinder.retry_on_no_path,
1010 min_timeout: if executing_path.path.len() == 50 {
1011 PathfinderTimeout::Time(Duration::from_secs(5))
1014 } else {
1015 PathfinderTimeout::Time(Duration::from_secs(1))
1016 },
1017 max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
1018 });
1019 pathfinder.is_calculating = true;
1020
1021 if executing_path.path.is_empty() {
1022 if let Some(new_path) = executing_path.queued_path.take() {
1023 executing_path.path = new_path;
1024 if executing_path.path.is_empty() {
1025 info!(
1026 "the path we just swapped to was empty, so reached end of path"
1027 );
1028 walk_events.write(StartWalkEvent {
1029 entity,
1030 direction: WalkDirection::None,
1031 });
1032 commands.entity(entity).remove::<ExecutingPath>();
1033 break;
1034 }
1035 } else {
1036 walk_events.write(StartWalkEvent {
1037 entity,
1038 direction: WalkDirection::None,
1039 });
1040 commands.entity(entity).remove::<ExecutingPath>();
1041 }
1042 }
1043 }
1044 _ => {
1045 if executing_path.path.is_empty() {
1046 walk_events.write(StartWalkEvent {
1048 entity,
1049 direction: WalkDirection::None,
1050 });
1051 }
1052 }
1053 }
1054 }
1055 }
1056}
1057
1058#[allow(clippy::type_complexity)]
1059pub fn tick_execute_path(
1060 mut query: Query<(
1061 Entity,
1062 &mut ExecutingPath,
1063 &Position,
1064 &Physics,
1065 Option<&Mining>,
1066 &InstanceHolder,
1067 &Inventory,
1068 )>,
1069 mut look_at_events: EventWriter<LookAtEvent>,
1070 mut sprint_events: EventWriter<StartSprintEvent>,
1071 mut walk_events: EventWriter<StartWalkEvent>,
1072 mut jump_events: EventWriter<JumpEvent>,
1073 mut start_mining_events: EventWriter<StartMiningBlockEvent>,
1074 mut set_selected_hotbar_slot_events: EventWriter<SetSelectedHotbarSlotEvent>,
1075) {
1076 for (entity, executing_path, position, physics, mining, instance_holder, inventory_component) in
1077 &mut query
1078 {
1079 if let Some(edge) = executing_path.path.front() {
1080 let ctx = ExecuteCtx {
1081 entity,
1082 target: edge.movement.target,
1083 position: **position,
1084 start: executing_path.last_reached_node,
1085 physics,
1086 is_currently_mining: mining.is_some(),
1087 instance: instance_holder.instance.clone(),
1088 menu: inventory_component.inventory_menu.clone(),
1089
1090 look_at_events: &mut look_at_events,
1091 sprint_events: &mut sprint_events,
1092 walk_events: &mut walk_events,
1093 jump_events: &mut jump_events,
1094 start_mining_events: &mut start_mining_events,
1095 set_selected_hotbar_slot_events: &mut set_selected_hotbar_slot_events,
1096 };
1097 trace!(
1098 "executing move, position: {}, last_reached_node: {}",
1099 **position, executing_path.last_reached_node
1100 );
1101 (edge.movement.data.execute)(ctx);
1102 }
1103 }
1104}
1105
1106pub fn recalculate_if_has_goal_but_no_path(
1107 mut query: Query<(Entity, &mut Pathfinder), Without<ExecutingPath>>,
1108 mut goto_events: EventWriter<GotoEvent>,
1109) {
1110 for (entity, mut pathfinder) in &mut query {
1111 if pathfinder.goal.is_some()
1112 && !pathfinder.is_calculating
1113 && let Some(goal) = pathfinder.goal.as_ref().cloned()
1114 {
1115 debug!("Recalculating path because it has a goal but no ExecutingPath");
1116 goto_events.write(GotoEvent {
1117 entity,
1118 goal,
1119 successors_fn: pathfinder.successors_fn.unwrap(),
1120 allow_mining: pathfinder.allow_mining,
1121 retry_on_no_path: pathfinder.retry_on_no_path,
1122 min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"),
1123 max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"),
1124 });
1125 pathfinder.is_calculating = true;
1126 }
1127 }
1128}
1129
1130#[derive(Event)]
1131pub struct StopPathfindingEvent {
1132 pub entity: Entity,
1133 pub force: bool,
1137}
1138
1139pub fn handle_stop_pathfinding_event(
1140 mut events: EventReader<StopPathfindingEvent>,
1141 mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
1142 mut walk_events: EventWriter<StartWalkEvent>,
1143 mut commands: Commands,
1144) {
1145 for event in events.read() {
1146 commands.entity(event.entity).remove::<ComputePath>();
1148
1149 let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
1150 continue;
1151 };
1152 pathfinder.goal = None;
1153 if event.force {
1154 executing_path.path.clear();
1155 executing_path.queued_path = None;
1156 } else {
1157 executing_path.queued_path = Some(VecDeque::new());
1159 executing_path.is_path_partial = false;
1161 }
1162
1163 if executing_path.path.is_empty() {
1164 walk_events.write(StartWalkEvent {
1165 entity: event.entity,
1166 direction: WalkDirection::None,
1167 });
1168 commands.entity(event.entity).remove::<ExecutingPath>();
1169 }
1170 }
1171}
1172
1173pub fn stop_pathfinding_on_instance_change(
1174 mut query: Query<(Entity, &mut ExecutingPath), Changed<InstanceName>>,
1175 mut stop_pathfinding_events: EventWriter<StopPathfindingEvent>,
1176) {
1177 for (entity, mut executing_path) in &mut query {
1178 if !executing_path.path.is_empty() {
1179 debug!("instance changed, clearing path");
1180 executing_path.path.clear();
1181 stop_pathfinding_events.write(StopPathfindingEvent {
1182 entity,
1183 force: true,
1184 });
1185 }
1186 }
1187}
1188
1189pub fn check_path_obstructed<SuccessorsFn>(
1192 origin: BlockPos,
1193 mut current_position: RelBlockPos,
1194 path: &VecDeque<astar::Edge<BlockPos, moves::MoveData>>,
1195 successors_fn: SuccessorsFn,
1196) -> Option<usize>
1197where
1198 SuccessorsFn: Fn(RelBlockPos) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>>,
1199{
1200 for (i, edge) in path.iter().enumerate() {
1201 let movement_target = RelBlockPos::from_origin(origin, edge.movement.target);
1202
1203 let mut found_edge = None;
1204 for candidate_edge in successors_fn(current_position) {
1205 if candidate_edge.movement.target == movement_target {
1206 found_edge = Some(candidate_edge);
1207 break;
1208 }
1209 }
1210
1211 current_position = movement_target;
1212 if found_edge
1214 .map(|found_edge| found_edge.cost > edge.cost)
1215 .unwrap_or(true)
1216 {
1217 if i == 0 {
1220 warn!("path obstructed at index 0, ignoring");
1221 continue;
1222 }
1223
1224 return Some(i);
1225 }
1226 }
1227
1228 None
1229}
1230
1231pub fn call_successors_fn(
1232 cached_world: &CachedWorld,
1233 mining_cache: &MiningCache,
1234 custom_state: &CustomPathfinderStateRef,
1235 successors_fn: SuccessorsFn,
1236 pos: RelBlockPos,
1237) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>> {
1238 let mut edges = Vec::with_capacity(16);
1239 let mut ctx = PathfinderCtx {
1240 edges: &mut edges,
1241 world: cached_world,
1242 mining_cache,
1243 custom_state,
1244 };
1245 successors_fn(&mut ctx, pos);
1246 edges
1247}