azalea/azalea/src/pathfinder/mod.rs
2023-08-25 02:34:31 -05:00

400 lines
13 KiB
Rust

mod astar;
pub mod goals;
mod moves;
pub mod simulation;
use crate::bot::{JumpEvent, LookAtEvent};
use crate::pathfinder::astar::a_star;
use crate::{SprintDirection, WalkDirection};
use crate::app::{App, Plugin};
use crate::ecs::{
component::Component,
entity::Entity,
event::{EventReader, EventWriter},
query::{With, Without},
system::{Commands, Query, Res},
};
use astar::Edge;
use azalea_client::{StartSprintEvent, StartWalkEvent};
use azalea_core::{BlockPos, CardinalDirection};
use azalea_entity::metadata::Player;
use azalea_entity::Local;
use azalea_entity::{Physics, Position};
use azalea_physics::PhysicsSet;
use azalea_world::{InstanceContainer, InstanceName};
use bevy_app::{FixedUpdate, PreUpdate, Update};
use bevy_ecs::prelude::Event;
use bevy_ecs::query::Changed;
use bevy_ecs::schedule::IntoSystemConfigs;
use bevy_tasks::{AsyncComputeTaskPool, Task};
use futures_lite::future;
use log::{debug, error, trace};
use std::collections::VecDeque;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct PathfinderPlugin;
impl Plugin for PathfinderPlugin {
fn build(&self, app: &mut App) {
app.add_event::<GotoEvent>()
.add_event::<PathFoundEvent>()
.add_systems(
FixedUpdate,
// putting systems in the FixedUpdate schedule makes them run every Minecraft tick
// (every 50 milliseconds).
tick_execute_path.before(PhysicsSet),
)
.add_systems(PreUpdate, add_default_pathfinder)
.add_systems(
Update,
(
goto_listener,
(handle_tasks, path_found_listener).chain(),
stop_pathfinding_on_instance_change,
),
);
}
}
/// A component that makes this entity able to pathfind.
#[derive(Component, Default)]
pub struct Pathfinder {
pub path: VecDeque<Node>,
}
#[allow(clippy::type_complexity)]
fn add_default_pathfinder(
mut commands: Commands,
mut query: Query<Entity, (Without<Pathfinder>, With<Local>, With<Player>)>,
) {
for entity in &mut query {
commands.entity(entity).insert(Pathfinder::default());
}
}
pub trait PathfinderClientExt {
fn goto(&self, goal: impl Goal + Send + Sync + 'static);
}
impl PathfinderClientExt for azalea_client::Client {
/// ```
/// # use azalea::prelude::*;
/// # use azalea::{BlockPos, pathfinder::goals::BlockPosGoal};
/// # fn example(bot: &Client) {
/// bot.goto(BlockPosGoal::from(BlockPos::new(0, 70, 0)));
/// # }
/// ```
fn goto(&self, goal: impl Goal + Send + Sync + 'static) {
self.ecs.lock().send_event(GotoEvent {
entity: self.entity,
goal: Arc::new(goal),
});
}
}
#[derive(Event)]
pub struct GotoEvent {
pub entity: Entity,
pub goal: Arc<dyn Goal + Send + Sync>,
}
#[derive(Event)]
pub struct PathFoundEvent {
pub entity: Entity,
pub path: VecDeque<Node>,
}
#[derive(Component)]
pub struct ComputePath(Task<Option<PathFoundEvent>>);
fn goto_listener(
mut commands: Commands,
mut events: EventReader<GotoEvent>,
mut query: Query<(&Position, &InstanceName)>,
instance_container: Res<InstanceContainer>,
) {
let thread_pool = AsyncComputeTaskPool::get();
for event in events.iter() {
let (position, world_name) = query
.get_mut(event.entity)
.expect("Called goto on an entity that's not in the world");
let start = Node {
pos: BlockPos::from(position),
vertical_vel: VerticalVel::None,
};
let world_lock = instance_container
.get(world_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
let end = event.goal.goal_node();
let goal = event.goal.clone();
let entity = event.entity;
let task = thread_pool.spawn(async move {
debug!("start: {start:?}, end: {end:?}");
let possible_moves: Vec<&dyn moves::Move> = vec![
&moves::ForwardMove(CardinalDirection::North),
&moves::ForwardMove(CardinalDirection::East),
&moves::ForwardMove(CardinalDirection::South),
&moves::ForwardMove(CardinalDirection::West),
//
&moves::AscendMove(CardinalDirection::North),
&moves::AscendMove(CardinalDirection::East),
&moves::AscendMove(CardinalDirection::South),
&moves::AscendMove(CardinalDirection::West),
//
&moves::DescendMove(CardinalDirection::North),
&moves::DescendMove(CardinalDirection::East),
&moves::DescendMove(CardinalDirection::South),
&moves::DescendMove(CardinalDirection::West),
//
&moves::DiagonalMove(CardinalDirection::North),
&moves::DiagonalMove(CardinalDirection::East),
&moves::DiagonalMove(CardinalDirection::South),
&moves::DiagonalMove(CardinalDirection::West),
];
let successors = |node: &Node| {
let mut edges = Vec::new();
let world = world_lock.read();
for possible_move in &possible_moves {
let possible_move = possible_move.get(&world, node);
if let Some(possible_move) = possible_move {
edges.push(Edge {
target: possible_move.node,
cost: possible_move.cost,
});
}
}
edges
};
// let mut pf = MTDStarLite::new(
// start,
// end,
// |n| goal.heuristic(n),
// successors,
// successors,
// |n| goal.success(n),
// );
let start_time = std::time::Instant::now();
let p = a_star(
start,
|n| goal.heuristic(n),
successors,
|n| goal.success(n),
);
let end_time = std::time::Instant::now();
debug!("path: {p:?}");
debug!("time: {:?}", end_time - start_time);
// convert the Option<Vec<Node>> to a VecDeque<Node>
if let Some(p) = p {
let path = p.into_iter().collect::<VecDeque<_>>();
// commands.entity(event.entity).insert(Pathfinder { path: p });
Some(PathFoundEvent { entity, path })
} else {
error!("no path found");
None
}
});
commands.spawn(ComputePath(task));
}
}
// poll the tasks and send the PathFoundEvent if they're done
fn handle_tasks(
mut commands: Commands,
mut transform_tasks: Query<(Entity, &mut ComputePath)>,
mut path_found_events: EventWriter<PathFoundEvent>,
) {
for (entity, mut task) in &mut transform_tasks {
if let Some(optional_path_found_event) = future::block_on(future::poll_once(&mut task.0)) {
if let Some(path_found_event) = optional_path_found_event {
path_found_events.send(path_found_event);
}
// Task is complete, so remove task component from entity
commands.entity(entity).remove::<ComputePath>();
}
}
}
// set the path for the target entity when we get the PathFoundEvent
fn path_found_listener(mut events: EventReader<PathFoundEvent>, mut query: Query<&mut Pathfinder>) {
for event in events.iter() {
let mut pathfinder = query
.get_mut(event.entity)
.expect("Path found for an entity that doesn't have a pathfinder");
pathfinder.path = event.path.clone();
}
}
fn tick_execute_path(
mut query: Query<(Entity, &mut Pathfinder, &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, mut pathfinder, position, physics) in &mut query {
loop {
let Some(target) = pathfinder.path.front() else {
break;
};
let center = target.pos.center();
// println!("going to {center:?} (at {pos:?})", pos = bot.entity().pos());
look_at_events.send(LookAtEvent {
entity,
position: center,
});
trace!(
"tick: pathfinder {entity:?}; going to {:?}; currently at {position:?}",
target.pos
);
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
// check if we should jump
if target.pos.y > position.y.floor() as i32 {
jump_events.send(JumpEvent(entity));
}
if target.is_reached(position, physics) {
// println!("reached target");
pathfinder.path.pop_front();
if pathfinder.path.is_empty() {
// println!("reached goal");
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
}
// tick again, maybe we already reached the next node!
} else {
break;
}
}
}
}
fn stop_pathfinding_on_instance_change(
mut query: Query<(Entity, &mut Pathfinder), Changed<InstanceName>>,
mut walk_events: EventWriter<StartWalkEvent>,
) {
for (entity, mut pathfinder) in &mut query {
if !pathfinder.path.is_empty() {
debug!("instance changed, clearing path");
pathfinder.path.clear();
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
}
}
}
/// Information about our vertical velocity
#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)]
pub enum VerticalVel {
None,
/// No vertical velocity, but we're not on the ground
NoneMidair,
// less than 3 blocks (no fall damage)
FallingLittle,
}
#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)]
pub struct Node {
pub pos: BlockPos,
pub vertical_vel: VerticalVel,
}
pub trait Goal {
fn heuristic(&self, n: &Node) -> f32;
fn success(&self, n: &Node) -> bool;
// TODO: this should be removed and mtdstarlite should stop depending on
// being given a goal node
fn goal_node(&self) -> Node;
}
impl Node {
/// Returns whether the entity is at the node and should start going to the
/// next node.
#[must_use]
pub fn is_reached(&self, position: &Position, physics: &Physics) -> bool {
// println!(
// "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}",
// entity.delta.y,
// BlockPos::from(entity.pos()),
// self.pos,
// self.vertical_vel
// );
BlockPos::from(position) == self.pos
&& match self.vertical_vel {
VerticalVel::NoneMidair => physics.delta.y > -0.1 && physics.delta.y < 0.1,
VerticalVel::None => physics.on_ground,
VerticalVel::FallingLittle => physics.delta.y < -0.1,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use azalea_core::{BlockPos, ChunkPos, Vec3};
use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage};
use bevy_log::LogPlugin;
use super::{
goals::BlockPosGoal,
simulation::{SimulatedPlayerBundle, Simulation},
GotoEvent,
};
#[test]
fn test_simple_forward() {
let mut chunks = ChunkStorage::default();
let mut partial_chunks = PartialChunkStorage::default();
partial_chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut chunks,
);
chunks.set_block_state(
&BlockPos::new(0, 70, 0),
azalea_registry::Block::Stone.into(),
);
chunks.set_block_state(
&BlockPos::new(0, 70, 1),
azalea_registry::Block::Stone.into(),
);
let player = SimulatedPlayerBundle::new(Vec3::new(0.5, 71., 0.5));
let mut simulation = Simulation::new(chunks, player);
simulation.app.add_plugins(LogPlugin {
level: bevy_log::Level::DEBUG,
filter: "".to_string(),
});
simulation.app.world.send_event(GotoEvent {
entity: simulation.entity,
goal: Arc::new(BlockPosGoal::from(BlockPos::new(0, 71, 1))),
});
for _ in 0..20 {
simulation.tick();
}
assert_eq!(
BlockPos::from(simulation.position()),
BlockPos::new(0, 71, 1)
);
}
}