diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs index 9b520d496..8c0cec607 100644 --- a/azalea-block/src/range.rs +++ b/azalea-block/src/range.rs @@ -44,3 +44,19 @@ impl Add for BlockStates { } } } + +impl From> for BlockStates { + fn from(set: HashSet) -> Self { + Self { + set: set.into_iter().map(|block| block.into()).collect(), + } + } +} + +impl From<&HashSet> for BlockStates { + fn from(set: &HashSet) -> Self { + Self { + set: set.iter().map(|&block| block.into()).collect(), + } + } +} diff --git a/azalea-client/src/chunks.rs b/azalea-client/src/chunks.rs index 072fbd313..40e295aa3 100644 --- a/azalea-client/src/chunks.rs +++ b/azalea-client/src/chunks.rs @@ -71,7 +71,7 @@ pub struct ChunkBatchFinishedEvent { pub batch_size: u32, } -fn handle_receive_chunk_events( +pub fn handle_receive_chunk_events( mut events: EventReader, mut query: Query<&mut InstanceHolder>, ) { diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs index 7b14be09b..6e436f550 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/interact.rs @@ -370,3 +370,58 @@ fn update_modifiers_for_held_item( )); } } + +#[cfg(test)] +mod tests { + use azalea_core::position::ChunkPos; + use azalea_world::{Chunk, ChunkStorage, PartialInstance}; + + use super::*; + + #[test] + fn test_pick() { + let mut partial_world = PartialInstance::default(); + let mut world = ChunkStorage::default(); + + partial_world + .chunks + .update_view_center(ChunkPos { x: -184, z: -2 }); + partial_world.chunks.set( + &ChunkPos { x: -184, z: -2 }, + Some(Chunk::default()), + &mut world, + ); + + let set_block = |x, y, z| { + partial_world + .chunks + .set_block_state( + &BlockPos::new(x, y, z), + azalea_registry::Block::Stone.into(), + &world, + ) + .expect(&format!("failed to set block at {x} {y} {z}")); + }; + + for x in -2940..=-2936 { + for z in -24..=-20 { + set_block(x, 64, z); + set_block(x, 65, z); + } + } + + let hit_result = pick( + &LookDirection { + y_rot: 45., + x_rot: 35.66751, + // x_rot: 35., + }, + &Vec3::new(-2936.5, 66. + 1.53, -22.5), + &world, + 4.5, + ); + + assert!(!hit_result.miss); + assert_eq!(hit_result.block_pos, BlockPos::new(-2939, 65, -21)); + } +} diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs index 97eb98eaf..f27f1f8dd 100644 --- a/azalea-client/src/inventory.rs +++ b/azalea-client/src/inventory.rs @@ -750,7 +750,7 @@ pub struct SetSelectedHotbarSlotEvent { /// The hotbar slot to select. This should be in the range 0..=8. pub slot: u8, } -fn handle_set_selected_hotbar_slot_event( +pub fn handle_set_selected_hotbar_slot_event( mut events: EventReader, mut send_packet_events: EventWriter, mut query: Query<&mut InventoryComponent>, 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/delta.rs b/azalea-core/src/delta.rs index 646bcc955..19cb50ad5 100755 --- a/azalea-core/src/delta.rs +++ b/azalea-core/src/delta.rs @@ -48,10 +48,6 @@ impl Vec3 { } } - pub fn length_squared(&self) -> f64 { - self.x * self.x + self.y * self.y + self.z * self.z - } - pub fn normalize(&self) -> Vec3 { let length = f64::sqrt(self.x * self.x + self.y * self.y + self.z * self.z); if length < 1e-4 { diff --git a/azalea-core/src/math.rs b/azalea-core/src/math.rs index aa9d88c8d..25be18df2 100644 --- a/azalea-core/src/math.rs +++ b/azalea-core/src/math.rs @@ -25,7 +25,7 @@ pub fn cos(x: f32) -> f32 { } // TODO: make this generic -pub fn binary_search(mut min: i32, max: i32, predicate: &dyn Fn(i32) -> bool) -> i32 { +pub fn binary_search(mut min: i32, max: i32, predicate: impl Fn(i32) -> bool) -> i32 { let mut diff = max - min; while diff > 0 { let diff_mid = diff / 2; @@ -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-core/src/position.rs b/azalea-core/src/position.rs index 31b2d008f..6d6010215 100755 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -24,25 +24,25 @@ macro_rules! vec3_impl { /// Get the distance of this vector to the origin by doing `x^2 + y^2 + /// z^2`. #[inline] - pub fn length_sqr(&self) -> $type { + pub fn length_squared(&self) -> $type { self.x * self.x + self.y * self.y + self.z * self.z } /// Get the squared distance from this position to another position. - /// Equivalent to `(self - other).length_sqr()`. + /// Equivalent to `(self - other).length_squared()`. #[inline] - pub fn distance_to_sqr(&self, other: &Self) -> $type { - (self - other).length_sqr() + pub fn distance_squared_to(&self, other: &Self) -> $type { + (self - other).length_squared() } #[inline] - pub fn horizontal_distance_sqr(&self) -> $type { + pub fn horizontal_distance_squared(&self) -> $type { self.x * self.x + self.z * self.z } #[inline] - pub fn horizontal_distance_to_sqr(&self, other: &Self) -> $type { - (self - other).horizontal_distance_sqr() + pub fn horizontal_distance_squared_to(&self, other: &Self) -> $type { + (self - other).horizontal_distance_squared() } /// Return a new instance of this position with the y coordinate @@ -266,6 +266,46 @@ impl BlockPos { pub fn length_manhattan(&self) -> u32 { (self.x.abs() + self.y.abs() + self.z.abs()) as u32 } + + /// Make a new BlockPos with the lower coordinates for each axis. + /// + /// ``` + /// # use azalea_core::position::BlockPos; + /// assert_eq!( + /// BlockPos::min( + /// &BlockPos::new(1, 20, 300), + /// &BlockPos::new(50, 40, 30), + /// ), + /// BlockPos::new(1, 20, 30), + /// ); + /// ``` + pub fn min(&self, other: &Self) -> Self { + Self { + x: self.x.min(other.x), + y: self.y.min(other.y), + z: self.z.min(other.z), + } + } + + /// Make a new BlockPos with the higher coordinates for each axis. + /// + /// ``` + /// # use azalea_core::position::BlockPos; + /// assert_eq!( + /// BlockPos::max( + /// &BlockPos::new(1, 20, 300), + /// &BlockPos::new(50, 40, 30), + /// ), + /// BlockPos::new(50, 40, 300), + /// ); + /// ``` + pub fn max(&self, other: &Self) -> Self { + Self { + x: self.x.max(other.x), + y: self.y.max(other.y), + z: self.z.max(other.z), + } + } } /// Chunk coordinates are used to represent where a chunk is in the world. You diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 8761fa249..0081189eb 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -26,6 +26,7 @@ use derive_more::{Deref, DerefMut}; pub use dimensions::EntityDimensions; use plugin::indexing::EntityChunkPos; use std::{ + f64::consts::PI, fmt::Debug, hash::{Hash, Hasher}, }; @@ -426,24 +427,53 @@ impl FluidOnEyes { #[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] pub struct OnClimbable(bool); -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::PartialWorld; - -// #[test] -// fn from_mut_entity_to_ref_entity() { -// let mut world = PartialWorld::default(); -// let uuid = Uuid::from_u128(100); -// world.add_entity( -// 0, -// EntityData::new( -// uuid, -// Vec3::default(), -// EntityMetadata::Player(metadata::Player::default()), -// ), -// ); -// let entity: Entity = world.entity_mut(0).unwrap(); -// assert_eq!(entity.uuid, uuid); -// } -// } +/// Return the look direction that would make a client at `current` be +/// looking at `target`. +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); + + // 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, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_direction_looking_at() { + let direction = direction_looking_at(&Vec3::new(0.0, 0.0, 0.0), &Vec3::new(0.0, 0.0, 1.0)); + assert_eq!(direction.y_rot, 0.0); + assert_eq!(direction.x_rot, 0.0); + + let direction = direction_looking_at(&Vec3::new(0.0, 0.0, 0.0), &Vec3::new(1.0, 0.0, 0.0)); + assert_eq!(direction.y_rot, 270.0); + assert_eq!(direction.x_rot, 0.0); + + let direction = direction_looking_at(&Vec3::new(0.0, 0.0, 0.0), &Vec3::new(0.0, 0.0, -1.0)); + assert_eq!(direction.y_rot, 180.0); + assert_eq!(direction.x_rot, 0.0); + + let direction = direction_looking_at(&Vec3::new(0.0, 0.0, 0.0), &Vec3::new(-1.0, 0.0, 0.0)); + assert_eq!(direction.y_rot, 90.0); + assert_eq!(direction.x_rot, 0.0); + + let direction = direction_looking_at(&Vec3::new(0.0, 0.0, 0.0), &Vec3::new(0.0, 1.0, 0.0)); + assert_eq!(direction.y_rot, 0.0); + assert_eq!(direction.x_rot, -90.0); + + let direction = direction_looking_at(&Vec3::new(0.0, 0.0, 0.0), &Vec3::new(0.0, -1.0, 0.0)); + assert_eq!(direction.y_rot, 0.0); + assert_eq!(direction.x_rot, 90.0); + } +} diff --git a/azalea-entity/src/particle.rs b/azalea-entity/src/particle.rs index 6d8c9fd9b..ab8ac5bbe 100755 --- a/azalea-entity/src/particle.rs +++ b/azalea-entity/src/particle.rs @@ -4,30 +4,19 @@ use azalea_inventory::ItemSlot; use azalea_registry::ParticleKind; use bevy_ecs::component::Component; -#[derive(Component, Debug, Clone, McBuf, Default)] -pub struct Particle { - #[var] - pub id: i32, - pub data: ParticleData, -} - -#[derive(Clone, Debug, McBuf, Default)] -pub enum ParticleData { +// the order of this enum must be kept in-sync with ParticleKind, otherwise +// we get errors parsing particles. +/// A [`ParticleKind`] with data potentially attached to it. +#[derive(Component, Clone, Debug, McBuf, Default)] +pub enum Particle { AngryVillager, - BlockMarker(BlockParticle), Block(BlockParticle), + BlockMarker(BlockParticle), Bubble, - BubbleColumnUp, - BubblePop, - CampfireCosySmoke, - CampfireSignalSmoke, Cloud, - Composter, Crit, - CurrentDown, DamageIndicator, DragonBreath, - Dolphin, DrippingLava, FallingLava, LandingLava, @@ -44,33 +33,35 @@ pub enum ParticleData { EntityEffect, ExplosionEmitter, Explosion, - SonicBoom, - FallingDust(BlockParticle), Gust, SmallGust, GustEmitterLarge, GustEmitterSmall, + SonicBoom, + FallingDust(BlockParticle), Firework, Fishing, Flame, Infested, + CherryLeaves, SculkSoul, SculkCharge(SculkChargeParticle), SculkChargePop, - Soul, SoulFireFlame, + Soul, Flash, HappyVillager, + Composter, Heart, InstantEffect, Item(ItemParticle), + Vibration(VibrationParticle), ItemSlime, ItemCobweb, ItemSnowball, LargeSmoke, Lava, Mycelium, - Nautilus, Note, Poof, Portal, @@ -78,35 +69,40 @@ pub enum ParticleData { Smoke, WhiteSmoke, Sneeze, - Snowflake, Spit, + SquidInk, SweepAttack, TotemOfUndying, - SquidInk, Underwater, Splash, Witch, + BubblePop, + CurrentDown, + BubbleColumnUp, + Nautilus, + Dolphin, + CampfireCosySmoke, + CampfireSignalSmoke, DrippingHoney, FallingHoney, LandingHoney, FallingNectar, FallingSporeBlossom, - SporeBlossomAir, Ash, CrimsonSpore, WarpedSpore, + SporeBlossomAir, DrippingObsidianTear, FallingObsidianTear, LandingObsidianTear, ReversePortal, WhiteAsh, SmallFlame, - DrippingDripstoneWater, - FallingDripstoneWater, - CherryLeaves, + Snowflake, DrippingDripstoneLava, FallingDripstoneLava, - Vibration(VibrationParticle), + DrippingDripstoneWater, + FallingDripstoneWater, GlowSquidInk, Glow, WaxOn, @@ -120,12 +116,12 @@ pub enum ParticleData { TrialSpawnerDetectionOminous, VaultConnection, DustPillar, + OminousSpawning, RaidOmen, TrialOmen, - OminousSpawning, } -impl From for ParticleData { +impl From for Particle { /// Convert a particle kind into particle data. If the particle has data /// attached (like block particles), then it's set to the default. fn from(kind: ParticleKind) -> Self { diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs index 8c2d0c8fb..923e877ac 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; @@ -62,21 +62,15 @@ pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResul context.from, context.to, context, - |context, block_pos| { + |ctx, block_pos| { let block_state = chunk_storage.get_block_state(block_pos).unwrap_or_default(); // TODO: add fluid stuff to this (see getFluidState in vanilla source) - let block_shape = context.block_shape(block_state); - clip_with_interaction_override( - &context.from, - &context.to, - block_pos, - block_shape, - &block_state, - ) + let block_shape = ctx.block_shape(block_state); + clip_with_interaction_override(&ctx.from, &ctx.to, block_pos, block_shape, &block_state) // let block_distance = if let Some(block_hit_result) = - // block_hit_result { context.from.distance_to_sqr(& + // block_hit_result { context.from.distance_squared_to(& // block_hit_result.location) } else { - // f64::MAX + // f64::INFINITY // }; }, |context| { @@ -90,19 +84,6 @@ pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResul ) } -// default BlockHitResult clipWithInteractionOverride(Vec3 world, Vec3 from, -// BlockPos to, VoxelShape shape, BlockState block) { -// BlockHitResult blockHitResult = shape.clip(world, from, to); -// if (blockHitResult != null) { -// BlockHitResult var7 = block.getInteractionShape(this, to).clip(world, -// from, to); if (var7 != null -// && var7.getLocation().subtract(world).lengthSqr() < -// blockHitResult.getLocation().subtract(world).lengthSqr()) { return -// blockHitResult.withDirection(var7.getDirection()); } -// } - -// return blockHitResult; -// } fn clip_with_interaction_override( from: &Vec3, to: &Vec3, @@ -111,18 +92,17 @@ 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 { - if interaction_hit_result.location.distance_to_sqr(from) - < block_hit_result.location.distance_to_sqr(from) + if interaction_hit_result.location.distance_squared_to(from) + < block_hit_result.location.distance_squared_to(from) { return Some(block_hit_result.with_direction(interaction_hit_result.direction)); } @@ -191,24 +171,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-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 3986dc477..7d7ddc5e4 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -58,7 +58,7 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics) // let entity_collisions = world.get_entity_collisions(self, // entity_bounding_box.expand_towards(movement)); let entity_collisions = Vec::new(); - let collided_delta = if movement.length_sqr() == 0.0 { + let collided_delta = if movement.length_squared() == 0.0 { *movement } else { collide_bounding_box( @@ -109,12 +109,16 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics) entity_collisions.clone(), ) .add(directly_up_delta); - if target_movement.horizontal_distance_sqr() > step_to_delta.horizontal_distance_sqr() { + if target_movement.horizontal_distance_squared() + > step_to_delta.horizontal_distance_squared() + { step_to_delta = target_movement; } } - if step_to_delta.horizontal_distance_sqr() > collided_delta.horizontal_distance_sqr() { + if step_to_delta.horizontal_distance_squared() + > collided_delta.horizontal_distance_squared() + { return step_to_delta.add(collide_bounding_box( &Vec3 { x: 0., @@ -162,7 +166,7 @@ pub fn move_colliding( let collide_result = collide(movement, world, physics); - let move_distance = collide_result.length_sqr(); + let move_distance = collide_result.length_squared(); if move_distance > EPSILON { // TODO: fall damage diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs index edcfbf24d..5e06cc407 100755 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -421,15 +421,15 @@ impl VoxelShape { return None; } let vector = to - from; - if vector.length_sqr() < EPSILON { + if vector.length_squared() < EPSILON { return None; } - let right_after_start = from + &(vector * 0.0001); + let right_after_start = from + &(vector * 0.001); if self.shape().is_full_wide( - self.find_index(Axis::X, right_after_start.x - block_pos.x as f64), - self.find_index(Axis::Y, right_after_start.y - block_pos.y as f64), - self.find_index(Axis::Z, right_after_start.z - block_pos.z as f64), + self.find_index(Axis::X, right_after_start.x - (block_pos.x as f64)), + self.find_index(Axis::Y, right_after_start.y - (block_pos.y as f64)), + self.find_index(Axis::Z, right_after_start.z - (block_pos.z as f64)), ) { Some(BlockHitResult { block_pos: *block_pos, diff --git a/azalea-protocol/src/packets/game/clientbound_level_particles_packet.rs b/azalea-protocol/src/packets/game/clientbound_level_particles_packet.rs index b543d4f0a..eae996347 100755 --- a/azalea-protocol/src/packets/game/clientbound_level_particles_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_level_particles_packet.rs @@ -15,3 +15,25 @@ pub struct ClientboundLevelParticlesPacket { pub count: u32, pub particle: Particle, } + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use azalea_buf::McBufReadable; + + use super::*; + + #[test] + fn test_clientbound_level_particles_packet() { + let slice = &[ + 0, 64, 139, 10, 0, 0, 0, 0, 0, 192, 26, 0, 0, 0, 0, 0, 0, 64, 144, 58, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 13, 63, 128, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 63, 128, 0, 0, + ][..]; + let mut bytes = Cursor::new(slice); + + let _packet = ClientboundLevelParticlesPacket::read_from(&mut bytes).unwrap(); + assert_eq!(bytes.position(), slice.len() as u64); + } +} diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index 77345acca..593478e3b 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -1,8 +1,15 @@ use azalea_block::{Block, BlockState}; -use azalea_client::{inventory::InventoryComponent, Client}; -use azalea_entity::{FluidOnEyes, Physics}; +use azalea_client::{ + inventory::{InventoryComponent, SetSelectedHotbarSlotEvent}, + mining::StartMiningBlockEvent, + Client, InstanceHolder, +}; +use azalea_core::position::BlockPos; +use azalea_entity::{update_fluid_on_eyes, FluidOnEyes, Physics}; use azalea_inventory::{ItemSlot, Menu}; use azalea_registry::{DataComponentKind, Fluid}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; #[derive(Debug)] pub struct BestToolResult { @@ -10,6 +17,20 @@ pub struct BestToolResult { pub percentage_per_tick: f32, } +pub struct AutoToolPlugin; +impl Plugin for AutoToolPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_systems( + Update, + start_mining_block_with_auto_tool_listener + .before(azalea_client::inventory::handle_set_selected_hotbar_slot_event) + .after(update_fluid_on_eyes) + .after(azalea_client::chunks::handle_receive_chunk_events), + ); + } +} + pub trait AutoToolClientExt { fn best_tool_in_hotbar_for_block(&self, block: BlockState) -> BestToolResult; } @@ -139,3 +160,49 @@ pub fn accurate_best_tool_in_hotbar_for_block( percentage_per_tick: best_speed, } } + +/// An event to mine a given block, while automatically picking the best tool in +/// our hotbar to use. +#[derive(Event)] +pub struct StartMiningBlockWithAutoToolEvent { + pub entity: Entity, + pub position: BlockPos, +} + +pub fn start_mining_block_with_auto_tool_listener( + mut query: Query<( + &mut InstanceHolder, + &InventoryComponent, + &Physics, + &FluidOnEyes, + )>, + mut events: EventReader, + mut set_selected_hotbar_slot_events: EventWriter, + mut start_mining_block_events: EventWriter, +) { + for event in events.read() { + let (instance_holder, inventory, physics, fluid_on_eyes) = + query.get_mut(event.entity).unwrap(); + let instance = instance_holder.instance.read(); + let block_state = instance + .chunks + .get_block_state(&event.position) + .unwrap_or_default(); + + let best_tool_result = accurate_best_tool_in_hotbar_for_block( + block_state, + &inventory.inventory_menu, + physics, + fluid_on_eyes, + ); + + set_selected_hotbar_slot_events.send(SetSelectedHotbarSlotEvent { + entity: event.entity, + slot: best_tool_result.index as u8, + }); + start_mining_block_events.send(StartMiningBlockEvent { + entity: event.entity, + position: event.position, + }); + } +} diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 529bb251a..463fc0137 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -1,6 +1,7 @@ use crate::accept_resource_packs::AcceptResourcePacksPlugin; use crate::app::{App, Plugin, PluginGroup, PluginGroupBuilder}; use crate::auto_respawn::AutoRespawnPlugin; +use crate::auto_tool::AutoToolPlugin; use crate::container::ContainerPlugin; use crate::ecs::{ component::Component, @@ -14,6 +15,7 @@ use azalea_client::mining::Mining; use azalea_client::TickBroadcast; use azalea_core::position::{BlockPos, Vec3}; use azalea_core::tick::GameTick; +use azalea_entity::direction_looking_at; use azalea_entity::{ clamp_look_direction, metadata::Player, EyeHeight, Jumping, LocalEntity, LookDirection, Position, @@ -23,7 +25,6 @@ use bevy_app::Update; use bevy_ecs::prelude::Event; use bevy_ecs::schedule::IntoSystemConfigs; use futures_lite::Future; -use std::f64::consts::PI; use tracing::trace; use crate::pathfinder::PathfinderPlugin; @@ -164,7 +165,7 @@ pub struct LookAtEvent { /// The position we want the entity to be looking at. pub position: Vec3, } -fn look_at_listener( +pub fn look_at_listener( mut events: EventReader, mut query: Query<(&Position, &EyeHeight, &mut LookDirection)>, ) { @@ -182,25 +183,6 @@ fn look_at_listener( } } -/// Return the look direction that would make a client at `current` be -/// looking at `target`. -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); - - // 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 /// client. pub struct DefaultBotPlugins; @@ -213,5 +195,6 @@ impl PluginGroup for DefaultBotPlugins { .add(ContainerPlugin) .add(AutoRespawnPlugin) .add(AcceptResourcePacksPlugin) + .add(AutoToolPlugin) } } diff --git a/azalea/src/pathfinder/block_box.rs b/azalea/src/pathfinder/block_box.rs new file mode 100644 index 000000000..4ab0d4504 --- /dev/null +++ b/azalea/src/pathfinder/block_box.rs @@ -0,0 +1,78 @@ +use azalea_core::position::BlockPos; + +#[derive(Debug, Clone)] +pub struct BlockBox { + min: BlockPos, + max: BlockPos, +} + +impl BlockBox { + /// Create a new box from two corners. + pub fn new(corner1: BlockPos, corner2: BlockPos) -> Self { + Self { + min: BlockPos::min(&corner1, &corner2), + max: BlockPos::max(&corner1, &corner2), + } + } + + /// The lower corner of the box. + pub fn min(&self) -> BlockPos { + self.min + } + + /// The upper corner of the box. + pub fn max(&self) -> BlockPos { + self.max + } + + pub fn contains(&self, pos: BlockPos) -> bool { + pos.x >= self.min.x + && pos.x <= self.max.x + && pos.y >= self.min.y + && pos.y <= self.max.y + && pos.z >= self.min.z + && pos.z <= self.max.z + } + + pub fn distances_to(&self, pos: BlockPos) -> (i32, i32, i32) { + let dx = if pos.x < self.min.x { + self.min.x - pos.x + } else if pos.x > self.max.x { + pos.x - self.max.x + } else { + 0 + }; + let dy = if pos.y < self.min.y { + self.min.y - pos.y + } else if pos.y > self.max.y { + pos.y - self.max.y + } else { + 0 + }; + let dz = if pos.z < self.min.z { + self.min.z - pos.z + } else if pos.z > self.max.z { + pos.z - self.max.z + } else { + 0 + }; + + (dx, dy, dz) + } + + pub fn distance_squared_to(&self, pos: BlockPos) -> u32 { + if self.contains(pos) { + return 0; + } + + let (dx, dy, dz) = self.distances_to(pos); + (dx * dx + dy * dy + dz * dz) as u32 + } + + /// Get the block position inside of the box that is closest to the given + /// position. + pub fn closest_block_pos(&self, pos: BlockPos) -> BlockPos { + let (dx, dy, dz) = self.distances_to(pos); + BlockPos::new(pos.x + dx, pos.y + dy, pos.z + dz) + } +} diff --git a/azalea/src/pathfinder/debug.rs b/azalea/src/pathfinder/debug.rs index 201803c9b..b8252726f 100644 --- a/azalea/src/pathfinder/debug.rs +++ b/azalea/src/pathfinder/debug.rs @@ -62,7 +62,7 @@ pub fn debug_render_path_with_particles( 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 step_count = (start_vec3.distance_squared_to(&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 @@ -91,7 +91,7 @@ pub fn debug_render_path_with_particles( 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}", + "/particle dust{{color:[{r},{g},{b}],scale:{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, diff --git a/azalea/src/pathfinder/extras/goals.rs b/azalea/src/pathfinder/extras/goals.rs new file mode 100644 index 000000000..def3b83cb --- /dev/null +++ b/azalea/src/pathfinder/extras/goals.rs @@ -0,0 +1,113 @@ +//! Slightly more unusual goals than the normal +//! [pathfinder ones](crate::pathfinder::goals). + +use azalea_core::position::BlockPos; +use azalea_world::ChunkStorage; + +use crate::pathfinder::{ + block_box::BlockBox, + goals::{xz_heuristic, y_heuristic, BlockPosGoal, Goal}, +}; + +use super::utils::get_hit_result_while_looking_at; + +/// 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 distance = (self.pos - n).length_squared(); + if distance > max_pick_range * max_pick_range { + return false; + } + + let block_hit_result = get_hit_result_while_looking_at(&self.chunk_storage, n, self.pos); + + block_hit_result == self.pos + } +} + +/// Move to a position inside of the given box (inclusive, so the corners are +/// included in the box). +#[derive(Debug)] +pub struct BoxGoal(pub BlockBox); + +impl Goal for BoxGoal { + fn heuristic(&self, n: BlockPos) -> f32 { + let dx = if n.x < self.0.min().x { + self.0.min().x - n.x + } else if n.x > self.0.max().x { + n.x - self.0.max().x + } else { + 0 + }; + let dy = if n.y < self.0.min().y { + self.0.min().y - n.y + } else if n.y > self.0.max().y { + n.y - self.0.max().y + } else { + 0 + }; + let dz = if n.z < self.0.min().z { + self.0.min().z - n.z + } else if n.z > self.0.max().z { + n.z - self.0.max().z + } else { + 0 + }; + + xz_heuristic(dx as f32, dz as f32) + y_heuristic(dy as f32) + } + + fn success(&self, n: BlockPos) -> bool { + n.x >= self.0.min().x + && n.x <= self.0.max().x + && n.y >= self.0.min().y + && n.y <= self.0.max().y + && n.z >= self.0.min().z + && n.z <= self.0.max().z + } +} + +/// Move to a position where we can reach at least one block from the given box. +/// This is usually used when digging out an area. +#[derive(Debug)] +pub struct ReachBoxGoal { + pub bb: BlockBox, + pub chunk_storage: ChunkStorage, +} +impl Goal for ReachBoxGoal { + fn heuristic(&self, n: BlockPos) -> f32 { + BoxGoal(self.bb.clone()).heuristic(n) + } + + fn success(&self, n: BlockPos) -> bool { + // succeed if we're already in the box + if self.bb.contains(n) { + return true; + } + + // only do the expensive check if we're close enough + let max_pick_range = 6; + + let distance = self.bb.distance_squared_to(n); + if distance > max_pick_range * max_pick_range { + return false; + } + + // look at the closest block + let look_target = self.bb.closest_block_pos(n); + let hit_result = get_hit_result_while_looking_at(&self.chunk_storage, n, look_target); + + self.bb.contains(hit_result) + } +} diff --git a/azalea/src/pathfinder/extras/mod.rs b/azalea/src/pathfinder/extras/mod.rs new file mode 100644 index 000000000..c9ba9d308 --- /dev/null +++ b/azalea/src/pathfinder/extras/mod.rs @@ -0,0 +1,74 @@ +//! Adds utility functions that all depend on the pathfinder. + +pub mod goals; +pub mod pickup; +pub mod process; +pub mod utils; + +use crate::ecs::prelude::*; +use azalea_block::BlockStates; +use azalea_client::{mining::MiningSet, Client}; +use azalea_core::{position::BlockPos, tick::GameTick}; +use azalea_physics::PhysicsSet; +use bevy_app::Update; + +use crate::app::{App, Plugin}; + +use self::process::{mine_area::MineArea, Process, SetActiveProcessEvent}; + +pub struct PathfinderExtrasPlugin; + +impl Plugin for PathfinderExtrasPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_systems( + Update, + ( + process::set_active_pathfinder_process_listener + .after(crate::pathfinder::stop_pathfinding_on_instance_change) + .before(crate::pathfinder::handle_stop_pathfinding_event), + pickup::add_pickup_components_to_player, + pickup::remove_pickup_components_from_player, + ( + pickup::watch_for_mined_blocks, + pickup::watch_for_item_spawns_from_blocks_we_mined, + ) + .chain() + .after(MiningSet), + ), + ) + .add_systems( + GameTick, + ( + pickup::remove_despawned_items_to_pickup, + process::process_tick.before(PhysicsSet), + ) + .chain(), + ); + } +} + +pub trait PathfinderExtrasClientExt { + fn set_active_pathfinder_process(&self, process: impl Into); + fn mine_area(&self, corner1: BlockPos, corner2: BlockPos); + fn mine_forever(&self, block_states: impl Into); +} + +impl PathfinderExtrasClientExt for Client { + fn set_active_pathfinder_process(&self, process: impl Into) { + let process = process.into(); + self.ecs.lock().send_event(SetActiveProcessEvent { + entity: self.entity, + process, + }); + } + + fn mine_area(&self, corner1: BlockPos, corner2: BlockPos) { + self.set_active_pathfinder_process(MineArea { corner1, corner2 }); + } + + fn mine_forever(&self, block_states: impl Into) { + let block_states = block_states.into(); + self.set_active_pathfinder_process(process::mine_forever::MineForever { block_states }); + } +} diff --git a/azalea/src/pathfinder/extras/pickup.rs b/azalea/src/pathfinder/extras/pickup.rs new file mode 100644 index 000000000..b403b4ec1 --- /dev/null +++ b/azalea/src/pathfinder/extras/pickup.rs @@ -0,0 +1,136 @@ +use std::{collections::VecDeque, time::Instant}; + +use azalea_client::mining::FinishMiningBlockEvent; +use azalea_core::position::BlockPos; +use azalea_entity::Position; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; + +#[derive(Debug)] +pub struct RecentlyMinedBlock { + pub block: BlockPos, + pub time: Instant, +} + +/// A component that contains the blocks that we finished mining recently. When +/// a new item is added, the ones that were added more than 5 seconds ago are +/// removed. +/// +/// This is only present when the entity has the +/// [`Process`](super::process::Process) component, since it's currently only +/// used for picking up items we mined while pathfinding. +#[derive(Component, Debug, Default)] +pub struct RecentlyMinedBlocks { + pub blocks: VecDeque, +} + +#[derive(Component, Debug, Default)] +pub struct ItemsToPickup { + pub items: Vec, +} + +/// This is used internally to recalculate the path when there's a new item to +/// pickup. +#[derive(Component, Debug, Default)] +pub struct LastItemsToPickup { + pub items: Vec, +} +/// A component that tracks whether we've acknowledged the items to pickup +/// change. +/// +/// This is only used internally for recalculating paths when there's a new item +/// to pick up. +#[derive(Component, Debug, Deref, DerefMut)] +pub struct ItemsToPickupChangeAcknowledged(pub bool); + +pub fn add_pickup_components_to_player( + mut commands: Commands, + mut query: Query>, +) { + for entity in &mut query { + commands.entity(entity).insert(( + RecentlyMinedBlocks::default(), + ItemsToPickup::default(), + LastItemsToPickup::default(), + ItemsToPickupChangeAcknowledged(true), + )); + } +} + +pub fn remove_pickup_components_from_player( + mut commands: Commands, + mut query: RemovedComponents, +) { + for entity in query.read() { + commands + .entity(entity) + .remove::() + .remove::() + .remove::() + .remove::(); + } +} + +pub fn watch_for_mined_blocks( + mut finish_mining_block_events: EventReader, + mut query: Query<&mut RecentlyMinedBlocks, With>, +) { + for event in finish_mining_block_events.read() { + let mut recently_mined_blocks = query.get_mut(event.entity).unwrap(); + + // remove blocks that are too old + let now = Instant::now(); + recently_mined_blocks + .blocks + .retain(|block| now.duration_since(block.time).as_secs_f32() < 5.0); + + recently_mined_blocks.blocks.push_back(RecentlyMinedBlock { + block: event.position, + time: now, + }); + } +} + +pub fn watch_for_item_spawns_from_blocks_we_mined( + mut player_query: Query<(&RecentlyMinedBlocks, &Position, &mut ItemsToPickup)>, + spawned_items_query: Query<(Entity, &Position), Added>, +) { + for (recently_mined_blocks, player_position, mut items_to_pickup) in &mut player_query { + for (entity, position) in &mut spawned_items_query.iter() { + if recently_mined_blocks + .blocks + .iter() + .any(|block| block.block == BlockPos::from(position)) + { + // if we're already within 1 block of the item, ignore because we probably + // already picked it up + if (player_position.distance_squared_to(position) < 1.0) + || (player_position + .up(player_position.y + 1.8) + .distance_squared_to(position) + < 1.0) + { + // this check isn't perfect since minecraft checks with the bounding box, and + // the distance is different vertically, but it's good enough for our purposes + continue; + } + + items_to_pickup.items.push(entity); + println!("added item to pickup: {:?}", entity); + } + } + } +} + +/// Remove items from [`ItemsToPickup`] that no longer exist. This doesn't need +/// to run super frequently, so it only runs every tick. +pub fn remove_despawned_items_to_pickup( + mut player_query: Query<&mut ItemsToPickup>, + items_query: Query>, +) { + for mut items_to_pickup in &mut player_query { + items_to_pickup + .items + .retain(|entity| items_query.get(*entity).is_ok()); + } +} diff --git a/azalea/src/pathfinder/extras/process/mine_area.rs b/azalea/src/pathfinder/extras/process/mine_area.rs new file mode 100644 index 000000000..a9848ed6f --- /dev/null +++ b/azalea/src/pathfinder/extras/process/mine_area.rs @@ -0,0 +1,210 @@ +use std::sync::Arc; + +use azalea_block::BlockState; +use azalea_core::position::BlockPos; +use azalea_world::ChunkStorage; +use tracing::info; + +use crate::{ + auto_tool::StartMiningBlockWithAutoToolEvent, + ecs::prelude::*, + pathfinder::{ + self, + block_box::BlockBox, + extras::{ + goals::{ReachBlockPosGoal, ReachBoxGoal}, + utils::{get_reachable_blocks_around_player, pick_closest_block}, + }, + goals::Goal, + GotoEvent, + }, + LookAtEvent, +}; + +use super::{Process, ProcessSystemComponents}; + +#[derive(Clone, Debug)] +pub struct MineArea { + pub corner1: BlockPos, + pub corner2: BlockPos, +} + +pub fn mine_area( + mine_area: &MineArea, + commands: &mut Commands, + ProcessSystemComponents { + entity, + position, + instance_holder, + pathfinder, + mining, + executing_path, + .. + }: ProcessSystemComponents<'_>, + goto_events: &mut EventWriter, + look_at_events: &mut EventWriter, + start_mining_block_events: &mut EventWriter, +) { + if pathfinder.goal.is_some() || executing_path.is_some() { + // already pathfinding + return; + } + + if mining.is_some() { + // currently mining, so wait for that to finish + return; + } + + let bb = BlockBox::new(mine_area.corner1, mine_area.corner2); + let chunk_storage = instance_holder.instance.read().chunks.clone(); + let player_position = BlockPos::from(position); + + println!("player_position: {player_position}"); + + // the index is from the top-down, so 0 means the top layer + let layer_index = determine_layer(&bb, &chunk_storage); + let layer_bb = BlockBox::new( + BlockPos::new( + bb.min().x, + i32::max(bb.min().y, bb.max().y - layer_index as i32), + bb.min().z, + ), + BlockPos::new( + bb.max().x, + i32::max(bb.min().y, bb.max().y - layer_index as i32), + bb.max().z, + ), + ); + + let reachable_blocks = get_reachable_blocks_around_player(player_position, &chunk_storage); + let mineable_blocks = reachable_blocks + .into_iter() + .filter(|block_pos| { + // must be within box + if !layer_bb.contains(*block_pos) { + return false; + } + + // and must be mineable + let block = chunk_storage.get_block_state(block_pos).unwrap_or_default(); + + is_block_mineable(block) + }) + .collect::>(); + + println!("mineable_blocks: {:?}", mineable_blocks); + + if !mineable_blocks.is_empty() { + // pick the closest one and mine it + let closest_block_pos = pick_closest_block(player_position, &mineable_blocks) + .expect("there must be a closest block because mineable_blocks wasn't empty"); + + look_at_events.send(LookAtEvent { + entity, + position: closest_block_pos.center(), + }); + start_mining_block_events.send(StartMiningBlockWithAutoToolEvent { + entity, + position: closest_block_pos, + }); + + println!("start mining block {closest_block_pos:?}"); + return; + } + + // no mineable blocks, so go towards the blocks that can be mined + + let goal: Arc = if bb.distance_squared_to(player_position) < 16 * 16 { + // already close enough to the box, path to the closest + // block instead + + let mut block_positions_and_distances = Vec::new(); + for x in layer_bb.min().x..=layer_bb.max().x { + for y in layer_bb.min().y..=layer_bb.max().y { + for z in layer_bb.min().z..=layer_bb.max().z { + let block_pos = BlockPos::new(x, y, z); + + if !is_block_mineable( + chunk_storage + .get_block_state(&block_pos) + .unwrap_or_default(), + ) { + continue; + } + + let distance = block_pos.distance_squared_to(&player_position); + block_positions_and_distances.push((block_pos, distance)); + } + } + } + + if block_positions_and_distances.is_empty() { + info!("MineArea process is done, no more blocks to mine!"); + commands.entity(entity).remove::(); + return; + } + + // use the closest 64 blocks as the goals + + block_positions_and_distances.sort_by_key(|(_, distance)| *distance); + let mut goals = Vec::new(); + for (block_pos, _) in block_positions_and_distances.into_iter().take(64) { + goals.push(ReachBlockPosGoal { + pos: block_pos, + chunk_storage: chunk_storage.clone(), + }); + } + + let reach_blocks_goal = pathfinder::goals::OrGoals(goals); + + println!("reaching for block"); + + Arc::new(reach_blocks_goal) + } else { + println!("reaching for box because we're at {player_position}"); + + let reach_box_goal = ReachBoxGoal { + bb: bb.clone(), + chunk_storage: chunk_storage.clone(), + }; + + Arc::new(reach_box_goal) + }; + + goto_events.send(GotoEvent { + entity, + goal, + successors_fn: pathfinder::moves::default_move, + allow_mining: true, + }); +} + +fn is_block_mineable(block: BlockState) -> bool { + !block.is_air() +} + +/// Determine what layer should be mined first. This is from the top-down, so 0 +/// means the top layer. +fn determine_layer(bb: &BlockBox, chunks: &ChunkStorage) -> usize { + let mut layer = 0; + let mut y = bb.max().y; + while y >= bb.min().y { + let mut x = bb.min().x; + while x <= bb.max().x { + let mut z = bb.min().z; + while z <= bb.max().z { + let block = chunks + .get_block_state(&BlockPos::new(x, y, z)) + .unwrap_or_default(); + if is_block_mineable(block) { + return layer; + } + z += 1; + } + x += 1; + } + y -= 1; + layer += 1; + } + layer +} diff --git a/azalea/src/pathfinder/extras/process/mine_forever.rs b/azalea/src/pathfinder/extras/process/mine_forever.rs new file mode 100644 index 000000000..d2423a864 --- /dev/null +++ b/azalea/src/pathfinder/extras/process/mine_forever.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use azalea_block::BlockStates; +use azalea_core::position::{BlockPos, Vec3}; +use tracing::info; + +use crate::{ + auto_tool::StartMiningBlockWithAutoToolEvent, + ecs::prelude::*, + pathfinder::{ + self, + extras::{ + goals::ReachBlockPosGoal, + utils::{can_reach_block, pick_closest_block}, + }, + GotoEvent, + }, + LookAtEvent, +}; + +use super::{Process, ProcessSystemComponents}; + +#[derive(Clone, Debug)] +pub struct MineForever { + pub block_states: BlockStates, +} + +pub fn mine_forever( + mine_forever: &MineForever, + commands: &mut Commands, + ProcessSystemComponents { + entity, + position, + instance_holder, + pathfinder, + mining, + executing_path, + mut items_to_pickup_change_acknowledged, + }: ProcessSystemComponents<'_>, + items_to_pickup_positions: &[Vec3], + goto_events: &mut EventWriter, + look_at_events: &mut EventWriter, + start_mining_block_events: &mut EventWriter, +) { + let mut should_force_recalculate_path = false; + + if !pathfinder.is_calculating { + if !**items_to_pickup_change_acknowledged { + should_force_recalculate_path = true; + **items_to_pickup_change_acknowledged = true; + println!("items_to_pickup_change_acknowledged = true"); + } + } + + if !should_force_recalculate_path { + if mining.is_some() { + // currently mining, so wait for that to finish + return; + } + + if pathfinder.goal.is_some() || executing_path.is_some() { + // already pathfinding + return; + } + } + + let instance = &instance_holder.instance.read(); + + let target_blocks = instance + .find_blocks(position, &mine_forever.block_states) + .take(16) + .collect::>(); + + let chunk_storage = instance.chunks.clone(); + let player_position = BlockPos::from(position); + + let mineable_blocks = target_blocks + .iter() + .filter(|target_pos| can_reach_block(&chunk_storage, player_position, **target_pos)) + .copied() + .collect::>(); + + if !mineable_blocks.is_empty() { + // pick the closest one and mine it + let closest_block_pos = pick_closest_block(player_position, &mineable_blocks) + .expect("there must be a closest block because mineable_blocks wasn't empty"); + + look_at_events.send(LookAtEvent { + entity, + position: closest_block_pos.center(), + }); + start_mining_block_events.send(StartMiningBlockWithAutoToolEvent { + entity, + position: closest_block_pos, + }); + + println!("start mining block {closest_block_pos:?}"); + return; + } + + let mut reach_block_goals = Vec::new(); + for target_pos in target_blocks { + reach_block_goals.push(ReachBlockPosGoal { + pos: target_pos, + chunk_storage: chunk_storage.clone(), + }); + } + + let mut reach_item_goals = Vec::new(); + for &item_position in items_to_pickup_positions { + println!("item_position: {item_position:?}"); + reach_item_goals.push(pathfinder::goals::RadiusGoal { + pos: item_position, + radius: 1.0, + }); + } + + if reach_block_goals.is_empty() && reach_item_goals.is_empty() { + info!("MineForever process is done, can't find any more blocks to mine"); + commands.entity(entity).remove::(); + return; + } + + goto_events.send(GotoEvent { + entity, + goal: Arc::new(pathfinder::goals::OrGoal( + pathfinder::goals::OrGoals(reach_block_goals), + pathfinder::goals::ScaleGoal(pathfinder::goals::OrGoals(reach_item_goals), 0.5), + )), + successors_fn: pathfinder::moves::default_move, + allow_mining: true, + }); +} diff --git a/azalea/src/pathfinder/extras/process/mod.rs b/azalea/src/pathfinder/extras/process/mod.rs new file mode 100644 index 000000000..9a1165926 --- /dev/null +++ b/azalea/src/pathfinder/extras/process/mod.rs @@ -0,0 +1,145 @@ +pub mod mine_area; +pub mod mine_forever; + +use azalea_client::{mining::Mining, InstanceHolder}; +use azalea_entity::Position; + +use crate::{ + auto_tool::StartMiningBlockWithAutoToolEvent, + ecs::prelude::*, + pathfinder::{self, ExecutingPath, GotoEvent, Pathfinder}, + LookAtEvent, +}; + +use super::pickup::{ItemsToPickup, ItemsToPickupChangeAcknowledged, LastItemsToPickup}; + +#[derive(Component, Clone, Debug)] +pub enum Process { + MineArea(mine_area::MineArea), + MineForever(mine_forever::MineForever), +} + +impl From for Process { + fn from(mine_area: mine_area::MineArea) -> Self { + Self::MineArea(mine_area) + } +} +impl From for Process { + fn from(mine_forever: mine_forever::MineForever) -> Self { + Self::MineForever(mine_forever) + } +} + +#[derive(Event)] +pub struct SetActiveProcessEvent { + pub entity: Entity, + pub process: Process, +} + +pub fn set_active_pathfinder_process_listener( + mut commands: Commands, + mut events: EventReader, + mut stop_pathfinding_events: EventWriter, +) { + for event in events.read() { + stop_pathfinding_events.send(pathfinder::StopPathfindingEvent { + entity: event.entity, + force: false, + }); + commands.entity(event.entity).insert(event.process.clone()); + } +} + +pub struct ProcessSystemComponents<'a> { + pub entity: Entity, + pub position: &'a Position, + pub instance_holder: &'a InstanceHolder, + pub pathfinder: &'a Pathfinder, + pub items_to_pickup_change_acknowledged: Mut<'a, ItemsToPickupChangeAcknowledged>, + pub mining: Option<&'a Mining>, + pub executing_path: Option<&'a ExecutingPath>, +} + +#[allow(clippy::type_complexity)] +pub fn process_tick( + mut commands: Commands, + mut query: Query<( + Entity, + &Process, + &Position, + &InstanceHolder, + &Pathfinder, + &ItemsToPickup, + &mut LastItemsToPickup, + &mut ItemsToPickupChangeAcknowledged, + Option<&Mining>, + Option<&ExecutingPath>, + )>, + position_query: Query<&Position>, + mut goto_events: EventWriter, + mut look_at_events: EventWriter, + mut start_mining_block_events: EventWriter, +) { + for ( + entity, + process, + position, + instance_holder, + pathfinder, + items_to_pickup, + mut last_items_to_pickup, + mut items_to_pickup_change_acknowledged, + mining, + executing_path, + ) in &mut query + { + let items_to_pickup_positions = items_to_pickup + .items + .iter() + .filter_map(|&e| position_query.get(e).ok()) + .map(|p| **p) + .collect::>(); + // if there's any item in items_to_pickup that isn't in last_items_to_pickup + let is_items_to_pickup_changed = items_to_pickup + .items + .iter() + .any(|&e| !last_items_to_pickup.items.contains(&e)); + if is_items_to_pickup_changed { + **items_to_pickup_change_acknowledged = false; + last_items_to_pickup.items = items_to_pickup.items.clone(); + } + + let components = ProcessSystemComponents { + entity, + position, + instance_holder, + pathfinder, + items_to_pickup_change_acknowledged, + mining, + executing_path, + }; + match process { + Process::MineArea(mine_area) => { + mine_area::mine_area( + mine_area, + &mut commands, + components, + &mut goto_events, + &mut look_at_events, + &mut start_mining_block_events, + ); + } + Process::MineForever(mine_forever) => { + mine_forever::mine_forever( + mine_forever, + &mut commands, + components, + &items_to_pickup_positions, + &mut goto_events, + &mut look_at_events, + &mut start_mining_block_events, + ); + } + } + } +} 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/goals.rs b/azalea/src/pathfinder/goals.rs index 3f8c79932..e4057e9eb 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -3,18 +3,17 @@ 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}; -pub trait Goal { +pub trait Goal: Send + Sync { #[must_use] fn heuristic(&self, n: BlockPos) -> f32; #[must_use] fn success(&self, n: BlockPos) -> bool; } -/// Move to the given block position. +/// Move to the given block position. This is the most commonly used goal. #[derive(Debug)] pub struct BlockPosGoal(pub BlockPos); impl Goal for BlockPosGoal { @@ -30,7 +29,7 @@ impl Goal for BlockPosGoal { } } -fn xz_heuristic(dx: f32, dz: f32) -> f32 { +pub fn xz_heuristic(dx: f32, dz: f32) -> f32 { let x = dx.abs(); let z = dz.abs(); @@ -65,7 +64,7 @@ impl Goal for XZGoal { } } -fn y_heuristic(dy: f32) -> f32 { +pub fn y_heuristic(dy: f32) -> f32 { if dy > 0.0 { *JUMP_ONE_BLOCK_COST * dy } else { @@ -100,13 +99,15 @@ impl Goal for RadiusGoal { let dx = (self.pos.x - n.x) as f32; let dy = (self.pos.y - n.y) as f32; let dz = (self.pos.z - n.z) as f32; - dx * dx + dy * dy + dz * dz + + xz_heuristic(dx, dz) + y_heuristic(dy) } fn success(&self, n: BlockPos) -> bool { let n = n.center(); let dx = (self.pos.x - n.x) as f32; let dy = (self.pos.y - n.y) as f32; let dz = (self.pos.z - n.z) as f32; + dx * dx + dy * dy + dz * dz <= self.radius * self.radius } } @@ -179,35 +180,24 @@ impl Goal for AndGoals { } } -/// Move to a position where we can reach the given block. +/// Multiply the heuristic of the given goal by the given factor. +/// +/// Setting the value to less than 1 makes it be biased towards the goal, and +/// setting it to more than 1 makes it be biased away from the goal. For +/// example, setting the value to 0.5 makes the pathfinder think that the +/// goal is half the distance that it actually is. +/// +/// Note that this may reduce the quality of paths or make the pathfinder slower +/// if used incorrectly. +/// +/// This goal is most useful when combined with [`OrGoal`]. #[derive(Debug)] -pub struct ReachBlockPosGoal { - pub pos: BlockPos, - pub chunk_storage: ChunkStorage, -} -impl Goal for ReachBlockPosGoal { +pub struct ScaleGoal(pub T, pub f32); +impl Goal for ScaleGoal { fn heuristic(&self, n: BlockPos) -> f32 { - BlockPosGoal(self.pos).heuristic(n) + self.0.heuristic(n) * self.1 } 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 + self.0.success(n) } } diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index a1bdaaad3..42c68bb2e 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -3,14 +3,17 @@ //! Much of this code is based on [Baritone](https://github.com/cabaletta/baritone). pub mod astar; +pub mod block_box; pub mod costs; mod debug; +pub mod extras; pub mod goals; pub mod mining; pub mod moves; pub mod simulation; pub mod world; +use crate::auto_tool::StartMiningBlockWithAutoToolEvent; use crate::bot::{JumpEvent, LookAtEvent}; use crate::pathfinder::astar::a_star; use crate::WalkDirection; @@ -26,7 +29,7 @@ use crate::ecs::{ use crate::pathfinder::moves::PathfinderCtx; use crate::pathfinder::world::CachedWorld; use azalea_client::inventory::{InventoryComponent, InventorySet, SetSelectedHotbarSlotEvent}; -use azalea_client::mining::{Mining, StartMiningBlockEvent}; +use azalea_client::mining::Mining; use azalea_client::movement::MoveEventsSet; use azalea_client::{InstanceHolder, StartSprintEvent, StartWalkEvent}; use azalea_core::position::BlockPos; @@ -91,14 +94,23 @@ impl Plugin for PathfinderPlugin { .chain() .before(MoveEventsSet) .before(InventorySet), - ); + ) + .add_plugins(crate::pathfinder::extras::PathfinderExtrasPlugin); } } +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); +} + /// A component that makes this client able to pathfind. #[derive(Component, Default, Clone)] pub struct Pathfinder { - pub goal: Option>, + /// The goal that the pathfinder is currently trying to reach. If this is + /// None, that means we're not pathfinding. + pub goal: Option>, pub successors_fn: Option, pub is_calculating: bool, pub allow_mining: bool, @@ -120,7 +132,7 @@ pub struct ExecutingPath { #[derive(Event)] pub struct GotoEvent { pub entity: Entity, - pub goal: Arc, + pub goal: Arc, /// The function that's used for checking what moves are possible. Usually /// `pathfinder::moves::default_move` pub successors_fn: SuccessorsFn, @@ -148,12 +160,6 @@ 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); -} - impl PathfinderClientExt for azalea_client::Client { /// ``` /// # use azalea::prelude::*; @@ -193,7 +199,7 @@ impl PathfinderClientExt for azalea_client::Client { #[derive(Component)] pub struct ComputePath(Task>); -fn goto_listener( +pub fn goto_listener( mut commands: Commands, mut events: EventReader, mut query: Query<( @@ -453,7 +459,7 @@ fn timeout_movement( // 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) { + if mining.pos.distance_squared_to(&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(); @@ -694,7 +700,7 @@ fn tick_execute_path( mut sprint_events: EventWriter, mut walk_events: EventWriter, mut jump_events: EventWriter, - mut start_mining_events: EventWriter, + mut start_mining_events: EventWriter, mut set_selected_hotbar_slot_events: EventWriter, ) { for (entity, executing_path, position, physics, mining, instance_holder, inventory_component) in @@ -747,13 +753,14 @@ fn recalculate_if_has_goal_but_no_path( #[derive(Event)] pub struct StopPathfindingEvent { pub entity: Entity, - /// If false, then let the current movement finish before stopping. If true, - /// then stop moving immediately. This might cause the bot to fall if it was - /// in the middle of parkouring. + /// Stop moving immediately. This may cause the bot to fall if it was in the + /// middle of parkouring. + /// + /// If this is false, it'll stop moving after the current movement is done. pub force: bool, } -fn handle_stop_pathfinding_event( +pub fn handle_stop_pathfinding_event( mut events: EventReader, mut query: Query<(&mut Pathfinder, &mut ExecutingPath)>, mut walk_events: EventWriter, @@ -787,7 +794,7 @@ fn handle_stop_pathfinding_event( } } -fn stop_pathfinding_on_instance_change( +pub fn stop_pathfinding_on_instance_change( mut query: Query<(Entity, &mut ExecutingPath), Changed>, mut stop_pathfinding_events: EventWriter, ) { @@ -866,11 +873,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 +918,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 +956,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 +974,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 +996,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 +1008,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 +1027,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 +1044,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 +1061,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 +1078,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/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs index 54a6dc6a8..d35ef345f 100644 --- a/azalea/src/pathfinder/moves/basic.rs +++ b/azalea/src/pathfinder/moves/basic.rs @@ -229,9 +229,10 @@ fn execute_descend_move(mut ctx: ExecuteCtx) { let start_center = start.center(); let center = target.center(); - let horizontal_distance_from_target = (center - position).horizontal_distance_sqr().sqrt(); - let horizontal_distance_from_start = - (start.center() - position).horizontal_distance_sqr().sqrt(); + let horizontal_distance_from_target = (center - position).horizontal_distance_squared().sqrt(); + let horizontal_distance_from_start = (start.center() - position) + .horizontal_distance_squared() + .sqrt(); let dest_ahead = Vec3::new( start_center.x + (center.x - start_center.x) * 1.5, @@ -402,8 +403,9 @@ fn execute_downward_move(mut ctx: ExecuteCtx) { let target_center = target.center(); - let horizontal_distance_from_target = - (target_center - position).horizontal_distance_sqr().sqrt(); + let horizontal_distance_from_target = (target_center - position) + .horizontal_distance_squared() + .sqrt(); if horizontal_distance_from_target > 0.25 { ctx.look_at(target_center); diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs index bb10b1928..733a6729d 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -3,7 +3,7 @@ pub mod parkour; use std::{fmt::Debug, sync::Arc}; -use crate::{auto_tool::best_tool_in_hotbar_for_block, JumpEvent, LookAtEvent}; +use crate::{auto_tool::StartMiningBlockWithAutoToolEvent, JumpEvent, LookAtEvent}; use super::{ astar, @@ -11,8 +11,8 @@ use super::{ world::{is_block_state_passable, CachedWorld}, }; use azalea_client::{ - inventory::SetSelectedHotbarSlotEvent, mining::StartMiningBlockEvent, SprintDirection, - StartSprintEvent, StartWalkEvent, WalkDirection, + inventory::SetSelectedHotbarSlotEvent, SprintDirection, StartSprintEvent, StartWalkEvent, + WalkDirection, }; use azalea_core::position::{BlockPos, Vec3}; use azalea_inventory::Menu; @@ -61,7 +61,7 @@ pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'w5, 'w6, 'a> { 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 start_mining_events: &'a mut EventWriter<'w5, StartMiningBlockWithAutoToolEvent>, pub set_selected_hotbar_slot_events: &'a mut EventWriter<'w6, SetSelectedHotbarSlotEvent>, } @@ -133,22 +133,15 @@ impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> { 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, - }); + self.start_mining_events + .send(StartMiningBlockWithAutoToolEvent { + entity: self.entity, + position: block, + }); true } @@ -157,7 +150,7 @@ impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> { /// 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() + .horizontal_distance_squared() .sqrt(); let at_start_position = BlockPos::from(self.position) == self.start && horizontal_distance_from_start < 0.25; diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index 2803b846f..56bd6e784 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -68,6 +68,7 @@ fn create_simulation_instance(chunks: ChunkStorage) -> (App, Arc(&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() } } diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index b4d66aaf9..2d16b7228 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -2,8 +2,8 @@ //! re-exported here. pub use crate::{ - bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt, - ClientBuilder, + bot::BotClientExt, container::ContainerClientExt, + pathfinder::extras::PathfinderExtrasClientExt, pathfinder::PathfinderClientExt, ClientBuilder, }; pub use azalea_client::{Account, Client, Event}; // this is necessary to make the macros that reference bevy_ecs work