From 73091d8f937192aca4c4bbc740c78d4d188f6ee1 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 15 Aug 2024 01:25:11 +0000 Subject: [PATCH] fix sometimes being able to mine blocks through walls --- azalea-client/src/mining.rs | 6 +- azalea-core/src/math.rs | 6 + azalea-physics/src/clip.rs | 26 +++-- azalea/src/pathfinder/extras/utils.rs | 153 ++++++++++++++++++++++++++ azalea/src/pathfinder/mod.rs | 72 +++++++----- azalea/src/pathfinder/simulation.rs | 15 ++- 6 files changed, 232 insertions(+), 46 deletions(-) create mode 100644 azalea/src/pathfinder/extras/utils.rs diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs index 0bf416ffc..753b00e70 100644 --- a/azalea-client/src/mining.rs +++ b/azalea-client/src/mining.rs @@ -396,16 +396,16 @@ impl MineProgress { /// A component that stores the number of ticks that we've been mining the same /// block for. This is a float even though it should only ever be a round /// number. -#[derive(Component, Debug, Default, Deref, DerefMut)] +#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] pub struct MineTicks(pub f32); /// A component that stores the position of the block we're currently mining. -#[derive(Component, Debug, Default, Deref, DerefMut)] +#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] pub struct MineBlockPos(pub Option); /// A component that contains the item we're currently using to mine. If we're /// not mining anything, it'll be [`ItemSlot::Empty`]. -#[derive(Component, Debug, Default, Deref, DerefMut)] +#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] pub struct MineItem(pub ItemSlot); /// Sent when we completed mining a block. diff --git a/azalea-core/src/math.rs b/azalea-core/src/math.rs index aa9d88c8d..7c0ac7b42 100644 --- a/azalea-core/src/math.rs +++ b/azalea-core/src/math.rs @@ -62,6 +62,12 @@ pub fn ceil_log2(x: u32) -> u32 { u32::BITS - x.leading_zeros() } +pub fn fract(x: f64) -> f64 { + let x_int = x as i64 as f64; + let floor = if x < x_int { x_int - 1. } else { x_int }; + x - floor +} + #[cfg(test)] mod tests { use super::*; diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs index 8c2d0c8fb..e44c59ca6 100644 --- a/azalea-physics/src/clip.rs +++ b/azalea-physics/src/clip.rs @@ -2,7 +2,7 @@ use azalea_block::BlockState; use azalea_core::{ block_hit_result::BlockHitResult, direction::Direction, - math::{lerp, EPSILON}, + math::{self, lerp, EPSILON}, position::{BlockPos, Vec3}, }; use azalea_inventory::ItemSlot; @@ -111,13 +111,12 @@ fn clip_with_interaction_override( block_state: &BlockState, ) -> Option { let block_hit_result = block_shape.clip(from, to, block_pos); + println!("block_hit_result: {block_hit_result:?}"); if let Some(block_hit_result) = block_hit_result { // TODO: minecraft calls .getInteractionShape here - // are there even any blocks that have a physics shape different from the - // interaction shape??? - // (if not then you can delete this comment) - // (if there are then you have to implement BlockState::interaction_shape, lol - // have fun) + // some blocks (like tall grass) have a physics shape that's different from the + // interaction shape, so we need to implement BlockState::interaction_shape. lol + // have fun let interaction_shape = block_state.shape(); let interaction_hit_result = interaction_shape.clip(from, to, block_pos); if let Some(interaction_hit_result) = interaction_hit_result { @@ -191,24 +190,27 @@ pub fn traverse_blocks( let mut percentage = Vec3 { x: percentage_step.x * if vec_sign.x > 0. { - 1. - right_before_start.x.fract() + 1. - math::fract(right_before_start.x) } else { - right_before_start.x.fract().abs() + math::fract(right_before_start.x) }, y: percentage_step.y * if vec_sign.y > 0. { - 1. - right_before_start.y.fract() + 1. - math::fract(right_before_start.y) } else { - right_before_start.y.fract().abs() + math::fract(right_before_start.y) }, z: percentage_step.z * if vec_sign.z > 0. { - 1. - right_before_start.z.fract() + 1. - math::fract(right_before_start.z) } else { - right_before_start.z.fract().abs() + math::fract(right_before_start.z) }, }; + println!("percentage_step: {percentage_step:?}"); + println!("percentage: {percentage:?}"); + loop { if percentage.x > 1. && percentage.y > 1. && percentage.z > 1. { return get_miss_result(&context); diff --git a/azalea/src/pathfinder/extras/utils.rs b/azalea/src/pathfinder/extras/utils.rs new file mode 100644 index 000000000..30b1ae522 --- /dev/null +++ b/azalea/src/pathfinder/extras/utils.rs @@ -0,0 +1,153 @@ +//! Random utility functions that are useful for bots. + +use azalea_core::position::{BlockPos, Vec3}; +use azalea_entity::direction_looking_at; +use azalea_world::ChunkStorage; + +/// Get a vec of block positions that we can reach from this position. +pub fn get_reachable_blocks_around_player( + player_position: BlockPos, + chunk_storage: &ChunkStorage, +) -> Vec { + // check a 12x12x12 area around the player + let mut blocks = Vec::new(); + + for x in -6..=6 { + // y is 1 up to somewhat offset for the eye height + for y in -5..=7 { + for z in -6..=6 { + let block_pos = player_position + BlockPos::new(x, y, z); + let block_state = chunk_storage + .get_block_state(&block_pos) + .unwrap_or_default(); + + if block_state.is_air() { + // fast path, skip if it's air + continue; + } + + if can_reach_block(chunk_storage, player_position, block_pos) { + blocks.push(block_pos); + } + } + } + } + + blocks +} + +pub fn pick_closest_block(position: BlockPos, blocks: &[BlockPos]) -> Option { + // pick the closest one and mine it + let mut closest_block_pos = None; + let mut closest_distance = i32::MAX; + for block_pos in &blocks[1..] { + if block_pos.y < position.y { + // skip blocks below us at first + continue; + } + let distance = block_pos.distance_squared_to(&position); + if distance < closest_distance { + closest_block_pos = Some(*block_pos); + closest_distance = distance; + } + } + + if closest_block_pos.is_none() { + // ok now check every block if the only ones around us are below + for block_pos in blocks { + let distance = block_pos.distance_squared_to(&position); + if distance < closest_distance { + closest_block_pos = Some(*block_pos); + closest_distance = distance; + } + } + } + + closest_block_pos +} + +/// Return the block that we'd be looking at if we were at a given position and +/// looking at a given block. +/// +/// This is useful for telling if we'd be able to reach a block from a certain +/// position, like for the pathfinder's [`ReachBlockPosGoal`]. +/// +/// Also see [`get_hit_result_while_looking_at_with_eye_position`]. +/// +/// [`ReachBlockPosGoal`]: crate::pathfinder::goals::ReachBlockPosGoal +pub fn get_hit_result_while_looking_at( + chunk_storage: &ChunkStorage, + player_position: BlockPos, + look_target: BlockPos, +) -> BlockPos { + let eye_position = Vec3 { + x: player_position.x as f64 + 0.5, + y: player_position.y as f64 + 1.53, + z: player_position.z as f64 + 0.5, + }; + get_hit_result_while_looking_at_with_eye_position(chunk_storage, eye_position, look_target) +} + +pub fn can_reach_block( + chunk_storage: &ChunkStorage, + player_position: BlockPos, + look_target: BlockPos, +) -> bool { + let hit_result = get_hit_result_while_looking_at(chunk_storage, player_position, look_target); + hit_result == look_target +} + +/// Return the block that we'd be looking at if our eyes are at a given position +/// and looking at a given block. +/// +/// This is called by [`get_hit_result_while_looking_at`]. +pub fn get_hit_result_while_looking_at_with_eye_position( + chunk_storage: &azalea_world::ChunkStorage, + eye_position: Vec3, + look_target: BlockPos, +) -> BlockPos { + let look_direction = direction_looking_at(&eye_position, &look_target.center()); + let block_hit_result = + azalea_client::interact::pick(&look_direction, &eye_position, chunk_storage, 4.5); + block_hit_result.block_pos +} + +#[cfg(test)] +mod tests { + use azalea_core::position::ChunkPos; + use azalea_world::{Chunk, PartialInstance}; + + use super::*; + + #[test] + fn test_cannot_reach_block_through_wall_when_y_is_negative() { + let mut partial_world = PartialInstance::default(); + let mut world = ChunkStorage::default(); + partial_world + .chunks + .set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world); + + let set_solid_block_at = |x, y, z| { + partial_world.chunks.set_block_state( + &BlockPos::new(x, y, z), + azalea_registry::Block::Stone.into(), + &world, + ); + }; + + let y_offset = -8; + + // walls + set_solid_block_at(1, y_offset, 0); + set_solid_block_at(1, y_offset + 1, 0); + set_solid_block_at(0, y_offset, 1); + set_solid_block_at(0, y_offset + 1, 1); + // target + set_solid_block_at(1, y_offset, 1); + + let player_position = BlockPos::new(0, y_offset, 0); + let look_target = BlockPos::new(1, y_offset, 1); + + assert!(!can_reach_block(&world, player_position, look_target)); + } +} diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index a1bdaaad3..becc163de 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -866,11 +866,33 @@ mod tests { GotoEvent, }; - fn setup_simulation( + fn setup_blockposgoal_simulation( partial_chunks: &mut PartialChunkStorage, start_pos: BlockPos, end_pos: BlockPos, solid_blocks: Vec, + ) -> Simulation { + let mut simulation = setup_simulation_world(partial_chunks, start_pos, solid_blocks); + + // you can uncomment this while debugging tests to get trace logs + // simulation.app.add_plugins(bevy_log::LogPlugin { + // level: bevy_log::Level::TRACE, + // filter: "".to_string(), + // }); + + simulation.app.world.send_event(GotoEvent { + entity: simulation.entity, + goal: Arc::new(BlockPosGoal(end_pos)), + successors_fn: moves::default_move, + allow_mining: false, + }); + simulation + } + + fn setup_simulation_world( + partial_chunks: &mut PartialChunkStorage, + start_pos: BlockPos, + solid_blocks: Vec, ) -> Simulation { let mut chunk_positions = HashSet::new(); for block_pos in &solid_blocks { @@ -889,43 +911,33 @@ mod tests { start_pos.y as f64, start_pos.z as f64 + 0.5, )); - let mut simulation = Simulation::new(chunks, player); - - // you can uncomment this while debugging tests to get trace logs - // simulation.app.add_plugins(bevy_log::LogPlugin { - // level: bevy_log::Level::TRACE, - // filter: "".to_string(), - // }); - - simulation.app.world.send_event(GotoEvent { - entity: simulation.entity, - goal: Arc::new(BlockPosGoal(end_pos)), - successors_fn: moves::default_move, - allow_mining: false, - }); - simulation + Simulation::new(chunks, player) } pub fn assert_simulation_reaches(simulation: &mut Simulation, ticks: usize, end_pos: BlockPos) { - // wait until the bot starts moving + wait_until_bot_starts_moving(simulation); + for _ in 0..ticks { + simulation.tick(); + } + assert_eq!(BlockPos::from(simulation.position()), end_pos); + } + + pub fn wait_until_bot_starts_moving(simulation: &mut Simulation) { let start_pos = simulation.position(); let start_time = Instant::now(); while simulation.position() == start_pos + && !simulation.is_mining() && start_time.elapsed() < Duration::from_millis(500) { simulation.tick(); std::thread::yield_now(); } - for _ in 0..ticks { - simulation.tick(); - } - assert_eq!(BlockPos::from(simulation.position()), end_pos,); } #[test] fn test_simple_forward() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(0, 71, 1), @@ -937,7 +949,7 @@ mod tests { #[test] fn test_double_diagonal_with_walls() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(2, 71, 2), @@ -955,7 +967,7 @@ mod tests { #[test] fn test_jump_with_sideways_momentum() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 3), BlockPos::new(5, 76, 0), @@ -977,7 +989,7 @@ mod tests { #[test] fn test_parkour_2_block_gap() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(0, 71, 3), @@ -989,7 +1001,7 @@ mod tests { #[test] fn test_descend_and_parkour_2_block_gap() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(3, 67, 4), @@ -1008,7 +1020,7 @@ mod tests { #[test] fn test_small_descend_and_parkour_2_block_gap() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(0, 70, 5), @@ -1025,7 +1037,7 @@ mod tests { #[test] fn test_quickly_descend() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(0, 68, 3), @@ -1042,7 +1054,7 @@ mod tests { #[test] fn test_2_gap_ascend_thrice() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(3, 74, 0), @@ -1059,7 +1071,7 @@ mod tests { #[test] fn test_consecutive_3_gap_parkour() { let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_simulation( + let mut simulation = setup_blockposgoal_simulation( &mut partial_chunks, BlockPos::new(0, 71, 0), BlockPos::new(4, 71, 12), diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index 2803b846f..e8ba4dbd4 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -144,8 +144,21 @@ impl Simulation { self.app.update(); self.app.world.run_schedule(GameTick); } + pub fn component(&self) -> T { + self.app.world.get::(self.entity).unwrap().clone() + } + pub fn get_component(&self) -> Option { + self.app.world.get::(self.entity).cloned() + } pub fn position(&self) -> Vec3 { - **self.app.world.get::(self.entity).unwrap() + *self.component::() + } + pub fn is_mining(&self) -> bool { + // return true if the component is present and Some + self.get_component::() + .map(|c| *c) + .flatten() + .is_some() } }