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