1pub mod patching;
2pub mod simulation;
3
4use std::{cmp, time::Duration};
5
6use azalea_block::{BlockState, BlockTrait};
7use azalea_client::{
8 StartSprintEvent, StartWalkEvent,
9 local_player::WorldHolder,
10 mining::{Mining, MiningSystems, StartMiningBlockEvent},
11};
12use azalea_core::{position::Vec3, tick::GameTick};
13use azalea_entity::{Physics, Position, inventory::Inventory};
14use azalea_physics::{PhysicsSystems, get_block_pos_below_that_affects_movement};
15use azalea_world::{WorldName, Worlds};
16use bevy_app::{App, Plugin};
17use bevy_ecs::prelude::*;
18use tracing::{debug, info, trace, warn};
19
20use crate::{
21 WalkDirection,
22 bot::{JumpEvent, LookAtEvent},
23 ecs::{
24 entity::Entity,
25 query::Without,
26 system::{Commands, Query, Res},
27 },
28 pathfinder::{
29 ExecutingPath, GotoEvent, Pathfinder, PathfinderSystems,
30 astar::PathfinderTimeout,
31 custom_state::CustomPathfinderState,
32 debug::debug_render_path_with_particles,
33 execute::simulation::SimulatingPathState,
34 moves::{ExecuteCtx, IsReachedCtx},
35 player_pos_to_block_pos,
36 },
37};
38
39pub struct DefaultPathfinderExecutionPlugin;
40impl Plugin for DefaultPathfinderExecutionPlugin {
41 fn build(&self, _app: &mut App) {}
42
43 fn finish(&self, app: &mut App) {
44 if app.is_plugin_added::<simulation::SimulationPathfinderExecutionPlugin>() {
45 info!("pathfinder simulation executor plugin is enabled, disabling default executor.");
46 return;
47 }
48
49 app.add_systems(
50 GameTick,
51 (
52 timeout_movement,
53 patching::check_for_path_obstruction,
54 check_node_reached,
55 tick_execute_path,
56 recalculate_near_end_of_path,
57 recalculate_if_has_goal_but_no_path,
58 )
59 .chain()
60 .after(PhysicsSystems)
61 .after(azalea_client::movement::send_position)
62 .after(MiningSystems)
63 .after(debug_render_path_with_particles)
64 .in_set(PathfinderSystems),
65 );
66 }
67}
68
69#[allow(clippy::type_complexity)]
70pub fn tick_execute_path(
71 mut commands: Commands,
72 mut query: Query<(
73 Entity,
74 &mut ExecutingPath,
75 &Position,
76 &Physics,
77 Option<&Mining>,
78 &WorldHolder,
79 &Inventory,
80 )>,
81 mut look_at_events: MessageWriter<LookAtEvent>,
82 mut sprint_events: MessageWriter<StartSprintEvent>,
83 mut walk_events: MessageWriter<StartWalkEvent>,
84 mut jump_events: MessageWriter<JumpEvent>,
85 mut start_mining_events: MessageWriter<StartMiningBlockEvent>,
86) {
87 for (entity, mut executing_path, position, physics, mining, world_holder, inventory) in
88 &mut query
89 {
90 executing_path.ticks_since_last_node_reached += 1;
91
92 if let Some(edge) = executing_path.path.front() {
93 let mut ctx = ExecuteCtx {
94 entity,
95 target: edge.movement.target,
96 position: **position,
97 start: executing_path.last_reached_node,
98 physics,
99 is_currently_mining: mining.is_some(),
100 can_mine: true,
101 world: world_holder.shared.clone(),
102 menu: inventory.inventory_menu.clone(),
103
104 commands: &mut commands,
105 look_at_events: &mut look_at_events,
106 sprint_events: &mut sprint_events,
107 walk_events: &mut walk_events,
108 jump_events: &mut jump_events,
109 start_mining_events: &mut start_mining_events,
110 };
111 ctx.on_tick_start();
112 trace!(
113 "executing move, position: {}, last_reached_node: {}",
114 **position, executing_path.last_reached_node
115 );
116 (edge.movement.data.execute)(ctx);
117 }
118 }
119}
120
121pub fn check_node_reached(
122 mut query: Query<(
123 Entity,
124 &mut Pathfinder,
125 &mut ExecutingPath,
126 &Position,
127 &Physics,
128 &WorldName,
129 )>,
130 mut walk_events: MessageWriter<StartWalkEvent>,
131 mut commands: Commands,
132 worlds: Res<Worlds>,
133) {
134 for (entity, mut pathfinder, mut executing_path, position, physics, world_name) in &mut query {
135 let Some(world) = worlds.get(world_name) else {
136 warn!("entity is pathfinding but not in a valid world");
137 continue;
138 };
139
140 'skip: loop {
141 for (i, edge) in executing_path
146 .path
147 .clone()
148 .into_iter()
149 .enumerate()
150 .take(30)
151 .rev()
152 {
153 let movement = edge.movement;
154 let is_reached_ctx = IsReachedCtx {
155 target: movement.target,
156 start: executing_path.last_reached_node,
157 position: **position,
158 physics,
159 };
160 let extra_check = if i == executing_path.path.len() - 1
161 && executing_path.is_empty_queued_path()
163 {
164 let x_difference_from_center = position.x - (movement.target.x as f64 + 0.5);
168 let z_difference_from_center = position.z - (movement.target.z as f64 + 0.5);
169
170 let block_pos_below = get_block_pos_below_that_affects_movement(*position);
171
172 let block_state_below = {
173 let world = world.read();
174 world
175 .chunks
176 .get_block_state(block_pos_below)
177 .unwrap_or(BlockState::AIR)
178 };
179 let block_below: Box<dyn BlockTrait> = block_state_below.into();
180 let block_friction = block_below.behavior().friction as f64;
182
183 let scaled_velocity = physics.velocity * (0.4 / (1. - block_friction));
186
187 let x_predicted_offset = (x_difference_from_center + scaled_velocity.x).abs();
188 let z_predicted_offset = (z_difference_from_center + scaled_velocity.z).abs();
189
190 (physics.on_ground() || physics.is_in_water())
192 && player_pos_to_block_pos(**position) == movement.target
193 && x_predicted_offset < 0.2
196 && z_predicted_offset < 0.2
197 } else {
198 true
199 };
200
201 if (movement.data.is_reached)(is_reached_ctx) && extra_check {
202 executing_path.path = executing_path.path.split_off(i + 1);
203 executing_path.last_reached_node = movement.target;
204 executing_path.ticks_since_last_node_reached = 0;
205 trace!("reached node {}", movement.target);
206
207 if let Some(new_path) = executing_path.queued_path.take() {
208 debug!(
209 "swapped path to {:?}",
210 new_path.iter().take(10).collect::<Vec<_>>()
211 );
212 executing_path.path = new_path;
213
214 if executing_path.path.is_empty() {
215 info!("the path we just swapped to was empty, so reached end of path");
216 walk_events.write(StartWalkEvent {
217 entity,
218 direction: WalkDirection::None,
219 });
220 commands.entity(entity).remove::<ExecutingPath>();
221 break;
222 }
223
224 continue 'skip;
226 }
227
228 if executing_path.path.is_empty() {
229 debug!("pathfinder path is now empty");
230 walk_events.write(StartWalkEvent {
231 entity,
232 direction: WalkDirection::None,
233 });
234 commands.entity(entity).remove::<ExecutingPath>();
235 if let Some(goal) = pathfinder.goal.clone()
236 && goal.success(movement.target)
237 {
238 info!("goal was reached!");
239 pathfinder.goal = None;
240 pathfinder.opts = None;
241 }
242 }
243
244 break;
245 }
246 }
247 break;
248 }
249 }
250}
251
252#[allow(clippy::type_complexity)]
253pub fn timeout_movement(
254 mut query: Query<(
255 Entity,
256 &mut Pathfinder,
257 &mut ExecutingPath,
258 &Position,
259 Option<&Mining>,
260 &WorldName,
261 &Inventory,
262 Option<&CustomPathfinderState>,
263 Option<&mut SimulatingPathState>,
264 )>,
265 worlds: Res<Worlds>,
266) {
267 for (
268 entity,
269 mut pathfinder,
270 mut executing_path,
271 position,
272 mining,
273 world_name,
274 inventory,
275 custom_state,
276 simulating_path_state,
277 ) in &mut query
278 {
279 if !executing_path.path.is_empty() {
280 let (start, end) = if let Some(s) = &simulating_path_state
281 && let SimulatingPathState::Simulated(simulating_path_state) = &**s
282 {
283 (simulating_path_state.start, simulating_path_state.target)
284 } else {
285 (
286 executing_path.last_reached_node,
287 executing_path.path[0].movement.target,
288 )
289 };
290
291 let (start, end) = (start.center_bottom(), end.center_bottom());
292 let xz_distance =
295 point_line_distance_3d(&position.with_y(0.), &(start.with_y(0.), end.with_y(0.)));
296 let y_distance = point_line_distance_1d(position.y, (start.y, end.y));
297
298 let xz_tolerance = 3.;
299 let y_tolerance = start.horizontal_distance_to(end) / 2. + 1.5;
302
303 if xz_distance > xz_tolerance || y_distance > y_tolerance {
304 warn!(
305 "pathfinder went too far from path (xz_distance={xz_distance}/{xz_tolerance}, y_distance={y_distance}/{y_tolerance}, line is {start} to {end}, point at {}), trying to patch!",
306 **position
307 );
308
309 if let Some(mut simulating_path_state) = simulating_path_state {
310 *simulating_path_state = SimulatingPathState::Fail;
312 }
313
314 patch_path_from_timeout(
315 entity,
316 &mut executing_path,
317 &mut pathfinder,
318 &worlds,
319 position,
320 world_name,
321 custom_state,
322 inventory,
323 );
324 continue;
325 }
326 }
327
328 if let Some(mining) = mining {
330 if mining.pos.distance_squared_to(position.into()) < 6_i32.pow(2) {
332 executing_path.ticks_since_last_node_reached = 0;
335 continue;
336 }
337 }
338
339 let mut timeout = 2 * 20;
340
341 if simulating_path_state.is_some() {
342 timeout = 5 * 20;
345 }
346
347 if executing_path.ticks_since_last_node_reached > timeout
348 && !pathfinder.is_calculating
349 && !executing_path.path.is_empty()
350 {
351 warn!("pathfinder timeout, trying to patch path");
352
353 patch_path_from_timeout(
354 entity,
355 &mut executing_path,
356 &mut pathfinder,
357 &worlds,
358 position,
359 world_name,
360 custom_state,
361 inventory,
362 );
363 }
364 }
365}
366
367#[allow(clippy::too_many_arguments)]
368fn patch_path_from_timeout(
369 entity: Entity,
370 executing_path: &mut ExecutingPath,
371 pathfinder: &mut Pathfinder,
372 worlds: &Worlds,
373 position: &Position,
374 world_name: &WorldName,
375 custom_state: Option<&CustomPathfinderState>,
376 inventory: &Inventory,
377) {
378 executing_path.queued_path = None;
379 let cur_pos = player_pos_to_block_pos(**position);
380 executing_path.last_reached_node = cur_pos;
381
382 let world_lock = worlds
383 .get(world_name)
384 .expect("Entity tried to pathfind but the entity isn't in a valid world");
385 let Some(opts) = pathfinder.opts.clone() else {
386 warn!(
387 "pathfinder was going to patch path because of timeout, but pathfinder.opts was None"
388 );
389 return;
390 };
391
392 let custom_state = custom_state.cloned().unwrap_or_default();
393
394 patching::patch_path(
398 0..=cmp::min(20, executing_path.path.len() - 1),
399 executing_path,
400 pathfinder,
401 inventory,
402 entity,
403 world_lock,
404 custom_state,
405 opts,
406 );
407 executing_path.ticks_since_last_node_reached = 0
409}
410
411pub fn recalculate_near_end_of_path(
412 mut query: Query<(Entity, &mut Pathfinder, &mut ExecutingPath)>,
413 mut walk_events: MessageWriter<StartWalkEvent>,
414 mut goto_events: MessageWriter<GotoEvent>,
415 mut commands: Commands,
416) {
417 for (entity, mut pathfinder, mut executing_path) in &mut query {
418 let Some(mut opts) = pathfinder.opts.clone() else {
419 continue;
420 };
421
422 if (executing_path.path.len() == 50 || executing_path.path.len() < 5)
428 && !pathfinder.is_calculating
429 && executing_path.is_path_partial
430 {
431 match pathfinder.goal.as_ref().cloned() {
432 Some(goal) => {
433 debug!("Recalculating path because it's empty or ends soon");
434 debug!(
435 "recalculate_near_end_of_path executing_path.is_path_partial: {}",
436 executing_path.is_path_partial
437 );
438
439 opts.min_timeout = if executing_path.path.len() == 50 {
440 PathfinderTimeout::Time(Duration::from_secs(5))
443 } else {
444 PathfinderTimeout::Time(Duration::from_secs(1))
445 };
446
447 goto_events.write(GotoEvent { entity, goal, opts });
448 pathfinder.is_calculating = true;
449
450 if executing_path.path.is_empty() {
451 if let Some(new_path) = executing_path.queued_path.take() {
452 executing_path.path = new_path;
453 if executing_path.path.is_empty() {
454 info!(
455 "the path we just swapped to was empty, so reached end of path"
456 );
457 walk_events.write(StartWalkEvent {
458 entity,
459 direction: WalkDirection::None,
460 });
461 commands.entity(entity).remove::<ExecutingPath>();
462 break;
463 }
464 } else {
465 walk_events.write(StartWalkEvent {
466 entity,
467 direction: WalkDirection::None,
468 });
469 commands.entity(entity).remove::<ExecutingPath>();
470 }
471 }
472 }
473 _ => {
474 if executing_path.path.is_empty() {
475 walk_events.write(StartWalkEvent {
477 entity,
478 direction: WalkDirection::None,
479 });
480 }
481 }
482 }
483 }
484 }
485}
486
487pub fn recalculate_if_has_goal_but_no_path(
488 mut query: Query<(Entity, &mut Pathfinder), Without<ExecutingPath>>,
489 mut goto_events: MessageWriter<GotoEvent>,
490) {
491 for (entity, mut pathfinder) in &mut query {
492 if pathfinder.goal.is_some()
493 && !pathfinder.is_calculating
494 && let Some(goal) = pathfinder.goal.as_ref().cloned()
495 && let Some(opts) = pathfinder.opts.clone()
496 {
497 debug!("Recalculating path because it has a goal but no ExecutingPath");
498 goto_events.write(GotoEvent { entity, goal, opts });
499 pathfinder.is_calculating = true;
500 }
501 }
502}
503
504pub fn point_line_distance_3d(point: &Vec3, (start, end): &(Vec3, Vec3)) -> f64 {
510 let start_to_end = end - start;
511 let start_to_point = point - start;
512
513 if start_to_point.dot(start_to_end) <= 0. {
514 return start_to_point.length();
515 }
516
517 let end_to_point = point - end;
518 if end_to_point.dot(start_to_end) >= 0. {
519 return end_to_point.length();
520 }
521
522 start_to_end.cross(start_to_point).length() / start_to_end.length()
523}
524pub fn point_line_distance_1d(point: f64, (start, end): (f64, f64)) -> f64 {
525 let min = start.min(end);
526 let max = start.max(end);
527 if point < min {
528 min - point
529 } else if point > max {
530 point - max
531 } else {
532 0.
533 }
534}