split pathfinder execution into multiple systems (and fix some bugs)

This commit is contained in:
mat 2023-10-07 23:12:19 -05:00
parent 9281e4fdb9
commit 971f42e3db
5 changed files with 351 additions and 192 deletions

View file

@ -17,7 +17,7 @@ A collection of Rust crates for making Minecraft bots, clients, and tools.
## Features ## Features
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like knockback and water physics aren't yet implemented) - [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like knockback and water physics aren't yet implemented)
- [Pathfinder](https://azalea.matdoes.dev/azalea/pathfinder/index.html) (parkour isn't perfect yet) - [Pathfinder](https://azalea.matdoes.dev/azalea/pathfinder/index.html)
- [Swarms](https://azalea.matdoes.dev/azalea/swarm/index.html) - [Swarms](https://azalea.matdoes.dev/azalea/swarm/index.html)
- [Breaking blocks](https://azalea.matdoes.dev/azalea/struct.Client.html#method.mine) - [Breaking blocks](https://azalea.matdoes.dev/azalea/struct.Client.html#method.mine)
- [Block interactions & building](https://azalea.matdoes.dev/azalea/struct.Client.html#method.block_interact) (this doesn't predict the block interactions/placement on the client yet but it's usually fine) - [Block interactions & building](https://azalea.matdoes.dev/azalea/struct.Client.html#method.block_interact) (this doesn't predict the block interactions/placement on the client yet but it's usually fine)

View file

@ -4,6 +4,7 @@
#![feature(async_fn_in_trait)] #![feature(async_fn_in_trait)]
#![feature(type_changing_struct_update)] #![feature(type_changing_struct_update)]
#![feature(lazy_cell)] #![feature(lazy_cell)]
#![feature(let_chains)]
pub mod accept_resource_packs; pub mod accept_resource_packs;
mod auto_respawn; mod auto_respawn;

View file

@ -51,17 +51,23 @@ impl Plugin for PathfinderPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<GotoEvent>() app.add_event::<GotoEvent>()
.add_event::<PathFoundEvent>() .add_event::<PathFoundEvent>()
.add_event::<StopPathfindingEvent>()
.add_systems( .add_systems(
FixedUpdate, FixedUpdate,
// putting systems in the FixedUpdate schedule makes them run every Minecraft tick // putting systems in the FixedUpdate schedule makes them run every Minecraft tick
// (every 50 milliseconds). // (every 50 milliseconds).
( (
tick_execute_path timeout_movement,
.after(PhysicsSet) check_node_reached,
.after(azalea_client::movement::send_position), tick_execute_path,
check_for_path_obstruction,
debug_render_path_with_particles, debug_render_path_with_particles,
recalculate_near_end_of_path,
recalculate_if_has_goal_but_no_path,
) )
.chain(), .chain()
.after(PhysicsSet)
.after(azalea_client::movement::send_position),
) )
.add_systems(PreUpdate, add_default_pathfinder) .add_systems(PreUpdate, add_default_pathfinder)
.add_systems( .add_systems(
@ -70,28 +76,36 @@ impl Plugin for PathfinderPlugin {
goto_listener, goto_listener,
handle_tasks, handle_tasks,
path_found_listener, path_found_listener,
stop_pathfinding_on_instance_change.before(walk_listener), stop_pathfinding_on_instance_change,
handle_stop_pathfinding_event,
) )
.chain(), .chain()
.before(walk_listener),
); );
} }
} }
/// A component that makes this entity able to pathfind. /// A component that makes this client able to pathfind.
#[derive(Component, Default)] #[derive(Component, Default)]
pub struct Pathfinder { pub struct Pathfinder {
pub path: VecDeque<astar::Movement<BlockPos, moves::MoveData>>,
pub queued_path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
pub is_path_partial: bool,
pub last_reached_node: Option<BlockPos>,
pub last_node_reached_at: Option<Instant>,
pub goal: Option<Arc<dyn Goal + Send + Sync>>, pub goal: Option<Arc<dyn Goal + Send + Sync>>,
pub successors_fn: Option<SuccessorsFn>, pub successors_fn: Option<SuccessorsFn>,
pub is_calculating: bool, pub is_calculating: bool,
pub goto_id: Arc<AtomicUsize>, pub goto_id: Arc<AtomicUsize>,
} }
/// A component that's present on clients that are actively following a
/// pathfinder path.
#[derive(Component)]
pub struct ExecutingPath {
pub path: VecDeque<astar::Movement<BlockPos, moves::MoveData>>,
pub queued_path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
pub last_reached_node: BlockPos,
pub last_node_reached_at: Instant,
pub is_path_partial: bool,
}
#[derive(Event)] #[derive(Event)]
pub struct GotoEvent { pub struct GotoEvent {
pub entity: Entity, pub entity: Entity,
@ -121,6 +135,7 @@ fn add_default_pathfinder(
pub trait PathfinderClientExt { pub trait PathfinderClientExt {
fn goto(&self, goal: impl Goal + Send + Sync + 'static); fn goto(&self, goal: impl Goal + Send + Sync + 'static);
fn stop_pathfinding(&self);
} }
impl PathfinderClientExt for azalea_client::Client { impl PathfinderClientExt for azalea_client::Client {
@ -138,6 +153,13 @@ impl PathfinderClientExt for azalea_client::Client {
successors_fn: moves::default_move, successors_fn: moves::default_move,
}); });
} }
fn stop_pathfinding(&self) {
self.ecs.lock().send_event(StopPathfindingEvent {
entity: self.entity,
force: false,
});
}
} }
#[derive(Component)] #[derive(Component)]
@ -146,13 +168,18 @@ pub struct ComputePath(Task<Option<PathFoundEvent>>);
fn goto_listener( fn goto_listener(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<GotoEvent>, mut events: EventReader<GotoEvent>,
mut query: Query<(&mut Pathfinder, &Position, &InstanceName)>, mut query: Query<(
&mut Pathfinder,
Option<&ExecutingPath>,
&Position,
&InstanceName,
)>,
instance_container: Res<InstanceContainer>, instance_container: Res<InstanceContainer>,
) { ) {
let thread_pool = AsyncComputeTaskPool::get(); let thread_pool = AsyncComputeTaskPool::get();
for event in events.iter() { for event in events.iter() {
let (mut pathfinder, position, instance_name) = query let (mut pathfinder, executing_path, position, instance_name) = query
.get_mut(event.entity) .get_mut(event.entity)
.expect("Called goto on an entity that's not in the world"); .expect("Called goto on an entity that's not in the world");
@ -161,15 +188,16 @@ fn goto_listener(
pathfinder.successors_fn = Some(event.successors_fn); pathfinder.successors_fn = Some(event.successors_fn);
pathfinder.is_calculating = true; pathfinder.is_calculating = true;
let start = if pathfinder.path.is_empty() { let start = if let Some(executing_path) = executing_path
BlockPos::from(position) && let Some(final_node) = executing_path.path.back() {
} else {
// if we're currently pathfinding and got a goto event, start a little ahead // if we're currently pathfinding and got a goto event, start a little ahead
pathfinder executing_path
.path .path
.get(20) .get(20)
.unwrap_or_else(|| pathfinder.path.back().unwrap()) .unwrap_or(final_node)
.target .target
} else {
BlockPos::from(position)
}; };
info!( info!(
"got goto, starting from {start:?} (currently at {:?})", "got goto, starting from {start:?} (currently at {:?})",
@ -216,7 +244,7 @@ fn goto_listener(
debug!("partial: {partial:?}"); debug!("partial: {partial:?}");
let duration = end_time - start_time; let duration = end_time - start_time;
if partial { if partial {
info!("Pathfinder took {duration:?} (timed out)"); info!("Pathfinder took {duration:?} (incomplete path)");
// wait a bit so it's not a busy loop // wait a bit so it's not a busy loop
std::thread::sleep(Duration::from_millis(100)); std::thread::sleep(Duration::from_millis(100));
} else { } else {
@ -285,46 +313,47 @@ fn handle_tasks(
// set the path for the target entity when we get the PathFoundEvent // set the path for the target entity when we get the PathFoundEvent
fn path_found_listener( fn path_found_listener(
mut events: EventReader<PathFoundEvent>, mut events: EventReader<PathFoundEvent>,
mut query: Query<(&mut Pathfinder, &InstanceName)>, mut query: Query<(&mut Pathfinder, Option<&mut ExecutingPath>, &InstanceName)>,
instance_container: Res<InstanceContainer>, instance_container: Res<InstanceContainer>,
mut commands: Commands,
) { ) {
for event in events.iter() { for event in events.iter() {
let (mut pathfinder, instance_name) = query let (mut pathfinder, executing_path, instance_name) = query
.get_mut(event.entity) .get_mut(event.entity)
.expect("Path found for an entity that doesn't have a pathfinder"); .expect("Path found for an entity that doesn't have a pathfinder");
if let Some(path) = &event.path { if let Some(path) = &event.path {
if pathfinder.path.is_empty() { if let Some(mut executing_path) = executing_path {
pathfinder.path = path.to_owned();
debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
pathfinder.last_reached_node = Some(event.start);
pathfinder.last_node_reached_at = Some(Instant::now());
} else {
let mut new_path = VecDeque::new(); let mut new_path = VecDeque::new();
// combine the old and new paths if the first node of the new path is a // combine the old and new paths if the first node of the new path is a
// successor of the last node of the old path // successor of the last node of the old path
if let Some(first_node) = path.front() { if let Some(last_node_of_current_path) = executing_path.path.back() {
if let Some(last_node) = pathfinder.path.back() { let world_lock = instance_container
let world_lock = instance_container.get(instance_name).expect( .get(instance_name)
"Entity tried to pathfind but the entity isn't in a valid world", .expect("Entity tried to pathfind but the entity isn't in a valid world");
); let successors_fn: moves::SuccessorsFn = event.successors_fn;
let successors_fn: moves::SuccessorsFn = event.successors_fn; let ctx = PathfinderCtx::new(world_lock);
let ctx = PathfinderCtx::new(world_lock); let successors = |pos: BlockPos| {
let successors = |pos: BlockPos| { let mut edges = Vec::with_capacity(16);
let mut edges = Vec::with_capacity(16); successors_fn(&mut edges, &ctx, pos);
successors_fn(&mut edges, &ctx, pos); edges
edges };
};
if successors(last_node.target) if let Some(first_node_of_new_path) = path.front() {
if successors(last_node_of_current_path.target)
.iter() .iter()
.any(|edge| edge.movement.target == first_node.target) .any(|edge| edge.movement.target == first_node_of_new_path.target)
{ {
debug!("combining old and new paths"); debug!("combining old and new paths");
debug!("old path: {:?}", pathfinder.path.iter().collect::<Vec<_>>()); debug!(
"old path: {:?}",
executing_path.path.iter().collect::<Vec<_>>()
);
debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>()); debug!("new path: {:?}", path.iter().take(10).collect::<Vec<_>>());
new_path.extend(pathfinder.path.iter().cloned()); new_path.extend(executing_path.path.iter().cloned());
} }
} else {
new_path.extend(executing_path.path.iter().cloned());
} }
} }
@ -334,62 +363,74 @@ fn path_found_listener(
"set queued path to {:?}", "set queued path to {:?}",
new_path.iter().take(10).collect::<Vec<_>>() new_path.iter().take(10).collect::<Vec<_>>()
); );
pathfinder.queued_path = Some(new_path); executing_path.queued_path = Some(new_path);
executing_path.is_path_partial = event.is_partial;
} else if path.is_empty() {
debug!("calculated path is empty, so didn't add ExecutingPath");
} else {
commands.entity(event.entity).insert(ExecutingPath {
path: path.to_owned(),
queued_path: None,
last_reached_node: event.start,
last_node_reached_at: Instant::now(),
is_path_partial: event.is_partial,
});
debug!("set path to {:?}", path.iter().take(10).collect::<Vec<_>>());
debug!("partial: {}", event.is_partial);
} }
} else { } else {
error!("No path found"); error!("No path found");
pathfinder.path.clear(); if let Some(mut executing_path) = executing_path {
pathfinder.queued_path = None; // set the queued path so we don't stop in the middle of a move
executing_path.queued_path = Some(VecDeque::new());
} else {
// wasn't executing a path, don't need to do anything
}
} }
pathfinder.is_calculating = false; pathfinder.is_calculating = false;
pathfinder.is_path_partial = event.is_partial;
} }
} }
fn tick_execute_path( fn timeout_movement(mut query: Query<(&Pathfinder, &mut ExecutingPath, &Position)>) {
mut query: Query<(Entity, &mut Pathfinder, &Position, &Physics, &InstanceName)>, for (pathfinder, mut executing_path, position) in &mut query {
mut look_at_events: EventWriter<LookAtEvent>, if executing_path.last_node_reached_at.elapsed() > Duration::from_secs(2)
mut sprint_events: EventWriter<StartSprintEvent>, && !pathfinder.is_calculating
&& !executing_path.path.is_empty()
{
warn!("pathfinder timeout");
// the path wasn't being followed anyways, so clearing it is fine
executing_path.path.clear();
executing_path.queued_path = None;
executing_path.last_reached_node = BlockPos::from(position);
// invalidate whatever calculation we were just doing, if any
pathfinder.goto_id.fetch_add(1, atomic::Ordering::Relaxed);
// set partial to true to make sure that a recalculation will happen
executing_path.is_path_partial = true;
// the path will get recalculated automatically because the path is
// empty
}
}
}
fn check_node_reached(
mut query: Query<(
Entity,
&mut Pathfinder,
&mut ExecutingPath,
&Position,
&Physics,
)>,
mut walk_events: EventWriter<StartWalkEvent>, mut walk_events: EventWriter<StartWalkEvent>,
mut jump_events: EventWriter<JumpEvent>, mut commands: Commands,
mut goto_events: EventWriter<GotoEvent>,
instance_container: Res<InstanceContainer>,
) { ) {
for (entity, mut pathfinder, position, physics, instance_name) in &mut query { for (entity, mut pathfinder, mut executing_path, position, physics) in &mut query {
if pathfinder.goal.is_none() {
// no goal, no pathfinding
continue;
}
let successors_fn: moves::SuccessorsFn = pathfinder
.successors_fn
.expect("pathfinder.successors_fn should be Some if the goal is Some");
let world_lock = instance_container
.get(instance_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
if !pathfinder.is_calculating {
// timeout check
if let Some(last_node_reached_at) = pathfinder.last_node_reached_at {
if last_node_reached_at.elapsed() > Duration::from_secs(2) {
warn!("pathfinder timeout");
pathfinder.path.clear();
pathfinder.queued_path = None;
pathfinder.last_reached_node = None;
pathfinder.goto_id.fetch_add(1, atomic::Ordering::Relaxed);
// set partial to true to make sure that the recalculation happens
pathfinder.is_path_partial = true;
}
}
}
'skip: loop { 'skip: loop {
// we check if the goal was reached *before* actually executing the movement so // we check if the goal was reached *before* actually executing the movement so
// we don't unnecessarily execute a movement when it wasn't necessary // we don't unnecessarily execute a movement when it wasn't necessary
// see if we already reached any future nodes and can skip ahead // see if we already reached any future nodes and can skip ahead
for (i, movement) in pathfinder for (i, movement) in executing_path
.path .path
.clone() .clone()
.into_iter() .into_iter()
@ -399,43 +440,42 @@ fn tick_execute_path(
{ {
let is_reached_ctx = IsReachedCtx { let is_reached_ctx = IsReachedCtx {
target: movement.target, target: movement.target,
start: pathfinder.last_reached_node.expect( start: executing_path.last_reached_node,
"pathfinder.last_node_reached_at should always be present if there's a path",
),
position: **position, position: **position,
physics, physics,
}; };
let extra_strict_if_last = if i == pathfinder.path.len() - 1 { let extra_strict_if_last = if i == executing_path.path.len() - 1 {
let x_difference_from_center = position.x - (movement.target.x as f64 + 0.5); let x_difference_from_center = position.x - (movement.target.x as f64 + 0.5);
let z_difference_from_center = position.z - (movement.target.z as f64 + 0.5); let z_difference_from_center = position.z - (movement.target.z as f64 + 0.5);
// this is to make sure we don't fall off immediately after finishing the path // this is to make sure we don't fall off immediately after finishing the path
physics.on_ground physics.on_ground
&& BlockPos::from(position) == movement.target && BlockPos::from(position) == movement.target
// adding the delta like this isn't a perfect solution but it helps to make // adding the delta like this isn't a perfect solution but it helps to make
// sure we don't keep going if our delta is high // sure we don't keep going if our delta is high
&& (x_difference_from_center + physics.delta.x).abs() < 0.2 && (x_difference_from_center + physics.delta.x).abs() < 0.2
&& (z_difference_from_center + physics.delta.z).abs() < 0.2 && (z_difference_from_center + physics.delta.z).abs() < 0.2
} else { } else {
true true
}; };
if (movement.data.is_reached)(is_reached_ctx) && extra_strict_if_last { if (movement.data.is_reached)(is_reached_ctx) && extra_strict_if_last {
pathfinder.path = pathfinder.path.split_off(i + 1); executing_path.path = executing_path.path.split_off(i + 1);
pathfinder.last_reached_node = Some(movement.target); executing_path.last_reached_node = movement.target;
pathfinder.last_node_reached_at = Some(Instant::now()); executing_path.last_node_reached_at = Instant::now();
if let Some(new_path) = pathfinder.queued_path.take() { if let Some(new_path) = executing_path.queued_path.take() {
debug!( debug!(
"swapped path to {:?}", "swapped path to {:?}",
new_path.iter().take(10).collect::<Vec<_>>() new_path.iter().take(10).collect::<Vec<_>>()
); );
pathfinder.path = new_path; executing_path.path = new_path;
if pathfinder.path.is_empty() { if executing_path.path.is_empty() {
info!("the path we just swapped to was empty, so reached end of path"); info!("the path we just swapped to was empty, so reached end of path");
walk_events.send(StartWalkEvent { walk_events.send(StartWalkEvent {
entity, entity,
direction: WalkDirection::None, direction: WalkDirection::None,
}); });
commands.entity(entity).remove::<ExecutingPath>();
break; break;
} }
@ -443,12 +483,13 @@ fn tick_execute_path(
continue 'skip; continue 'skip;
} }
if pathfinder.path.is_empty() { if executing_path.path.is_empty() {
debug!("pathfinder path is now empty"); debug!("pathfinder path is now empty");
walk_events.send(StartWalkEvent { walk_events.send(StartWalkEvent {
entity, entity,
direction: WalkDirection::None, direction: WalkDirection::None,
}); });
commands.entity(entity).remove::<ExecutingPath>();
if let Some(goal) = pathfinder.goal.clone() { if let Some(goal) = pathfinder.goal.clone() {
if goal.success(movement.target) { if goal.success(movement.target) {
info!("goal was reached!"); info!("goal was reached!");
@ -463,15 +504,119 @@ fn tick_execute_path(
} }
break; break;
} }
}
}
if let Some(movement) = pathfinder.path.front() { fn check_for_path_obstruction(
mut query: Query<(&Pathfinder, &mut ExecutingPath, &InstanceName)>,
instance_container: Res<InstanceContainer>,
) {
for (pathfinder, mut executing_path, instance_name) in &mut query {
let Some(successors_fn) = pathfinder.successors_fn else {
continue;
};
let world_lock = instance_container
.get(instance_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
// obstruction check (the path we're executing isn't possible anymore)
let ctx = PathfinderCtx::new(world_lock);
let successors = |pos: BlockPos| {
let mut edges = Vec::with_capacity(16);
successors_fn(&mut edges, &ctx, pos);
edges
};
if let Some(obstructed_index) = check_path_obstructed(
executing_path.last_reached_node,
&executing_path.path,
successors,
) {
warn!(
"path obstructed at index {obstructed_index} (starting at {:?}, path: {:?})",
executing_path.last_reached_node, executing_path.path
);
executing_path.path.truncate(obstructed_index);
executing_path.is_path_partial = true;
}
}
}
fn recalculate_near_end_of_path(
mut query: Query<(Entity, &mut Pathfinder, &mut ExecutingPath)>,
mut walk_events: EventWriter<StartWalkEvent>,
mut goto_events: EventWriter<GotoEvent>,
mut commands: Commands,
) {
for (entity, mut pathfinder, mut executing_path) in &mut query {
let Some(successors_fn) = pathfinder.successors_fn else {
continue;
};
// start recalculating if the path ends soon
if (executing_path.path.len() == 20 || executing_path.path.len() < 5)
&& !pathfinder.is_calculating
&& executing_path.is_path_partial
{
if let Some(goal) = pathfinder.goal.as_ref().cloned() {
debug!("Recalculating path because it ends soon");
debug!(
"recalculate_near_end_of_path executing_path.is_path_partial: {}",
executing_path.is_path_partial
);
goto_events.send(GotoEvent {
entity,
goal,
successors_fn,
});
pathfinder.is_calculating = true;
if executing_path.path.is_empty() {
if let Some(new_path) = executing_path.queued_path.take() {
executing_path.path = new_path;
if executing_path.path.is_empty() {
info!("the path we just swapped to was empty, so reached end of path");
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
commands.entity(entity).remove::<ExecutingPath>();
break;
}
} else {
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
commands.entity(entity).remove::<ExecutingPath>();
}
}
} else if executing_path.path.is_empty() {
// idk when this can happen but stop moving just in case
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
}
}
}
}
fn tick_execute_path(
mut query: Query<(Entity, &mut ExecutingPath, &Position, &Physics)>,
mut look_at_events: EventWriter<LookAtEvent>,
mut sprint_events: EventWriter<StartSprintEvent>,
mut walk_events: EventWriter<StartWalkEvent>,
mut jump_events: EventWriter<JumpEvent>,
) {
for (entity, executing_path, position, physics) in &mut query {
if let Some(movement) = executing_path.path.front() {
let ctx = ExecuteCtx { let ctx = ExecuteCtx {
entity, entity,
target: movement.target, target: movement.target,
position: **position, position: **position,
start: pathfinder.last_reached_node.expect( start: executing_path.last_reached_node,
"pathfinder.last_reached_node should always be present if there's a path",
),
physics, physics,
look_at_events: &mut look_at_events, look_at_events: &mut look_at_events,
sprint_events: &mut sprint_events, sprint_events: &mut sprint_events,
@ -481,86 +626,74 @@ fn tick_execute_path(
trace!("executing move"); trace!("executing move");
(movement.data.execute)(ctx); (movement.data.execute)(ctx);
} }
}
}
{ fn recalculate_if_has_goal_but_no_path(
// obstruction check (the path we're executing isn't possible anymore) mut query: Query<(Entity, &mut Pathfinder), Without<ExecutingPath>>,
let ctx = PathfinderCtx::new(world_lock); mut goto_events: EventWriter<GotoEvent>,
let successors = |pos: BlockPos| { ) {
let mut edges = Vec::with_capacity(16); for (entity, mut pathfinder) in &mut query {
successors_fn(&mut edges, &ctx, pos); if pathfinder.goal.is_some() && !pathfinder.is_calculating {
edges if let Some(goal) = pathfinder.goal.as_ref().cloned() {
}; debug!("Recalculating path because it has a goal but no ExecutingPath");
goto_events.send(GotoEvent {
if let Some(last_reached_node) = pathfinder.last_reached_node { entity,
if let Some(obstructed_index) = goal,
check_path_obstructed(last_reached_node, &pathfinder.path, successors) successors_fn: pathfinder.successors_fn.unwrap(),
{ });
warn!("path obstructed at index {obstructed_index} (starting at {last_reached_node:?}, path: {:?})", pathfinder.path); pathfinder.is_calculating = true;
pathfinder.path.truncate(obstructed_index);
pathfinder.is_path_partial = true;
}
}
}
{
// start recalculating if the path ends soon
if (pathfinder.path.len() == 20 || pathfinder.path.len() < 5)
&& !pathfinder.is_calculating
&& pathfinder.is_path_partial
{
if let Some(goal) = pathfinder.goal.as_ref().cloned() {
debug!("Recalculating path because it ends soon");
goto_events.send(GotoEvent {
entity,
goal,
successors_fn,
});
pathfinder.is_calculating = true;
if pathfinder.path.is_empty() {
if let Some(new_path) = pathfinder.queued_path.take() {
pathfinder.path = new_path;
if pathfinder.path.is_empty() {
info!(
"the path we just swapped to was empty, so reached end of path"
);
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
break;
}
} else {
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
}
}
} else if pathfinder.path.is_empty() {
// idk when this can happen but stop moving just in case
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
}
} }
} }
} }
} }
fn stop_pathfinding_on_instance_change( #[derive(Event)]
mut query: Query<(Entity, &mut Pathfinder), Changed<InstanceName>>, pub struct StopPathfindingEvent {
pub entity: Entity,
/// If false, then let the current movement finish before stopping. If true,
/// then stop moving immediately. This might cause the bot to fall if it was
/// in the middle of parkouring.
pub force: bool,
}
fn handle_stop_pathfinding_event(
mut events: EventReader<StopPathfindingEvent>,
mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>,
mut walk_events: EventWriter<StartWalkEvent>, mut walk_events: EventWriter<StartWalkEvent>,
mut commands: Commands,
) { ) {
for (entity, mut pathfinder) in &mut query { for event in events.iter() {
if !pathfinder.path.is_empty() { let Ok((mut pathfinder, mut executing_path)) = query.get_mut(event.entity) else {
debug!("instance changed, clearing path"); continue;
pathfinder.path.clear(); };
pathfinder.goal = None;
if event.force {
executing_path.path.clear();
executing_path.queued_path = None;
walk_events.send(StartWalkEvent { walk_events.send(StartWalkEvent {
entity, entity: event.entity,
direction: WalkDirection::None, direction: WalkDirection::None,
}); });
commands.entity(event.entity).remove::<ExecutingPath>();
} else {
executing_path.queued_path = Some(VecDeque::new());
}
}
}
fn stop_pathfinding_on_instance_change(
mut query: Query<(Entity, &mut ExecutingPath), Changed<InstanceName>>,
mut stop_pathfinding_events: EventWriter<StopPathfindingEvent>,
) {
for (entity, mut executing_path) in &mut query {
if !executing_path.path.is_empty() {
debug!("instance changed, clearing path");
executing_path.path.clear();
stop_pathfinding_events.send(StopPathfindingEvent {
entity,
force: true,
});
} }
} }
} }
@ -572,7 +705,7 @@ fn stop_pathfinding_on_instance_change(
pub struct PathfinderDebugParticles; pub struct PathfinderDebugParticles;
fn debug_render_path_with_particles( fn debug_render_path_with_particles(
mut query: Query<(Entity, &Pathfinder), With<PathfinderDebugParticles>>, mut query: Query<(Entity, &ExecutingPath), With<PathfinderDebugParticles>>,
// chat_events is Option because the tests don't have SendChatEvent // chat_events is Option because the tests don't have SendChatEvent
// and we have to use ResMut<Events> because bevy doesn't support Option<EventWriter> // and we have to use ResMut<Events> because bevy doesn't support Option<EventWriter>
chat_events: Option<ResMut<Events<SendChatEvent>>>, chat_events: Option<ResMut<Events<SendChatEvent>>>,
@ -587,15 +720,13 @@ fn debug_render_path_with_particles(
*tick_count += 1; *tick_count += 1;
return; return;
} }
for (entity, pathfinder) in &mut query { for (entity, executing_path) in &mut query {
if pathfinder.path.is_empty() { if executing_path.path.is_empty() {
continue; continue;
} }
let mut start = pathfinder let mut start = executing_path.last_reached_node;
.last_reached_node for (i, movement) in executing_path.path.iter().enumerate() {
.unwrap_or_else(|| pathfinder.path.front().unwrap().target);
for movement in &pathfinder.path {
// /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100 // /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100
let end = movement.target; let end = movement.target;
@ -605,6 +736,8 @@ fn debug_render_path_with_particles(
let step_count = (start_vec3.distance_to_sqr(&end_vec3).sqrt() * 4.0) as usize; let step_count = (start_vec3.distance_to_sqr(&end_vec3).sqrt() * 4.0) as usize;
let (r, g, b): (f64, f64, f64) = if i == 0 { (0., 1., 0.) } else { (0., 1., 1.) };
// interpolate between the start and end positions // interpolate between the start and end positions
for i in 0..step_count { for i in 0..step_count {
let percent = i as f64 / step_count as f64; let percent = i as f64 / step_count as f64;
@ -615,9 +748,6 @@ fn debug_render_path_with_particles(
}; };
let particle_command = format!( let particle_command = format!(
"/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}", "/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}",
r = 0,
g = 1,
b = 1,
size = 1, size = 1,
start_x = pos.x, start_x = pos.x,
start_y = pos.y, start_y = pos.y,
@ -906,4 +1036,28 @@ mod tests {
BlockPos::new(3, 74, 0) BlockPos::new(3, 74, 0)
); );
} }
#[test]
fn test_consecutive_3_gap_parkour() {
let mut partial_chunks = PartialChunkStorage::default();
let mut simulation = setup_simulation(
&mut partial_chunks,
BlockPos::new(0, 71, 0),
BlockPos::new(4, 71, 12),
vec![
BlockPos::new(0, 70, 0),
BlockPos::new(0, 70, 4),
BlockPos::new(0, 70, 8),
BlockPos::new(0, 70, 12),
BlockPos::new(4, 70, 12),
],
);
for _ in 0..80 {
simulation.tick();
}
assert_eq!(
BlockPos::from(simulation.position()),
BlockPos::new(4, 71, 12)
);
}
} }

View file

@ -3,7 +3,7 @@ use azalea_core::{direction::CardinalDirection, position::BlockPos};
use crate::pathfinder::{astar, costs::*}; use crate::pathfinder::{astar, costs::*};
use super::{default_is_reached, Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx}; use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx};
pub fn parkour_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, node: BlockPos) { pub fn parkour_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, node: BlockPos) {
parkour_forward_1_move(edges, ctx, node); parkour_forward_1_move(edges, ctx, node);
@ -109,7 +109,7 @@ fn parkour_forward_2_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
target: pos + offset.up(ascend), target: pos + offset.up(ascend),
data: MoveData { data: MoveData {
execute: &execute_parkour_move, execute: &execute_parkour_move,
is_reached: &default_is_reached, is_reached: &parkour_is_reached,
}, },
}, },
cost, cost,
@ -161,7 +161,7 @@ fn parkour_forward_3_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
target: pos + offset, target: pos + offset,
data: MoveData { data: MoveData {
execute: &execute_parkour_move, execute: &execute_parkour_move,
is_reached: &default_is_reached, is_reached: &parkour_is_reached,
}, },
}, },
cost, cost,
@ -212,8 +212,8 @@ fn execute_parkour_move(mut ctx: ExecuteCtx) {
if !is_at_start_block if !is_at_start_block
&& !is_at_jump_block && !is_at_jump_block
&& position.y == start.y as f64 && (position.y - start.y as f64) < 0.094
&& distance_from_start < 0.8 && distance_from_start < 0.81
{ {
// we have to be on the start block to jump // we have to be on the start block to jump
ctx.look_at(start_center); ctx.look_at(start_center);

View file

@ -390,7 +390,11 @@ where
while let Some((Some(event), bot)) = bots_rx.recv().await { while let Some((Some(event), bot)) = bots_rx.recv().await {
if let Some(handler) = &self.handler { if let Some(handler) = &self.handler {
let state = bot.component::<S>(); let state = bot.component::<S>();
tokio::spawn((handler)(bot, event, state)); tokio::spawn((handler)(bot, event, state.clone()));
// this makes it not have to keep locking the ecs
while let Ok((Some(event), bot)) = bots_rx.try_recv() {
tokio::spawn((handler)(bot, event, state.clone()));
}
} }
} }