Skip to content

Commit 87bfc64

Browse files
committed
improve pathfinder path execution
1 parent 94ef48d commit 87bfc64

File tree

6 files changed

+254
-129
lines changed

6 files changed

+254
-129
lines changed

azalea/src/pathfinder/costs.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,41 @@ use num_traits::Float;
55
// based on https://github.com/cabaletta/baritone/blob/1.20.1/src/api/java/baritone/api/pathing/movement/ActionCosts.java
66
pub const WALK_ONE_BLOCK_COST: f32 = 20. / 4.317; // 4.633
77
pub const SPRINT_ONE_BLOCK_COST: f32 = 20. / 5.612; // 3.564
8-
pub const FALL_ONE_BLOCK_COST: f32 = 1.;
98
pub const WALK_OFF_BLOCK_COST: f32 = WALK_ONE_BLOCK_COST * 0.8;
109
pub const SPRINT_MULTIPLIER: f32 = SPRINT_ONE_BLOCK_COST / WALK_ONE_BLOCK_COST;
1110
pub const JUMP_PENALTY: f32 = 2.;
11+
pub const CENTER_AFTER_FALL_COST: f32 = WALK_ONE_BLOCK_COST - WALK_OFF_BLOCK_COST; // 0.927
1212

1313
pub static FALL_1_25_BLOCKS_COST: LazyLock<f32> = LazyLock::new(|| distance_to_ticks(1.25));
1414
pub static FALL_0_25_BLOCKS_COST: LazyLock<f32> = LazyLock::new(|| distance_to_ticks(0.25));
1515
pub static JUMP_ONE_BLOCK_COST: LazyLock<f32> =
1616
LazyLock::new(|| *FALL_1_25_BLOCKS_COST - *FALL_0_25_BLOCKS_COST); // 3.163
1717

18+
pub static FALL_N_BLOCKS_COST: LazyLock<[f32; 4097]> = LazyLock::new(|| {
19+
let mut fall_n_blocks_cost = [0.; 4097];
20+
21+
let mut distance = 0;
22+
23+
// this is the same as calling distance_to_ticks a bunch of times but more
24+
// efficient
25+
let mut temp_distance = distance as f32;
26+
let mut tick_count = 0;
27+
loop {
28+
let fall_distance = velocity(tick_count);
29+
if temp_distance <= fall_distance {
30+
fall_n_blocks_cost[distance] = tick_count as f32 + temp_distance / fall_distance;
31+
distance += 1;
32+
if distance >= fall_n_blocks_cost.len() {
33+
break;
34+
}
35+
}
36+
temp_distance -= fall_distance;
37+
tick_count += 1;
38+
}
39+
40+
fall_n_blocks_cost
41+
});
42+
1843
fn velocity(ticks: usize) -> f32 {
1944
(0.98.powi(ticks.try_into().unwrap()) - 1.) * -3.92
2045
}

azalea/src/pathfinder/goals.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::f32::consts::SQRT_2;
33
use azalea_core::position::{BlockPos, Vec3};
44

55
use super::{
6-
costs::{FALL_ONE_BLOCK_COST, JUMP_ONE_BLOCK_COST},
6+
costs::{FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST},
77
Goal,
88
};
99

@@ -55,10 +55,10 @@ impl Goal for XZGoal {
5555
}
5656

5757
fn y_heuristic(dy: f32) -> f32 {
58-
if dy < 0.0 {
59-
FALL_ONE_BLOCK_COST * -dy
60-
} else {
58+
if dy > 0.0 {
6159
*JUMP_ONE_BLOCK_COST * dy
60+
} else {
61+
FALL_N_BLOCKS_COST[2] / 2. * -dy
6262
}
6363
}
6464

azalea/src/pathfinder/mod.rs

Lines changed: 141 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,26 @@ use crate::ecs::{
2020
system::{Commands, Query, Res},
2121
};
2222
use crate::pathfinder::moves::PathfinderCtx;
23+
use azalea_client::chat::SendChatEvent;
2324
use azalea_client::movement::walk_listener;
2425
use azalea_client::{StartSprintEvent, StartWalkEvent};
25-
use azalea_core::position::BlockPos;
26+
use azalea_core::position::{BlockPos, Vec3};
2627
use azalea_entity::metadata::Player;
2728
use azalea_entity::LocalEntity;
2829
use azalea_entity::{Physics, Position};
2930
use azalea_physics::PhysicsSet;
3031
use azalea_world::{InstanceContainer, InstanceName};
3132
use bevy_app::{FixedUpdate, PreUpdate, Update};
33+
use bevy_ecs::event::Events;
3234
use bevy_ecs::prelude::Event;
3335
use bevy_ecs::query::Changed;
3436
use bevy_ecs::schedule::IntoSystemConfigs;
37+
use bevy_ecs::system::{Local, ResMut};
3538
use bevy_tasks::{AsyncComputeTaskPool, Task};
3639
use futures_lite::future;
3740
use log::{debug, error, info, trace, warn};
3841
use std::collections::VecDeque;
42+
use std::sync::atomic::{self, AtomicUsize};
3943
use std::sync::Arc;
4044
use std::time::{Duration, Instant};
4145

@@ -51,9 +55,13 @@ impl Plugin for PathfinderPlugin {
5155
FixedUpdate,
5256
// putting systems in the FixedUpdate schedule makes them run every Minecraft tick
5357
// (every 50 milliseconds).
54-
tick_execute_path
55-
.after(PhysicsSet)
56-
.after(azalea_client::movement::send_position),
58+
(
59+
tick_execute_path
60+
.after(PhysicsSet)
61+
.after(azalea_client::movement::send_position),
62+
debug_render_path_with_particles,
63+
)
64+
.chain(),
5765
)
5866
.add_systems(PreUpdate, add_default_pathfinder)
5967
.add_systems(
@@ -81,6 +89,8 @@ pub struct Pathfinder {
8189
pub goal: Option<Arc<dyn Goal + Send + Sync>>,
8290
pub successors_fn: Option<SuccessorsFn>,
8391
pub is_calculating: bool,
92+
93+
pub goto_id: Arc<AtomicUsize>,
8494
}
8595
#[derive(Event)]
8696
pub struct GotoEvent {
@@ -157,7 +167,7 @@ fn goto_listener(
157167
// if we're currently pathfinding and got a goto event, start a little ahead
158168
pathfinder
159169
.path
160-
.get(5)
170+
.get(20)
161171
.unwrap_or_else(|| pathfinder.path.back().unwrap())
162172
.target
163173
};
@@ -175,6 +185,9 @@ fn goto_listener(
175185
let goal = event.goal.clone();
176186
let entity = event.entity;
177187

188+
let goto_id_atomic = pathfinder.goto_id.clone();
189+
let goto_id = goto_id_atomic.fetch_add(1, atomic::Ordering::Relaxed) + 1;
190+
178191
let task = thread_pool.spawn(async move {
179192
debug!("start: {start:?}");
180193

@@ -204,6 +217,8 @@ fn goto_listener(
204217
let duration = end_time - start_time;
205218
if partial {
206219
info!("Pathfinder took {duration:?} (timed out)");
220+
// wait a bit so it's not a busy loop
221+
std::thread::sleep(Duration::from_millis(100));
207222
} else {
208223
info!("Pathfinder took {duration:?}");
209224
}
@@ -216,6 +231,13 @@ fn goto_listener(
216231
path = movements.into_iter().collect::<VecDeque<_>>();
217232
is_partial = partial;
218233

234+
let goto_id_now = goto_id_atomic.load(atomic::Ordering::Relaxed);
235+
if goto_id != goto_id_now {
236+
// we must've done another goto while calculating this path, so throw it away
237+
warn!("finished calculating a path, but it's outdated");
238+
return None;
239+
}
240+
219241
if path.is_empty() && partial {
220242
if attempt_number == 0 {
221243
debug!("this path is empty, retrying with a higher timeout");
@@ -275,6 +297,7 @@ fn path_found_listener(
275297
pathfinder.path = path.to_owned();
276298
debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
277299
pathfinder.last_reached_node = Some(event.start);
300+
pathfinder.last_node_reached_at = Some(Instant::now());
278301
} else {
279302
let mut new_path = VecDeque::new();
280303

@@ -313,7 +336,6 @@ fn path_found_listener(
313336
);
314337
pathfinder.queued_path = Some(new_path);
315338
}
316-
pathfinder.last_node_reached_at = Some(Instant::now());
317339
} else {
318340
error!("No path found");
319341
pathfinder.path.clear();
@@ -353,6 +375,9 @@ fn tick_execute_path(
353375
if last_node_reached_at.elapsed() > Duration::from_secs(2) {
354376
warn!("pathfinder timeout");
355377
pathfinder.path.clear();
378+
pathfinder.queued_path = None;
379+
pathfinder.last_reached_node = None;
380+
pathfinder.goto_id.fetch_add(1, atomic::Ordering::Relaxed);
356381
// set partial to true to make sure that the recalculation happens
357382
pathfinder.is_path_partial = true;
358383
}
@@ -386,8 +411,10 @@ fn tick_execute_path(
386411
// this is to make sure we don't fall off immediately after finishing the path
387412
physics.on_ground
388413
&& BlockPos::from(position) == movement.target
389-
&& x_difference_from_center.abs() < 0.2
390-
&& z_difference_from_center.abs() < 0.2
414+
// adding the delta like this isn't a perfect solution but it helps to make
415+
// sure we don't keep going if our delta is high
416+
&& (x_difference_from_center + physics.delta.x).abs() < 0.2
417+
&& (z_difference_from_center + physics.delta.z).abs() < 0.2
391418
} else {
392419
true
393420
};
@@ -470,13 +497,16 @@ fn tick_execute_path(
470497
{
471498
warn!("path obstructed at index {obstructed_index} (starting at {last_reached_node:?}, path: {:?})", pathfinder.path);
472499
pathfinder.path.truncate(obstructed_index);
500+
pathfinder.is_path_partial = true;
473501
}
474502
}
475503
}
476504

477505
{
478506
// start recalculating if the path ends soon
479-
if pathfinder.path.len() < 5 && !pathfinder.is_calculating && pathfinder.is_path_partial
507+
if (pathfinder.path.len() == 20 || pathfinder.path.len() < 5)
508+
&& !pathfinder.is_calculating
509+
&& pathfinder.is_path_partial
480510
{
481511
if let Some(goal) = pathfinder.goal.as_ref().cloned() {
482512
debug!("Recalculating path because it ends soon");
@@ -507,6 +537,12 @@ fn tick_execute_path(
507537
});
508538
}
509539
}
540+
} else if pathfinder.path.is_empty() {
541+
// idk when this can happen but stop moving just in case
542+
walk_events.send(StartWalkEvent {
543+
entity,
544+
direction: WalkDirection::None,
545+
});
510546
}
511547
}
512548
}
@@ -529,6 +565,79 @@ fn stop_pathfinding_on_instance_change(
529565
}
530566
}
531567

568+
/// A component that makes bots run /particle commands while pathfinding to show
569+
/// where they're going. This requires the bots to have op permissions, and
570+
/// it'll make them spam *a lot* of commands.
571+
#[derive(Component)]
572+
pub struct PathfinderDebugParticles;
573+
574+
fn debug_render_path_with_particles(
575+
mut query: Query<(Entity, &Pathfinder), With<PathfinderDebugParticles>>,
576+
// chat_events is Option because the tests don't have SendChatEvent
577+
// and we have to use ResMut<Events> because bevy doesn't support Option<EventWriter>
578+
chat_events: Option<ResMut<Events<SendChatEvent>>>,
579+
mut tick_count: Local<usize>,
580+
) {
581+
let Some(mut chat_events) = chat_events else {
582+
return;
583+
};
584+
if *tick_count >= 2 {
585+
*tick_count = 0;
586+
} else {
587+
*tick_count += 1;
588+
return;
589+
}
590+
for (entity, pathfinder) in &mut query {
591+
if pathfinder.path.is_empty() {
592+
continue;
593+
}
594+
595+
let mut start = pathfinder
596+
.last_reached_node
597+
.unwrap_or_else(|| pathfinder.path.front().unwrap().target);
598+
for movement in &pathfinder.path {
599+
// /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100
600+
601+
let end = movement.target;
602+
603+
let start_vec3 = start.center();
604+
let end_vec3 = end.center();
605+
606+
let step_count = (start_vec3.distance_to_sqr(&end_vec3).sqrt() * 4.0) as usize;
607+
608+
// interpolate between the start and end positions
609+
for i in 0..step_count {
610+
let percent = i as f64 / step_count as f64;
611+
let pos = Vec3 {
612+
x: start_vec3.x + (end_vec3.x - start_vec3.x) * percent,
613+
y: start_vec3.y + (end_vec3.y - start_vec3.y) * percent,
614+
z: start_vec3.z + (end_vec3.z - start_vec3.z) * percent,
615+
};
616+
let particle_command = format!(
617+
"/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}",
618+
r = 0,
619+
g = 1,
620+
b = 1,
621+
size = 1,
622+
start_x = pos.x,
623+
start_y = pos.y,
624+
start_z = pos.z,
625+
delta_x = 0,
626+
delta_y = 0,
627+
delta_z = 0,
628+
count = 1
629+
);
630+
chat_events.send(SendChatEvent {
631+
entity,
632+
content: particle_command,
633+
});
634+
}
635+
636+
start = movement.target;
637+
}
638+
}
639+
}
640+
532641
pub trait Goal {
533642
fn heuristic(&self, n: BlockPos) -> f32;
534643
fn success(&self, n: BlockPos) -> bool;
@@ -729,6 +838,29 @@ mod tests {
729838
);
730839
}
731840

841+
#[test]
842+
fn test_small_descend_and_parkour_2_block_gap() {
843+
let mut partial_chunks = PartialChunkStorage::default();
844+
let mut simulation = setup_simulation(
845+
&mut partial_chunks,
846+
BlockPos::new(0, 71, 0),
847+
BlockPos::new(0, 70, 5),
848+
vec![
849+
BlockPos::new(0, 70, 0),
850+
BlockPos::new(0, 70, 1),
851+
BlockPos::new(0, 69, 2),
852+
BlockPos::new(0, 69, 5),
853+
],
854+
);
855+
for _ in 0..40 {
856+
simulation.tick();
857+
}
858+
assert_eq!(
859+
BlockPos::from(simulation.position()),
860+
BlockPos::new(0, 70, 5)
861+
);
862+
}
863+
732864
#[test]
733865
fn test_quickly_descend() {
734866
let mut partial_chunks = PartialChunkStorage::default();

0 commit comments

Comments
 (0)