aboutsummaryrefslogtreecommitdiff
path: root/azalea/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2023-12-15 11:26:40 -0600
committerGitHub <noreply@github.com>2023-12-15 11:26:40 -0600
commita707e2eb82b74994a16083b31fa4576332cf1995 (patch)
treedb6c2ac94dd73590befd68a9b1b0ef960410b0df /azalea/src
parent59e140ddd655c7dc6e35109b91286118c51bcc06 (diff)
downloadazalea-drasl-a707e2eb82b74994a16083b31fa4576332cf1995.tar.xz
Add mining to the pathfinder (#122)
* basic pathfinder mining poc * mining descending and autotool * pathfinder mining descending * pathfinder fixes * allow disabling pathfinder miner and other fixes * small optimization to avoid chunk vec iter lookup sometimes * seeded rng in pathfinder bench * consistently use f32::INFINITY this brings performance much closer to how it was before * astar heuristic optimization from baritone * add downward_move * fix downward move execute * avoid liquids and falling blocks when mining * fix COST_HEURISTIC * fix to not path through flowing liquids * only reset pathfinder timeout while mining if the block is close enough * cache mining costs of block positions * fix mine_while_at_start and move PathfinderDebugParticles to its own module * add ReachBlockPosGoal in other news: azalea's sin/cos functions were broken this whole time and i never noticed * clippy and add things that i accidentally didn't commit * improve wording on doc for azalea::pathfinder
Diffstat (limited to 'azalea/src')
-rw-r--r--azalea/src/auto_tool.rs51
-rw-r--r--azalea/src/bot.rs18
-rw-r--r--azalea/src/pathfinder/astar.rs6
-rw-r--r--azalea/src/pathfinder/costs.rs9
-rw-r--r--azalea/src/pathfinder/debug.rs113
-rw-r--r--azalea/src/pathfinder/goals.rs96
-rw-r--r--azalea/src/pathfinder/mining.rs99
-rw-r--r--azalea/src/pathfinder/mod.rs213
-rw-r--r--azalea/src/pathfinder/moves/basic.rs141
-rw-r--r--azalea/src/pathfinder/moves/mod.rs108
-rw-r--r--azalea/src/pathfinder/world.rs240
11 files changed, 922 insertions, 172 deletions
diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs
index 2cf53085..bc9bb474 100644
--- a/azalea/src/auto_tool.rs
+++ b/azalea/src/auto_tool.rs
@@ -4,6 +4,7 @@ use azalea_entity::{FluidOnEyes, Physics};
use azalea_inventory::{ItemSlot, Menu};
use azalea_registry::Fluid;
+#[derive(Debug)]
pub struct BestToolResult {
pub index: usize,
pub percentage_per_tick: f32,
@@ -62,7 +63,57 @@ pub fn accurate_best_tool_in_hotbar_for_block(
let mut best_slot = None;
let block = Box::<dyn Block>::from(block);
+ let registry_block = block.as_registry_block();
+ if matches!(
+ registry_block,
+ azalea_registry::Block::Water | azalea_registry::Block::Lava
+ ) {
+ // can't mine fluids
+ return BestToolResult {
+ index: 0,
+ percentage_per_tick: 0.,
+ };
+ }
+
+ // find the first slot that has an item without durability
+ for (i, item_slot) in hotbar_slots.iter().enumerate() {
+ let this_item_speed;
+ match item_slot {
+ ItemSlot::Empty => {
+ this_item_speed = Some(azalea_entity::mining::get_mine_progress(
+ block.as_ref(),
+ azalea_registry::Item::Air,
+ menu,
+ fluid_on_eyes,
+ physics,
+ ));
+ }
+ ItemSlot::Present(item_slot) => {
+ // lazy way to avoid checking durability since azalea doesn't have durability
+ // data yet
+ if item_slot.nbt.is_none() {
+ this_item_speed = Some(azalea_entity::mining::get_mine_progress(
+ block.as_ref(),
+ item_slot.kind,
+ menu,
+ fluid_on_eyes,
+ physics,
+ ));
+ } else {
+ this_item_speed = None;
+ }
+ }
+ }
+ if let Some(this_item_speed) = this_item_speed {
+ if this_item_speed > best_speed {
+ best_slot = Some(i);
+ best_speed = this_item_speed;
+ }
+ }
+ }
+
+ // now check every item
for (i, item_slot) in hotbar_slots.iter().enumerate() {
if let ItemSlot::Present(item_slot) = item_slot {
let this_item_speed = azalea_entity::mining::get_mine_progress(
diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs
index ccc016e6..529bb251 100644
--- a/azalea/src/bot.rs
+++ b/azalea/src/bot.rs
@@ -170,27 +170,35 @@ fn look_at_listener(
) {
for event in events.read() {
if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
- let (y_rot, x_rot) =
+ let new_look_direction =
direction_looking_at(&position.up(eye_height.into()), &event.position);
trace!(
"look at {:?} (currently at {:?})",
event.position,
**position
);
- (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
+ *look_direction = new_look_direction;
}
}
}
-/// Return the (`y_rot`, `x_rot`) that would make a client at `current` be
+/// Return the look direction that would make a client at `current` be
/// looking at `target`.
-fn direction_looking_at(current: &Vec3, target: &Vec3) -> (f32, f32) {
+pub fn direction_looking_at(current: &Vec3, target: &Vec3) -> LookDirection {
// borrowed from mineflayer's Bot.lookAt because i didn't want to do math
let delta = target - current;
let y_rot = (PI - f64::atan2(-delta.x, -delta.z)) * (180.0 / PI);
let ground_distance = f64::sqrt(delta.x * delta.x + delta.z * delta.z);
let x_rot = f64::atan2(delta.y, ground_distance) * -(180.0 / PI);
- (y_rot as f32, x_rot as f32)
+
+ // clamp
+ let y_rot = y_rot.rem_euclid(360.0);
+ let x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
+
+ LookDirection {
+ x_rot: x_rot as f32,
+ y_rot: y_rot as f32,
+ }
}
/// A [`PluginGroup`] for the plugins that add extra bot functionality to the
diff --git a/azalea/src/pathfinder/astar.rs b/azalea/src/pathfinder/astar.rs
index 163189af..cc1e2242 100644
--- a/azalea/src/pathfinder/astar.rs
+++ b/azalea/src/pathfinder/astar.rs
@@ -48,7 +48,7 @@ where
movement_data: None,
came_from: None,
g_score: f32::default(),
- f_score: f32::MAX,
+ f_score: f32::INFINITY,
},
);
@@ -70,14 +70,14 @@ where
let current_g_score = nodes
.get(&current_node)
.map(|n| n.g_score)
- .unwrap_or(f32::MAX);
+ .unwrap_or(f32::INFINITY);
for neighbor in successors(current_node) {
let tentative_g_score = current_g_score + neighbor.cost;
let neighbor_g_score = nodes
.get(&neighbor.movement.target)
.map(|n| n.g_score)
- .unwrap_or(f32::MAX);
+ .unwrap_or(f32::INFINITY);
if tentative_g_score - neighbor_g_score < MIN_IMPROVEMENT {
let heuristic = heuristic(neighbor.movement.target);
let f_score = tentative_g_score + heuristic;
diff --git a/azalea/src/pathfinder/costs.rs b/azalea/src/pathfinder/costs.rs
index 5c72b73a..f9b67e5f 100644
--- a/azalea/src/pathfinder/costs.rs
+++ b/azalea/src/pathfinder/costs.rs
@@ -10,6 +10,15 @@ pub const SPRINT_MULTIPLIER: f32 = SPRINT_ONE_BLOCK_COST / WALK_ONE_BLOCK_COST;
pub const JUMP_PENALTY: f32 = 2.;
pub const CENTER_AFTER_FALL_COST: f32 = WALK_ONE_BLOCK_COST - WALK_OFF_BLOCK_COST; // 0.927
+// explanation here:
+// https://github.com/cabaletta/baritone/blob/f147519a5c291015d4f18c94558a3f1bdcdb9588/src/api/java/baritone/api/Settings.java#L405
+// it's basically just the heuristic multiplier
+pub const COST_HEURISTIC: f32 = 3.563;
+
+// this one is also from baritone, it's helpful as a tiebreaker to avoid
+// breaking blocks if it can be avoided
+pub const BLOCK_BREAK_ADDITIONAL_PENALTY: f32 = 2.;
+
pub static FALL_1_25_BLOCKS_COST: LazyLock<f32> = LazyLock::new(|| distance_to_ticks(1.25));
pub static FALL_0_25_BLOCKS_COST: LazyLock<f32> = LazyLock::new(|| distance_to_ticks(0.25));
pub static JUMP_ONE_BLOCK_COST: LazyLock<f32> =
diff --git a/azalea/src/pathfinder/debug.rs b/azalea/src/pathfinder/debug.rs
new file mode 100644
index 00000000..201803c9
--- /dev/null
+++ b/azalea/src/pathfinder/debug.rs
@@ -0,0 +1,113 @@
+use azalea_client::{chat::SendChatEvent, InstanceHolder};
+use azalea_core::position::Vec3;
+use bevy_ecs::prelude::*;
+
+use super::ExecutingPath;
+
+/// A component that makes bots run /particle commands while pathfinding to show
+/// where they're going. This requires the bots to have server operator
+/// permissions, and it'll make them spam *a lot* of commands.
+///
+/// ```
+/// # use azalea::prelude::*;
+/// # use azalea::pathfinder::PathfinderDebugParticles;
+/// # #[derive(Component, Clone, Default)]
+/// # pub struct State;
+///
+/// async fn handle(mut bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
+/// match event {
+/// azalea::Event::Init => {
+/// bot.ecs
+/// .lock()
+/// .entity_mut(bot.entity)
+/// .insert(PathfinderDebugParticles);
+/// }
+/// _ => {}
+/// }
+/// Ok(())
+/// }
+/// ```
+#[derive(Component)]
+pub struct PathfinderDebugParticles;
+
+pub fn debug_render_path_with_particles(
+ mut query: Query<(Entity, &ExecutingPath, &InstanceHolder), With<PathfinderDebugParticles>>,
+ // 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>
+ chat_events: Option<ResMut<Events<SendChatEvent>>>,
+ mut tick_count: Local<usize>,
+) {
+ let Some(mut chat_events) = chat_events else {
+ return;
+ };
+ if *tick_count >= 2 {
+ *tick_count = 0;
+ } else {
+ *tick_count += 1;
+ return;
+ }
+ for (entity, executing_path, instance_holder) in &mut query {
+ if executing_path.path.is_empty() {
+ continue;
+ }
+
+ let chunks = &instance_holder.instance.read().chunks;
+
+ let mut start = executing_path.last_reached_node;
+ for (i, movement) in executing_path.path.iter().enumerate() {
+ // /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100
+
+ let end = movement.target;
+
+ let start_vec3 = start.center();
+ let end_vec3 = end.center();
+
+ let step_count = (start_vec3.distance_to_sqr(&end_vec3).sqrt() * 4.0) as usize;
+
+ let target_block_state = chunks.get_block_state(&movement.target).unwrap_or_default();
+ let above_target_block_state = chunks
+ .get_block_state(&movement.target.up(1))
+ .unwrap_or_default();
+ // this isn't foolproof, there might be another block that could be mined
+ // depending on the move, but it's good enough for debugging
+ // purposes
+ let is_mining = !super::world::is_block_state_passable(target_block_state)
+ || !super::world::is_block_state_passable(above_target_block_state);
+
+ let (r, g, b): (f64, f64, f64) = if i == 0 {
+ (0., 1., 0.)
+ } else if is_mining {
+ (1., 0., 0.)
+ } else {
+ (0., 1., 1.)
+ };
+
+ // interpolate between the start and end positions
+ for i in 0..step_count {
+ let percent = i as f64 / step_count as f64;
+ let pos = Vec3 {
+ x: start_vec3.x + (end_vec3.x - start_vec3.x) * percent,
+ y: start_vec3.y + (end_vec3.y - start_vec3.y) * percent,
+ z: start_vec3.z + (end_vec3.z - start_vec3.z) * percent,
+ };
+ let particle_command = format!(
+ "/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}",
+ size = 1,
+ start_x = pos.x,
+ start_y = pos.y,
+ start_z = pos.z,
+ delta_x = 0,
+ delta_y = 0,
+ delta_z = 0,
+ count = 1
+ );
+ chat_events.send(SendChatEvent {
+ entity,
+ content: particle_command,
+ });
+ }
+
+ start = movement.target;
+ }
+ }
+}
diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs
index 9c01d486..3f8c7993 100644
--- a/azalea/src/pathfinder/goals.rs
+++ b/azalea/src/pathfinder/goals.rs
@@ -1,12 +1,21 @@
+//! The goals that a pathfinder can try to reach.
+
use std::f32::consts::SQRT_2;
use azalea_core::position::{BlockPos, Vec3};
+use azalea_world::ChunkStorage;
+
+use super::costs::{COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST};
-use super::{
- costs::{FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST},
- Goal,
-};
+pub trait Goal {
+ #[must_use]
+ fn heuristic(&self, n: BlockPos) -> f32;
+ #[must_use]
+ fn success(&self, n: BlockPos) -> bool;
+}
+/// Move to the given block position.
+#[derive(Debug)]
pub struct BlockPosGoal(pub BlockPos);
impl Goal for BlockPosGoal {
fn heuristic(&self, n: BlockPos) -> f32 {
@@ -36,9 +45,11 @@ fn xz_heuristic(dx: f32, dz: f32) -> f32 {
diagonal = z;
}
- diagonal * SQRT_2 + straight
+ (diagonal * SQRT_2 + straight) * COST_HEURISTIC
}
+/// Move to the given block position, ignoring the y axis.
+#[derive(Debug)]
pub struct XZGoal {
pub x: i32,
pub z: i32,
@@ -62,6 +73,8 @@ fn y_heuristic(dy: f32) -> f32 {
}
}
+/// Move to the given y coordinate.
+#[derive(Debug)]
pub struct YGoal {
pub y: i32,
}
@@ -75,6 +88,8 @@ impl Goal for YGoal {
}
}
+/// Get within the given radius of the given position.
+#[derive(Debug)]
pub struct RadiusGoal {
pub pos: Vec3,
pub radius: f32,
@@ -96,6 +111,8 @@ impl Goal for RadiusGoal {
}
}
+/// Do the opposite of the given goal.
+#[derive(Debug)]
pub struct InverseGoal<T: Goal>(pub T);
impl<T: Goal> Goal for InverseGoal<T> {
fn heuristic(&self, n: BlockPos) -> f32 {
@@ -106,6 +123,8 @@ impl<T: Goal> Goal for InverseGoal<T> {
}
}
+/// Do either of the given goals, whichever is closer.
+#[derive(Debug)]
pub struct OrGoal<T: Goal, U: Goal>(pub T, pub U);
impl<T: Goal, U: Goal> Goal for OrGoal<T, U> {
fn heuristic(&self, n: BlockPos) -> f32 {
@@ -116,6 +135,24 @@ impl<T: Goal, U: Goal> Goal for OrGoal<T, U> {
}
}
+/// Do any of the given goals, whichever is closest.
+#[derive(Debug)]
+pub struct OrGoals<T: Goal>(pub Vec<T>);
+impl<T: Goal> Goal for OrGoals<T> {
+ fn heuristic(&self, n: BlockPos) -> f32 {
+ self.0
+ .iter()
+ .map(|goal| goal.heuristic(n))
+ .min_by(|a, b| a.partial_cmp(b).unwrap())
+ .unwrap_or(f32::INFINITY)
+ }
+ fn success(&self, n: BlockPos) -> bool {
+ self.0.iter().any(|goal| goal.success(n))
+ }
+}
+
+/// Try to reach both of the given goals.
+#[derive(Debug)]
pub struct AndGoal<T: Goal, U: Goal>(pub T, pub U);
impl<T: Goal, U: Goal> Goal for AndGoal<T, U> {
fn heuristic(&self, n: BlockPos) -> f32 {
@@ -125,3 +162,52 @@ impl<T: Goal, U: Goal> Goal for AndGoal<T, U> {
self.0.success(n) && self.1.success(n)
}
}
+
+/// Try to reach all of the given goals.
+#[derive(Debug)]
+pub struct AndGoals<T: Goal>(pub Vec<T>);
+impl<T: Goal> Goal for AndGoals<T> {
+ fn heuristic(&self, n: BlockPos) -> f32 {
+ self.0
+ .iter()
+ .map(|goal| goal.heuristic(n))
+ .max_by(|a, b| a.partial_cmp(b).unwrap())
+ .unwrap_or(f32::INFINITY)
+ }
+ fn success(&self, n: BlockPos) -> bool {
+ self.0.iter().all(|goal| goal.success(n))
+ }
+}
+
+/// Move to a position where we can reach the given block.
+#[derive(Debug)]
+pub struct ReachBlockPosGoal {
+ pub pos: BlockPos,
+ pub chunk_storage: ChunkStorage,
+}
+impl Goal for ReachBlockPosGoal {
+ fn heuristic(&self, n: BlockPos) -> f32 {
+ BlockPosGoal(self.pos).heuristic(n)
+ }
+ fn success(&self, n: BlockPos) -> bool {
+ // only do the expensive check if we're close enough
+ let max_pick_range = 6;
+ let actual_pick_range = 4.5;
+
+ let distance = (self.pos - n).length_sqr();
+ if distance > max_pick_range * max_pick_range {
+ return false;
+ }
+
+ let eye_position = n.to_vec3_floored() + Vec3::new(0.5, 1.62, 0.5);
+ let look_direction = crate::direction_looking_at(&eye_position, &self.pos.center());
+ let block_hit_result = azalea_client::interact::pick(
+ &look_direction,
+ &eye_position,
+ &self.chunk_storage,
+ actual_pick_range,
+ );
+
+ block_hit_result.block_pos == self.pos
+ }
+}
diff --git a/azalea/src/pathfinder/mining.rs b/azalea/src/pathfinder/mining.rs
index d5977973..1bc08c43 100644
--- a/azalea/src/pathfinder/mining.rs
+++ b/azalea/src/pathfinder/mining.rs
@@ -1,30 +1,109 @@
-use azalea_block::BlockState;
+use std::{cell::UnsafeCell, ops::RangeInclusive};
+
+use azalea_block::{BlockState, BlockStates};
use azalea_inventory::Menu;
use nohash_hasher::IntMap;
use crate::auto_tool::best_tool_in_hotbar_for_block;
+use super::costs::BLOCK_BREAK_ADDITIONAL_PENALTY;
+
pub struct MiningCache {
- block_state_id_costs: IntMap<u32, f32>,
- inventory_menu: Menu,
+ block_state_id_costs: UnsafeCell<IntMap<u32, f32>>,
+ inventory_menu: Option<Menu>,
+
+ water_block_state_range: RangeInclusive<u32>,
+ lava_block_state_range: RangeInclusive<u32>,
+
+ falling_blocks: Vec<BlockState>,
}
impl MiningCache {
- pub fn new(inventory_menu: Menu) -> Self {
+ pub fn new(inventory_menu: Option<Menu>) -> Self {
+ let water_block_states = BlockStates::from(azalea_registry::Block::Water);
+ let lava_block_states = BlockStates::from(azalea_registry::Block::Lava);
+
+ let mut water_block_state_range_min = u32::MAX;
+ let mut water_block_state_range_max = u32::MIN;
+ for state in water_block_states {
+ water_block_state_range_min = water_block_state_range_min.min(state.id);
+ water_block_state_range_max = water_block_state_range_max.max(state.id);
+ }
+ let water_block_state_range = water_block_state_range_min..=water_block_state_range_max;
+
+ let mut lava_block_state_range_min = u32::MAX;
+ let mut lava_block_state_range_max = u32::MIN;
+ for state in lava_block_states {
+ lava_block_state_range_min = lava_block_state_range_min.min(state.id);
+ lava_block_state_range_max = lava_block_state_range_max.max(state.id);
+ }
+ let lava_block_state_range = lava_block_state_range_min..=lava_block_state_range_max;
+
+ let mut falling_blocks: Vec<BlockState> = vec![
+ azalea_registry::Block::Sand.into(),
+ azalea_registry::Block::RedSand.into(),
+ azalea_registry::Block::Gravel.into(),
+ azalea_registry::Block::Anvil.into(),
+ azalea_registry::Block::ChippedAnvil.into(),
+ azalea_registry::Block::DamagedAnvil.into(),
+ // concrete powders
+ azalea_registry::Block::WhiteConcretePowder.into(),
+ azalea_registry::Block::OrangeConcretePowder.into(),
+ azalea_registry::Block::MagentaConcretePowder.into(),
+ azalea_registry::Block::LightBlueConcretePowder.into(),
+ azalea_registry::Block::YellowConcretePowder.into(),
+ azalea_registry::Block::LimeConcretePowder.into(),
+ azalea_registry::Block::PinkConcretePowder.into(),
+ azalea_registry::Block::GrayConcretePowder.into(),
+ azalea_registry::Block::LightGrayConcretePowder.into(),
+ azalea_registry::Block::CyanConcretePowder.into(),
+ azalea_registry::Block::PurpleConcretePowder.into(),
+ azalea_registry::Block::BlueConcretePowder.into(),
+ azalea_registry::Block::BrownConcretePowder.into(),
+ azalea_registry::Block::GreenConcretePowder.into(),
+ azalea_registry::Block::RedConcretePowder.into(),
+ azalea_registry::Block::BlackConcretePowder.into(),
+ ];
+ falling_blocks.sort_unstable_by_key(|block| block.id);
+
Self {
- block_state_id_costs: IntMap::default(),
+ block_state_id_costs: UnsafeCell::new(IntMap::default()),
inventory_menu,
+ water_block_state_range,
+ lava_block_state_range,
+ falling_blocks,
}
}
- pub fn cost_for(&mut self, block: BlockState) -> f32 {
- if let Some(cost) = self.block_state_id_costs.get(&block.id) {
+ pub fn cost_for(&self, block: BlockState) -> f32 {
+ let Some(inventory_menu) = &self.inventory_menu else {
+ return f32::INFINITY;
+ };
+
+ // SAFETY: mining is single-threaded, so this is safe
+ let block_state_id_costs = unsafe { &mut *self.block_state_id_costs.get() };
+
+ if let Some(cost) = block_state_id_costs.get(&block.id) {
*cost
} else {
- let best_tool_result = best_tool_in_hotbar_for_block(block, &self.inventory_menu);
- let cost = 1. / best_tool_result.percentage_per_tick;
- self.block_state_id_costs.insert(block.id, cost);
+ let best_tool_result = best_tool_in_hotbar_for_block(block, inventory_menu);
+ let mut cost = 1. / best_tool_result.percentage_per_tick;
+
+ cost += BLOCK_BREAK_ADDITIONAL_PENALTY;
+
+ block_state_id_costs.insert(block.id, cost);
cost
}
}
+
+ pub fn is_liquid(&self, block: BlockState) -> bool {
+ self.water_block_state_range.contains(&block.id)
+ || self.lava_block_state_range.contains(&block.id)
+ }
+
+ pub fn is_falling_block(&self, block: BlockState) -> bool {
+ self.falling_blocks
+ .binary_search_by_key(&block.id, |block| block.id)
+ .is_ok()
+ }
}
diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs
index 525f982d..9fd769e6 100644
--- a/azalea/src/pathfinder/mod.rs
+++ b/azalea/src/pathfinder/mod.rs
@@ -1,8 +1,10 @@
-//! A pathfinding plugin to make bots navigate the world. A lot of this code is
-//! based on [Baritone](https://github.com/cabaletta/baritone).
+//! A pathfinding plugin to make bots able to traverse the world.
+//!
+//! Much of this code is based on [Baritone](https://github.com/cabaletta/baritone).
pub mod astar;
pub mod costs;
+mod debug;
pub mod goals;
pub mod mining;
pub mod moves;
@@ -23,11 +25,11 @@ use crate::ecs::{
};
use crate::pathfinder::moves::PathfinderCtx;
use crate::pathfinder::world::CachedWorld;
-use azalea_client::chat::SendChatEvent;
-use azalea_client::inventory::{InventoryComponent, InventorySet};
+use azalea_client::inventory::{InventoryComponent, InventorySet, SetSelectedHotbarSlotEvent};
+use azalea_client::mining::{Mining, StartMiningBlockEvent};
use azalea_client::movement::MoveEventsSet;
-use azalea_client::{StartSprintEvent, StartWalkEvent};
-use azalea_core::position::{BlockPos, Vec3};
+use azalea_client::{InstanceHolder, StartSprintEvent, StartWalkEvent};
+use azalea_core::position::BlockPos;
use azalea_core::tick::GameTick;
use azalea_entity::metadata::Player;
use azalea_entity::LocalEntity;
@@ -35,11 +37,9 @@ use azalea_entity::{Physics, Position};
use azalea_physics::PhysicsSet;
use azalea_world::{InstanceContainer, InstanceName};
use bevy_app::{PreUpdate, Update};
-use bevy_ecs::event::Events;
use bevy_ecs::prelude::Event;
use bevy_ecs::query::Changed;
use bevy_ecs::schedule::IntoSystemConfigs;
-use bevy_ecs::system::{Local, ResMut};
use bevy_tasks::{AsyncComputeTaskPool, Task};
use futures_lite::future;
use std::collections::VecDeque;
@@ -48,6 +48,9 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug, error, info, trace, warn};
+use self::debug::debug_render_path_with_particles;
+pub use self::debug::PathfinderDebugParticles;
+use self::goals::Goal;
use self::mining::MiningCache;
use self::moves::{ExecuteCtx, IsReachedCtx, SuccessorsFn};
@@ -93,11 +96,12 @@ impl Plugin for PathfinderPlugin {
}
/// A component that makes this client able to pathfind.
-#[derive(Component, Default)]
+#[derive(Component, Default, Clone)]
pub struct Pathfinder {
pub goal: Option<Arc<dyn Goal + Send + Sync>>,
pub successors_fn: Option<SuccessorsFn>,
pub is_calculating: bool,
+ pub allow_mining: bool,
pub goto_id: Arc<AtomicUsize>,
}
@@ -120,6 +124,9 @@ pub struct GotoEvent {
/// The function that's used for checking what moves are possible. Usually
/// `pathfinder::moves::default_move`
pub successors_fn: SuccessorsFn,
+
+ /// Whether the bot is allowed to break blocks while pathfinding.
+ pub allow_mining: bool,
}
#[derive(Event, Clone)]
pub struct PathFoundEvent {
@@ -128,6 +135,7 @@ pub struct PathFoundEvent {
pub path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
pub is_partial: bool,
pub successors_fn: SuccessorsFn,
+ pub allow_mining: bool,
}
#[allow(clippy::type_complexity)]
@@ -142,6 +150,7 @@ fn add_default_pathfinder(
pub trait PathfinderClientExt {
fn goto(&self, goal: impl Goal + Send + Sync + 'static);
+ fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static);
fn stop_pathfinding(&self);
}
@@ -158,6 +167,18 @@ impl PathfinderClientExt for azalea_client::Client {
entity: self.entity,
goal: Arc::new(goal),
successors_fn: moves::default_move,
+ allow_mining: true,
+ });
+ }
+
+ /// Same as [`goto`](Self::goto). but the bot won't break any blocks while
+ /// executing the path.
+ fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static) {
+ self.ecs.lock().send_event(GotoEvent {
+ entity: self.entity,
+ goal: Arc::new(goal),
+ successors_fn: moves::default_move,
+ allow_mining: false,
});
}
@@ -191,10 +212,19 @@ fn goto_listener(
.get_mut(event.entity)
.expect("Called goto on an entity that's not in the world");
+ if event.goal.success(BlockPos::from(position)) {
+ // we're already at the goal, nothing to do
+ pathfinder.goal = None;
+ pathfinder.successors_fn = None;
+ pathfinder.is_calculating = false;
+ continue;
+ }
+
// we store the goal so it can be recalculated later if necessary
pathfinder.goal = Some(event.goal.clone());
pathfinder.successors_fn = Some(event.successors_fn);
pathfinder.is_calculating = true;
+ pathfinder.allow_mining = event.allow_mining;
let start = if let Some(executing_path) = executing_path
&& let Some(final_node) = executing_path.path.back()
@@ -220,7 +250,13 @@ fn goto_listener(
let goto_id_atomic = pathfinder.goto_id.clone();
let goto_id = goto_id_atomic.fetch_add(1, atomic::Ordering::Relaxed) + 1;
- let mining_cache = MiningCache::new(inventory.inventory_menu.clone());
+
+ let allow_mining = event.allow_mining;
+ let mining_cache = MiningCache::new(if allow_mining {
+ Some(inventory.inventory_menu.clone())
+ } else {
+ None
+ });
let task = thread_pool.spawn(async move {
debug!("start: {start:?}");
@@ -248,7 +284,11 @@ fn goto_listener(
debug!("partial: {partial:?}");
let duration = end_time - start_time;
if partial {
- info!("Pathfinder took {duration:?} (incomplete path)");
+ if movements.is_empty() {
+ info!("Pathfinder took {duration:?} (empty path)");
+ } else {
+ info!("Pathfinder took {duration:?} (incomplete path)");
+ }
// wait a bit so it's not a busy loop
std::thread::sleep(Duration::from_millis(100));
} else {
@@ -289,6 +329,7 @@ fn goto_listener(
path: Some(path),
is_partial,
successors_fn,
+ allow_mining,
})
});
@@ -342,7 +383,11 @@ fn path_found_listener(
.expect("Entity tried to pathfind but the entity isn't in a valid world");
let successors_fn: moves::SuccessorsFn = event.successors_fn;
let cached_world = CachedWorld::new(world_lock);
- let mining_cache = MiningCache::new(inventory.inventory_menu.clone());
+ let mining_cache = MiningCache::new(if event.allow_mining {
+ Some(inventory.inventory_menu.clone())
+ } else {
+ None
+ });
let successors = |pos: BlockPos| {
call_successors_fn(&cached_world, &mining_cache, successors_fn, pos)
};
@@ -399,8 +444,21 @@ fn path_found_listener(
}
}
-fn timeout_movement(mut query: Query<(&Pathfinder, &mut ExecutingPath, &Position)>) {
- for (pathfinder, mut executing_path, position) in &mut query {
+fn timeout_movement(
+ mut query: Query<(&Pathfinder, &mut ExecutingPath, &Position, Option<&Mining>)>,
+) {
+ for (pathfinder, mut executing_path, position, mining) in &mut query {
+ // don't timeout if we're mining
+ if let Some(mining) = mining {
+ // also make sure we're close enough to the block that's being mined
+ if mining.pos.distance_to_sqr(&BlockPos::from(position)) < 6_i32.pow(2) {
+ // also reset the last_node_reached_at so we don't timeout after we finish
+ // mining
+ executing_path.last_node_reached_at = Instant::now();
+ continue;
+ }
+ }
+
if executing_path.last_node_reached_at.elapsed() > Duration::from_secs(2)
&& !pathfinder.is_calculating
&& !executing_path.path.is_empty()
@@ -535,7 +593,11 @@ fn check_for_path_obstruction(
// obstruction check (the path we're executing isn't possible anymore)
let cached_world = CachedWorld::new(world_lock);
- let mining_cache = MiningCache::new(inventory.inventory_menu.clone());
+ let mining_cache = MiningCache::new(if pathfinder.allow_mining {
+ Some(inventory.inventory_menu.clone())
+ } else {
+ None
+ });
let successors =
|pos: BlockPos| call_successors_fn(&cached_world, &mining_cache, successors_fn, pos);
@@ -580,6 +642,7 @@ fn recalculate_near_end_of_path(
entity,
goal,
successors_fn,
+ allow_mining: pathfinder.allow_mining,
});
pathfinder.is_calculating = true;
@@ -614,14 +677,27 @@ fn recalculate_near_end_of_path(
}
}
+#[allow(clippy::type_complexity)]
fn tick_execute_path(
- mut query: Query<(Entity, &mut ExecutingPath, &Position, &Physics)>,
+ mut query: Query<(
+ Entity,
+ &mut ExecutingPath,
+ &Position,
+ &Physics,
+ Option<&Mining>,
+ &InstanceHolder,
+ &InventoryComponent,
+ )>,
mut look_at_events: EventWriter<LookAtEvent>,
mut sprint_events: EventWriter<StartSprintEvent>,
mut walk_events: EventWriter<StartWalkEvent>,
mut jump_events: EventWriter<JumpEvent>,
+ mut start_mining_events: EventWriter<StartMiningBlockEvent>,
+ mut set_selected_hotbar_slot_events: EventWriter<SetSelectedHotbarSlotEvent>,
) {
- for (entity, executing_path, position, physics) in &mut query {
+ for (entity, executing_path, position, physics, mining, instance_holder, inventory_component) in
+ &mut query
+ {
if let Some(movement) = executing_path.path.front() {
let ctx = ExecuteCtx {
entity,
@@ -629,10 +705,16 @@ fn tick_execute_path(
position: **position,
start: executing_path.last_reached_node,
physics,
+ is_currently_mining: mining.is_some(),
+ instance: instance_holder.instance.clone(),
+ menu: inventory_component.inventory_menu.clone(),
+
look_at_events: &mut look_at_events,
sprint_events: &mut sprint_events,
walk_events: &mut walk_events,
jump_events: &mut jump_events,
+ start_mining_events: &mut start_mining_events,
+ set_selected_hotbar_slot_events: &mut set_selected_hotbar_slot_events,
};
trace!("executing move");
(movement.data.execute)(ctx);
@@ -652,6 +734,7 @@ fn recalculate_if_has_goal_but_no_path(
entity,
goal,
successors_fn: pathfinder.successors_fn.unwrap(),
+ allow_mining: pathfinder.allow_mining,
});
pathfinder.is_calculating = true;
}
@@ -718,101 +801,6 @@ fn stop_pathfinding_on_instance_change(
}
}
-/// A component that makes bots run /particle commands while pathfinding to show
-/// where they're going. This requires the bots to have server operator
-/// permissions, and it'll make them spam *a lot* of commands.
-///
-/// ```
-/// # use azalea::prelude::*;
-/// # use azalea::pathfinder::PathfinderDebugParticles;
-/// # #[derive(Component, Clone, Default)]
-/// # pub struct State;
-///
-/// async fn handle(mut bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
-/// match event {
-/// azalea::Event::Init => {
-/// bot.ecs
-/// .lock()
-/// .entity_mut(bot.entity)
-/// .insert(PathfinderDebugParticles);
-/// }
-/// _ => {}
-/// }
-/// Ok(())
-/// }
-/// ```
-#[derive(Component)]
-pub struct PathfinderDebugParticles;
-
-fn debug_render_path_with_particles(
- mut query: Query<(Entity, &ExecutingPath), With<PathfinderDebugParticles>>,
- // 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>
- chat_events: Option<ResMut<Events<SendChatEvent>>>,
- mut tick_count: Local<usize>,
-) {
- let Some(mut chat_events) = chat_events else {
- return;
- };
- if *tick_count >= 2 {
- *tick_count = 0;
- } else {
- *tick_count += 1;
- return;
- }
- for (entity, executing_path) in &mut query {
- if executing_path.path.is_empty() {
- continue;
- }
-
- let mut start = executing_path.last_reached_node;
- for (i, movement) in executing_path.path.iter().enumerate() {
- // /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100
-
- let end = movement.target;
-
- let start_vec3 = start.center();
- let end_vec3 = end.center();
-
- 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
- for i in 0..step_count {
- let percent = i as f64 / step_count as f64;
- let pos = Vec3 {
- x: start_vec3.x + (end_vec3.x - start_vec3.x) * percent,
- y: start_vec3.y + (end_vec3.y - start_vec3.y) * percent,
- z: start_vec3.z + (end_vec3.z - start_vec3.z) * percent,
- };
- let particle_command = format!(
- "/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}",
- size = 1,
- start_x = pos.x,
- start_y = pos.y,
- start_z = pos.z,
- delta_x = 0,
- delta_y = 0,
- delta_z = 0,
- count = 1
- );
- chat_events.send(SendChatEvent {
- entity,
- content: particle_command,
- });
- }
-
- start = movement.target;
- }
- }
-}
-
-pub trait Goal {
- fn heuristic(&self, n: BlockPos) -> f32;
- fn success(&self, n: BlockPos) -> bool;
-}
-
/// Checks whether the path has been obstructed, and returns Some(index) if it
/// has been. The index is of the first obstructed node.
fn check_path_obstructed<SuccessorsFn>(
@@ -911,6 +899,7 @@ mod tests {
entity: simulation.entity,
goal: Arc::new(BlockPosGoal(end_pos)),
successors_fn: moves::default_move,
+ allow_mining: false,
});
simulation
}
diff --git a/azalea/src/pathfinder/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs
index 957e24c6..54a6dc6a 100644
--- a/azalea/src/pathfinder/moves/basic.rs
+++ b/azalea/src/pathfinder/moves/basic.rs
@@ -16,17 +16,20 @@ pub fn basic_move(ctx: &mut PathfinderCtx, node: BlockPos) {
descend_move(ctx, node);
diagonal_move(ctx, node);
descend_forward_1_move(ctx, node);
+ downward_move(ctx, node);
}
fn forward_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
for dir in CardinalDirection::iter() {
let offset = BlockPos::new(dir.x(), 0, dir.z());
- if !ctx.world.is_standable(pos + offset) {
+ let mut cost = SPRINT_ONE_BLOCK_COST;
+
+ let break_cost = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
+ if break_cost == f32::INFINITY {
continue;
}
-
- let cost = SPRINT_ONE_BLOCK_COST;
+ cost += break_cost;
ctx.edges.push(Edge {
movement: astar::Movement {
@@ -43,6 +46,14 @@ fn forward_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
fn execute_forward_move(mut ctx: ExecuteCtx) {
let center = ctx.target.center();
+
+ if ctx.mine_while_at_start(ctx.target.up(1)) {
+ return;
+ }
+ if ctx.mine_while_at_start(ctx.target) {
+ return;
+ }
+
ctx.look_at(center);
ctx.sprint(SprintDirection::Forward);
}
@@ -51,14 +62,22 @@ fn ascend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
for dir in CardinalDirection::iter() {
let offset = BlockPos::new(dir.x(), 1, dir.z());
- if !ctx.world.is_block_passable(pos.up(2)) {
+ let break_cost_1 = ctx
+ .world
+ .cost_for_breaking_block(pos.up(2), ctx.mining_cache);
+ if break_cost_1 == f32::INFINITY {
continue;
}
- if !ctx.world.is_standable(pos + offset) {
+ let break_cost_2 = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
+ if break_cost_2 == f32::INFINITY {
continue;
}
- let cost = SPRINT_ONE_BLOCK_COST + JUMP_PENALTY + *JUMP_ONE_BLOCK_COST;
+ let cost = SPRINT_ONE_BLOCK_COST
+ + JUMP_PENALTY
+ + *JUMP_ONE_BLOCK_COST
+ + break_cost_1
+ + break_cost_2;
ctx.edges.push(Edge {
movement: astar::Movement {
@@ -81,6 +100,16 @@ fn execute_ascend_move(mut ctx: ExecuteCtx) {
..
} = ctx;
+ if ctx.mine_while_at_start(start.up(2)) {
+ return;
+ }
+ if ctx.mine_while_at_start(target) {
+ return;
+ }
+ if ctx.mine_while_at_start(target.up(1)) {
+ return;
+ }
+
let target_center = target.center();
ctx.look_at(target_center);
@@ -123,19 +152,39 @@ fn descend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
for dir in CardinalDirection::iter() {
let dir_delta = BlockPos::new(dir.x(), 0, dir.z());
let new_horizontal_position = pos + dir_delta;
- let fall_distance = ctx.world.fall_distance(new_horizontal_position);
- if fall_distance == 0 || fall_distance > 3 {
+
+ let break_cost_1 = ctx
+ .world
+ .cost_for_passing(new_horizontal_position, ctx.mining_cache);
+ if break_cost_1 == f32::INFINITY {
continue;
}
- let new_position = new_horizontal_position.down(fall_distance as i32);
- // check whether 3 blocks vertically forward are passable
- if !ctx.world.is_passable(new_horizontal_position) {
+ let mut fall_distance = ctx.world.fall_distance(new_horizontal_position);
+ if fall_distance > 3 {
continue;
}
- // check whether we can stand on the target position
- if !ctx.world.is_standable(new_position) {
- continue;
+
+ if fall_distance == 0 {
+ // if the fall distance is 0, set it to 1 so we try mining
+ fall_distance = 1
+ }
+
+ let new_position = new_horizontal_position.down(fall_distance as i32);
+
+ // only mine if we're descending 1 block
+ let break_cost_2;
+ if fall_distance == 1 {
+ break_cost_2 = ctx.world.cost_for_standing(new_position, ctx.mining_cache);
+ if break_cost_2 == f32::INFINITY {
+ continue;
+ }
+ } else {
+ // check whether we can stand on the target position
+ if !ctx.world.is_standable(new_position) {
+ continue;
+ }
+ break_cost_2 = 0.;
}
let cost = WALK_OFF_BLOCK_COST
@@ -145,9 +194,11 @@ fn descend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
.copied()
// avoid panicking if we fall more than the size of FALL_N_BLOCKS_COST
// probably not possible but just in case
- .unwrap_or(f32::MAX),
+ .unwrap_or(f32::INFINITY),
CENTER_AFTER_FALL_COST,
- );
+ )
+ + break_cost_1
+ + break_cost_2;
ctx.edges.push(Edge {
movement: astar::Movement {
@@ -169,6 +220,12 @@ fn execute_descend_move(mut ctx: ExecuteCtx) {
..
} = ctx;
+ for i in (0..=(start.y - target.y + 1)).rev() {
+ if ctx.mine_while_at_start(target.up(i)) {
+ return;
+ }
+ }
+
let start_center = start.center();
let center = target.center();
@@ -249,7 +306,7 @@ fn descend_forward_1_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
.copied()
// avoid panicking if we fall more than the size of FALL_N_BLOCKS_COST
// probably not possible but just in case
- .unwrap_or(f32::MAX),
+ .unwrap_or(f32::INFINITY),
CENTER_AFTER_FALL_COST,
);
@@ -310,3 +367,53 @@ fn execute_diagonal_move(mut ctx: ExecuteCtx) {
ctx.look_at(target_center);
ctx.sprint(SprintDirection::Forward);
}
+
+/// Go directly down, usually by mining.
+fn downward_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
+ // make sure we land on a solid block after breaking the one below us
+ if !ctx.world.is_block_solid(pos.down(2)) {
+ return;
+ }
+
+ let break_cost = ctx
+ .world
+ .cost_for_breaking_block(pos.down(1), ctx.mining_cache);
+ if break_cost == f32::INFINITY {
+ return;
+ }
+
+ let cost = FALL_N_BLOCKS_COST[1] + break_cost;
+
+ ctx.edges.push(Edge {
+ movement: astar::Movement {
+ target: pos.down(1),
+ data: MoveData {
+ execute: &execute_downward_move,
+ is_reached: &default_is_reached,
+ },
+ },
+ cost,
+ })
+}
+fn execute_downward_move(mut ctx: ExecuteCtx) {
+ let ExecuteCtx {
+ target, position, ..
+ } = ctx;
+
+ let target_center = target.center();
+
+ let horizontal_distance_from_target =
+ (target_center - position).horizontal_distance_sqr().sqrt();
+
+ if horizontal_distance_from_target > 0.25 {
+ ctx.look_at(target_center);
+ ctx.walk(WalkDirection::Forward);
+ } else if ctx.mine_while_at_start(target) {
+ ctx.walk(WalkDirection::None);
+ } else if BlockPos::from(position) != target {
+ ctx.look_at(target_center);
+ ctx.walk(WalkDirection::Forward);
+ } else {
+ ctx.walk(WalkDirection::None);
+ }
+}
diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs
index e5b837ea..bb10b192 100644
--- a/azalea/src/pathfinder/moves/mod.rs
+++ b/azalea/src/pathfinder/moves/mod.rs
@@ -1,14 +1,24 @@
pub mod basic;
pub mod parkour;
-use std::fmt::Debug;
-
-use crate::{JumpEvent, LookAtEvent};
-
-use super::{astar, mining::MiningCache, world::CachedWorld};
-use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
+use std::{fmt::Debug, sync::Arc};
+
+use crate::{auto_tool::best_tool_in_hotbar_for_block, JumpEvent, LookAtEvent};
+
+use super::{
+ astar,
+ mining::MiningCache,
+ world::{is_block_state_passable, CachedWorld},
+};
+use azalea_client::{
+ inventory::SetSelectedHotbarSlotEvent, mining::StartMiningBlockEvent, SprintDirection,
+ StartSprintEvent, StartWalkEvent, WalkDirection,
+};
use azalea_core::position::{BlockPos, Vec3};
+use azalea_inventory::Menu;
+use azalea_world::Instance;
use bevy_ecs::{entity::Entity, event::EventWriter};
+use parking_lot::RwLock;
type Edge = astar::Edge<BlockPos, MoveData>;
@@ -35,7 +45,7 @@ impl Debug for MoveData {
}
}
-pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
+pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'w5, 'w6, 'a> {
pub entity: Entity,
/// The node that we're trying to reach.
pub target: BlockPos,
@@ -43,14 +53,19 @@ pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
pub start: BlockPos,
pub position: Vec3,
pub physics: &'a azalea_entity::Physics,
+ pub is_currently_mining: bool,
+ pub instance: Arc<RwLock<Instance>>,
+ pub menu: Menu,
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 start_mining_events: &'a mut EventWriter<'w5, StartMiningBlockEvent>,
+ pub set_selected_hotbar_slot_events: &'a mut EventWriter<'w6, SetSelectedHotbarSlotEvent>,
}
-impl ExecuteCtx<'_, '_, '_, '_, '_> {
+impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> {
pub fn look_at(&mut self, position: Vec3) {
self.look_at_events.send(LookAtEvent {
entity: self.entity,
@@ -63,6 +78,13 @@ impl ExecuteCtx<'_, '_, '_, '_, '_> {
});
}
+ pub fn look_at_exact(&mut self, position: Vec3) {
+ self.look_at_events.send(LookAtEvent {
+ entity: self.entity,
+ position,
+ });
+ }
+
pub fn sprint(&mut self, direction: SprintDirection) {
self.sprint_events.send(StartSprintEvent {
entity: self.entity,
@@ -82,6 +104,76 @@ impl ExecuteCtx<'_, '_, '_, '_, '_> {
entity: self.entity,
});
}
+
+ /// Returns whether this block could be mined.
+ pub fn should_mine(&mut self, block: BlockPos) -> bool {
+ let block_state = self
+ .instance
+ .read()
+ .get_block_state(&block)
+ .unwrap_or_default();
+ if is_block_state_passable(block_state) {
+ // block is already passable, no need to mine it
+ return false;
+ }
+
+ true
+ }
+
+ /// Mine the block at the given position. Returns whether the block is being
+ /// mined.
+ pub fn mine(&mut self, block: BlockPos) -> bool {
+ let block_state = self
+ .instance
+ .read()
+ .get_block_state(&block)
+ .unwrap_or_default();
+ if is_block_state_passable(block_state) {
+ // block is already passable, no need to mine it
+ return false;
+ }
+
+ let best_tool_result = best_tool_in_hotbar_for_block(block_state, &self.menu);
+
+ self.set_selected_hotbar_slot_events
+ .send(SetSelectedHotbarSlotEvent {
+ entity: self.entity,
+ slot: best_tool_result.index as u8,
+ });
+
+ self.is_currently_mining = true;
+
+ self.walk(WalkDirection::None);
+ self.look_at_exact(block.center());
+ self.start_mining_events.send(StartMiningBlockEvent {
+ entity: self.entity,
+ position: block,
+ });
+
+ true
+ }
+
+ /// Mine the given block, but make sure the player is standing at the start
+ /// of the current node first.
+ pub fn mine_while_at_start(&mut self, block: BlockPos) -> bool {
+ let horizontal_distance_from_start = (self.start.center() - self.position)
+ .horizontal_distance_sqr()
+ .sqrt();
+ let at_start_position =
+ BlockPos::from(self.position) == self.start && horizontal_distance_from_start < 0.25;
+
+ if self.should_mine(block) {
+ if at_start_position {
+ self.mine(block);
+ } else {
+ self.look_at(self.start.center());
+ self.walk(WalkDirection::Forward);
+ }
+ true
+ } else {
+ false
+ }
+ }
}
pub struct IsReachedCtx<'a> {
diff --git a/azalea/src/pathfinder/world.rs b/azalea/src/pathfinder/world.rs
index 4b48f921..a5a273fb 100644
--- a/azalea/src/pathfinder/world.rs
+++ b/azalea/src/pathfinder/world.rs
@@ -11,6 +11,9 @@ use azalea_core::{
use azalea_physics::collision::BlockWithShape;
use azalea_world::Instance;
use parking_lot::RwLock;
+use rustc_hash::FxHashMap;
+
+use super::mining::MiningCache;
/// An efficient representation of the world used for the pathfinder.
pub struct CachedWorld {
@@ -20,8 +23,11 @@ pub struct CachedWorld {
// we store `PalettedContainer`s instead of `Chunk`s or `Section`s because it doesn't contain
// any unnecessary data like heightmaps or biomes.
cached_chunks: RefCell<Vec<(ChunkPos, Vec<azalea_world::palette::PalettedContainer>)>>,
+ last_chunk_cache_index: RefCell<Option<usize>>,
cached_blocks: UnsafeCell<CachedSections>,
+
+ cached_mining_costs: RefCell<FxHashMap<BlockPos, f32>>,
}
#[derive(Default)]
@@ -82,7 +88,9 @@ impl CachedWorld {
min_y,
world_lock,
cached_chunks: Default::default(),
+ last_chunk_cache_index: Default::default(),
cached_blocks: Default::default(),
+ cached_mining_costs: Default::default(),
}
}
@@ -100,24 +108,50 @@ impl CachedWorld {
section_pos: ChunkSectionPos,
f: impl FnOnce(&azalea_world::palette::PalettedContainer) -> T,
) -> Option<T> {
+ if section_pos.y * 16 < self.min_y {
+ // y position is out of bounds
+ return None;
+ }
+
let chunk_pos = ChunkPos::from(section_pos);
let section_index =
azalea_world::chunk_storage::section_index(section_pos.y * 16, self.min_y) as usize;
let mut cached_chunks = self.cached_chunks.borrow_mut();
- // get section from cache
- if let Some(sections) = cached_chunks.iter().find_map(|(pos, sections)| {
- if *pos == chunk_pos {
- Some(sections)
- } else {
- None
+ // optimization: avoid doing the iter lookup if the last chunk we looked up is
+ // the same
+ if let Some(last_chunk_cache_index) = *self.last_chunk_cache_index.borrow() {
+ if cached_chunks[last_chunk_cache_index].0 == chunk_pos {
+ // don't bother with the iter lookup
+ let sections = &cached_chunks[last_chunk_cache_index].1;
+ if section_index >= sections.len() {
+ // y position is out of bounds
+ return None;
+ };
+ let section: &azalea_world::palette::PalettedContainer = &sections[section_index];
+ return Some(f(section));
}
- }) {
+ }
+
+ // get section from cache
+ if let Some((chunk_index, sections)) =
+ cached_chunks
+ .iter()
+ .enumerate()
+ .find_map(|(i, (pos, sections))| {
+ if *pos == chunk_pos {
+ Some((i, sections))
+ } else {
+ None
+ }
+ })
+ {
if section_index >= sections.len() {
// y position is out of bounds
return None;
};
+ *self.last_chunk_cache_index.borrow_mut() = Some(chunk_index);
let section: &azalea_world::palette::PalettedContainer = &sections[section_index];
return Some(f(section));
}
@@ -206,18 +240,189 @@ impl CachedWorld {
solid
}
+ /// Returns how much it costs to break this block. Returns 0 if the block is
+ /// already passable.
+ pub fn cost_for_breaking_block(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
+ let mut cached_mining_costs = self.cached_mining_costs.borrow_mut();
+
+ if let Some(&cost) = cached_mining_costs.get(&pos) {
+ return cost;
+ }
+
+ let cost = self.uncached_cost_for_breaking_block(pos, mining_cache);
+ cached_mining_costs.insert(pos, cost);
+ cost
+ }
+
+ fn uncached_cost_for_breaking_block(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
+ if self.is_block_passable(pos) {
+ // if the block is passable then it doesn't need to be broken
+ return 0.;
+ }
+
+ let (section_pos, section_block_pos) =
+ (ChunkSectionPos::from(pos), ChunkSectionBlockPos::from(pos));
+
+ // we use this as an optimization to avoid getting the section again if the
+ // block is in the same section
+ let up_is_in_same_section = section_block_pos.y != 15;
+ let north_is_in_same_section = section_block_pos.z != 0;
+ let east_is_in_same_section = section_block_pos.x != 15;
+ let south_is_in_same_section = section_block_pos.z != 15;
+ let west_is_in_same_section = section_block_pos.x != 0;
+
+ let Some(mining_cost) = self.with_section(section_pos, |section| {
+ let block_state =
+ BlockState::try_from(section.get_at_index(u16::from(section_block_pos) as usize))
+ .unwrap_or_default();
+ let mining_cost = mining_cache.cost_for(block_state);
+
+ if mining_cost == f32::INFINITY {
+ // the block is unbreakable
+ return f32::INFINITY;
+ }
+
+ // if there's a falling block or liquid above this block, abort
+ if up_is_in_same_section {
+ let up_block = BlockState::try_from(
+ section.get_at_index(u16::from(section_block_pos.up(1)) as usize),
+ )
+ .unwrap_or_default();
+ if mining_cache.is_liquid(up_block) || mining_cache.is_falling_block(up_block) {
+ return f32::INFINITY;
+ }
+ }
+
+ // if there's a liquid to the north of this block, abort
+ if north_is_in_same_section {
+ let north_block = BlockState::try_from(
+ section.get_at_index(u16::from(section_block_pos.north(1)) as usize),
+ )
+ .unwrap_or_default();
+ if mining_cache.is_liquid(north_block) {
+ return f32::INFINITY;
+ }
+ }
+
+ // liquid to the east
+ if east_is_in_same_section {
+ let east_block = BlockState::try_from(
+ section.get_at_index(u16::from(section_block_pos.east(1)) as usize),
+ )
+ .unwrap_or_default();
+ if mining_cache.is_liquid(east_block) {
+ return f32::INFINITY;
+ }
+ }
+
+ // liquid to the south
+ if south_is_in_same_section {
+ let south_block = BlockState::try_from(
+ section.get_at_index(u16::from(section_block_pos.south(1)) as usize),
+ )
+ .unwrap_or_default();
+ if mining_cache.is_liquid(south_block) {
+ return f32::INFINITY;
+ }
+ }
+
+ // liquid to the west
+ if west_is_in_same_section {
+ let west_block = BlockState::try_from(
+ section.get_at_index(u16::from(section_block_pos.west(1)) as usize),
+ )
+ .unwrap_or_default();
+ if mining_cache.is_liquid(west_block) {
+ return f32::INFINITY;
+ }
+ }
+
+ // the block is probably safe to break, we'll have to check the adjacent blocks
+ // that weren't in the same section next though
+ mining_cost
+ }) else {
+ // the chunk isn't loaded
+ let cost = if self.is_block_solid(pos) {
+ // assume it's unbreakable if it's solid and out of render distance
+ f32::INFINITY
+ } else {
+ 0.
+ };
+ return cost;
+ };
+
+ if mining_cost == f32::INFINITY {
+ // the block is unbreakable
+ return f32::INFINITY;
+ }
+
+ let check_should_avoid_this_block = |pos: BlockPos, check: &dyn Fn(BlockState) -> bool| {
+ let block_state = self
+ .with_section(ChunkSectionPos::from(pos), |section| {
+ BlockState::try_from(
+ section.get_at_index(u16::from(ChunkSectionBlockPos::from(pos)) as usize),
+ )
+ .unwrap_or_default()
+ })
+ .unwrap_or_default();
+ check(block_state)
+ };
+
+ // check the adjacent blocks that weren't in the same section
+ if !up_is_in_same_section
+ && check_should_avoid_this_block(pos.up(1), &|b| {
+ mining_cache.is_liquid(b) || mining_cache.is_falling_block(b)
+ })
+ {
+ return f32::INFINITY;
+ }
+ if !north_is_in_same_section
+ && check_should_avoid_this_block(pos.north(1), &|b| mining_cache.is_liquid(b))
+ {
+ return f32::INFINITY;
+ }
+ if !east_is_in_same_section
+ && check_should_avoid_this_block(pos.east(1), &|b| mining_cache.is_liquid(b))
+ {
+ return f32::INFINITY;
+ }
+ if !south_is_in_same_section
+ && check_should_avoid_this_block(pos.south(1), &|b| mining_cache.is_liquid(b))
+ {
+ return f32::INFINITY;
+ }
+ if !west_is_in_same_section
+ && check_should_avoid_this_block(pos.west(1), &|b| mining_cache.is_liquid(b))
+ {
+ return f32::INFINITY;
+ }
+
+ mining_cost
+ }
+
/// Whether this block and the block above are passable
pub fn is_passable(&self, pos: BlockPos) -> bool {
self.is_block_passable(pos) && self.is_block_passable(pos.up(1))
}
+ pub fn cost_for_passing(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
+ self.cost_for_breaking_block(pos, mining_cache)
+ + self.cost_for_breaking_block(pos.up(1), mining_cache)
+ }
+
/// Whether we can stand in this position. Checks if the block below is
/// solid, and that the two blocks above that are passable.
-
pub fn is_standable(&self, pos: BlockPos) -> bool {
self.is_block_solid(pos.down(1)) && self.is_passable(pos)
}
+ pub fn cost_for_standing(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
+ if !self.is_block_solid(pos.down(1)) {
+ return f32::INFINITY;
+ }
+ self.cost_for_passing(pos, mining_cache)
+ }
+
/// Get the amount of air blocks until the next solid block below this one.
pub fn fall_distance(&self, pos: BlockPos) -> u32 {
let mut distance = 0;
@@ -235,7 +440,10 @@ impl CachedWorld {
}
/// whether this block is passable
-fn is_block_state_passable(block: BlockState) -> bool {
+pub fn is_block_state_passable(block: BlockState) -> bool {
+ // i already tried optimizing this by having it cache in an IntMap/FxHashMap but
+ // it wasn't measurably faster
+
if block.is_air() {
// fast path
return true;
@@ -243,7 +451,8 @@ fn is_block_state_passable(block: BlockState) -> bool {
if !block.is_shape_empty() {
return false;
}
- if block == azalea_registry::Block::Water.into() {
+ let registry_block = azalea_registry::Block::from(block);
+ if registry_block == azalea_registry::Block::Water {
return false;
}
if block
@@ -252,7 +461,7 @@ fn is_block_state_passable(block: BlockState) -> bool {
{
return false;
}
- if block == azalea_registry::Block::Lava.into() {
+ if registry_block == azalea_registry::Block::Lava {
return false;
}
// block.waterlogged currently doesn't account for seagrass and some other water
@@ -261,11 +470,18 @@ fn is_block_state_passable(block: BlockState) -> bool {
return false;
}
+ // don't walk into fire
+ if registry_block == azalea_registry::Block::Fire
+ || registry_block == azalea_registry::Block::SoulFire
+ {
+ return false;
+ }
+
true
}
/// whether this block has a solid hitbox (i.e. we can stand on it)
-fn is_block_state_solid(block: BlockState) -> bool {
+pub fn is_block_state_solid(block: BlockState) -> bool {
if block.is_air() {
// fast path
return false;