mirror of
https://github.com/mat-1/azalea.git
synced 2024-09-19 22:52:32 +00:00
split pathfinder execution into multiple systems (and fix some bugs)
This commit is contained in:
parent
9281e4fdb9
commit
971f42e3db
5 changed files with 351 additions and 192 deletions
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue