From a707e2eb82b74994a16083b31fa4576332cf1995 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:26:40 -0600 Subject: [PATCH] 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 --- azalea-block/src/range.rs | 15 +- azalea-client/src/interact.rs | 6 +- azalea-client/src/inventory.rs | 33 +++ azalea-client/src/mining.rs | 15 +- azalea-core/src/math.rs | 28 ++- azalea-core/src/position.rs | 45 ++++ azalea-world/src/chunk_storage.rs | 9 +- azalea-world/src/find_blocks.rs | 306 +++++++++++++++++++++++++++ azalea-world/src/lib.rs | 1 + azalea-world/src/world.rs | 144 +------------ azalea/benches/pathfinder.rs | 120 ++++++++--- azalea/src/auto_tool.rs | 51 +++++ azalea/src/bot.rs | 18 +- azalea/src/pathfinder/astar.rs | 6 +- azalea/src/pathfinder/costs.rs | 9 + azalea/src/pathfinder/debug.rs | 113 ++++++++++ azalea/src/pathfinder/goals.rs | 96 ++++++++- azalea/src/pathfinder/mining.rs | 99 ++++++++- azalea/src/pathfinder/mod.rs | 213 +++++++++---------- azalea/src/pathfinder/moves/basic.rs | 141 ++++++++++-- azalea/src/pathfinder/moves/mod.rs | 108 +++++++++- azalea/src/pathfinder/world.rs | 240 +++++++++++++++++++-- 22 files changed, 1457 insertions(+), 359 deletions(-) create mode 100644 azalea-world/src/find_blocks.rs create mode 100644 azalea/src/pathfinder/debug.rs diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs index 6ccf4152e..9b520d496 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 c8332d20b..026e94caa 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 7bbefbee1..527feae78 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::() .add_event::() .add_event::() + .add_event::() .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, + mut send_packet_events: EventWriter, + 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 d1bc169ec..d2e66ca84 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 83e6020eb..aa9d88c8d 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 4cdf3f18a..e98640357 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 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 681f979b7..8bc0b32cb 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 000000000..2d2c6d7ac --- /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, + block_states: &BlockStates, + ) -> Option { + // 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 = 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(§ion.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, + 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, +} + +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 { + if let Some(queued) = self.queued.pop() { + return Some(queued); + } + + let mut found = Vec::new(); + + let mut nearest_found_pos: Option = 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(§ion.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 6677b326e..8514ce242 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 7b6854f72..84a5857ce 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 { 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, - block_states: &BlockStates, - ) -> Option { - // 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 = 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 §ion.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 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 3b58ae514..842c6b5e2 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 2cf530858..bc9bb4747 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::::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 ccc016e67..529bb251a 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 163189af8..cc1e2242d 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(¤t_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 5c72b73a7..f9b67e5fa 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 = LazyLock::new(|| distance_to_ticks(1.25)); pub static FALL_0_25_BLOCKS_COST: LazyLock = LazyLock::new(|| distance_to_ticks(0.25)); pub static JUMP_ONE_BLOCK_COST: LazyLock = diff --git a/azalea/src/pathfinder/debug.rs b/azalea/src/pathfinder/debug.rs new file mode 100644 index 000000000..201803c9b --- /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>, + // chat_events is Option because the tests don't have SendChatEvent + // and we have to use ResMut because bevy doesn't support Option + chat_events: Option>>, + mut tick_count: Local, +) { + 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 9c01d486f..3f8c79932 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(pub T); impl Goal for InverseGoal { fn heuristic(&self, n: BlockPos) -> f32 { @@ -106,6 +123,8 @@ impl Goal for InverseGoal { } } +/// Do either of the given goals, whichever is closer. +#[derive(Debug)] pub struct OrGoal(pub T, pub U); impl Goal for OrGoal { fn heuristic(&self, n: BlockPos) -> f32 { @@ -116,6 +135,24 @@ impl Goal for OrGoal { } } +/// Do any of the given goals, whichever is closest. +#[derive(Debug)] +pub struct OrGoals(pub Vec); +impl Goal for OrGoals { + 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(pub T, pub U); impl Goal for AndGoal { fn heuristic(&self, n: BlockPos) -> f32 { @@ -125,3 +162,52 @@ impl Goal for AndGoal { self.0.success(n) && self.1.success(n) } } + +/// Try to reach all of the given goals. +#[derive(Debug)] +pub struct AndGoals(pub Vec); +impl Goal for AndGoals { + 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 d5977973b..1bc08c436 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, - inventory_menu: Menu, + block_state_id_costs: UnsafeCell>, + inventory_menu: Option, + + water_block_state_range: RangeInclusive, + lava_block_state_range: RangeInclusive, + + falling_blocks: Vec, } impl MiningCache { - pub fn new(inventory_menu: Menu) -> Self { + pub fn new(inventory_menu: Option) -> 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 = 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 525f982d5..9fd769e64 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>, pub successors_fn: Option, pub is_calculating: bool, + pub allow_mining: bool, pub goto_id: Arc, } @@ -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>>, 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, mut sprint_events: EventWriter, mut walk_events: EventWriter, mut jump_events: EventWriter, + mut start_mining_events: EventWriter, + mut set_selected_hotbar_slot_events: EventWriter, ) { - 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>, - // chat_events is Option because the tests don't have SendChatEvent - // and we have to use ResMut because bevy doesn't support Option - chat_events: Option>>, - mut tick_count: Local, -) { - 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( @@ -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 957e24c60..54a6dc6a8 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 e5b837ea2..bb10b1928 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; @@ -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>, + 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 4b48f9211..a5a273fb2 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)>>, + last_chunk_cache_index: RefCell>, cached_blocks: UnsafeCell, + + cached_mining_costs: RefCell>, } #[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 { + 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 = §ions[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 = §ions[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;