From a9820dfd79bf24a0a6fcb2345aad6c79a21585a5 Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 15 Apr 2025 22:04:43 -0430 Subject: make goto async and clean up some examples --- azalea/src/bot.rs | 40 +++++++++++++++++++++++++++- azalea/src/pathfinder/goals.rs | 7 ++++- azalea/src/pathfinder/mod.rs | 60 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 94 insertions(+), 13 deletions(-) (limited to 'azalea/src') diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 514cea1e..dd8e6209 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -1,8 +1,8 @@ use std::f64::consts::PI; -use azalea_client::TickBroadcast; use azalea_client::interact::SwingArmEvent; use azalea_client::mining::Mining; +use azalea_client::tick_broadcast::{TickBroadcast, UpdateBroadcast}; use azalea_core::position::{BlockPos, Vec3}; use azalea_core::tick::GameTick; use azalea_entity::{ @@ -86,6 +86,12 @@ pub trait BotClientExt { fn look_at(&self, pos: Vec3); /// Get a receiver that will receive a message every tick. fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>; + /// Get a receiver that will receive a message every ECS Update. + fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>; + /// Wait for one tick. + fn wait_one_tick(&self) -> impl Future + Send; + /// Wait for one ECS Update. + fn wait_one_update(&self) -> impl Future + Send; /// Mine a block. This won't turn the bot's head towards the block, so if /// that's necessary you'll have to do that yourself with [`look_at`]. /// @@ -133,6 +139,38 @@ impl BotClientExt for azalea_client::Client { tick_broadcast.subscribe() } + /// Returns a Receiver that receives a message every ECS Update. + /// + /// ECS Updates happen at least at the frequency of game ticks, usually + /// faster. + /// + /// This is useful if you're sending an ECS event and want to make sure it's + /// been handled before continuing. + fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { + let ecs = self.ecs.lock(); + let update_broadcast = ecs.resource::(); + update_broadcast.subscribe() + } + + /// Wait for one tick using [`Self::get_tick_broadcaster`]. + /// + /// If you're going to run this in a loop, you may want to use that function + /// instead and use the `Receiver` from it as it'll be more efficient. + async fn wait_one_tick(&self) { + let mut receiver = self.get_tick_broadcaster(); + // wait for the next tick + let _ = receiver.recv().await; + } + /// Waits for one ECS Update using [`Self::get_update_broadcaster`]. + /// + /// If you're going to run this in a loop, you may want to use that function + /// instead and use the `Receiver` from it as it'll be more efficient. + async fn wait_one_update(&self) { + let mut receiver = self.get_update_broadcaster(); + // wait for the next tick + let _ = receiver.recv().await; + } + async fn mine(&self, position: BlockPos) { self.start_mining(position); // vanilla sends an extra swing arm packet when we start mining diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index 0fb72446..9052c8fd 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use super::costs::{COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST}; -pub trait Goal: Debug { +pub trait Goal: Debug + Send + Sync { #[must_use] fn heuristic(&self, n: BlockPos) -> f32; #[must_use] @@ -100,6 +100,11 @@ pub struct RadiusGoal { pub pos: Vec3, pub radius: f32, } +impl RadiusGoal { + pub fn new(pos: Vec3, radius: f32) -> Self { + Self { pos, radius } + } +} impl Goal for RadiusGoal { fn heuristic(&self, n: BlockPos) -> f32 { let n = n.center(); diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 0db627ac..81ac5337 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -47,7 +47,6 @@ use self::debug::debug_render_path_with_particles; use self::goals::Goal; use self::mining::MiningCache; use self::moves::{ExecuteCtx, IsReachedCtx, SuccessorsFn}; -use crate::WalkDirection; use crate::app::{App, Plugin}; use crate::bot::{JumpEvent, LookAtEvent}; use crate::ecs::{ @@ -58,6 +57,7 @@ use crate::ecs::{ system::{Commands, Query, Res}, }; use crate::pathfinder::{astar::a_star, moves::PathfinderCtx, world::CachedWorld}; +use crate::{BotClientExt, WalkDirection}; #[derive(Clone, Default)] pub struct PathfinderPlugin; @@ -103,7 +103,7 @@ impl Plugin for PathfinderPlugin { /// A component that makes this client able to pathfind. #[derive(Component, Default, Clone)] pub struct Pathfinder { - pub goal: Option>, + pub goal: Option>, pub successors_fn: Option, pub is_calculating: bool, pub allow_mining: bool, @@ -134,7 +134,7 @@ pub struct ExecutingPath { pub struct GotoEvent { /// The local bot entity that will do the pathfinding and execute the path. 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, @@ -180,22 +180,40 @@ pub 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 goto(&self, goal: impl Goal + 'static) -> impl Future; + fn start_goto(&self, goal: impl Goal + 'static); + fn start_goto_without_mining(&self, goal: impl Goal + 'static); fn stop_pathfinding(&self); + fn wait_until_goto_target_reached(&self) -> impl Future; + 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. + /// + /// ``` + /// # use azalea::prelude::*; + /// # use azalea::{BlockPos, pathfinder::goals::BlockPosGoal}; + /// # fn example(bot: &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; + } + /// Start pathfinding to a given goal. /// /// ``` /// # use azalea::prelude::*; /// # use azalea::{BlockPos, pathfinder::goals::BlockPosGoal}; /// # fn example(bot: &Client) { - /// bot.goto(BlockPosGoal(BlockPos::new(0, 70, 0))); + /// bot.start_goto(BlockPosGoal(BlockPos::new(0, 70, 0))); /// # } /// ``` - fn goto(&self, goal: impl Goal + Send + Sync + 'static) { + fn start_goto(&self, goal: impl Goal + 'static) { self.ecs.lock().send_event(GotoEvent { entity: self.entity, goal: Arc::new(goal), @@ -206,9 +224,9 @@ impl PathfinderClientExt for azalea_client::Client { }); } - /// Same as [`goto`](Self::goto). but the bot won't break any blocks while - /// executing the path. - fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static) { + /// 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 { entity: self.entity, goal: Arc::new(goal), @@ -225,6 +243,26 @@ impl PathfinderClientExt for azalea_client::Client { force: false, }); } + + /// 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 + self.wait_one_update().await; + + let mut tick_broadcaster = self.get_tick_broadcaster(); + while !self.is_goto_target_reached() { + // check every tick + tick_broadcaster.recv().await.unwrap(); + } + } + + fn is_goto_target_reached(&self) -> bool { + self.map_get_component::(|p| { + p.map(|p| p.goal.is_none() && !p.is_calculating) + .unwrap_or(true) + }) + } } #[derive(Component)] @@ -331,7 +369,7 @@ pub fn goto_listener( pub struct CalculatePathOpts { pub entity: Entity, pub start: BlockPos, - pub goal: Arc, + pub goal: Arc, pub successors_fn: SuccessorsFn, pub world_lock: Arc>, pub goto_id_atomic: Arc, -- cgit v1.2.3