diff options
| author | mat <git@matdoes.dev> | 2025-08-15 00:50:42 -0330 |
|---|---|---|
| committer | mat <git@matdoes.dev> | 2025-08-15 07:20:52 +0300 |
| commit | 980f41be2283857eecf113aa75f187fed35f4270 (patch) | |
| tree | 9add2135c110a921998932b43ac4b0b2c0d6409f | |
| parent | 6758d58109925fbe59bb5693296b995697faaf3a (diff) | |
| download | azalea-drasl-980f41be2283857eecf113aa75f187fed35f4270.tar.xz | |
add PathfinderOpts and clean up some pathfinder code
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | azalea-physics/src/local_player.rs | 3 | ||||
| -rw-r--r-- | azalea/README.md | 9 | ||||
| -rw-r--r-- | azalea/src/pathfinder/astar.rs | 7 | ||||
| -rw-r--r-- | azalea/src/pathfinder/goto_event.rs | 100 | ||||
| -rw-r--r-- | azalea/src/pathfinder/mod.rs | 249 | ||||
| -rw-r--r-- | azalea/src/pathfinder/tests.rs | 21 |
7 files changed, 204 insertions, 186 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 935363d0..935e4484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ is breaking anyways, semantic versioning is not followed. - The fields in `LookDirection` have been replaced with getters. - Renamed `Client::entity_by` to `any_entity_by`, and `Client::entities_by` to `nearest_entities_by`. - `EyeHeight` was moved into `EntityDimensions`, and `EntityDimensions` is now its own component. +- Replaced `start_goto_without_mining` with `start_goto_with_opts`. ### Fixed diff --git a/azalea-physics/src/local_player.rs b/azalea-physics/src/local_player.rs index 6ce196b6..f36e7ede 100644 --- a/azalea-physics/src/local_player.rs +++ b/azalea-physics/src/local_player.rs @@ -23,7 +23,8 @@ pub struct PhysicsState { /// how the nametags of other entities are displayed. /// /// To check whether we're actually sneaking, you can check the - /// [`Crouching`] or [`Pose`] components. + /// [`Crouching`](azalea_entity::Crouching) or [`Pose`](azalea_entity::Pose) + /// components. pub trying_to_crouch: bool, pub move_direction: WalkDirection, diff --git a/azalea/README.md b/azalea/README.md index 3454ed2e..5bb18d38 100644 --- a/azalea/README.md +++ b/azalea/README.md @@ -89,6 +89,10 @@ Also note that just because something is an entity in the ECS doesn't mean that See the [Bevy Cheatbook](https://bevy-cheatbook.github.io/programming/ecs-intro.html) to learn more about Bevy ECS (and the ECS paradigm in general). +# Using a single-threaded Tokio runtime + +Due to the fact that Azalea clients store the ECS in a Mutex that's frequently locked and unlocked, bots that rely on the `Client` or `Swarm` types may run into race condition bugs (like out-of-order events and ticks happening at suboptimal moments) if they do not set Tokio to use a single thread with `#[tokio::main(flavor = "current_thread")]`. This may change in a future version of Azalea. Setting this option will usually not result in a performance hit, and Azalea internally will keep using multiple threads for running the ECS itself (because Tokio is not used for this). + # Debugging Azalea uses several relatively complex features of Rust, which may make debugging certain issues more tricky if you're not familiar with them. @@ -109,9 +113,4 @@ If your code is simply hanging, it might be a deadlock. Enable `parking_lot`'s ` Backtraces are also useful, though they're sometimes hard to read and don't always contain the actual location of the error. Run your code with `RUST_BACKTRACE=1` to enable full backtraces. If it's very long, often searching for the keyword "azalea" will help you filter out unrelated things and find the actual source of the issue. -# Using a single-threaded Tokio runtime - -Due to the fact that Azalea clients store the ECS in a Mutex that's frequently locked and unlocked, bots that rely on the `Client` or `Swarm` types may run into race condition bugs (like out-of-order events and ticks happening at suboptimal moments) if they do not set Tokio to use a single thread with `#[tokio::main(flavor = "current_thread")]`. This may change in a future version of Azalea. Setting this option will usually not result in a performance hit, and Azalea internally will keep using multiple threads for running the ECS itself (because Tokio is not used for this). - -[`azalea_client`]: https://docs.rs/azalea-client [`bevy_log`]: https://docs.rs/bevy_log diff --git a/azalea/src/pathfinder/astar.rs b/azalea/src/pathfinder/astar.rs index 15ae7020..da11e352 100644 --- a/azalea/src/pathfinder/astar.rs +++ b/azalea/src/pathfinder/astar.rs @@ -296,6 +296,13 @@ impl PartialOrd for WeightedNode { } } +/// A timeout that the pathfinder will consider when calculating a path. +/// +/// See [`PathfinderOpts::min_timeout`] and [`PathfinderOpts::max_timeout`] if +/// you want to modify this. +/// +/// [`PathfinderOpts::min_timeout`]: super::goto_event::PathfinderOpts::min_timeout +/// [`PathfinderOpts::max_timeout`]: super::goto_event::PathfinderOpts::max_timeout #[derive(Debug, Clone, Copy, PartialEq)] pub enum PathfinderTimeout { /// Time out after a certain duration has passed. This is a good default so diff --git a/azalea/src/pathfinder/goto_event.rs b/azalea/src/pathfinder/goto_event.rs index d87d1586..8a4f78d0 100644 --- a/azalea/src/pathfinder/goto_event.rs +++ b/azalea/src/pathfinder/goto_event.rs @@ -22,41 +22,48 @@ pub struct GotoEvent { /// The local bot entity that will do the pathfinding and execute the path. pub entity: Entity, pub goal: Arc<dyn Goal>, - /// The function that's used for checking what moves are possible. Usually - /// [`moves::default_move`]. - pub successors_fn: SuccessorsFn, - - /// Whether the bot is allowed to break blocks while pathfinding. - pub allow_mining: bool, - - /// Whether we should recalculate the path when the pathfinder timed out and - /// there's no partial path to try. - /// - /// Should usually be set to true. - pub retry_on_no_path: bool, - - /// The minimum amount of time that should pass before the A* pathfinder - /// function can return a timeout. It may take up to [`Self::max_timeout`] - /// if it can't immediately find a usable path. - /// - /// A good default value for this is - /// `PathfinderTimeout::Time(Duration::from_secs(1))`. - /// - /// Also see [`PathfinderTimeout::Nodes`] - pub min_timeout: PathfinderTimeout, - /// The absolute maximum amount of time that the pathfinder function can - /// take to find a path. If it takes this long, it means no usable path was - /// found (so it might be impossible). - /// - /// A good default value for this is - /// `PathfinderTimeout::Time(Duration::from_secs(5))`. - pub max_timeout: PathfinderTimeout, + pub opts: PathfinderOpts, } + impl GotoEvent { - pub fn new(entity: Entity, goal: impl Goal + 'static) -> Self { + pub fn new(entity: Entity, goal: impl Goal + 'static, opts: PathfinderOpts) -> Self { Self { entity, goal: Arc::new(goal), + opts, + } + } +} + +/// Configuration options that the pathfinder will use when calculating and +/// executing a path. +/// +/// This can be passed into [`Client::goto_with_opts`] or +/// [`Client::start_goto_with_opts`]. +/// +/// ``` +/// # use azalea::pathfinder::{moves, PathfinderOpts}; +/// // example config to disallow mining blocks and to not do parkour +/// let opts = PathfinderOpts::new() +/// .allow_mining(false) +/// .successors_fn(moves::basic::basic_move); +/// ``` +/// +/// [`Client::goto_with_opts`]: super::PathfinderClientExt::goto_with_opts +/// [`Client::start_goto_with_opts`]: super::PathfinderClientExt::start_goto_with_opts +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct PathfinderOpts { + pub(crate) successors_fn: SuccessorsFn, + pub(crate) allow_mining: bool, + pub(crate) retry_on_no_path: bool, + pub(crate) min_timeout: PathfinderTimeout, + pub(crate) max_timeout: PathfinderTimeout, +} + +impl PathfinderOpts { + pub const fn new() -> Self { + Self { successors_fn: moves::default_move, allow_mining: true, retry_on_no_path: true, @@ -64,23 +71,46 @@ impl GotoEvent { max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)), } } - pub fn with_successors_fn(mut self, successors_fn: SuccessorsFn) -> Self { + /// Set the function that's used for checking what moves are possible. + /// + /// Defaults to [`moves::default_move`]. + pub fn successors_fn(mut self, successors_fn: SuccessorsFn) -> Self { self.successors_fn = successors_fn; self } - pub fn with_allow_mining(mut self, allow_mining: bool) -> Self { + /// Set whether the bot is allowed to break blocks while pathfinding. + /// + /// Defaults to `true`. + pub fn allow_mining(mut self, allow_mining: bool) -> Self { self.allow_mining = allow_mining; self } - pub fn with_retry_on_no_path(mut self, retry_on_no_path: bool) -> Self { + /// Whether we should recalculate the path when the pathfinder timed out and + /// there's no partial path to try. + /// + /// Defaults to `true`. + pub fn retry_on_no_path(mut self, retry_on_no_path: bool) -> Self { self.retry_on_no_path = retry_on_no_path; self } - pub fn with_min_timeout(mut self, min_timeout: PathfinderTimeout) -> Self { + /// The minimum amount of time that should pass before the A* pathfinder + /// function can return a timeout if it finds a path that seems good enough. + /// It may take up to [`Self::max_timeout`] if it can't immediately find + /// a usable path. + /// + /// Defaults to `PathfinderTimeout::Time(Duration::from_secs(1))`. + /// + /// Also see [`PathfinderTimeout::Nodes`] + pub fn min_timeout(mut self, min_timeout: PathfinderTimeout) -> Self { self.min_timeout = min_timeout; self } - pub fn with_max_timeout(mut self, max_timeout: PathfinderTimeout) -> Self { + /// The absolute maximum amount of time that the pathfinder function can + /// take to find a path. If it takes this long, it means no usable path was + /// found (so it might be impossible). + /// + /// Defaults to `PathfinderTimeout::Time(Duration::from_secs(5))`. + pub fn max_timeout(mut self, max_timeout: PathfinderTimeout) -> Self { self.max_timeout = max_timeout; self } diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index f07b3470..736309a2 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -46,7 +46,7 @@ use bevy_tasks::{AsyncComputeTaskPool, Task}; use custom_state::{CustomPathfinderState, CustomPathfinderStateRef}; use futures_lite::future; use goals::BlockPosGoal; -pub use goto_event::GotoEvent; +pub use goto_event::{GotoEvent, PathfinderOpts}; use parking_lot::RwLock; use rel_block_pos::RelBlockPos; use tokio::sync::broadcast::error::RecvError; @@ -119,14 +119,8 @@ impl Plugin for PathfinderPlugin { #[non_exhaustive] pub struct Pathfinder { pub goal: Option<Arc<dyn Goal>>, - pub successors_fn: Option<SuccessorsFn>, + pub opts: Option<PathfinderOpts>, pub is_calculating: bool, - pub allow_mining: bool, - pub retry_on_no_path: bool, - - pub min_timeout: Option<PathfinderTimeout>, - pub max_timeout: Option<PathfinderTimeout>, - pub goto_id: Arc<AtomicUsize>, } @@ -163,16 +157,6 @@ pub fn add_default_pathfinder( } pub trait PathfinderClientExt { - fn goto(&self, goal: impl Goal + 'static) -> impl Future<Output = ()>; - fn start_goto(&self, goal: impl Goal + 'static); - fn start_goto_without_mining(&self, goal: impl Goal + 'static); - fn stop_pathfinding(&self); - fn force_stop_pathfinding(&self); - fn wait_until_goto_target_reached(&self) -> impl Future<Output = ()>; - fn is_goto_target_reached(&self) -> bool; -} - -impl PathfinderClientExt for azalea_client::Client { /// Pathfind to the given goal and wait until either the target is reached /// or the pathfinding is canceled. /// @@ -185,11 +169,26 @@ impl PathfinderClientExt for azalea_client::Client { /// bot.goto(BlockPosGoal(BlockPos::new(0, 70, 0))).await; /// # } /// ``` - async fn goto(&self, goal: impl Goal + 'static) { - self.start_goto(goal); - self.wait_until_goto_target_reached().await; - } - + fn goto(&self, goal: impl Goal + 'static) -> impl Future<Output = ()>; + /// Same as [`Self::goto`], but allows you to set custom options for + /// pathfinding, including disabling mining and setting custom moves. + /// + /// ``` + /// # use azalea::prelude::*; + /// # use azalea::{BlockPos, pathfinder::{goals::BlockPosGoal, PathfinderOpts}}; + /// # async fn example(bot: &Client) { + /// bot.goto_with_opts( + /// BlockPosGoal(BlockPos::new(0, 70, 0)), + /// PathfinderOpts::new().allow_mining(false), + /// ) + /// .await; + /// # } + /// ``` + fn goto_with_opts( + &self, + goal: impl Goal + 'static, + opts: PathfinderOpts, + ) -> impl Future<Output = ()>; /// Start pathfinding to a given goal. /// /// ``` @@ -199,20 +198,13 @@ impl PathfinderClientExt for azalea_client::Client { /// bot.start_goto(BlockPosGoal(BlockPos::new(0, 70, 0))); /// # } /// ``` - fn start_goto(&self, goal: impl Goal + 'static) { - self.ecs - .lock() - .send_event(GotoEvent::new(self.entity, goal)); - } - - /// Same as [`start_goto`](Self::start_goto). but the bot won't break any - /// blocks while executing the path. - fn start_goto_without_mining(&self, goal: impl Goal + 'static) { - self.ecs - .lock() - .send_event(GotoEvent::new(self.entity, goal).with_allow_mining(false)); - } - + fn start_goto(&self, goal: impl Goal + 'static); + /// Same as [`Self::start_goto`], but allows you to set custom + /// options for pathfinding, including disabling mining and setting custom + /// moves. + /// + /// Also see [`Self::goto_with_opts`]. + fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts); /// Stop calculating a path, and stop moving once the current movement is /// finished. /// @@ -220,23 +212,45 @@ impl PathfinderClientExt for azalea_client::Client { /// `stop_pathfinding` was called while executing a parkour jump, but if /// it's undesirable then you may want to consider using /// [`Self::force_stop_pathfinding`] instead. + fn stop_pathfinding(&self); + /// Stop calculating a path and stop executing the current movement + /// immediately. + fn force_stop_pathfinding(&self); + /// Waits forever until the bot no longer has a pathfinder goal. + fn wait_until_goto_target_reached(&self) -> impl Future<Output = ()>; + /// Returns true if the pathfinder has no active goal and isn't calculating + /// a path. + fn is_goto_target_reached(&self) -> bool; +} + +impl PathfinderClientExt for azalea_client::Client { + async fn goto(&self, goal: impl Goal + 'static) { + self.goto_with_opts(goal, PathfinderOpts::new()).await; + } + async fn goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) { + self.start_goto_with_opts(goal, opts); + self.wait_until_goto_target_reached().await; + } + fn start_goto(&self, goal: impl Goal + 'static) { + self.start_goto_with_opts(goal, PathfinderOpts::new()); + } + fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) { + self.ecs + .lock() + .send_event(GotoEvent::new(self.entity, goal, opts)); + } fn stop_pathfinding(&self) { self.ecs.lock().send_event(StopPathfindingEvent { entity: self.entity, force: false, }); } - - /// Stop calculating a path and stop executing the current movement - /// immediately. fn force_stop_pathfinding(&self) { self.ecs.lock().send_event(StopPathfindingEvent { entity: self.entity, force: true, }); } - - /// Waits forever until the bot no longer has a pathfinder goal. async fn wait_until_goto_target_reached(&self) { // we do this to make sure the event got handled before we start checking // is_goto_target_reached @@ -252,7 +266,6 @@ impl PathfinderClientExt for azalea_client::Client { }; } } - fn is_goto_target_reached(&self) -> bool { self.map_get_component::<Pathfinder, _>(|p| p.goal.is_none() && !p.is_calculating) .unwrap_or(true) @@ -289,7 +302,7 @@ pub fn goto_listener( if event.goal.success(BlockPos::from(position)) { // we're already at the goal, nothing to do pathfinder.goal = None; - pathfinder.successors_fn = None; + pathfinder.opts = None; pathfinder.is_calculating = false; debug!("already at goal, not pathfinding"); continue; @@ -297,11 +310,8 @@ pub fn goto_listener( // we store the goal so it can be recalculated later if necessary pathfinder.goal = Some(event.goal.clone()); - pathfinder.successors_fn = Some(event.successors_fn); + pathfinder.opts = Some(event.opts.clone()); pathfinder.is_calculating = true; - pathfinder.allow_mining = event.allow_mining; - pathfinder.min_timeout = Some(event.min_timeout); - pathfinder.max_timeout = Some(event.max_timeout); let start = if let Some(executing_path) = executing_path && let Some(final_node) = executing_path.path.back() @@ -327,8 +337,6 @@ pub fn goto_listener( ); } - let successors_fn: moves::SuccessorsFn = event.successors_fn; - let world_lock = instance_container .get(instance_name) .expect("Entity tried to pathfind but the entity isn't in a valid world"); @@ -338,8 +346,7 @@ pub fn goto_listener( let goto_id_atomic = pathfinder.goto_id.clone(); - let allow_mining = event.allow_mining; - let retry_on_no_path = event.retry_on_no_path; + let allow_mining = event.opts.allow_mining; let mining_cache = MiningCache::new(if allow_mining { Some(inventory.inventory_menu.clone()) } else { @@ -347,24 +354,17 @@ pub fn goto_listener( }); let custom_state = custom_state.cloned().unwrap_or_default(); - - let min_timeout = event.min_timeout; - let max_timeout = event.max_timeout; - + let opts = event.opts.clone(); let task = thread_pool.spawn(async move { - calculate_path(CalculatePathOpts { + calculate_path(CalculatePathCtx { entity, start, goal, - successors_fn, world_lock, goto_id_atomic, - allow_mining, mining_cache, - retry_on_no_path, custom_state, - min_timeout, - max_timeout, + opts, }) }); @@ -372,23 +372,16 @@ pub fn goto_listener( } } -pub struct CalculatePathOpts { +pub struct CalculatePathCtx { pub entity: Entity, pub start: BlockPos, pub goal: Arc<dyn Goal>, - pub successors_fn: SuccessorsFn, pub world_lock: Arc<RwLock<azalea_world::Instance>>, pub goto_id_atomic: Arc<AtomicUsize>, - pub allow_mining: bool, pub mining_cache: MiningCache, - /// See [`GotoEvent::retry_on_no_path`]. - pub retry_on_no_path: bool, - - /// See [`GotoEvent::min_timeout`]. - pub min_timeout: PathfinderTimeout, - pub max_timeout: PathfinderTimeout, - pub custom_state: CustomPathfinderState, + + pub opts: PathfinderOpts, } /// Calculate the [`PathFoundEvent`] for the given pathfinder options. @@ -399,19 +392,19 @@ pub struct CalculatePathOpts { /// You are expected to immediately send the `PathFoundEvent` you received after /// calling this function. `None` will be returned if the pathfinding was /// interrupted by another path calculation. -pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> { - debug!("start: {:?}", opts.start); +pub fn calculate_path(ctx: CalculatePathCtx) -> Option<PathFoundEvent> { + debug!("start: {:?}", ctx.start); - let goto_id = opts.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1; + let goto_id = ctx.goto_id_atomic.fetch_add(1, atomic::Ordering::SeqCst) + 1; - let origin = opts.start; - let cached_world = CachedWorld::new(opts.world_lock, origin); + let origin = ctx.start; + let cached_world = CachedWorld::new(ctx.world_lock, origin); let successors = |pos: RelBlockPos| { call_successors_fn( &cached_world, - &opts.mining_cache, - &opts.custom_state.0.read(), - opts.successors_fn, + &ctx.mining_cache, + &ctx.custom_state.0.read(), + ctx.opts.successors_fn, pos, ) }; @@ -423,11 +416,11 @@ pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> { is_partial, } = a_star( RelBlockPos::get_origin(origin), - |n| opts.goal.heuristic(n.apply(origin)), + |n| ctx.goal.heuristic(n.apply(origin)), successors, - |n| opts.goal.success(n.apply(origin)), - opts.min_timeout, - opts.max_timeout, + |n| ctx.goal.success(n.apply(origin)), + ctx.opts.min_timeout, + ctx.opts.max_timeout, ); let end_time = Instant::now(); debug!("partial: {is_partial:?}"); @@ -451,7 +444,7 @@ pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> { let path = movements.into_iter().collect::<VecDeque<_>>(); - let goto_id_now = opts.goto_id_atomic.load(atomic::Ordering::SeqCst); + let goto_id_now = ctx.goto_id_atomic.load(atomic::Ordering::SeqCst); if goto_id != goto_id_now { // we must've done another goto while calculating this path, so throw it away warn!("finished calculating a path, but it's outdated"); @@ -490,12 +483,12 @@ pub fn calculate_path(opts: CalculatePathOpts) -> Option<PathFoundEvent> { } Some(PathFoundEvent { - entity: opts.entity, - start: opts.start, + entity: ctx.entity, + start: ctx.start, path: Some(mapped_path), is_partial, - successors_fn: opts.successors_fn, - allow_mining: opts.allow_mining, + successors_fn: ctx.opts.successors_fn, + allow_mining: ctx.opts.allow_mining, }) } @@ -602,7 +595,7 @@ pub fn path_found_listener( executing_path.is_path_partial = event.is_partial; } else if path.is_empty() { debug!("calculated path is empty, so didn't add ExecutingPath"); - if !pathfinder.retry_on_no_path { + if !pathfinder.opts.as_ref().is_some_and(|o| o.retry_on_no_path) { debug!("retry_on_no_path is set to false, removing goal"); pathfinder.goal = None; } @@ -677,9 +670,9 @@ pub fn timeout_movement( let world_lock = instance_container .get(instance_name) .expect("Entity tried to pathfind but the entity isn't in a valid world"); - let Some(successors_fn) = pathfinder.successors_fn else { + let Some(opts) = pathfinder.opts.clone() else { warn!( - "pathfinder was going to patch path because of timeout, but there was no successors_fn" + "pathfinder was going to patch path because of timeout, but pathfinder.opts was None" ); return; }; @@ -695,9 +688,9 @@ pub fn timeout_movement( &mut pathfinder, inventory, entity, - successors_fn, world_lock, custom_state, + opts, ); // reset last_node_reached_at so we don't immediately try to patch again executing_path.last_node_reached_at = Instant::now(); @@ -790,7 +783,7 @@ pub fn check_node_reached( { info!("goal was reached!"); pathfinder.goal = None; - pathfinder.successors_fn = None; + pathfinder.opts = None; } } @@ -817,7 +810,7 @@ pub fn check_for_path_obstruction( for (entity, mut pathfinder, mut executing_path, instance_name, inventory, custom_state) in &mut query { - let Some(successors_fn) = pathfinder.successors_fn else { + let Some(opts) = pathfinder.opts.clone() else { continue; }; @@ -828,7 +821,7 @@ pub fn check_for_path_obstruction( // obstruction check (the path we're executing isn't possible anymore) let origin = executing_path.last_reached_node; let cached_world = CachedWorld::new(world_lock, origin); - let mining_cache = MiningCache::new(if pathfinder.allow_mining { + let mining_cache = MiningCache::new(if opts.allow_mining { Some(inventory.inventory_menu.clone()) } else { None @@ -840,7 +833,7 @@ pub fn check_for_path_obstruction( &cached_world, &mining_cache, &custom_state_ref, - successors_fn, + opts.successors_fn, pos, ) }; @@ -872,8 +865,8 @@ pub fn check_for_path_obstruction( continue; } - let Some(successors_fn) = pathfinder.successors_fn else { - error!("got PatchExecutingPathEvent but the bot has no successors_fn"); + let Some(opts) = pathfinder.opts.clone() else { + error!("got PatchExecutingPathEvent but the bot has no pathfinder opts"); continue; }; @@ -890,9 +883,9 @@ pub fn check_for_path_obstruction( &mut pathfinder, inventory, entity, - successors_fn, world_lock, custom_state.clone(), + opts, ); } } @@ -909,9 +902,9 @@ fn patch_path( pathfinder: &mut Pathfinder, inventory: &Inventory, entity: Entity, - successors_fn: SuccessorsFn, world_lock: Arc<RwLock<azalea_world::Instance>>, custom_state: CustomPathfinderState, + opts: PathfinderOpts, ) { let patch_start = if *patch_nodes.start() == 0 { executing_path.last_reached_node @@ -928,8 +921,7 @@ fn patch_path( let goal = Arc::new(BlockPosGoal(patch_end)); let goto_id_atomic = pathfinder.goto_id.clone(); - let allow_mining = pathfinder.allow_mining; - let retry_on_no_path = pathfinder.retry_on_no_path; + let allow_mining = opts.allow_mining; let mining_cache = MiningCache::new(if allow_mining { Some(inventory.inventory_menu.clone()) @@ -938,20 +930,19 @@ fn patch_path( }); // the timeout is small enough that this doesn't need to be async - let path_found_event = calculate_path(CalculatePathOpts { + let path_found_event = calculate_path(CalculatePathCtx { entity, start: patch_start, goal, - successors_fn, world_lock, goto_id_atomic, - allow_mining, mining_cache, - retry_on_no_path, - custom_state, - min_timeout: PathfinderTimeout::Nodes(10_000), - max_timeout: PathfinderTimeout::Nodes(10_000), + opts: PathfinderOpts { + min_timeout: PathfinderTimeout::Nodes(10_000), + max_timeout: PathfinderTimeout::Nodes(10_000), + ..opts + }, }); // this is necessary in case we interrupted another ongoing path calculation @@ -1002,7 +993,7 @@ pub fn recalculate_near_end_of_path( mut commands: Commands, ) { for (entity, mut pathfinder, mut executing_path) in &mut query { - let Some(successors_fn) = pathfinder.successors_fn else { + let Some(mut opts) = pathfinder.opts.clone() else { continue; }; @@ -1018,21 +1009,16 @@ pub fn recalculate_near_end_of_path( "recalculate_near_end_of_path executing_path.is_path_partial: {}", executing_path.is_path_partial ); - goto_events.write(GotoEvent { - entity, - goal, - successors_fn, - allow_mining: pathfinder.allow_mining, - retry_on_no_path: pathfinder.retry_on_no_path, - min_timeout: if executing_path.path.len() == 50 { - // we have quite some time until the node is reached, soooo we might as - // well burn some cpu cycles to get a good path - PathfinderTimeout::Time(Duration::from_secs(5)) - } else { - PathfinderTimeout::Time(Duration::from_secs(1)) - }, - max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"), - }); + + opts.min_timeout = if executing_path.path.len() == 50 { + // we have quite some time until the node is reached, soooo we might as + // well burn some cpu cycles to get a good path + PathfinderTimeout::Time(Duration::from_secs(5)) + } else { + PathfinderTimeout::Time(Duration::from_secs(1)) + }; + + goto_events.write(GotoEvent { entity, goal, opts }); pathfinder.is_calculating = true; if executing_path.path.is_empty() { @@ -1128,17 +1114,10 @@ pub fn recalculate_if_has_goal_but_no_path( if pathfinder.goal.is_some() && !pathfinder.is_calculating && let Some(goal) = pathfinder.goal.as_ref().cloned() + && let Some(opts) = pathfinder.opts.clone() { debug!("Recalculating path because it has a goal but no ExecutingPath"); - goto_events.write(GotoEvent { - entity, - goal, - successors_fn: pathfinder.successors_fn.unwrap(), - allow_mining: pathfinder.allow_mining, - retry_on_no_path: pathfinder.retry_on_no_path, - min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"), - max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"), - }); + goto_events.write(GotoEvent { entity, goal, opts }); pathfinder.is_calculating = true; } } diff --git a/azalea/src/pathfinder/tests.rs b/azalea/src/pathfinder/tests.rs index 8458a982..4f9d2296 100644 --- a/azalea/src/pathfinder/tests.rs +++ b/azalea/src/pathfinder/tests.rs @@ -16,6 +16,7 @@ use super::{ moves, simulation::{SimulatedPlayerBundle, Simulation}, }; +use crate::pathfinder::goto_event::PathfinderOpts; fn setup_blockposgoal_simulation( partial_chunks: &mut PartialChunkStorage, @@ -35,11 +36,13 @@ fn setup_blockposgoal_simulation( simulation.app.world_mut().send_event(GotoEvent { entity: simulation.entity, goal: Arc::new(BlockPosGoal(end_pos)), - successors_fn: moves::default_move, - allow_mining: false, - retry_on_no_path: true, - min_timeout: PathfinderTimeout::Nodes(1_000_000), - max_timeout: PathfinderTimeout::Nodes(5_000_000), + opts: PathfinderOpts { + successors_fn: moves::default_move, + allow_mining: false, + retry_on_no_path: true, + min_timeout: PathfinderTimeout::Nodes(1_000_000), + max_timeout: PathfinderTimeout::Nodes(5_000_000), + }, }); simulation } @@ -299,11 +302,9 @@ fn test_mine_through_non_colliding_block() { simulation.app.world_mut().send_event(GotoEvent { entity: simulation.entity, goal: Arc::new(BlockPosGoal(BlockPos::new(0, 69, 0))), - successors_fn: moves::default_move, - allow_mining: true, - retry_on_no_path: true, - min_timeout: PathfinderTimeout::Nodes(1_000_000), - max_timeout: PathfinderTimeout::Nodes(5_000_000), + opts: PathfinderOpts::new() + .min_timeout(PathfinderTimeout::Nodes(1_000_000)) + .max_timeout(PathfinderTimeout::Nodes(5_000_000)), }); assert_simulation_reaches(&mut simulation, 200, BlockPos::new(0, 70, 0)); |
