From 71697b34006371a35255c52de499f56b1834fbc0 Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 16 Dec 2023 02:06:49 -0600 Subject: [PATCH 1/9] pathfinder_extras and MineArea implementation --- azalea-client/src/chunks.rs | 2 +- azalea-client/src/interact.rs | 55 +++++ azalea-client/src/inventory.rs | 2 +- azalea-core/src/delta.rs | 4 - azalea-core/src/math.rs | 2 +- azalea-core/src/position.rs | 52 +++- azalea-entity/src/lib.rs | 2 +- azalea-physics/src/clip.rs | 39 +-- azalea-physics/src/collision/mod.rs | 12 +- azalea-physics/src/collision/shape.rs | 12 +- azalea/src/auto_tool.rs | 67 ++++- azalea/src/bot.rs | 27 +- azalea/src/lib.rs | 2 + azalea/src/pathfinder/block_box.rs | 78 ++++++ azalea/src/pathfinder/debug.rs | 2 +- azalea/src/pathfinder/goals.rs | 101 ++++++-- azalea/src/pathfinder/mod.rs | 33 +-- azalea/src/pathfinder/moves/basic.rs | 12 +- azalea/src/pathfinder/moves/mod.rs | 26 +- azalea/src/pathfinder/simulation.rs | 5 + azalea/src/pathfinder_extras/mod.rs | 39 +++ azalea/src/pathfinder_extras/process/mod.rs | 257 ++++++++++++++++++++ azalea/src/prelude.rs | 2 +- azalea/src/utils.rs | 129 ++++++++++ 24 files changed, 836 insertions(+), 126 deletions(-) create mode 100644 azalea/src/pathfinder/block_box.rs create mode 100644 azalea/src/pathfinder_extras/mod.rs create mode 100644 azalea/src/pathfinder_extras/process/mod.rs create mode 100644 azalea/src/utils.rs 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 026e94caa..61cc66a88 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/interact.rs @@ -383,3 +383,58 @@ fn update_modifiers_for_held_item( .unwrap(); } } + +#[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 527feae78..2a7337420 100644 --- a/azalea-client/src/inventory.rs +++ b/azalea-client/src/inventory.rs @@ -744,7 +744,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-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..b07599e89 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; diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index e98640357..76ca349b5 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 @@ -247,6 +247,44 @@ 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. + /// + /// ``` + /// 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. + /// + /// ``` + /// 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 eb5b5b252..3f156c9f8 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -205,8 +205,8 @@ pub struct Jumping(bool); /// A component that contains the direction an entity is looking. #[derive(Debug, Component, Clone, Default)] pub struct LookDirection { - pub x_rot: f32, pub y_rot: f32, + pub x_rot: f32, } /// The physics data relating to the entity, such as position, velocity, and diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs index 8c2d0c8fb..78df9134a 100644 --- a/azalea-physics/src/clip.rs +++ b/azalea-physics/src/clip.rs @@ -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, @@ -121,8 +102,8 @@ fn clip_with_interaction_override( 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,19 +172,19 @@ pub fn traverse_blocks( let mut percentage = Vec3 { x: percentage_step.x * if vec_sign.x > 0. { - 1. - right_before_start.x.fract() + 1. - right_before_start.x.fract().abs() } else { right_before_start.x.fract().abs() }, y: percentage_step.y * if vec_sign.y > 0. { - 1. - right_before_start.y.fract() + 1. - right_before_start.y.fract().abs() } else { right_before_start.y.fract().abs() }, z: percentage_step.z * if vec_sign.z > 0. { - 1. - right_before_start.z.fract() + 1. - right_before_start.z.fract().abs() } else { right_before_start.z.fract().abs() }, diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 72151b6b3..fcbb42737 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 41ade73cb..539ad6787 100755 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -403,7 +403,7 @@ impl VoxelShape { match self { VoxelShape::Cube(s) => s.find_index(axis, coord), _ => { - binary_search(0, (self.shape().size(axis) + 1) as i32, &|t| { + binary_search(0, (self.shape().size(axis) + 1) as i32, |t| { coord < self.get(axis, t as usize) }) - 1 } @@ -415,15 +415,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/src/auto_tool.rs b/azalea/src/auto_tool.rs index bc9bb4747..89b4d38d4 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_client::{ + inventory::{InventoryComponent, SetSelectedHotbarSlotEvent}, + mining::StartMiningBlockEvent, + Client, InstanceHolder, +}; +use azalea_core::position::BlockPos; use azalea_entity::{FluidOnEyes, Physics}; use azalea_inventory::{ItemSlot, Menu}; use azalea_registry::Fluid; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::prelude::*; #[derive(Debug)] pub struct BestToolResult { @@ -10,6 +17,18 @@ 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), + ); + } +} + pub trait AutoToolClientExt { fn best_tool_in_hotbar_for_block(&self, block: BlockState) -> BestToolResult; } @@ -135,3 +154,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..89bb19215 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, @@ -9,6 +10,8 @@ use crate::ecs::{ query::{With, Without}, system::{Commands, Query}, }; +use crate::pathfinder_extras::PathfinderExtrasPlugin; +use crate::utils::direction_looking_at; use azalea_client::interact::SwingArmEvent; use azalea_client::mining::Mining; use azalea_client::TickBroadcast; @@ -23,7 +26,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 +166,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 +184,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; @@ -210,8 +193,10 @@ impl PluginGroup for DefaultBotPlugins { PluginGroupBuilder::start::() .add(BotPlugin) .add(PathfinderPlugin) + .add(PathfinderExtrasPlugin) .add(ContainerPlugin) .add(AutoRespawnPlugin) .add(AcceptResourcePacksPlugin) + .add(AutoToolPlugin) } } diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index fd2cb83ac..a755f170e 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -12,8 +12,10 @@ mod bot; pub mod container; pub mod nearest_entity; pub mod pathfinder; +pub mod pathfinder_extras; pub mod prelude; pub mod swarm; +pub mod utils; use app::Plugins; pub use azalea_auth as auth; 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..7aa895e00 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 diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index 3f8c79932..87ed3a54c 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -5,16 +5,21 @@ 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 crate::utils::get_hit_result_while_looking_at; -pub trait Goal { +use super::{ + block_box::BlockBox, + costs::{COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST}, +}; + +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 { @@ -192,22 +197,90 @@ impl Goal for ReachBlockPosGoal { 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(); + 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; } - 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, - ); + // 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); - block_hit_result.block_pos == self.pos + self.bb.contains(hit_result) } } diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 9fd769e64..790fcc227 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -3,6 +3,7 @@ //! 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 goals; @@ -11,6 +12,7 @@ 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; @@ -95,10 +97,18 @@ impl Plugin for PathfinderPlugin { } } +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 +130,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 +158,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 +197,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<( @@ -451,7 +455,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(); @@ -692,7 +696,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 @@ -745,9 +749,10 @@ 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, } 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..5ff41d14a 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -3,7 +3,10 @@ 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::{best_tool_in_hotbar_for_block, StartMiningBlockWithAutoToolEvent}, + JumpEvent, LookAtEvent, +}; use super::{ astar, @@ -61,7 +64,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 +136,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 +153,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 b41d895a6..61571605b 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -69,6 +69,11 @@ impl Simulation { super::PathfinderPlugin, crate::BotPlugin, azalea_client::task_pool::TaskPoolPlugin::default(), + // for mining + crate::auto_tool::AutoToolPlugin, + azalea_client::inventory::InventoryPlugin, + azalea_client::mining::MinePlugin, + azalea_client::interact::InteractPlugin, )) .insert_resource(InstanceContainer { instances: [(instance_name.clone(), Arc::downgrade(&instance.clone()))] diff --git a/azalea/src/pathfinder_extras/mod.rs b/azalea/src/pathfinder_extras/mod.rs new file mode 100644 index 000000000..5e00ae1ea --- /dev/null +++ b/azalea/src/pathfinder_extras/mod.rs @@ -0,0 +1,39 @@ +//! Adds utility functions that all depend on the pathfinder. + +pub mod process; + +use azalea_client::Client; +use azalea_core::{position::BlockPos, tick::GameTick}; +use bevy_app::Update; + +use crate::app::{App, Plugin}; + +use self::process::{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) + .add_systems(GameTick, process::process_tick); + } +} + +pub trait PathfinderExtrasClientExt { + fn set_active_pathfinder_process(&self, process: Process); + fn mine_area(&self, corner1: BlockPos, corner2: BlockPos); +} + +impl PathfinderExtrasClientExt for Client { + fn set_active_pathfinder_process(&self, process: Process) { + self.ecs.lock().send_event(SetActiveProcessEvent { + entity: self.entity, + process, + }); + } + + fn mine_area(&self, corner1: BlockPos, corner2: BlockPos) { + self.set_active_pathfinder_process(Process::MineArea { corner1, corner2 }); + } +} diff --git a/azalea/src/pathfinder_extras/process/mod.rs b/azalea/src/pathfinder_extras/process/mod.rs new file mode 100644 index 000000000..ee3b2c38c --- /dev/null +++ b/azalea/src/pathfinder_extras/process/mod.rs @@ -0,0 +1,257 @@ +use std::sync::Arc; + +use azalea_block::BlockState; +use azalea_client::{ + mining::{Mining, StartMiningBlockEvent}, + InstanceHolder, +}; +use azalea_core::position::{BlockPos, Vec3}; +use azalea_entity::Position; +use azalea_world::ChunkStorage; +use tracing::info; + +use crate::{ + auto_tool::StartMiningBlockWithAutoToolEvent, + ecs::prelude::*, + pathfinder::{ + self, + block_box::BlockBox, + goals::{Goal, OrGoals, ReachBlockPosGoal, ReachBoxGoal}, + ExecutingPath, GotoEvent, Pathfinder, + }, + utils::{get_hit_result_while_looking_at, get_reachable_blocks_around_player}, + LookAtEvent, +}; + +#[derive(Component, Clone)] +pub enum Process { + MineArea { + corner1: BlockPos, + corner2: BlockPos, + }, +} + +#[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()); + } +} + +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 +} + +pub fn process_tick( + mut commands: Commands, + query: Query<( + Entity, + &Process, + &Position, + &InstanceHolder, + &Pathfinder, + Option<&Mining>, + Option<&ExecutingPath>, + )>, + mut goto_events: EventWriter, + mut look_at_events: EventWriter, + mut start_mining_block_events: EventWriter, +) { + for (entity, process, position, instance_holder, pathfinder, mining, executing_path) in &query { + match process { + Process::MineArea { corner1, corner2 } => { + if pathfinder.goal.is_some() || executing_path.is_some() { + // already pathfinding + println!("currently pathfinding"); + continue; + } + + if mining.is_some() { + // currently mining, so wait for that to finish + println!("currently mining"); + continue; + } + + let bb = BlockBox::new(*corner1, *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, bb.max().y - layer_index as i32, bb.min().z), + BlockPos::new(bb.max().x, 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 mut closest_block_pos = None; + let mut closest_distance = i32::MAX; + for block_pos in &mineable_blocks[1..] { + if block_pos.y < player_position.y { + // skip blocks below us at first + continue; + } + let distance = block_pos.distance_squared_to(&player_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 &mineable_blocks { + let distance = block_pos.distance_squared_to(&player_position); + if distance < closest_distance { + closest_block_pos = Some(*block_pos); + closest_distance = distance; + } + } + } + + let closest_block_pos = closest_block_pos.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:?}"); + continue; + } + + // 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::(); + continue; + } + + // 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 = 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, + }); + } + } + } +} diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index b4d66aaf9..077f09164 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -3,7 +3,7 @@ pub use crate::{ bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt, - ClientBuilder, + pathfinder_extras::PathfinderExtrasClientExt, ClientBuilder, }; pub use azalea_client::{Account, Client, Event}; // this is necessary to make the macros that reference bevy_ecs work diff --git a/azalea/src/utils.rs b/azalea/src/utils.rs new file mode 100644 index 000000000..6cca42edf --- /dev/null +++ b/azalea/src/utils.rs @@ -0,0 +1,129 @@ +//! Random utility functions that are useful for bots. + +use std::f64::consts::PI; + +use azalea_core::position::{BlockPos, Vec3}; +use azalea_entity::LookDirection; +use azalea_world::ChunkStorage; + +/// 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, + } +} + +/// 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) +} + +/// 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 +} + +/// 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; + } + + let hit_result = + get_hit_result_while_looking_at(chunk_storage, player_position, block_pos); + if hit_result == block_pos { + blocks.push(block_pos); + } + } + } + } + + blocks +} + +#[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); + } +} From f2899ffc462f9f1ab36eb432764756db523f02af Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 16 Dec 2023 02:37:36 -0600 Subject: [PATCH 2/9] fix tests --- azalea-brigadier/src/tree/mod.rs | 2 +- azalea-core/src/position.rs | 2 ++ azalea/src/pathfinder/mod.rs | 2 +- azalea/src/pathfinder/moves/mod.rs | 9 +++------ azalea/src/pathfinder/simulation.rs | 8 +++++++- azalea/src/pathfinder_extras/process/mod.rs | 10 ++++------ 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/azalea-brigadier/src/tree/mod.rs b/azalea-brigadier/src/tree/mod.rs index a6c93679e..a982e82b3 100755 --- a/azalea-brigadier/src/tree/mod.rs +++ b/azalea-brigadier/src/tree/mod.rs @@ -294,7 +294,7 @@ impl PartialEq for CommandNode { if let Some(selfexecutes) = &self.command { // idk how to do this better since we can't compare `dyn Fn`s if let Some(otherexecutes) = &other.command { - #[allow(clippy::ambiguous_wide_pointer_comparisons)] + #[allow(ambiguous_wide_pointer_comparisons)] if !Arc::ptr_eq(selfexecutes, otherexecutes) { return false; } diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index 76ca349b5..d3786437f 100755 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -251,6 +251,7 @@ impl BlockPos { /// 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), @@ -270,6 +271,7 @@ impl BlockPos { /// 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), diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 790fcc227..54ec1d684 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -28,7 +28,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; diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs index 5ff41d14a..733a6729d 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -3,10 +3,7 @@ pub mod parkour; use std::{fmt::Debug, sync::Arc}; -use crate::{ - auto_tool::{best_tool_in_hotbar_for_block, StartMiningBlockWithAutoToolEvent}, - JumpEvent, LookAtEvent, -}; +use crate::{auto_tool::StartMiningBlockWithAutoToolEvent, JumpEvent, LookAtEvent}; use super::{ astar, @@ -14,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; diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index 61571605b..af4debc77 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -9,7 +9,7 @@ use azalea_core::{position::Vec3, resource_location::ResourceLocation, tick::Gam use azalea_entity::{ attributes::AttributeInstance, Attributes, EntityDimensions, Physics, Position, }; -use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId}; +use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId, PartialInstance}; use bevy_app::App; use bevy_ecs::prelude::*; use parking_lot::RwLock; @@ -97,6 +97,12 @@ impl Simulation { azalea_registry::EntityKind::Player, instance_name, ), + azalea_client::InstanceHolder { + // partial_instance is never actually used by the pathfinder so + partial_instance: Arc::new(RwLock::new(PartialInstance::default())), + instance: instance.clone(), + }, + InventoryComponent::default(), )); entity.insert(player); diff --git a/azalea/src/pathfinder_extras/process/mod.rs b/azalea/src/pathfinder_extras/process/mod.rs index ee3b2c38c..fcb95d4d1 100644 --- a/azalea/src/pathfinder_extras/process/mod.rs +++ b/azalea/src/pathfinder_extras/process/mod.rs @@ -1,11 +1,8 @@ use std::sync::Arc; use azalea_block::BlockState; -use azalea_client::{ - mining::{Mining, StartMiningBlockEvent}, - InstanceHolder, -}; -use azalea_core::position::{BlockPos, Vec3}; +use azalea_client::{mining::Mining, InstanceHolder}; +use azalea_core::position::BlockPos; use azalea_entity::Position; use azalea_world::ChunkStorage; use tracing::info; @@ -19,7 +16,7 @@ use crate::{ goals::{Goal, OrGoals, ReachBlockPosGoal, ReachBoxGoal}, ExecutingPath, GotoEvent, Pathfinder, }, - utils::{get_hit_result_while_looking_at, get_reachable_blocks_around_player}, + utils::get_reachable_blocks_around_player, LookAtEvent, }; @@ -81,6 +78,7 @@ fn determine_layer(bb: &BlockBox, chunks: &ChunkStorage) -> usize { layer } +#[allow(clippy::type_complexity)] pub fn process_tick( mut commands: Commands, query: Query<( From 75553b9217c4c23eaf1f3e875630a7e3a8345282 Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 16 Dec 2023 13:21:09 -0600 Subject: [PATCH 3/9] don't mine outside blockbox --- azalea/src/pathfinder_extras/process/mod.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/azalea/src/pathfinder_extras/process/mod.rs b/azalea/src/pathfinder_extras/process/mod.rs index fcb95d4d1..f40fde599 100644 --- a/azalea/src/pathfinder_extras/process/mod.rs +++ b/azalea/src/pathfinder_extras/process/mod.rs @@ -118,8 +118,16 @@ pub fn process_tick( // 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, bb.max().y - layer_index as i32, bb.min().z), - BlockPos::new(bb.max().x, bb.max().y - layer_index as i32, bb.max().z), + 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 = From 6b901947baef98694cdbae1f27fb6d158ebd31b3 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 17 Dec 2023 17:52:45 -0600 Subject: [PATCH 4/9] move mine_area to its own module --- azalea/src/auto_tool.rs | 6 +- azalea/src/pathfinder/mod.rs | 4 +- azalea/src/pathfinder_extras/mod.rs | 20 +- .../pathfinder_extras/process/mine_area.rs | 232 +++++++++++++++++ azalea/src/pathfinder_extras/process/mod.rs | 238 +++--------------- 5 files changed, 285 insertions(+), 215 deletions(-) create mode 100644 azalea/src/pathfinder_extras/process/mine_area.rs diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index 89b4d38d4..f35dd2447 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -5,7 +5,7 @@ use azalea_client::{ Client, InstanceHolder, }; use azalea_core::position::BlockPos; -use azalea_entity::{FluidOnEyes, Physics}; +use azalea_entity::{update_fluid_on_eyes, FluidOnEyes, Physics}; use azalea_inventory::{ItemSlot, Menu}; use azalea_registry::Fluid; use bevy_app::{App, Plugin, Update}; @@ -24,7 +24,9 @@ impl Plugin for AutoToolPlugin { .add_systems( Update, start_mining_block_with_auto_tool_listener - .before(azalea_client::inventory::handle_set_selected_hotbar_slot_event), + .before(azalea_client::inventory::handle_set_selected_hotbar_slot_event) + .after(update_fluid_on_eyes) + .after(azalea_client::chunks::handle_receive_chunk_events), ); } } diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 54ec1d684..48b2c8c6b 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -756,7 +756,7 @@ pub struct StopPathfindingEvent { 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, @@ -790,7 +790,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, ) { diff --git a/azalea/src/pathfinder_extras/mod.rs b/azalea/src/pathfinder_extras/mod.rs index 5e00ae1ea..168eee37e 100644 --- a/azalea/src/pathfinder_extras/mod.rs +++ b/azalea/src/pathfinder_extras/mod.rs @@ -2,31 +2,39 @@ pub mod process; +use crate::ecs::prelude::*; use azalea_client::Client; use azalea_core::{position::BlockPos, tick::GameTick}; +use azalea_physics::PhysicsSet; use bevy_app::Update; use crate::app::{App, Plugin}; -use self::process::{Process, SetActiveProcessEvent}; +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) - .add_systems(GameTick, process::process_tick); + .add_systems( + Update, + process::set_active_pathfinder_process_listener + .after(crate::pathfinder::stop_pathfinding_on_instance_change) + .before(crate::pathfinder::handle_stop_pathfinding_event), + ) + .add_systems(GameTick, process::process_tick.before(PhysicsSet)); } } pub trait PathfinderExtrasClientExt { - fn set_active_pathfinder_process(&self, process: Process); + fn set_active_pathfinder_process(&self, process: impl Into); fn mine_area(&self, corner1: BlockPos, corner2: BlockPos); } impl PathfinderExtrasClientExt for Client { - fn set_active_pathfinder_process(&self, process: Process) { + fn set_active_pathfinder_process(&self, process: impl Into) { + let process = process.into(); self.ecs.lock().send_event(SetActiveProcessEvent { entity: self.entity, process, @@ -34,6 +42,6 @@ impl PathfinderExtrasClientExt for Client { } fn mine_area(&self, corner1: BlockPos, corner2: BlockPos) { - self.set_active_pathfinder_process(Process::MineArea { corner1, corner2 }); + self.set_active_pathfinder_process(MineArea { corner1, corner2 }); } } 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..a8c1f7bcc --- /dev/null +++ b/azalea/src/pathfinder_extras/process/mine_area.rs @@ -0,0 +1,232 @@ +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, + goals::{Goal, ReachBlockPosGoal}, + GotoEvent, + }, + utils::get_reachable_blocks_around_player, + LookAtEvent, +}; + +use super::{Process, ProcessSystemComponents}; + +#[derive(Clone, Debug)] +pub struct MineArea { + pub corner1: BlockPos, + pub corner2: BlockPos, +} + +pub fn mine_area<'a>( + mine_area: &MineArea, + commands: &mut Commands, + ProcessSystemComponents { + entity, + position, + instance_holder, + pathfinder, + mining, + executing_path, + }: ProcessSystemComponents<'a>, + 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 + println!("currently pathfinding"); + return; + } + + if mining.is_some() { + // currently mining, so wait for that to finish + println!("currently mining"); + 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 mut closest_block_pos = None; + let mut closest_distance = i32::MAX; + for block_pos in &mineable_blocks[1..] { + if block_pos.y < player_position.y { + // skip blocks below us at first + continue; + } + let distance = block_pos.distance_squared_to(&player_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 &mineable_blocks { + let distance = block_pos.distance_squared_to(&player_position); + if distance < closest_distance { + closest_block_pos = Some(*block_pos); + closest_distance = distance; + } + } + } + + let closest_block_pos = closest_block_pos + .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 = pathfinder::goals::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/mod.rs b/azalea/src/pathfinder_extras/process/mod.rs index f40fde599..741af0f76 100644 --- a/azalea/src/pathfinder_extras/process/mod.rs +++ b/azalea/src/pathfinder_extras/process/mod.rs @@ -1,31 +1,24 @@ -use std::sync::Arc; +pub mod mine_area; -use azalea_block::BlockState; use azalea_client::{mining::Mining, InstanceHolder}; -use azalea_core::position::BlockPos; use azalea_entity::Position; -use azalea_world::ChunkStorage; -use tracing::info; use crate::{ auto_tool::StartMiningBlockWithAutoToolEvent, ecs::prelude::*, - pathfinder::{ - self, - block_box::BlockBox, - goals::{Goal, OrGoals, ReachBlockPosGoal, ReachBoxGoal}, - ExecutingPath, GotoEvent, Pathfinder, - }, - utils::get_reachable_blocks_around_player, + pathfinder::{self, ExecutingPath, GotoEvent, Pathfinder}, LookAtEvent, }; -#[derive(Component, Clone)] +#[derive(Component, Clone, Debug)] pub enum Process { - MineArea { - corner1: BlockPos, - corner2: BlockPos, - }, + MineArea(mine_area::MineArea), +} + +impl From for Process { + fn from(mine_area: mine_area::MineArea) -> Self { + Self::MineArea(mine_area) + } } #[derive(Event)] @@ -48,34 +41,13 @@ pub fn set_active_pathfinder_process_listener( } } -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 +pub struct ProcessSystemComponents<'a> { + pub entity: Entity, + pub position: &'a Position, + pub instance_holder: &'a InstanceHolder, + pub pathfinder: &'a Pathfinder, + pub mining: Option<&'a Mining>, + pub executing_path: Option<&'a ExecutingPath>, } #[allow(clippy::type_complexity)] @@ -95,168 +67,24 @@ pub fn process_tick( mut start_mining_block_events: EventWriter, ) { for (entity, process, position, instance_holder, pathfinder, mining, executing_path) in &query { + let components = ProcessSystemComponents { + entity, + position, + instance_holder, + pathfinder, + mining, + executing_path, + }; match process { - Process::MineArea { corner1, corner2 } => { - if pathfinder.goal.is_some() || executing_path.is_some() { - // already pathfinding - println!("currently pathfinding"); - continue; - } - - if mining.is_some() { - // currently mining, so wait for that to finish - println!("currently mining"); - continue; - } - - let bb = BlockBox::new(*corner1, *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, - ), + 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, ); - - 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 mut closest_block_pos = None; - let mut closest_distance = i32::MAX; - for block_pos in &mineable_blocks[1..] { - if block_pos.y < player_position.y { - // skip blocks below us at first - continue; - } - let distance = block_pos.distance_squared_to(&player_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 &mineable_blocks { - let distance = block_pos.distance_squared_to(&player_position); - if distance < closest_distance { - closest_block_pos = Some(*block_pos); - closest_distance = distance; - } - } - } - - let closest_block_pos = closest_block_pos.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:?}"); - continue; - } - - // 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::(); - continue; - } - - // 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 = 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, - }); } } } From 4242cfe56e660e689cc9c3d8371fe76bc58b41ae Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 17 Dec 2023 21:45:00 -0600 Subject: [PATCH 5/9] start implementing MineForever process --- azalea-block/src/range.rs | 16 +++ azalea-entity/src/lib.rs | 73 +++++++---- azalea/src/bot.rs | 2 +- azalea/src/lib.rs | 1 - azalea/src/pathfinder/goals.rs | 113 +----------------- azalea/src/pathfinder_extras/goals.rs | 113 ++++++++++++++++++ azalea/src/pathfinder_extras/mod.rs | 9 ++ .../pathfinder_extras/process/mine_area.rs | 44 ++----- .../pathfinder_extras/process/mine_forever.rs | 105 ++++++++++++++++ azalea/src/pathfinder_extras/process/mod.rs | 17 +++ azalea/src/{ => pathfinder_extras}/utils.rs | 90 ++++++-------- 11 files changed, 361 insertions(+), 222 deletions(-) create mode 100644 azalea/src/pathfinder_extras/goals.rs create mode 100644 azalea/src/pathfinder_extras/process/mine_forever.rs rename azalea/src/{ => pathfinder_extras}/utils.rs (53%) 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-entity/src/lib.rs b/azalea-entity/src/lib.rs index 3f156c9f8..2434557d1 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -23,7 +23,7 @@ use bevy_ecs::{bundle::Bundle, component::Component}; pub use data::*; use derive_more::{Deref, DerefMut}; pub use dimensions::EntityDimensions; -use std::fmt::Debug; +use std::{f64::consts::PI, fmt::Debug}; use uuid::Uuid; pub use crate::plugin::*; @@ -390,24 +390,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/src/bot.rs b/azalea/src/bot.rs index 89bb19215..473906bb3 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -11,12 +11,12 @@ use crate::ecs::{ system::{Commands, Query}, }; use crate::pathfinder_extras::PathfinderExtrasPlugin; -use crate::utils::direction_looking_at; use azalea_client::interact::SwingArmEvent; 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, diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index a755f170e..9975da288 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -15,7 +15,6 @@ pub mod pathfinder; pub mod pathfinder_extras; pub mod prelude; pub mod swarm; -pub mod utils; use app::Plugins; pub use azalea_auth as auth; diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index 87ed3a54c..7cd491f7f 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -3,14 +3,8 @@ use std::f32::consts::SQRT_2; use azalea_core::position::{BlockPos, Vec3}; -use azalea_world::ChunkStorage; -use crate::utils::get_hit_result_while_looking_at; - -use super::{ - block_box::BlockBox, - costs::{COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST}, -}; +use super::costs::{COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST}; pub trait Goal: Send + Sync { #[must_use] @@ -35,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(); @@ -70,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 { @@ -183,104 +177,3 @@ impl Goal for AndGoals { 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 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/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 index 168eee37e..e8b53894a 100644 --- a/azalea/src/pathfinder_extras/mod.rs +++ b/azalea/src/pathfinder_extras/mod.rs @@ -1,8 +1,11 @@ //! Adds utility functions that all depend on the pathfinder. +pub mod goals; pub mod process; +pub mod utils; use crate::ecs::prelude::*; +use azalea_block::BlockStates; use azalea_client::Client; use azalea_core::{position::BlockPos, tick::GameTick}; use azalea_physics::PhysicsSet; @@ -30,6 +33,7 @@ impl Plugin for PathfinderExtrasPlugin { 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 { @@ -44,4 +48,9 @@ impl PathfinderExtrasClientExt for Client { 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/process/mine_area.rs b/azalea/src/pathfinder_extras/process/mine_area.rs index a8c1f7bcc..7761c6ce2 100644 --- a/azalea/src/pathfinder_extras/process/mine_area.rs +++ b/azalea/src/pathfinder_extras/process/mine_area.rs @@ -8,13 +8,11 @@ use tracing::info; use crate::{ auto_tool::StartMiningBlockWithAutoToolEvent, ecs::prelude::*, - pathfinder::{ - self, - block_box::BlockBox, - goals::{Goal, ReachBlockPosGoal}, - GotoEvent, + pathfinder::{self, block_box::BlockBox, goals::Goal, GotoEvent}, + pathfinder_extras::{ + goals::{ReachBlockPosGoal, ReachBoxGoal}, + utils::{get_reachable_blocks_around_player, pick_closest_block}, }, - utils::get_reachable_blocks_around_player, LookAtEvent, }; @@ -26,7 +24,7 @@ pub struct MineArea { pub corner2: BlockPos, } -pub fn mine_area<'a>( +pub fn mine_area( mine_area: &MineArea, commands: &mut Commands, ProcessSystemComponents { @@ -36,7 +34,7 @@ pub fn mine_area<'a>( pathfinder, mining, executing_path, - }: ProcessSystemComponents<'a>, + }: ProcessSystemComponents<'_>, goto_events: &mut EventWriter, look_at_events: &mut EventWriter, start_mining_block_events: &mut EventWriter, @@ -94,33 +92,9 @@ pub fn mine_area<'a>( if !mineable_blocks.is_empty() { // pick the closest one and mine it - let mut closest_block_pos = None; - let mut closest_distance = i32::MAX; - for block_pos in &mineable_blocks[1..] { - if block_pos.y < player_position.y { - // skip blocks below us at first - continue; - } - let distance = block_pos.distance_squared_to(&player_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 &mineable_blocks { - let distance = block_pos.distance_squared_to(&player_position); - if distance < closest_distance { - closest_block_pos = Some(*block_pos); - closest_distance = distance; - } - } - } - - let closest_block_pos = closest_block_pos + 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(), @@ -185,7 +159,7 @@ pub fn mine_area<'a>( } else { println!("reaching for box because we're at {player_position}"); - let reach_box_goal = pathfinder::goals::ReachBoxGoal { + let reach_box_goal = ReachBoxGoal { bb: bb.clone(), chunk_storage: chunk_storage.clone(), }; 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..ef6c4b818 --- /dev/null +++ b/azalea/src/pathfinder_extras/process/mine_forever.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use azalea_block::BlockStates; +use azalea_core::position::BlockPos; +use tracing::info; + +use crate::{ + auto_tool::StartMiningBlockWithAutoToolEvent, + ecs::prelude::*, + pathfinder::{self, GotoEvent}, + pathfinder_extras::{ + goals::ReachBlockPosGoal, + utils::{can_reach_block, pick_closest_block}, + }, + 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, + }: 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 + println!("currently pathfinding"); + return; + } + + if mining.is_some() { + // currently mining, so wait for that to finish + println!("currently mining"); + 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 potential_goals = Vec::new(); + for target_pos in target_blocks { + potential_goals.push(ReachBlockPosGoal { + pos: target_pos, + chunk_storage: chunk_storage.clone(), + }); + } + + if potential_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::OrGoals(potential_goals)), + 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 index 741af0f76..ff0739ee9 100644 --- a/azalea/src/pathfinder_extras/process/mod.rs +++ b/azalea/src/pathfinder_extras/process/mod.rs @@ -1,4 +1,5 @@ pub mod mine_area; +pub mod mine_forever; use azalea_client::{mining::Mining, InstanceHolder}; use azalea_entity::Position; @@ -13,6 +14,7 @@ use crate::{ #[derive(Component, Clone, Debug)] pub enum Process { MineArea(mine_area::MineArea), + MineForever(mine_forever::MineForever), } impl From for Process { @@ -20,6 +22,11 @@ impl From for Process { 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 { @@ -86,6 +93,16 @@ pub fn process_tick( &mut start_mining_block_events, ); } + Process::MineForever(mine_forever) => { + mine_forever::mine_forever( + mine_forever, + &mut commands, + components, + &mut goto_events, + &mut look_at_events, + &mut start_mining_block_events, + ); + } } } } diff --git a/azalea/src/utils.rs b/azalea/src/pathfinder_extras/utils.rs similarity index 53% rename from azalea/src/utils.rs rename to azalea/src/pathfinder_extras/utils.rs index 6cca42edf..efd79c42f 100644 --- a/azalea/src/utils.rs +++ b/azalea/src/pathfinder_extras/utils.rs @@ -1,30 +1,9 @@ //! Random utility functions that are useful for bots. -use std::f64::consts::PI; - use azalea_core::position::{BlockPos, Vec3}; -use azalea_entity::LookDirection; +use azalea_entity::direction_looking_at; use azalea_world::ChunkStorage; -/// 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, - } -} - /// Return the block that we'd be looking at if we were at a given position and /// looking at a given block. /// @@ -47,6 +26,15 @@ pub fn get_hit_result_while_looking_at( 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. /// @@ -84,9 +72,7 @@ pub fn get_reachable_blocks_around_player( continue; } - let hit_result = - get_hit_result_while_looking_at(chunk_storage, player_position, block_pos); - if hit_result == block_pos { + if can_reach_block(chunk_storage, player_position, block_pos) { blocks.push(block_pos); } } @@ -96,34 +82,32 @@ pub fn get_reachable_blocks_around_player( blocks } -#[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); +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; + } + } - 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); + 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 } From 68bb32c094a48e03310d2b691eb4c8b12be10bab Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 20 Dec 2023 21:52:55 -0600 Subject: [PATCH 6/9] start adding picking up drops for mined blocks --- azalea/Cargo.toml | 2 +- azalea/src/bot.rs | 2 - azalea/src/lib.rs | 1 - .../extras}/goals.rs | 0 .../extras}/mod.rs | 22 ++- azalea/src/pathfinder/extras/pickup.rs | 136 ++++++++++++++++++ .../extras}/process/mine_area.rs | 16 ++- .../extras}/process/mine_forever.rs | 62 +++++--- .../extras}/process/mod.rs | 41 +++++- .../extras}/utils.rs | 0 azalea/src/pathfinder/goals.rs | 26 +++- azalea/src/pathfinder/mod.rs | 4 +- azalea/src/prelude.rs | 4 +- 13 files changed, 279 insertions(+), 37 deletions(-) rename azalea/src/{pathfinder_extras => pathfinder/extras}/goals.rs (100%) rename azalea/src/{pathfinder_extras => pathfinder/extras}/mod.rs (65%) create mode 100644 azalea/src/pathfinder/extras/pickup.rs rename azalea/src/{pathfinder_extras => pathfinder/extras}/process/mine_area.rs (95%) rename azalea/src/{pathfinder_extras => pathfinder/extras}/process/mine_forever.rs (59%) rename azalea/src/{pathfinder_extras => pathfinder/extras}/process/mod.rs (68%) rename azalea/src/{pathfinder_extras => pathfinder/extras}/utils.rs (100%) diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml index affbeab6d..8a8218594 100644 --- a/azalea/Cargo.toml +++ b/azalea/Cargo.toml @@ -29,7 +29,7 @@ azalea-buf = { version = "0.9.0", path = "../azalea-buf" } bevy_app = "0.12.1" bevy_ecs = "0.12.1" bevy_tasks = { version = "0.12.1", features = ["multi-threaded"] } -derive_more = { version = "0.99.17", features = ["deref", "deref_mut"] } +derive_more = { version = "0.99.17" } futures = "0.3.29" futures-lite = "2.1.0" tracing = "0.1.40" diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 473906bb3..463fc0137 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -10,7 +10,6 @@ use crate::ecs::{ query::{With, Without}, system::{Commands, Query}, }; -use crate::pathfinder_extras::PathfinderExtrasPlugin; use azalea_client::interact::SwingArmEvent; use azalea_client::mining::Mining; use azalea_client::TickBroadcast; @@ -193,7 +192,6 @@ impl PluginGroup for DefaultBotPlugins { PluginGroupBuilder::start::() .add(BotPlugin) .add(PathfinderPlugin) - .add(PathfinderExtrasPlugin) .add(ContainerPlugin) .add(AutoRespawnPlugin) .add(AcceptResourcePacksPlugin) diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 9975da288..fd2cb83ac 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -12,7 +12,6 @@ mod bot; pub mod container; pub mod nearest_entity; pub mod pathfinder; -pub mod pathfinder_extras; pub mod prelude; pub mod swarm; diff --git a/azalea/src/pathfinder_extras/goals.rs b/azalea/src/pathfinder/extras/goals.rs similarity index 100% rename from azalea/src/pathfinder_extras/goals.rs rename to azalea/src/pathfinder/extras/goals.rs diff --git a/azalea/src/pathfinder_extras/mod.rs b/azalea/src/pathfinder/extras/mod.rs similarity index 65% rename from azalea/src/pathfinder_extras/mod.rs rename to azalea/src/pathfinder/extras/mod.rs index e8b53894a..b3273c212 100644 --- a/azalea/src/pathfinder_extras/mod.rs +++ b/azalea/src/pathfinder/extras/mod.rs @@ -1,6 +1,7 @@ //! Adds utility functions that all depend on the pathfinder. pub mod goals; +pub mod pickup; pub mod process; pub mod utils; @@ -22,11 +23,24 @@ impl Plugin for PathfinderExtrasPlugin { 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), + ( + 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, + ), ) - .add_systems(GameTick, process::process_tick.before(PhysicsSet)); + .add_systems( + GameTick, + ( + pickup::remove_despawned_items_to_pickup, + process::process_tick.before(PhysicsSet), + ) + .chain(), + ); } } 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 similarity index 95% rename from azalea/src/pathfinder_extras/process/mine_area.rs rename to azalea/src/pathfinder/extras/process/mine_area.rs index 7761c6ce2..a9848ed6f 100644 --- a/azalea/src/pathfinder_extras/process/mine_area.rs +++ b/azalea/src/pathfinder/extras/process/mine_area.rs @@ -8,10 +8,15 @@ use tracing::info; use crate::{ auto_tool::StartMiningBlockWithAutoToolEvent, ecs::prelude::*, - pathfinder::{self, block_box::BlockBox, goals::Goal, GotoEvent}, - pathfinder_extras::{ - goals::{ReachBlockPosGoal, ReachBoxGoal}, - utils::{get_reachable_blocks_around_player, pick_closest_block}, + pathfinder::{ + self, + block_box::BlockBox, + extras::{ + goals::{ReachBlockPosGoal, ReachBoxGoal}, + utils::{get_reachable_blocks_around_player, pick_closest_block}, + }, + goals::Goal, + GotoEvent, }, LookAtEvent, }; @@ -34,6 +39,7 @@ pub fn mine_area( pathfinder, mining, executing_path, + .. }: ProcessSystemComponents<'_>, goto_events: &mut EventWriter, look_at_events: &mut EventWriter, @@ -41,13 +47,11 @@ pub fn mine_area( ) { if pathfinder.goal.is_some() || executing_path.is_some() { // already pathfinding - println!("currently pathfinding"); return; } if mining.is_some() { // currently mining, so wait for that to finish - println!("currently mining"); return; } diff --git a/azalea/src/pathfinder_extras/process/mine_forever.rs b/azalea/src/pathfinder/extras/process/mine_forever.rs similarity index 59% rename from azalea/src/pathfinder_extras/process/mine_forever.rs rename to azalea/src/pathfinder/extras/process/mine_forever.rs index ef6c4b818..d2423a864 100644 --- a/azalea/src/pathfinder_extras/process/mine_forever.rs +++ b/azalea/src/pathfinder/extras/process/mine_forever.rs @@ -1,16 +1,19 @@ use std::sync::Arc; use azalea_block::BlockStates; -use azalea_core::position::BlockPos; +use azalea_core::position::{BlockPos, Vec3}; use tracing::info; use crate::{ auto_tool::StartMiningBlockWithAutoToolEvent, ecs::prelude::*, - pathfinder::{self, GotoEvent}, - pathfinder_extras::{ - goals::ReachBlockPosGoal, - utils::{can_reach_block, pick_closest_block}, + pathfinder::{ + self, + extras::{ + goals::ReachBlockPosGoal, + utils::{can_reach_block, pick_closest_block}, + }, + GotoEvent, }, LookAtEvent, }; @@ -32,22 +35,35 @@ pub fn mine_forever( 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, ) { - if pathfinder.goal.is_some() || executing_path.is_some() { - // already pathfinding - println!("currently pathfinding"); - return; + 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 mining.is_some() { - // currently mining, so wait for that to finish - println!("currently mining"); - return; + 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 @@ -82,15 +98,24 @@ pub fn mine_forever( return; } - let mut potential_goals = Vec::new(); + let mut reach_block_goals = Vec::new(); for target_pos in target_blocks { - potential_goals.push(ReachBlockPosGoal { + reach_block_goals.push(ReachBlockPosGoal { pos: target_pos, chunk_storage: chunk_storage.clone(), }); } - if potential_goals.is_empty() { + 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; @@ -98,7 +123,10 @@ pub fn mine_forever( goto_events.send(GotoEvent { entity, - goal: Arc::new(pathfinder::goals::OrGoals(potential_goals)), + 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 similarity index 68% rename from azalea/src/pathfinder_extras/process/mod.rs rename to azalea/src/pathfinder/extras/process/mod.rs index ff0739ee9..9a1165926 100644 --- a/azalea/src/pathfinder_extras/process/mod.rs +++ b/azalea/src/pathfinder/extras/process/mod.rs @@ -11,6 +11,8 @@ use crate::{ LookAtEvent, }; +use super::pickup::{ItemsToPickup, ItemsToPickupChangeAcknowledged, LastItemsToPickup}; + #[derive(Component, Clone, Debug)] pub enum Process { MineArea(mine_area::MineArea), @@ -53,6 +55,7 @@ pub struct ProcessSystemComponents<'a> { 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>, } @@ -60,25 +63,58 @@ pub struct ProcessSystemComponents<'a> { #[allow(clippy::type_complexity)] pub fn process_tick( mut commands: Commands, - query: Query<( + 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, mining, executing_path) in &query { + 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, }; @@ -98,6 +134,7 @@ pub fn process_tick( 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 similarity index 100% rename from azalea/src/pathfinder_extras/utils.rs rename to azalea/src/pathfinder/extras/utils.rs diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index 7cd491f7f..e4057e9eb 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -99,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 } } @@ -177,3 +179,25 @@ impl Goal for AndGoals { self.0.iter().all(|goal| goal.success(n)) } } + +/// 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 ScaleGoal(pub T, pub f32); +impl Goal for ScaleGoal { + fn heuristic(&self, n: BlockPos) -> f32 { + self.0.heuristic(n) * self.1 + } + fn success(&self, n: BlockPos) -> bool { + self.0.success(n) + } +} diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 48b2c8c6b..4bd631a83 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -6,6 +6,7 @@ pub mod astar; pub mod block_box; pub mod costs; mod debug; +pub mod extras; pub mod goals; pub mod mining; pub mod moves; @@ -93,7 +94,8 @@ impl Plugin for PathfinderPlugin { .chain() .before(MoveEventsSet) .before(InventorySet), - ); + ) + .add_plugins(crate::pathfinder::extras::PathfinderExtrasPlugin); } } diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index 077f09164..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, - pathfinder_extras::PathfinderExtrasClientExt, 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 From 78e9813adf7a1af9ddc1436bb507abb91836b3ab Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 20 Apr 2024 21:05:51 +0000 Subject: [PATCH 7/9] fix edge case with reading FormattedText as nbt --- azalea-chat/src/component.rs | 139 +++++++++++------- .../game/clientbound_system_chat_packet.rs | 13 ++ 2 files changed, 95 insertions(+), 57 deletions(-) diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs index d301aba15..55fbd30dc 100755 --- a/azalea-chat/src/component.rs +++ b/azalea-chat/src/component.rs @@ -308,65 +308,89 @@ impl simdnbt::FromNbtTag for FormattedText { } else if let Some(translate) = compound.get("translate") { let translate = translate.string()?.into(); if let Some(with) = compound.get("with") { - let with = with.list()?.compounds()?; - let mut with_array = Vec::with_capacity(with.len()); - for item in with { - // if it's a string component with no styling and no siblings, just add - // a string to with_array otherwise add the - // component to the array - if let Some(primitive) = item.get("") { - // minecraft does this sometimes, for example - // for the /give system messages - match primitive { - simdnbt::borrow::NbtTag::Byte(b) => { - // interpreted as boolean - with_array.push(StringOrComponent::String( - if *b != 0 { "true" } else { "false" }.to_string(), + let mut with_array = Vec::new(); + match with.list()? { + simdnbt::borrow::NbtList::Empty => {} + simdnbt::borrow::NbtList::String(with) => { + for item in with { + with_array.push(StringOrComponent::String(item.to_string())); + } + } + simdnbt::borrow::NbtList::Compound(with) => { + for item in with { + // if it's a string component with no styling and no siblings, + // just add a string to + // with_array otherwise add the + // component to the array + if let Some(primitive) = item.get("") { + // minecraft does this sometimes, for example + // for the /give system messages + match primitive { + simdnbt::borrow::NbtTag::Byte(b) => { + // interpreted as boolean + with_array.push(StringOrComponent::String( + if *b != 0 { "true" } else { "false" } + .to_string(), + )); + } + simdnbt::borrow::NbtTag::Short(s) => { + with_array + .push(StringOrComponent::String(s.to_string())); + } + simdnbt::borrow::NbtTag::Int(i) => { + with_array + .push(StringOrComponent::String(i.to_string())); + } + simdnbt::borrow::NbtTag::Long(l) => { + with_array + .push(StringOrComponent::String(l.to_string())); + } + simdnbt::borrow::NbtTag::Float(f) => { + with_array + .push(StringOrComponent::String(f.to_string())); + } + simdnbt::borrow::NbtTag::Double(d) => { + with_array + .push(StringOrComponent::String(d.to_string())); + } + simdnbt::borrow::NbtTag::String(s) => { + with_array + .push(StringOrComponent::String(s.to_string())); + } + _ => { + warn!("couldn't parse {item:?} as FormattedText because it has a disallowed primitive"); + with_array.push(StringOrComponent::String( + "?".to_string(), + )); + } + } + } else if let Some(c) = FormattedText::from_nbt_tag( + &simdnbt::borrow::NbtTag::Compound(item.clone()), + ) { + if let FormattedText::Text(text_component) = c { + if text_component.base.siblings.is_empty() + && text_component.base.style.is_empty() + { + with_array.push(StringOrComponent::String( + text_component.text, + )); + continue; + } + } + with_array.push(StringOrComponent::FormattedText( + FormattedText::from_nbt_tag( + &simdnbt::borrow::NbtTag::Compound(item.clone()), + )?, )); - } - simdnbt::borrow::NbtTag::Short(s) => { - with_array.push(StringOrComponent::String(s.to_string())); - } - simdnbt::borrow::NbtTag::Int(i) => { - with_array.push(StringOrComponent::String(i.to_string())); - } - simdnbt::borrow::NbtTag::Long(l) => { - with_array.push(StringOrComponent::String(l.to_string())); - } - simdnbt::borrow::NbtTag::Float(f) => { - with_array.push(StringOrComponent::String(f.to_string())); - } - simdnbt::borrow::NbtTag::Double(d) => { - with_array.push(StringOrComponent::String(d.to_string())); - } - simdnbt::borrow::NbtTag::String(s) => { - with_array.push(StringOrComponent::String(s.to_string())); - } - _ => { - warn!("couldn't parse {item:?} as FormattedText because it has a disallowed primitive"); + } else { + warn!("couldn't parse {item:?} as FormattedText"); with_array.push(StringOrComponent::String("?".to_string())); } } - } else if let Some(c) = FormattedText::from_nbt_tag( - &simdnbt::borrow::NbtTag::Compound(item.clone()), - ) { - if let FormattedText::Text(text_component) = c { - if text_component.base.siblings.is_empty() - && text_component.base.style.is_empty() - { - with_array - .push(StringOrComponent::String(text_component.text)); - continue; - } - } - with_array.push(StringOrComponent::FormattedText( - FormattedText::from_nbt_tag( - &simdnbt::borrow::NbtTag::Compound(item.clone()), - )?, - )); - } else { - warn!("couldn't parse {item:?} as FormattedText"); - with_array.push(StringOrComponent::String("?".to_string())); + } + _ => { + warn!("couldn't parse {with:?} as FormattedText because it's not a list of compounds"); + return None; } } component = FormattedText::Translatable(TranslatableComponent::new( @@ -448,8 +472,9 @@ impl McBufReadable for FormattedText { fn read_from(buf: &mut std::io::Cursor<&[u8]>) -> Result { let nbt = simdnbt::borrow::NbtTag::read_optional(buf)?; if let Some(nbt) = nbt { - FormattedText::from_nbt_tag(&nbt) - .ok_or(BufReadError::Custom("couldn't read nbt".to_owned())) + FormattedText::from_nbt_tag(&nbt).ok_or(BufReadError::Custom( + "couldn't convert nbt to chat message".to_owned(), + )) } else { Ok(FormattedText::default()) } diff --git a/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs b/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs index 420dfd36f..691a62a15 100755 --- a/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_system_chat_packet.rs @@ -29,4 +29,17 @@ mod tests { "[py5: Gave 1 [Diamond Pickaxe] to py5]".to_string() ); } + + #[test] + fn test_translate_with_string_array_clientbound_system_chat_packet() { + #[rustfmt::skip] + let bytes = [ + 10, 9, 0, 4, 119, 105, 116, 104, 8, 0, 0, 0, 1, 0, 14, 109, 105, 110, 101, 99, 114, 97, 102, 116, 58, 100, 117, 115, 116, 8, 0, 9, 116, 114, 97, 110, 115, 108, 97, 116, 101, 0, 25, 99, 111, 109, 109, 97, 110, 100, 115, 46, 112, 97, 114, 116, 105, 99, 108, 101, 46, 115, 117, 99, 99, 101, 115, 115, 0, 0 + ]; + let packet = ClientboundSystemChatPacket::read_from(&mut Cursor::new(&bytes)).unwrap(); + assert_eq!( + packet.content.to_string(), + "Displaying particle minecraft:dust".to_string() + ); + } } From d9cbe3f1337867433de80d6d12d1813b51f54c53 Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 20 Apr 2024 21:12:38 +0000 Subject: [PATCH 8/9] fix schedule conflicts --- azalea/src/pathfinder/extras/mod.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/azalea/src/pathfinder/extras/mod.rs b/azalea/src/pathfinder/extras/mod.rs index b3273c212..c9ba9d308 100644 --- a/azalea/src/pathfinder/extras/mod.rs +++ b/azalea/src/pathfinder/extras/mod.rs @@ -7,7 +7,7 @@ pub mod utils; use crate::ecs::prelude::*; use azalea_block::BlockStates; -use azalea_client::Client; +use azalea_client::{mining::MiningSet, Client}; use azalea_core::{position::BlockPos, tick::GameTick}; use azalea_physics::PhysicsSet; use bevy_app::Update; @@ -29,8 +29,12 @@ impl Plugin for PathfinderExtrasPlugin { .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, + ( + pickup::watch_for_mined_blocks, + pickup::watch_for_item_spawns_from_blocks_we_mined, + ) + .chain() + .after(MiningSet), ), ) .add_systems( From 4effd7713536d94511c98fb33968ea3549f23ca9 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 11 Aug 2024 22:10:18 +0000 Subject: [PATCH 9/9] 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 | 132 +++++++++++++++++--------- azalea/src/pathfinder/mod.rs | 72 ++++++++------ azalea/src/pathfinder/simulation.rs | 15 ++- 6 files changed, 165 insertions(+), 92 deletions(-) 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 b07599e89..25be18df2 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 78df9134a..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; @@ -92,13 +92,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 { @@ -172,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().abs() + 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().abs() + 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().abs() + 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 index efd79c42f..30b1ae522 100644 --- a/azalea/src/pathfinder/extras/utils.rs +++ b/azalea/src/pathfinder/extras/utils.rs @@ -4,52 +4,6 @@ use azalea_core::position::{BlockPos, Vec3}; use azalea_entity::direction_looking_at; use azalea_world::ChunkStorage; -/// 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 -} - /// Get a vec of block positions that we can reach from this position. pub fn get_reachable_blocks_around_player( player_position: BlockPos, @@ -111,3 +65,89 @@ pub fn pick_closest_block(position: BlockPos, blocks: &[BlockPos]) -> Option 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 63ab2b135..42c68bb2e 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -873,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 { @@ -896,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), @@ -944,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), @@ -962,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), @@ -984,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), @@ -996,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), @@ -1015,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), @@ -1032,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), @@ -1049,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), @@ -1066,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/simulation.rs b/azalea/src/pathfinder/simulation.rs index 5a9a135ae..56bd6e784 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -145,8 +145,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() } }