mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
simplify pathfinder
This commit is contained in:
parent
ac1522db54
commit
5d7669f72b
6 changed files with 405 additions and 214 deletions
|
@ -62,7 +62,7 @@ impl Direction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make azalea_block use this instead of FacingCardinal
|
// TODO: make azalea_block use this instead of FacingCardinal
|
||||||
#[derive(Clone, Copy, Debug, McBuf)]
|
#[derive(Clone, Copy, Debug, McBuf, PartialEq, Eq, Hash)]
|
||||||
pub enum CardinalDirection {
|
pub enum CardinalDirection {
|
||||||
North,
|
North,
|
||||||
South,
|
South,
|
||||||
|
|
|
@ -87,7 +87,9 @@ pub trait BotClientExt {
|
||||||
impl BotClientExt for azalea_client::Client {
|
impl BotClientExt for azalea_client::Client {
|
||||||
fn jump(&mut self) {
|
fn jump(&mut self) {
|
||||||
let mut ecs = self.ecs.lock();
|
let mut ecs = self.ecs.lock();
|
||||||
ecs.send_event(JumpEvent(self.entity));
|
ecs.send_event(JumpEvent {
|
||||||
|
entity: self.entity,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn look_at(&mut self, position: Vec3) {
|
fn look_at(&mut self, position: Vec3) {
|
||||||
|
@ -136,14 +138,16 @@ impl BotClientExt for azalea_client::Client {
|
||||||
|
|
||||||
/// Event to jump once.
|
/// Event to jump once.
|
||||||
#[derive(Event)]
|
#[derive(Event)]
|
||||||
pub struct JumpEvent(pub Entity);
|
pub struct JumpEvent {
|
||||||
|
pub entity: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn jump_listener(
|
pub fn jump_listener(
|
||||||
mut query: Query<(&mut Jumping, &mut Bot)>,
|
mut query: Query<(&mut Jumping, &mut Bot)>,
|
||||||
mut events: EventReader<JumpEvent>,
|
mut events: EventReader<JumpEvent>,
|
||||||
) {
|
) {
|
||||||
for event in events.iter() {
|
for event in events.iter() {
|
||||||
if let Ok((mut jumping, mut bot)) = query.get_mut(event.0) {
|
if let Ok((mut jumping, mut bot)) = query.get_mut(event.entity) {
|
||||||
**jumping = true;
|
**jumping = true;
|
||||||
bot.jumping_once = true;
|
bot.jumping_once = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,110 +1,176 @@
|
||||||
use std::{cmp::Reverse, collections::HashMap, fmt::Debug, hash::Hash, ops::Add};
|
use std::{
|
||||||
|
cmp::Reverse,
|
||||||
|
collections::HashMap,
|
||||||
|
fmt::Debug,
|
||||||
|
hash::Hash,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
use priority_queue::PriorityQueue;
|
use priority_queue::PriorityQueue;
|
||||||
|
|
||||||
pub fn a_star<N, W, HeuristicFn, SuccessorsFn, SuccessFn>(
|
pub struct Path<P, M>
|
||||||
start: N,
|
where
|
||||||
|
P: Eq + Hash + Copy + Debug,
|
||||||
|
{
|
||||||
|
pub movements: Vec<Movement<P, M>>,
|
||||||
|
pub partial: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn a_star<P, M, HeuristicFn, SuccessorsFn, SuccessFn>(
|
||||||
|
start: P,
|
||||||
heuristic: HeuristicFn,
|
heuristic: HeuristicFn,
|
||||||
successors: SuccessorsFn,
|
successors: SuccessorsFn,
|
||||||
success: SuccessFn,
|
success: SuccessFn,
|
||||||
) -> Option<Vec<N>>
|
timeout: Duration,
|
||||||
|
) -> Path<P, M>
|
||||||
where
|
where
|
||||||
N: Eq + Hash + Copy + Debug,
|
P: Eq + Hash + Copy + Debug,
|
||||||
W: PartialOrd + Default + Copy + num_traits::Bounded + Debug + Add<Output = W>,
|
HeuristicFn: Fn(P) -> f32,
|
||||||
HeuristicFn: Fn(&N) -> W,
|
SuccessorsFn: Fn(P) -> Vec<Edge<P, M>>,
|
||||||
SuccessorsFn: Fn(&N) -> Vec<Edge<N, W>>,
|
SuccessFn: Fn(P) -> bool,
|
||||||
SuccessFn: Fn(&N) -> bool,
|
|
||||||
{
|
{
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let mut open_set = PriorityQueue::new();
|
let mut open_set = PriorityQueue::new();
|
||||||
open_set.push(start, Reverse(Weight(W::default())));
|
open_set.push(start, Reverse(Weight(0.)));
|
||||||
let mut nodes: HashMap<N, Node<N, W>> = HashMap::new();
|
let mut nodes: HashMap<P, Node<P, M>> = HashMap::new();
|
||||||
nodes.insert(
|
nodes.insert(
|
||||||
start,
|
start,
|
||||||
Node {
|
Node {
|
||||||
data: start,
|
position: start,
|
||||||
|
movement_data: None,
|
||||||
came_from: None,
|
came_from: None,
|
||||||
g_score: W::default(),
|
g_score: f32::default(),
|
||||||
f_score: W::max_value(),
|
f_score: f32::MAX,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut best_node = start;
|
||||||
|
let mut best_node_score = heuristic(start);
|
||||||
|
|
||||||
while let Some((current_node, _)) = open_set.pop() {
|
while let Some((current_node, _)) = open_set.pop() {
|
||||||
if success(¤t_node) {
|
if success(current_node) {
|
||||||
return Some(reconstruct_path(&nodes, current_node));
|
return Path {
|
||||||
|
movements: reconstruct_path(nodes, current_node),
|
||||||
|
partial: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_g_score = nodes
|
let current_g_score = nodes
|
||||||
.get(¤t_node)
|
.get(¤t_node)
|
||||||
.map(|n| n.g_score)
|
.map(|n| n.g_score)
|
||||||
.unwrap_or(W::max_value());
|
.unwrap_or(f32::MAX);
|
||||||
|
|
||||||
for neighbor in successors(¤t_node) {
|
for neighbor in successors(current_node) {
|
||||||
let tentative_g_score = current_g_score + neighbor.cost;
|
let tentative_g_score = current_g_score + neighbor.cost;
|
||||||
let neighbor_g_score = nodes
|
let neighbor_g_score = nodes
|
||||||
.get(&neighbor.target)
|
.get(&neighbor.movement.target)
|
||||||
.map(|n| n.g_score)
|
.map(|n| n.g_score)
|
||||||
.unwrap_or(W::max_value());
|
.unwrap_or(f32::MAX);
|
||||||
if tentative_g_score < neighbor_g_score {
|
if tentative_g_score < neighbor_g_score {
|
||||||
let f_score = tentative_g_score + heuristic(&neighbor.target);
|
let heuristic = heuristic(neighbor.movement.target);
|
||||||
|
let f_score = tentative_g_score + heuristic;
|
||||||
nodes.insert(
|
nodes.insert(
|
||||||
neighbor.target,
|
neighbor.movement.target,
|
||||||
Node {
|
Node {
|
||||||
data: neighbor.target,
|
position: neighbor.movement.target,
|
||||||
|
movement_data: Some(neighbor.movement.data),
|
||||||
came_from: Some(current_node),
|
came_from: Some(current_node),
|
||||||
g_score: tentative_g_score,
|
g_score: tentative_g_score,
|
||||||
f_score,
|
f_score,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
open_set.push(neighbor.target, Reverse(Weight(f_score)));
|
open_set.push(neighbor.movement.target, Reverse(Weight(f_score)));
|
||||||
|
|
||||||
|
let node_score = heuristic + tentative_g_score / 1.5;
|
||||||
|
if node_score < best_node_score {
|
||||||
|
best_node = neighbor.movement.target;
|
||||||
|
best_node_score = node_score;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if start_time.elapsed() > timeout {
|
||||||
|
// timeout, just return the best path we have so far
|
||||||
|
info!("Pathfinder timeout");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
Path {
|
||||||
|
movements: reconstruct_path(nodes, best_node),
|
||||||
|
partial: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reconstruct_path<N, W>(nodes: &HashMap<N, Node<N, W>>, current: N) -> Vec<N>
|
fn reconstruct_path<P, M>(mut nodes: HashMap<P, Node<P, M>>, current: P) -> Vec<Movement<P, M>>
|
||||||
where
|
where
|
||||||
N: Eq + Hash + Copy + Debug,
|
P: Eq + Hash + Copy + Debug,
|
||||||
W: PartialOrd + Default + Copy + num_traits::Bounded + Debug,
|
|
||||||
{
|
{
|
||||||
let mut path = vec![current];
|
let mut path = Vec::new();
|
||||||
let mut current = current;
|
let mut current = current;
|
||||||
while let Some(node) = nodes.get(¤t) {
|
while let Some(node) = nodes.remove(¤t) {
|
||||||
if let Some(came_from) = node.came_from {
|
if let Some(came_from) = node.came_from {
|
||||||
path.push(came_from);
|
|
||||||
current = came_from;
|
current = came_from;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
path.push(Movement {
|
||||||
|
target: node.position,
|
||||||
|
data: node.movement_data.unwrap(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
path.reverse();
|
path.reverse();
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Node<N, W> {
|
pub struct Node<P, M> {
|
||||||
pub data: N,
|
pub position: P,
|
||||||
pub came_from: Option<N>,
|
pub movement_data: Option<M>,
|
||||||
pub g_score: W,
|
pub came_from: Option<P>,
|
||||||
pub f_score: W,
|
pub g_score: f32,
|
||||||
|
pub f_score: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Edge<N: Eq + Hash + Copy, W: PartialOrd + Copy> {
|
pub struct Edge<P: Hash + Copy, M> {
|
||||||
pub target: N,
|
pub movement: Movement<P, M>,
|
||||||
pub cost: W,
|
pub cost: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Movement<P: Hash + Copy, M> {
|
||||||
|
pub target: P,
|
||||||
|
pub data: M,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Hash + Copy + Debug, M: Debug> Debug for Movement<P, M> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Movement")
|
||||||
|
.field("target", &self.target)
|
||||||
|
.field("data", &self.data)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<P: Hash + Copy + Clone, M: Clone> Clone for Movement<P, M> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
target: self.target,
|
||||||
|
data: self.data.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct Weight<W: PartialOrd + Debug>(W);
|
pub struct Weight(f32);
|
||||||
impl<W: PartialOrd + Debug> Ord for Weight<W> {
|
impl Ord for Weight {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
self.0
|
self.0
|
||||||
.partial_cmp(&other.0)
|
.partial_cmp(&other.0)
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<W: PartialOrd + Debug> Eq for Weight<W> {}
|
impl Eq for Weight {}
|
||||||
impl<W: PartialOrd + Debug> PartialOrd for Weight<W> {
|
impl PartialOrd for Weight {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,22 @@
|
||||||
use azalea_core::BlockPos;
|
use azalea_core::BlockPos;
|
||||||
|
|
||||||
use super::{Goal, Node, VerticalVel};
|
use super::Goal;
|
||||||
|
|
||||||
pub struct BlockPosGoal {
|
pub struct BlockPosGoal {
|
||||||
pub pos: BlockPos,
|
pub pos: BlockPos,
|
||||||
}
|
}
|
||||||
impl Goal for BlockPosGoal {
|
impl Goal for BlockPosGoal {
|
||||||
fn heuristic(&self, n: &Node) -> f32 {
|
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||||
let dx = (self.pos.x - n.pos.x) as f32;
|
let dx = (self.pos.x - n.x) as f32;
|
||||||
let dy = (self.pos.y - n.pos.y) as f32;
|
let dy = (self.pos.y - n.y) as f32;
|
||||||
let dz = (self.pos.z - n.pos.z) as f32;
|
let dz = (self.pos.z - n.z) as f32;
|
||||||
dx * dx + dy * dy + dz * dz
|
dx * dx + dy * dy + dz * dz
|
||||||
}
|
}
|
||||||
fn success(&self, n: &Node) -> bool {
|
fn success(&self, n: BlockPos) -> bool {
|
||||||
n.pos == self.pos
|
n == self.pos
|
||||||
}
|
}
|
||||||
fn goal_node(&self) -> Node {
|
fn goal_node(&self) -> BlockPos {
|
||||||
Node {
|
self.pos
|
||||||
pos: self.pos,
|
|
||||||
vertical_vel: VerticalVel::None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ pub mod simulation;
|
||||||
|
|
||||||
use crate::bot::{JumpEvent, LookAtEvent};
|
use crate::bot::{JumpEvent, LookAtEvent};
|
||||||
use crate::pathfinder::astar::a_star;
|
use crate::pathfinder::astar::a_star;
|
||||||
use crate::{SprintDirection, WalkDirection};
|
use crate::pathfinder::moves::DefaultMoves;
|
||||||
|
use crate::WalkDirection;
|
||||||
|
|
||||||
use crate::app::{App, Plugin};
|
use crate::app::{App, Plugin};
|
||||||
use crate::ecs::{
|
use crate::ecs::{
|
||||||
|
@ -15,7 +16,6 @@ use crate::ecs::{
|
||||||
query::{With, Without},
|
query::{With, Without},
|
||||||
system::{Commands, Query, Res},
|
system::{Commands, Query, Res},
|
||||||
};
|
};
|
||||||
use astar::Edge;
|
|
||||||
use azalea_client::movement::walk_listener;
|
use azalea_client::movement::walk_listener;
|
||||||
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
||||||
use azalea_core::{BlockPos, CardinalDirection};
|
use azalea_core::{BlockPos, CardinalDirection};
|
||||||
|
@ -33,6 +33,9 @@ use futures_lite::future;
|
||||||
use log::{debug, error, trace};
|
use log::{debug, error, trace};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use self::moves::ExecuteCtx;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct PathfinderPlugin;
|
pub struct PathfinderPlugin;
|
||||||
|
@ -65,7 +68,7 @@ impl Plugin for PathfinderPlugin {
|
||||||
/// A component that makes this entity able to pathfind.
|
/// A component that makes this entity able to pathfind.
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
pub struct Pathfinder {
|
pub struct Pathfinder {
|
||||||
pub path: VecDeque<Node>,
|
pub path: VecDeque<astar::Movement<BlockPos, moves::MoveData>>,
|
||||||
}
|
}
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn add_default_pathfinder(
|
fn add_default_pathfinder(
|
||||||
|
@ -104,7 +107,7 @@ pub struct GotoEvent {
|
||||||
#[derive(Event)]
|
#[derive(Event)]
|
||||||
pub struct PathFoundEvent {
|
pub struct PathFoundEvent {
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub path: VecDeque<Node>,
|
pub path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
|
@ -122,10 +125,7 @@ fn goto_listener(
|
||||||
let (position, world_name) = query
|
let (position, world_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");
|
||||||
let start = Node {
|
let start = BlockPos::from(position);
|
||||||
pos: BlockPos::from(position),
|
|
||||||
vertical_vel: VerticalVel::None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let world_lock = instance_container
|
let world_lock = instance_container
|
||||||
.get(world_name)
|
.get(world_name)
|
||||||
|
@ -138,64 +138,60 @@ fn goto_listener(
|
||||||
let task = thread_pool.spawn(async move {
|
let task = thread_pool.spawn(async move {
|
||||||
debug!("start: {start:?}, end: {end:?}");
|
debug!("start: {start:?}, end: {end:?}");
|
||||||
|
|
||||||
let possible_moves: Vec<&dyn moves::Move> = vec![
|
let possible_moves: Vec<DefaultMoves> = vec![
|
||||||
&moves::ForwardMove(CardinalDirection::North),
|
DefaultMoves::Forward(CardinalDirection::North),
|
||||||
&moves::ForwardMove(CardinalDirection::East),
|
DefaultMoves::Forward(CardinalDirection::East),
|
||||||
&moves::ForwardMove(CardinalDirection::South),
|
DefaultMoves::Forward(CardinalDirection::South),
|
||||||
&moves::ForwardMove(CardinalDirection::West),
|
DefaultMoves::Forward(CardinalDirection::West),
|
||||||
//
|
//
|
||||||
&moves::AscendMove(CardinalDirection::North),
|
DefaultMoves::Ascend(CardinalDirection::North),
|
||||||
&moves::AscendMove(CardinalDirection::East),
|
DefaultMoves::Ascend(CardinalDirection::East),
|
||||||
&moves::AscendMove(CardinalDirection::South),
|
DefaultMoves::Ascend(CardinalDirection::South),
|
||||||
&moves::AscendMove(CardinalDirection::West),
|
DefaultMoves::Ascend(CardinalDirection::West),
|
||||||
//
|
//
|
||||||
&moves::DescendMove(CardinalDirection::North),
|
DefaultMoves::Descend(CardinalDirection::North),
|
||||||
&moves::DescendMove(CardinalDirection::East),
|
DefaultMoves::Descend(CardinalDirection::East),
|
||||||
&moves::DescendMove(CardinalDirection::South),
|
DefaultMoves::Descend(CardinalDirection::South),
|
||||||
&moves::DescendMove(CardinalDirection::West),
|
DefaultMoves::Descend(CardinalDirection::West),
|
||||||
//
|
//
|
||||||
&moves::DiagonalMove(CardinalDirection::North),
|
DefaultMoves::Diagonal(CardinalDirection::North),
|
||||||
&moves::DiagonalMove(CardinalDirection::East),
|
DefaultMoves::Diagonal(CardinalDirection::East),
|
||||||
&moves::DiagonalMove(CardinalDirection::South),
|
DefaultMoves::Diagonal(CardinalDirection::South),
|
||||||
&moves::DiagonalMove(CardinalDirection::West),
|
DefaultMoves::Diagonal(CardinalDirection::West),
|
||||||
];
|
];
|
||||||
|
|
||||||
let successors = |node: &Node| {
|
let successors = |pos: BlockPos| {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
|
|
||||||
let world = world_lock.read();
|
let world = world_lock.read();
|
||||||
for possible_move in &possible_moves {
|
for possible_move in &possible_moves {
|
||||||
let possible_move = possible_move.get(&world, node);
|
let move_result = possible_move.get(&world, pos);
|
||||||
if let Some(possible_move) = possible_move {
|
if let Some(edge) = move_result {
|
||||||
edges.push(Edge {
|
edges.push(edge);
|
||||||
target: possible_move.node,
|
|
||||||
cost: possible_move.cost,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
edges
|
edges
|
||||||
};
|
};
|
||||||
|
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
let p = a_star(
|
let astar::Path { movements, partial } = a_star(
|
||||||
start,
|
start,
|
||||||
|n| goal.heuristic(n),
|
|n| goal.heuristic(n),
|
||||||
successors,
|
successors,
|
||||||
|n| goal.success(n),
|
|n| goal.success(n),
|
||||||
|
Duration::from_secs(1),
|
||||||
);
|
);
|
||||||
let end_time = std::time::Instant::now();
|
let end_time = std::time::Instant::now();
|
||||||
debug!("path: {p:?}");
|
debug!("movements: {movements:?}");
|
||||||
|
debug!("partial: {partial:?}");
|
||||||
debug!("time: {:?}", end_time - start_time);
|
debug!("time: {:?}", end_time - start_time);
|
||||||
|
|
||||||
// convert the Option<Vec<Node>> to a VecDeque<Node>
|
let path = movements.into_iter().collect::<VecDeque<_>>();
|
||||||
if let Some(p) = p {
|
Some(PathFoundEvent {
|
||||||
let path = p.into_iter().collect::<VecDeque<_>>();
|
entity,
|
||||||
// commands.entity(event.entity).insert(Pathfinder { path: p });
|
path: Some(path),
|
||||||
Some(PathFoundEvent { entity, path })
|
})
|
||||||
} else {
|
|
||||||
error!("no path found");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.spawn(ComputePath(task));
|
commands.spawn(ComputePath(task));
|
||||||
|
@ -226,7 +222,12 @@ fn path_found_listener(mut events: EventReader<PathFoundEvent>, mut query: Query
|
||||||
let mut pathfinder = query
|
let mut pathfinder = 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");
|
||||||
pathfinder.path = event.path.clone();
|
if let Some(path) = &event.path {
|
||||||
|
pathfinder.path = path.to_owned();
|
||||||
|
} else {
|
||||||
|
error!("No path found");
|
||||||
|
pathfinder.path.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,30 +240,12 @@ fn tick_execute_path(
|
||||||
) {
|
) {
|
||||||
for (entity, mut pathfinder, position, physics) in &mut query {
|
for (entity, mut pathfinder, position, physics) in &mut query {
|
||||||
loop {
|
loop {
|
||||||
let Some(target) = pathfinder.path.front() else {
|
let Some(movement) = pathfinder.path.front() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let center = target.pos.center();
|
// we check if the goal was reached *before* actually executing the movement so
|
||||||
// println!("going to {center:?} (at {pos:?})", pos = bot.entity().pos());
|
// we don't unnecessarily execute a movement when it wasn't necessary
|
||||||
look_at_events.send(LookAtEvent {
|
if is_goal_reached(movement.target, position, physics) {
|
||||||
entity,
|
|
||||||
position: center,
|
|
||||||
});
|
|
||||||
trace!(
|
|
||||||
"tick: pathfinder {entity:?}; going to {:?}; currently at {:?}",
|
|
||||||
target.pos,
|
|
||||||
**position
|
|
||||||
);
|
|
||||||
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");
|
// println!("reached target");
|
||||||
pathfinder.path.pop_front();
|
pathfinder.path.pop_front();
|
||||||
if pathfinder.path.is_empty() {
|
if pathfinder.path.is_empty() {
|
||||||
|
@ -273,9 +256,21 @@ fn tick_execute_path(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// tick again, maybe we already reached the next node!
|
// tick again, maybe we already reached the next node!
|
||||||
} else {
|
continue;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ctx = ExecuteCtx {
|
||||||
|
entity,
|
||||||
|
target: movement.target,
|
||||||
|
position: **position,
|
||||||
|
look_at_events: &mut look_at_events,
|
||||||
|
sprint_events: &mut sprint_events,
|
||||||
|
walk_events: &mut walk_events,
|
||||||
|
jump_events: &mut jump_events,
|
||||||
|
};
|
||||||
|
trace!("executing move {:?}", movement.data.move_kind);
|
||||||
|
movement.data.move_kind.execute(ctx);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,49 +291,26 @@ fn stop_pathfinding_on_instance_change(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
pub trait Goal {
|
||||||
fn heuristic(&self, n: &Node) -> f32;
|
fn heuristic(&self, n: BlockPos) -> f32;
|
||||||
fn success(&self, n: &Node) -> bool;
|
fn success(&self, n: BlockPos) -> bool;
|
||||||
// TODO: this should be removed and mtdstarlite should stop depending on
|
// TODO: this should be removed and mtdstarlite should stop depending on
|
||||||
// being given a goal node
|
// being given a goal node
|
||||||
fn goal_node(&self) -> Node;
|
fn goal_node(&self) -> BlockPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
/// Returns whether the entity is at the node and should start going to the
|
||||||
/// Returns whether the entity is at the node and should start going to the
|
/// next node.
|
||||||
/// next node.
|
#[must_use]
|
||||||
#[must_use]
|
pub fn is_goal_reached(goal_pos: BlockPos, current_pos: &Position, physics: &Physics) -> bool {
|
||||||
pub fn is_reached(&self, position: &Position, physics: &Physics) -> bool {
|
// println!(
|
||||||
// println!(
|
// "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}",
|
||||||
// "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}",
|
// entity.delta.y,
|
||||||
// entity.delta.y,
|
// BlockPos::from(entity.pos()),
|
||||||
// BlockPos::from(entity.pos()),
|
// self.pos,
|
||||||
// self.pos,
|
// self.vertical_vel
|
||||||
// self.vertical_vel
|
// );
|
||||||
// );
|
BlockPos::from(current_pos) == goal_pos && physics.on_ground
|
||||||
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)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1,11 +1,31 @@
|
||||||
use super::{Node, VerticalVel};
|
use crate::{JumpEvent, LookAtEvent};
|
||||||
use azalea_core::{BlockPos, CardinalDirection};
|
|
||||||
|
use super::astar;
|
||||||
|
use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent};
|
||||||
|
use azalea_core::{BlockPos, CardinalDirection, Vec3};
|
||||||
use azalea_physics::collision::{self, BlockWithShape};
|
use azalea_physics::collision::{self, BlockWithShape};
|
||||||
use azalea_world::Instance;
|
use azalea_world::Instance;
|
||||||
|
use bevy_ecs::{entity::Entity, event::EventWriter};
|
||||||
|
|
||||||
|
type Edge = astar::Edge<BlockPos, MoveData>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MoveData {
|
||||||
|
pub move_kind: DefaultMoves,
|
||||||
|
}
|
||||||
|
|
||||||
/// whether this block is passable
|
/// whether this block is passable
|
||||||
fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
|
fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
|
||||||
if let Some(block) = world.chunks.get_block_state(pos) {
|
if let Some(block) = world.chunks.get_block_state(pos) {
|
||||||
|
if block.shape() != &collision::empty_shape() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if block == azalea_registry::Block::Water.into() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if block.waterlogged() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
block.shape() == &collision::empty_shape()
|
block.shape() == &collision::empty_shape()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -15,8 +35,7 @@ fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
|
||||||
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
||||||
fn is_block_solid(pos: &BlockPos, world: &Instance) -> bool {
|
fn is_block_solid(pos: &BlockPos, world: &Instance) -> bool {
|
||||||
if let Some(block) = world.chunks.get_block_state(pos) {
|
if let Some(block) = world.chunks.get_block_state(pos) {
|
||||||
// block.shape() == &collision::block_shape()
|
block.shape() == &collision::block_shape()
|
||||||
block.shape() != &collision::empty_shape()
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -53,62 +72,148 @@ const JUMP_COST: f32 = 0.5;
|
||||||
const WALK_ONE_BLOCK_COST: f32 = 1.0;
|
const WALK_ONE_BLOCK_COST: f32 = 1.0;
|
||||||
const FALL_ONE_BLOCK_COST: f32 = 0.5;
|
const FALL_ONE_BLOCK_COST: f32 = 0.5;
|
||||||
|
|
||||||
pub trait Move: Send + Sync {
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
fn get(&self, world: &Instance, node: &Node) -> Option<MoveResult>;
|
pub enum DefaultMoves {
|
||||||
|
Forward(CardinalDirection),
|
||||||
|
Ascend(CardinalDirection),
|
||||||
|
Descend(CardinalDirection),
|
||||||
|
Diagonal(CardinalDirection),
|
||||||
}
|
}
|
||||||
pub struct MoveResult {
|
|
||||||
pub node: Node,
|
impl DefaultMoves {
|
||||||
pub cost: f32,
|
pub fn get(self, world: &Instance, node: BlockPos) -> Option<Edge> {
|
||||||
|
match self {
|
||||||
|
DefaultMoves::Forward(dir) => ForwardMove(dir).get(world, node),
|
||||||
|
DefaultMoves::Ascend(dir) => AscendMove(dir).get(world, node),
|
||||||
|
DefaultMoves::Descend(dir) => DescendMove(dir).get(world, node),
|
||||||
|
DefaultMoves::Diagonal(dir) => DiagonalMove(dir).get(world, node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(self, ctx: ExecuteCtx) {
|
||||||
|
match self {
|
||||||
|
DefaultMoves::Forward(_) => ForwardMove::execute(ctx),
|
||||||
|
DefaultMoves::Ascend(_) => AscendMove::execute(ctx),
|
||||||
|
DefaultMoves::Descend(_) => DescendMove::execute(ctx),
|
||||||
|
DefaultMoves::Diagonal(_) => DiagonalMove::execute(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MoveImpl: Send + Sync {
|
||||||
|
fn get(&self, world: &Instance, node: BlockPos) -> Option<Edge>;
|
||||||
|
fn execute(ctx: ExecuteCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
|
||||||
|
pub entity: Entity,
|
||||||
|
pub target: BlockPos,
|
||||||
|
pub position: Vec3,
|
||||||
|
|
||||||
|
pub look_at_events: &'a mut EventWriter<'w1, LookAtEvent>,
|
||||||
|
pub sprint_events: &'a mut EventWriter<'w2, StartSprintEvent>,
|
||||||
|
pub walk_events: &'a mut EventWriter<'w3, StartWalkEvent>,
|
||||||
|
pub jump_events: &'a mut EventWriter<'w4, JumpEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ForwardMove(pub CardinalDirection);
|
pub struct ForwardMove(pub CardinalDirection);
|
||||||
impl Move for ForwardMove {
|
impl MoveImpl for ForwardMove {
|
||||||
fn get(&self, world: &Instance, node: &Node) -> Option<MoveResult> {
|
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
|
||||||
let offset = BlockPos::new(self.0.x(), 0, self.0.z());
|
let offset = BlockPos::new(self.0.x(), 0, self.0.z());
|
||||||
|
|
||||||
if !is_standable(&(node.pos + offset), world) || node.vertical_vel != VerticalVel::None {
|
if !is_standable(&(pos + offset), world) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cost = WALK_ONE_BLOCK_COST;
|
let cost = WALK_ONE_BLOCK_COST;
|
||||||
|
|
||||||
Some(MoveResult {
|
Some(Edge {
|
||||||
node: Node {
|
movement: astar::Movement {
|
||||||
pos: node.pos + offset,
|
target: pos + offset,
|
||||||
vertical_vel: VerticalVel::None,
|
data: MoveData {
|
||||||
|
move_kind: DefaultMoves::Forward(self.0),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cost,
|
cost,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
ExecuteCtx {
|
||||||
|
entity,
|
||||||
|
target,
|
||||||
|
look_at_events,
|
||||||
|
sprint_events,
|
||||||
|
..
|
||||||
|
}: ExecuteCtx,
|
||||||
|
) {
|
||||||
|
let center = target.center();
|
||||||
|
look_at_events.send(LookAtEvent {
|
||||||
|
entity,
|
||||||
|
position: center,
|
||||||
|
});
|
||||||
|
sprint_events.send(StartSprintEvent {
|
||||||
|
entity,
|
||||||
|
direction: SprintDirection::Forward,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AscendMove(pub CardinalDirection);
|
pub struct AscendMove(pub CardinalDirection);
|
||||||
impl Move for AscendMove {
|
impl MoveImpl for AscendMove {
|
||||||
fn get(&self, world: &Instance, node: &Node) -> Option<MoveResult> {
|
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
|
||||||
let offset = BlockPos::new(self.0.x(), 1, self.0.z());
|
let offset = BlockPos::new(self.0.x(), 1, self.0.z());
|
||||||
|
|
||||||
if node.vertical_vel != VerticalVel::None
|
if !is_block_passable(&pos.up(2), world) || !is_standable(&(pos + offset), world) {
|
||||||
|| !is_block_passable(&node.pos.up(2), world)
|
|
||||||
|| !is_standable(&(node.pos + offset), world)
|
|
||||||
{
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cost = WALK_ONE_BLOCK_COST + JUMP_COST;
|
let cost = WALK_ONE_BLOCK_COST + JUMP_COST;
|
||||||
|
|
||||||
Some(MoveResult {
|
// Some(MoveResult {
|
||||||
node: Node {
|
// node: Node {
|
||||||
pos: node.pos + offset,
|
// pos: node.pos + offset,
|
||||||
vertical_vel: VerticalVel::None,
|
// vertical_vel: VerticalVel::None,
|
||||||
|
// },
|
||||||
|
// cost,
|
||||||
|
// })
|
||||||
|
Some(Edge {
|
||||||
|
movement: astar::Movement {
|
||||||
|
target: pos + offset,
|
||||||
|
data: MoveData {
|
||||||
|
move_kind: DefaultMoves::Ascend(self.0),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cost,
|
cost,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
ExecuteCtx {
|
||||||
|
entity,
|
||||||
|
target,
|
||||||
|
look_at_events,
|
||||||
|
sprint_events,
|
||||||
|
jump_events,
|
||||||
|
..
|
||||||
|
}: ExecuteCtx,
|
||||||
|
) {
|
||||||
|
let center = target.center();
|
||||||
|
look_at_events.send(LookAtEvent {
|
||||||
|
entity,
|
||||||
|
position: center,
|
||||||
|
});
|
||||||
|
jump_events.send(JumpEvent { entity });
|
||||||
|
sprint_events.send(StartSprintEvent {
|
||||||
|
entity,
|
||||||
|
direction: SprintDirection::Forward,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub struct DescendMove(pub CardinalDirection);
|
pub struct DescendMove(pub CardinalDirection);
|
||||||
impl Move for DescendMove {
|
impl MoveImpl for DescendMove {
|
||||||
fn get(&self, world: &Instance, node: &Node) -> Option<MoveResult> {
|
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
|
||||||
let new_horizontal_position = node.pos + BlockPos::new(self.0.x(), 0, self.0.z());
|
let new_horizontal_position = pos + BlockPos::new(self.0.x(), 0, self.0.z());
|
||||||
let fall_distance = fall_distance(&new_horizontal_position, world);
|
let fall_distance = fall_distance(&new_horizontal_position, world);
|
||||||
if fall_distance == 0 {
|
if fall_distance == 0 {
|
||||||
return None;
|
return None;
|
||||||
|
@ -119,57 +224,104 @@ impl Move for DescendMove {
|
||||||
let new_position = new_horizontal_position.down(fall_distance as i32);
|
let new_position = new_horizontal_position.down(fall_distance as i32);
|
||||||
|
|
||||||
// check whether 3 blocks vertically forward are passable
|
// check whether 3 blocks vertically forward are passable
|
||||||
if node.vertical_vel != VerticalVel::None || !is_passable(&new_horizontal_position, world) {
|
if !is_passable(&new_horizontal_position, world) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cost = WALK_ONE_BLOCK_COST + FALL_ONE_BLOCK_COST * fall_distance as f32;
|
let cost = WALK_ONE_BLOCK_COST + FALL_ONE_BLOCK_COST * fall_distance as f32;
|
||||||
|
|
||||||
Some(MoveResult {
|
Some(Edge {
|
||||||
node: Node {
|
movement: astar::Movement {
|
||||||
pos: new_position,
|
target: new_position,
|
||||||
vertical_vel: VerticalVel::None,
|
data: MoveData {
|
||||||
|
move_kind: DefaultMoves::Descend(self.0),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cost,
|
cost,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
ExecuteCtx {
|
||||||
|
entity,
|
||||||
|
target,
|
||||||
|
look_at_events,
|
||||||
|
sprint_events,
|
||||||
|
..
|
||||||
|
}: ExecuteCtx,
|
||||||
|
) {
|
||||||
|
let center = target.center();
|
||||||
|
look_at_events.send(LookAtEvent {
|
||||||
|
entity,
|
||||||
|
position: center,
|
||||||
|
});
|
||||||
|
sprint_events.send(StartSprintEvent {
|
||||||
|
entity,
|
||||||
|
direction: SprintDirection::Forward,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub struct DiagonalMove(pub CardinalDirection);
|
pub struct DiagonalMove(pub CardinalDirection);
|
||||||
impl Move for DiagonalMove {
|
impl MoveImpl for DiagonalMove {
|
||||||
fn get(&self, world: &Instance, node: &Node) -> Option<MoveResult> {
|
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
|
||||||
if node.vertical_vel != VerticalVel::None {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let right = self.0.right();
|
let right = self.0.right();
|
||||||
let offset = BlockPos::new(self.0.x() + right.x(), 0, self.0.z() + right.z());
|
let offset = BlockPos::new(self.0.x() + right.x(), 0, self.0.z() + right.z());
|
||||||
|
|
||||||
if !is_passable(
|
if !is_passable(
|
||||||
&BlockPos::new(node.pos.x + self.0.x(), node.pos.y, node.pos.z + self.0.z()),
|
&BlockPos::new(pos.x + self.0.x(), pos.y, pos.z + self.0.z()),
|
||||||
world,
|
world,
|
||||||
) && !is_passable(
|
) && !is_passable(
|
||||||
&BlockPos::new(
|
&BlockPos::new(
|
||||||
node.pos.x + self.0.right().x(),
|
pos.x + self.0.right().x(),
|
||||||
node.pos.y,
|
pos.y,
|
||||||
node.pos.z + self.0.right().z(),
|
pos.z + self.0.right().z(),
|
||||||
),
|
),
|
||||||
world,
|
world,
|
||||||
) {
|
) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !is_standable(&(node.pos + offset), world) {
|
if !is_standable(&(pos + offset), world) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let cost = WALK_ONE_BLOCK_COST * 1.4;
|
let cost = WALK_ONE_BLOCK_COST * 1.4;
|
||||||
|
|
||||||
Some(MoveResult {
|
// Some(MoveResult {
|
||||||
node: Node {
|
// node: Node {
|
||||||
pos: node.pos + offset,
|
// pos: node.pos + offset,
|
||||||
vertical_vel: VerticalVel::None,
|
// vertical_vel: VerticalVel::None,
|
||||||
|
// },
|
||||||
|
// cost,
|
||||||
|
// })
|
||||||
|
Some(Edge {
|
||||||
|
movement: astar::Movement {
|
||||||
|
target: pos + offset,
|
||||||
|
data: MoveData {
|
||||||
|
move_kind: DefaultMoves::Diagonal(self.0),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cost,
|
cost,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
ExecuteCtx {
|
||||||
|
entity,
|
||||||
|
target,
|
||||||
|
look_at_events,
|
||||||
|
sprint_events,
|
||||||
|
..
|
||||||
|
}: ExecuteCtx,
|
||||||
|
) {
|
||||||
|
let center = target.center();
|
||||||
|
look_at_events.send(LookAtEvent {
|
||||||
|
entity,
|
||||||
|
position: center,
|
||||||
|
});
|
||||||
|
sprint_events.send(StartSprintEvent {
|
||||||
|
entity,
|
||||||
|
direction: SprintDirection::Forward,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Add table
Reference in a new issue