aboutsummaryrefslogtreecommitdiff
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
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
-rw-r--r--azalea-block/src/range.rs15
-rw-r--r--azalea-client/src/interact.rs6
-rw-r--r--azalea-client/src/inventory.rs33
-rw-r--r--azalea-client/src/mining.rs15
-rw-r--r--azalea-core/src/math.rs28
-rwxr-xr-xazalea-core/src/position.rs45
-rwxr-xr-xazalea-world/src/chunk_storage.rs9
-rw-r--r--azalea-world/src/find_blocks.rs306
-rw-r--r--azalea-world/src/lib.rs1
-rw-r--r--azalea-world/src/world.rs144
-rw-r--r--azalea/benches/pathfinder.rs120
-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
22 files changed, 1457 insertions, 359 deletions
diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs
index 6ccf4152..9b520d49 100644
--- a/azalea-block/src/range.rs
+++ b/azalea-block/src/range.rs
@@ -1,4 +1,7 @@
-use std::{collections::HashSet, ops::RangeInclusive};
+use std::{
+ collections::HashSet,
+ ops::{Add, RangeInclusive},
+};
use crate::BlockState;
@@ -31,3 +34,13 @@ impl BlockStates {
self.set.contains(state)
}
}
+
+impl Add for BlockStates {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self {
+ set: self.set.union(&rhs.set).copied().collect(),
+ }
+ }
+}
diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs
index c8332d20..026e94ca 100644
--- a/azalea-client/src/interact.rs
+++ b/azalea-client/src/interact.rs
@@ -193,7 +193,7 @@ pub fn update_hit_result_component(
};
let instance = instance_lock.read();
- let hit_result = pick(look_direction, &eye_position, &instance, pick_range);
+ let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range);
if let Some(mut hit_result_ref) = hit_result_ref {
**hit_result_ref = hit_result;
} else {
@@ -212,13 +212,13 @@ pub fn update_hit_result_component(
pub fn pick(
look_direction: &LookDirection,
eye_position: &Vec3,
- instance: &Instance,
+ chunks: &azalea_world::ChunkStorage,
pick_range: f64,
) -> BlockHitResult {
let view_vector = view_vector(look_direction);
let end_position = eye_position + &(view_vector * pick_range);
azalea_physics::clip::clip(
- &instance.chunks,
+ chunks,
ClipContext {
from: *eye_position,
to: end_position,
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
index 7bbefbee..527feae7 100644
--- a/azalea-client/src/inventory.rs
+++ b/azalea-client/src/inventory.rs
@@ -12,6 +12,7 @@ use azalea_inventory::{
use azalea_protocol::packets::game::{
serverbound_container_click_packet::ServerboundContainerClickPacket,
serverbound_container_close_packet::ServerboundContainerClosePacket,
+ serverbound_set_carried_item_packet::ServerboundSetCarriedItemPacket,
};
use azalea_registry::MenuKind;
use bevy_app::{App, Plugin, Update};
@@ -40,9 +41,11 @@ impl Plugin for InventoryPlugin {
.add_event::<CloseContainerEvent>()
.add_event::<ContainerClickEvent>()
.add_event::<SetContainerContentEvent>()
+ .add_event::<SetSelectedHotbarSlotEvent>()
.add_systems(
Update,
(
+ handle_set_selected_hotbar_slot_event,
handle_menu_opened_event,
handle_set_container_content_event,
handle_container_click_event,
@@ -734,3 +737,33 @@ fn handle_set_container_content_event(
}
}
}
+
+#[derive(Event)]
+pub struct SetSelectedHotbarSlotEvent {
+ pub entity: Entity,
+ /// The hotbar slot to select. This should be in the range 0..=8.
+ pub slot: u8,
+}
+fn handle_set_selected_hotbar_slot_event(
+ mut events: EventReader<SetSelectedHotbarSlotEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.read() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+
+ // if the slot is already selected, don't send a packet
+ if inventory.selected_hotbar_slot == event.slot {
+ continue;
+ }
+
+ inventory.selected_hotbar_slot = event.slot;
+ send_packet_events.send(SendPacketEvent {
+ entity: event.entity,
+ packet: ServerboundSetCarriedItemPacket {
+ slot: event.slot as u16,
+ }
+ .get(),
+ });
+ }
+}
diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs
index d1bc169e..d2e66ca8 100644
--- a/azalea-client/src/mining.rs
+++ b/azalea-client/src/mining.rs
@@ -200,7 +200,20 @@ fn handle_start_mining_block_with_direction_event(
.get_block_state(&event.position)
.unwrap_or_default();
*sequence_number += 1;
- let block_is_solid = !target_block_state.is_air();
+ let target_registry_block = azalea_registry::Block::from(target_block_state);
+
+ // we can't break blocks if they don't have a bounding box
+
+ // TODO: So right now azalea doesn't differenciate between different types of
+ // bounding boxes. See ClipContext::block_shape for more info. Ideally this
+ // should just call ClipContext::block_shape and check if it's empty.
+ let block_is_solid = !target_block_state.is_air()
+ // this is a hack to make sure we can't break water or lava
+ && !matches!(
+ target_registry_block,
+ azalea_registry::Block::Water | azalea_registry::Block::Lava
+ );
+
if block_is_solid && **mine_progress == 0. {
// interact with the block (like note block left click) here
attack_block_events.send(AttackBlockEvent {
diff --git a/azalea-core/src/math.rs b/azalea-core/src/math.rs
index 83e6020e..aa9d88c8 100644
--- a/azalea-core/src/math.rs
+++ b/azalea-core/src/math.rs
@@ -13,15 +13,15 @@ pub static SIN: LazyLock<[f32; 65536]> = LazyLock::new(|| {
/// A sine function that uses a lookup table.
pub fn sin(x: f32) -> f32 {
let x = x * 10430.378;
- let x = x as usize;
- SIN[x & 65535]
+ let x = x as i32 as usize & 65535;
+ SIN[x]
}
/// A cosine function that uses a lookup table.
pub fn cos(x: f32) -> f32 {
let x = x * 10430.378 + 16384.0;
- let x = x as usize;
- SIN[x & 65535]
+ let x = x as i32 as usize & 65535;
+ SIN[x]
}
// TODO: make this generic
@@ -83,4 +83,24 @@ mod tests {
assert_eq!(gcd(12, 7), 1);
assert_eq!(gcd(7, 12), 1);
}
+
+ #[test]
+ fn test_sin() {
+ const PI: f32 = std::f32::consts::PI;
+ // check that they're close enough
+ fn assert_sin_eq_enough(number: f32) {
+ let a = sin(number);
+ let b = f32::sin(number);
+ assert!((a - b).abs() < 0.01, "sin({number}) failed, {a} != {b}");
+ }
+ assert_sin_eq_enough(0.0);
+ assert_sin_eq_enough(PI / 2.0);
+ assert_sin_eq_enough(PI);
+ assert_sin_eq_enough(PI * 2.0);
+ assert_sin_eq_enough(PI * 3.0 / 2.0);
+ assert_sin_eq_enough(-PI / 2.0);
+ assert_sin_eq_enough(-PI);
+ assert_sin_eq_enough(-PI * 2.0);
+ assert_sin_eq_enough(-PI * 3.0 / 2.0);
+ }
}
diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs
index 4cdf3f18..e9864035 100755
--- a/azalea-core/src/position.rs
+++ b/azalea-core/src/position.rs
@@ -5,6 +5,7 @@
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use std::{
+ fmt,
hash::Hash,
io::{Cursor, Write},
ops::{Add, AddAssign, Mul, Rem, Sub},
@@ -65,6 +66,43 @@ macro_rules! vec3_impl {
}
}
+ /// Return a new instance of this position with the z coordinate subtracted
+ /// by the given number.
+ pub fn north(&self, z: $type) -> Self {
+ Self {
+ x: self.x,
+ y: self.y,
+ z: self.z - z,
+ }
+ }
+ /// Return a new instance of this position with the x coordinate increased
+ /// by the given number.
+ pub fn east(&self, x: $type) -> Self {
+ Self {
+ x: self.x + x,
+ y: self.y,
+ z: self.z,
+ }
+ }
+ /// Return a new instance of this position with the z coordinate increased
+ /// by the given number.
+ pub fn south(&self, z: $type) -> Self {
+ Self {
+ x: self.x,
+ y: self.y,
+ z: self.z + z,
+ }
+ }
+ /// Return a new instance of this position with the x coordinate subtracted
+ /// by the given number.
+ pub fn west(&self, x: $type) -> Self {
+ Self {
+ x: self.x - x,
+ y: self.y,
+ z: self.z,
+ }
+ }
+
#[inline]
pub fn dot(&self, other: Self) -> $type {
self.x * other.x + self.y * other.y + self.z * other.z
@@ -501,6 +539,13 @@ impl From<Vec3> for ChunkBlockPos {
}
}
+impl fmt::Display for BlockPos {
+ /// Display a block position as `x y z`.
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} {} {}", self.x, self.y, self.z)
+ }
+}
+
const PACKED_X_LENGTH: u64 = 1 + 25; // minecraft does something a bit more complicated to get this 25
const PACKED_Z_LENGTH: u64 = PACKED_X_LENGTH;
const PACKED_Y_LENGTH: u64 = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH;
diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs
index 681f979b..8bc0b32c 100755
--- a/azalea-world/src/chunk_storage.rs
+++ b/azalea-world/src/chunk_storage.rs
@@ -34,7 +34,7 @@ pub struct PartialChunkStorage {
/// A storage for chunks where they're only stored weakly, so if they're not
/// actively being used somewhere else they'll be forgotten. This is used for
/// shared worlds.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct ChunkStorage {
pub height: u32,
pub min_y: i32,
@@ -514,7 +514,12 @@ impl Default for ChunkStorage {
/// and the minimum y coordinate of the world.
#[inline]
pub fn section_index(y: i32, min_y: i32) -> u32 {
- assert!(y >= min_y, "y ({y}) must be at least {min_y}");
+ if y < min_y {
+ #[cfg(debug_assertions)]
+ panic!("y ({y}) must be at most {min_y}");
+ #[cfg(not(debug_assertions))]
+ tracing::error!("y ({y}) must be at least {min_y}")
+ };
let min_section_index = min_y >> 4;
((y >> 4) - min_section_index) as u32
}
diff --git a/azalea-world/src/find_blocks.rs b/azalea-world/src/find_blocks.rs
new file mode 100644
index 00000000..2d2c6d7a
--- /dev/null
+++ b/azalea-world/src/find_blocks.rs
@@ -0,0 +1,306 @@
+use azalea_block::{BlockState, BlockStates};
+use azalea_core::position::{BlockPos, ChunkPos};
+
+use crate::{iterators::ChunkIterator, palette::Palette, ChunkStorage, Instance};
+
+fn palette_maybe_has_block(palette: &Palette, block_states: &BlockStates) -> bool {
+ match &palette {
+ Palette::SingleValue(id) => block_states.contains(&BlockState { id: *id }),
+ Palette::Linear(ids) => ids
+ .iter()
+ .any(|&id| block_states.contains(&BlockState { id })),
+ Palette::Hashmap(ids) => ids
+ .iter()
+ .any(|&id| block_states.contains(&BlockState { id })),
+ Palette::Global => true,
+ }
+}
+
+impl Instance {
+ /// Find the coordinates of a block in the world.
+ ///
+ /// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2` for
+ /// optimization purposes.
+ ///
+ /// ```
+ /// # fn example(client: &azalea_client::Client) {
+ /// client.world().read().find_block(client.position(), &azalea_registry::Block::Chest.into());
+ /// # }
+ /// ```
+ pub fn find_block(
+ &self,
+ nearest_to: impl Into<BlockPos>,
+ block_states: &BlockStates,
+ ) -> Option<BlockPos> {
+ // iterate over every chunk in a 3d spiral pattern
+ // and then check the palette for the block state
+
+ let nearest_to: BlockPos = nearest_to.into();
+ let start_chunk: ChunkPos = (&nearest_to).into();
+ let mut iter = ChunkIterator::new(start_chunk, 32);
+
+ let mut nearest_found_pos: Option<BlockPos> = None;
+ let mut nearest_found_distance = 0;
+
+ // we do `while` instead of `for` so we can access iter later
+ while let Some(chunk_pos) = iter.next() {
+ let Some(chunk) = self.chunks.get(&chunk_pos) else {
+ // if the chunk isn't loaded then we skip it.
+ // we don't just return since it *could* cause issues if there's a random
+ // unloaded chunk and then more that are loaded.
+ // unlikely but still something to consider, and it's not like this slows it
+ // down much anyways.
+ continue;
+ };
+
+ for (section_index, section) in chunk.read().sections.iter().enumerate() {
+ let maybe_has_block =
+ palette_maybe_has_block(&section.states.palette, block_states);
+ if !maybe_has_block {
+ continue;
+ }
+
+ for i in 0..4096 {
+ let block_state = section.states.get_at_index(i);
+ let block_state = BlockState { id: block_state };
+
+ if block_states.contains(&block_state) {
+ let (section_x, section_y, section_z) = section.states.coords_from_index(i);
+ let (x, y, z) = (
+ chunk_pos.x * 16 + (section_x as i32),
+ self.chunks.min_y + (section_index * 16) as i32 + section_y as i32,
+ chunk_pos.z * 16 + (section_z as i32),
+ );
+ let this_block_pos = BlockPos { x, y, z };
+ let this_block_distance = (nearest_to - this_block_pos).length_manhattan();
+ // only update if it's closer
+ if nearest_found_pos.is_none()
+ || this_block_distance < nearest_found_distance
+ {
+ nearest_found_pos = Some(this_block_pos);
+ nearest_found_distance = this_block_distance;
+ }
+ }
+ }
+ }
+
+ if let Some(nearest_found_pos) = nearest_found_pos {
+ // this is required because find_block searches chunk-by-chunk, which can cause
+ // us to find blocks first that aren't actually the closest
+ let required_chunk_distance = u32::max(
+ u32::max(
+ (chunk_pos.x - start_chunk.x).unsigned_abs(),
+ (chunk_pos.z - start_chunk.z).unsigned_abs(),
+ ),
+ (nearest_to.y - nearest_found_pos.y)
+ .unsigned_abs()
+ .div_ceil(16),
+ ) + 1;
+ let nearest_chunk_distance = iter.layer;
+
+ // if we found the position and there's no chance there's something closer,
+ // return it
+ if nearest_chunk_distance > required_chunk_distance {
+ return Some(nearest_found_pos);
+ }
+ }
+ }
+
+ if nearest_found_pos.is_some() {
+ nearest_found_pos
+ } else {
+ None
+ }
+ }
+
+ /// Find all the coordinates of a block in the world.
+ ///
+ /// This returns an iterator that yields the [`BlockPos`]s of blocks that
+ /// are in the given block states. It's sorted by `x+y+z`.
+ pub fn find_blocks<'a>(
+ &'a self,
+ nearest_to: impl Into<BlockPos>,
+ block_states: &'a BlockStates,
+ ) -> FindBlocks<'a> {
+ FindBlocks::new(nearest_to.into(), &self.chunks, block_states)
+ }
+}
+
+pub struct FindBlocks<'a> {
+ nearest_to: BlockPos,
+ start_chunk: ChunkPos,
+ chunk_iterator: ChunkIterator,
+ chunks: &'a ChunkStorage,
+ block_states: &'a BlockStates,
+
+ queued: Vec<BlockPos>,
+}
+
+impl<'a> FindBlocks<'a> {
+ pub fn new(
+ nearest_to: BlockPos,
+ chunks: &'a ChunkStorage,
+ block_states: &'a BlockStates,
+ ) -> Self {
+ let start_chunk: ChunkPos = (&nearest_to).into();
+ Self {
+ nearest_to,
+ start_chunk,
+ chunk_iterator: ChunkIterator::new(start_chunk, 32),
+ chunks,
+ block_states,
+
+ queued: Vec::new(),
+ }
+ }
+}
+
+impl<'a> Iterator for FindBlocks<'a> {
+ type Item = BlockPos;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(queued) = self.queued.pop() {
+ return Some(queued);
+ }
+
+ let mut found = Vec::new();
+
+ let mut nearest_found_pos: Option<BlockPos> = None;
+ let mut nearest_found_distance = 0;
+
+ while let Some(chunk_pos) = self.chunk_iterator.next() {
+ let Some(chunk) = self.chunks.get(&chunk_pos) else {
+ // if the chunk isn't loaded then we skip it.
+ // we don't just return since it *could* cause issues if there's a random
+ // unloaded chunk and then more that are loaded.
+ // unlikely but still something to consider, and it's not like this slows it
+ // down much anyways.
+ continue;
+ };
+
+ for (section_index, section) in chunk.read().sections.iter().enumerate() {
+ let maybe_has_block =
+ palette_maybe_has_block(&section.states.palette, self.block_states);
+ if !maybe_has_block {
+ continue;
+ }
+
+ for i in 0..4096 {
+ let block_state = section.states.get_at_index(i);
+ let block_state = BlockState { id: block_state };
+
+ if self.block_states.contains(&block_state) {
+ let (section_x, section_y, section_z) = section.states.coords_from_index(i);
+ let (x, y, z) = (
+ chunk_pos.x * 16 + (section_x as i32),
+ self.chunks.min_y + (section_index * 16) as i32 + section_y as i32,
+ chunk_pos.z * 16 + (section_z as i32),
+ );
+ let this_block_pos = BlockPos { x, y, z };
+ let this_block_distance =
+ (self.nearest_to - this_block_pos).length_manhattan();
+
+ found.push((this_block_pos, this_block_distance));
+
+ if nearest_found_pos.is_none()
+ || this_block_distance < nearest_found_distance
+ {
+ nearest_found_pos = Some(this_block_pos);
+ nearest_found_distance = this_block_distance;
+ }
+ }
+ }
+ }
+
+ if let Some(nearest_found_pos) = nearest_found_pos {
+ // this is required because find_block searches chunk-by-chunk, which can cause
+ // us to find blocks first that aren't actually the closest
+ let required_chunk_distance = u32::max(
+ u32::max(
+ (chunk_pos.x - self.start_chunk.x).unsigned_abs(),
+ (chunk_pos.z - self.start_chunk.z).unsigned_abs(),
+ ),
+ (self.nearest_to.y - nearest_found_pos.y)
+ .unsigned_abs()
+ .div_ceil(16),
+ ) + 1;
+ let nearest_chunk_distance = self.chunk_iterator.layer;
+
+ // if we found the position and there's no chance there's something closer,
+ // return it
+ if nearest_chunk_distance > required_chunk_distance {
+ // sort so nearest is at the end
+ found.sort_unstable_by_key(|(_, distance)| u32::MAX - distance);
+
+ self.queued = found.into_iter().map(|(pos, _)| pos).collect();
+ return self.queued.pop();
+ }
+ }
+ }
+
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use azalea_registry::Block;
+
+ use crate::{Chunk, PartialChunkStorage};
+
+ use super::*;
+
+ #[test]
+ fn find_block() {
+ let mut instance = Instance::default();
+
+ let chunk_storage = &mut instance.chunks;
+ let mut partial_chunk_storage = PartialChunkStorage::default();
+
+ // block at (17, 0, 0) and (0, 18, 0)
+
+ partial_chunk_storage.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ chunk_storage,
+ );
+ partial_chunk_storage.set(
+ &ChunkPos { x: 1, z: 0 },
+ Some(Chunk::default()),
+ chunk_storage,
+ );
+
+ chunk_storage.set_block_state(&BlockPos { x: 17, y: 0, z: 0 }, Block::Stone.into());
+ chunk_storage.set_block_state(&BlockPos { x: 0, y: 18, z: 0 }, Block::Stone.into());
+
+ let pos = instance.find_block(BlockPos { x: 0, y: 0, z: 0 }, &Block::Stone.into());
+ assert_eq!(pos, Some(BlockPos { x: 17, y: 0, z: 0 }));
+ }
+
+ #[test]
+ fn find_block_next_to_chunk_border() {
+ let mut instance = Instance::default();
+
+ let chunk_storage = &mut instance.chunks;
+ let mut partial_chunk_storage = PartialChunkStorage::default();
+
+ // block at (-1, 0, 0) and (15, 0, 0)
+
+ partial_chunk_storage.set(
+ &ChunkPos { x: -1, z: 0 },
+ Some(Chunk::default()),
+ chunk_storage,
+ );
+ partial_chunk_storage.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ chunk_storage,
+ );
+
+ chunk_storage.set_block_state(&BlockPos { x: -1, y: 0, z: 0 }, Block::Stone.into());
+ chunk_storage.set_block_state(&BlockPos { x: 15, y: 0, z: 0 }, Block::Stone.into());
+
+ let pos = instance.find_block(BlockPos { x: 0, y: 0, z: 0 }, &Block::Stone.into());
+ assert_eq!(pos, Some(BlockPos { x: -1, y: 0, z: 0 }));
+ }
+}
diff --git a/azalea-world/src/lib.rs b/azalea-world/src/lib.rs
index 6677b326..8514ce24 100644
--- a/azalea-world/src/lib.rs
+++ b/azalea-world/src/lib.rs
@@ -4,6 +4,7 @@
mod bit_storage;
pub mod chunk_storage;
mod container;
+pub mod find_blocks;
pub mod heightmap;
pub mod iterators;
pub mod palette;
diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs
index 7b6854f7..84a5857c 100644
--- a/azalea-world/src/world.rs
+++ b/azalea-world/src/world.rs
@@ -1,5 +1,5 @@
-use crate::{iterators::ChunkIterator, palette::Palette, ChunkStorage, PartialChunkStorage};
-use azalea_block::{BlockState, BlockStates, FluidState};
+use crate::{ChunkStorage, PartialChunkStorage};
+use azalea_block::{BlockState, FluidState};
use azalea_core::position::{BlockPos, ChunkPos};
use azalea_core::registry_holder::RegistryHolder;
use bevy_ecs::{component::Component, entity::Entity};
@@ -104,110 +104,6 @@ impl Instance {
pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
self.chunks.set_block_state(pos, state)
}
-
- /// Find the coordinates of a block in the world.
- ///
- /// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2` for
- /// optimization purposes.
- ///
- /// ```
- /// # fn example(client: &azalea_client::Client) {
- /// client.world().read().find_block(client.position(), &azalea_registry::Block::Chest.into());
- /// # }
- /// ```
- pub fn find_block(
- &self,
- nearest_to: impl Into<BlockPos>,
- block_states: &BlockStates,
- ) -> Option<BlockPos> {
- // iterate over every chunk in a 3d spiral pattern
- // and then check the palette for the block state
-
- let nearest_to: BlockPos = nearest_to.into();
- let start_chunk: ChunkPos = (&nearest_to).into();
- let mut iter = ChunkIterator::new(start_chunk, 32);
-
- let mut nearest_found_pos: Option<BlockPos> = None;
- let mut nearest_found_distance = 0;
-
- // we do `while` instead of `for` so we can access iter later
- while let Some(chunk_pos) = iter.next() {
- let Some(chunk) = self.chunks.get(&chunk_pos) else {
- // if the chunk isn't loaded then we skip it.
- // we don't just return since it *could* cause issues if there's a random
- // unloaded chunk and then more that are loaded.
- // unlikely but still something to consider, and it's not like this slows it
- // down much anyways.
- continue;
- };
-
- for (section_index, section) in chunk.read().sections.iter().enumerate() {
- let maybe_has_block = match &section.states.palette {
- Palette::SingleValue(id) => block_states.contains(&BlockState { id: *id }),
- Palette::Linear(ids) => ids
- .iter()
- .any(|&id| block_states.contains(&BlockState { id })),
- Palette::Hashmap(ids) => ids
- .iter()
- .any(|&id| block_states.contains(&BlockState { id })),
- Palette::Global => true,
- };
- if !maybe_has_block {
- continue;
- }
-
- for i in 0..4096 {
- let block_state = section.states.get_at_index(i);
- let block_state = BlockState { id: block_state };
-
- if block_states.contains(&block_state) {
- let (section_x, section_y, section_z) = section.states.coords_from_index(i);
- let (x, y, z) = (
- chunk_pos.x * 16 + (section_x as i32),
- self.chunks.min_y + (section_index * 16) as i32 + section_y as i32,
- chunk_pos.z * 16 + (section_z as i32),
- );
- let this_block_pos = BlockPos { x, y, z };
- let this_block_distance = (nearest_to - this_block_pos).length_manhattan();
- // only update if it's closer
- if nearest_found_pos.is_none()
- || this_block_distance < nearest_found_distance
- {
- nearest_found_pos = Some(this_block_pos);
- nearest_found_distance = this_block_distance;
- }
- }
- }
- }
-
- if let Some(nearest_found_pos) = nearest_found_pos {
- // this is required because find_block searches chunk-by-chunk, which can cause
- // us to find blocks first that aren't actually the closest
- let required_chunk_distance = u32::max(
- u32::max(
- (chunk_pos.x - start_chunk.x).unsigned_abs(),
- (chunk_pos.z - start_chunk.z).unsigned_abs(),
- ),
- (nearest_to.y - nearest_found_pos.y)
- .unsigned_abs()
- .div_ceil(16),
- );
- let nearest_chunk_distance = iter.layer;
-
- // if we found the position and there's no chance there's something closer,
- // return it
- if nearest_chunk_distance >= required_chunk_distance {
- return Some(nearest_found_pos);
- }
- }
- }
-
- if nearest_found_pos.is_some() {
- nearest_found_pos
- } else {
- None
- }
- }
}
impl Debug for PartialInstance {
@@ -244,39 +140,3 @@ impl From<ChunkStorage> for Instance {
}
}
}
-
-#[cfg(test)]
-mod tests {
- use azalea_registry::Block;
-
- use crate::Chunk;
-
- use super::*;
-
- #[test]
- fn find_block() {
- let mut instance = Instance::default();
-
- let chunk_storage = &mut instance.chunks;
- let mut partial_chunk_storage = PartialChunkStorage::default();
-
- // block at (17, 0, 0) and (0, 18, 0)
-
- partial_chunk_storage.set(
- &ChunkPos { x: 0, z: 0 },
- Some(Chunk::default()),
- chunk_storage,
- );
- partial_chunk_storage.set(
- &ChunkPos { x: 1, z: 0 },
- Some(Chunk::default()),
- chunk_storage,
- );
-
- chunk_storage.set_block_state(&BlockPos { x: 17, y: 0, z: 0 }, Block::Stone.into());
- chunk_storage.set_block_state(&BlockPos { x: 0, y: 18, z: 0 }, Block::Stone.into());
-
- let pos = instance.find_block(BlockPos { x: 0, y: 0, z: 0 }, &Block::Stone.into());
- assert_eq!(pos, Some(BlockPos { x: 17, y: 0, z: 0 }));
- }
-}
diff --git a/azalea/benches/pathfinder.rs b/azalea/benches/pathfinder.rs
index 3b58ae51..842c6b5e 100644
--- a/azalea/benches/pathfinder.rs
+++ b/azalea/benches/pathfinder.rs
@@ -3,17 +3,16 @@ use std::{hint::black_box, sync::Arc, time::Duration};
use azalea::{
pathfinder::{
astar::{self, a_star},
- goals::BlockPosGoal,
+ goals::{BlockPosGoal, Goal},
mining::MiningCache,
world::CachedWorld,
- Goal,
},
BlockPos,
};
use azalea_core::position::{ChunkBlockPos, ChunkPos};
use azalea_inventory::Menu;
use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage};
-use criterion::{criterion_group, criterion_main, Criterion};
+use criterion::{criterion_group, criterion_main, Bencher, Criterion};
use parking_lot::RwLock;
use rand::{rngs::StdRng, Rng, SeedableRng};
@@ -58,14 +57,14 @@ fn generate_bedrock_world(
}
let mut start = BlockPos::new(-64, 4, -64);
- // move start down until it's on bedrock
+ // move start down until it's on a solid block
while chunks.get_block_state(&start).unwrap().is_air() {
start = start.down(1);
}
start = start.up(1);
let mut end = BlockPos::new(63, 4, 63);
- // move end down until it's on bedrock
+ // move end down until it's on a solid block
while chunks.get_block_state(&end).unwrap().is_air() {
end = end.down(1);
}
@@ -74,37 +73,90 @@ fn generate_bedrock_world(
(chunks, start, end)
}
+fn generate_mining_world(
+ partial_chunks: &mut PartialChunkStorage,
+ size: u32,
+) -> (ChunkStorage, BlockPos, BlockPos) {
+ let size = size as i32;
+
+ let mut chunks = ChunkStorage::default();
+ for chunk_x in -size..size {
+ for chunk_z in -size..size {
+ let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
+ partial_chunks.set(&chunk_pos, Some(Chunk::default()), &mut chunks);
+ }
+ }
+
+ let mut rng = StdRng::seed_from_u64(0);
+
+ for chunk_x in -size..size {
+ for chunk_z in -size..size {
+ let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
+ let chunk = chunks.get(&chunk_pos).unwrap();
+ let mut chunk = chunk.write();
+ for y in chunks.min_y..(chunks.min_y + chunks.height as i32) {
+ for x in 0..16_u8 {
+ for z in 0..16_u8 {
+ chunk.set(
+ &ChunkBlockPos::new(x, y, z),
+ azalea_registry::Block::Stone.into(),
+ chunks.min_y,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ let start = BlockPos::new(-64, 4, -64);
+ let end = BlockPos::new(0, 4, 0);
+
+ (chunks, start, end)
+}
+
+fn run_pathfinder_benchmark(
+ b: &mut Bencher<'_>,
+ generate_world: fn(&mut PartialChunkStorage, u32) -> (ChunkStorage, BlockPos, BlockPos),
+) {
+ let mut partial_chunks = PartialChunkStorage::new(32);
+ let successors_fn = azalea::pathfinder::moves::default_move;
+
+ let (world, start, end) = generate_world(&mut partial_chunks, 4);
+
+ b.iter(|| {
+ let cached_world = CachedWorld::new(Arc::new(RwLock::new(world.clone().into())));
+ let mining_cache =
+ MiningCache::new(Some(Menu::Player(azalea_inventory::Player::default())));
+ let goal = BlockPosGoal(end);
+
+ let successors = |pos: BlockPos| {
+ azalea::pathfinder::call_successors_fn(&cached_world, &mining_cache, successors_fn, pos)
+ };
+
+ let astar::Path { movements, partial } = a_star(
+ start,
+ |n| goal.heuristic(n),
+ successors,
+ |n| goal.success(n),
+ Duration::MAX,
+ );
+
+ assert!(!partial);
+
+ black_box((movements, partial));
+ })
+}
+
fn bench_pathfinder(c: &mut Criterion) {
- c.bench_function("bedrock", |b| {
- let mut partial_chunks = PartialChunkStorage::new(32);
- let successors_fn = azalea::pathfinder::moves::default_move;
-
- b.iter(|| {
- let (world, start, end) = generate_bedrock_world(&mut partial_chunks, 4);
- let cached_world = CachedWorld::new(Arc::new(RwLock::new(world.into())));
- let mining_cache = MiningCache::new(Menu::Player(azalea_inventory::Player::default()));
- let goal = BlockPosGoal(end);
-
- let successors = |pos: BlockPos| {
- azalea::pathfinder::call_successors_fn(
- &cached_world,
- &mining_cache,
- successors_fn,
- pos,
- )
- };
-
- let astar::Path { movements, partial } = a_star(
- start,
- |n| goal.heuristic(n),
- successors,
- |n| goal.success(n),
- Duration::MAX,
- );
-
- black_box((movements, partial));
- })
+ // c.bench_function("bedrock", |b| {
+ // run_pathfinder_benchmark(b, generate_bedrock_world);
+ // });
+ let mut slow_group = c.benchmark_group("slow");
+ slow_group.sample_size(10);
+ slow_group.bench_function("mining", |b| {
+ run_pathfinder_benchmark(b, generate_mining_world);
});
+ slow_group.finish();
}
criterion_group!(benches, bench_pathfinder);
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;