diff options
| author | mat <git@matdoes.dev> | 2025-04-15 22:04:43 -0430 |
|---|---|---|
| committer | mat <git@matdoes.dev> | 2025-04-15 22:04:43 -0430 |
| commit | a9820dfd79bf24a0a6fcb2345aad6c79a21585a5 (patch) | |
| tree | a8e6290707fee0e1b18812aba599da74e7fc6eed /azalea | |
| parent | 1a0c4e2de9e6d82d5efdfac2bab17a73c79fc466 (diff) | |
| download | azalea-drasl-a9820dfd79bf24a0a6fcb2345aad6c79a21585a5.tar.xz | |
make goto async and clean up some examples
Diffstat (limited to 'azalea')
| -rw-r--r-- | azalea/README.md | 8 | ||||
| -rw-r--r-- | azalea/examples/steal.rs | 69 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands/movement.rs | 10 | ||||
| -rw-r--r-- | azalea/src/bot.rs | 40 | ||||
| -rw-r--r-- | azalea/src/pathfinder/goals.rs | 7 | ||||
| -rw-r--r-- | azalea/src/pathfinder/mod.rs | 60 |
6 files changed, 152 insertions, 42 deletions
diff --git a/azalea/README.md b/azalea/README.md index de11e202..d052f7c8 100644 --- a/azalea/README.md +++ b/azalea/README.md @@ -42,9 +42,10 @@ You can just replace these with `azalea` in your code since everything from `aza ```rust,no_run //! A bot that logs chat messages sent in the server to the console. +use std::sync::Arc; + use azalea::prelude::*; use parking_lot::Mutex; -use std::sync::Arc; #[tokio::main] async fn main() { @@ -59,12 +60,15 @@ async fn main() { } #[derive(Default, Clone, Component)] -pub struct State {} +pub struct State { + pub messages_received: Arc<Mutex<usize>> +} async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { match event { Event::Chat(m) => { println!("{}", m.message().to_ansi()); + *state.messages_received.lock() += 1; } _ => {} } diff --git a/azalea/examples/steal.rs b/azalea/examples/steal.rs index 464d94f8..1277fab2 100644 --- a/azalea/examples/steal.rs +++ b/azalea/examples/steal.rs @@ -2,6 +2,7 @@ use std::sync::Arc; +use azalea::pathfinder::goals::RadiusGoal; use azalea::{BlockPos, prelude::*}; use azalea_inventory::ItemStack; use azalea_inventory::operations::QuickMoveClick; @@ -21,6 +22,7 @@ async fn main() { #[derive(Default, Clone, Component)] struct State { + pub is_stealing: Arc<Mutex<bool>>, pub checked_chests: Arc<Mutex<Vec<BlockPos>>>, } @@ -32,43 +34,64 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { if m.content() != "go" { return Ok(()); } - { - state.checked_chests.lock().clear(); + + steal(bot, state).await?; + } + + Ok(()) +} + +async fn steal(bot: Client, state: State) -> anyhow::Result<()> { + { + let mut is_stealing = state.is_stealing.lock(); + if *is_stealing { + bot.chat("Already stealing"); + return Ok(()); } + *is_stealing = true; + } + + state.checked_chests.lock().clear(); + loop { let chest_block = bot .world() .read() - .find_block(bot.position(), &azalea::registry::Block::Chest.into()); - // TODO: update this when find_blocks is implemented + .find_blocks(bot.position(), &azalea::registry::Block::Chest.into()) + .filter( + // filter for chests that haven't been checked + |block_pos| !state.checked_chests.lock().contains(&block_pos), + ) + .next(); let Some(chest_block) = chest_block else { - bot.chat("No chest found"); - return Ok(()); + break; }; - // bot.goto(BlockPosGoal(chest_block)); + + state.checked_chests.lock().push(chest_block); + + bot.goto(RadiusGoal::new(chest_block.center(), 3.)).await; + let Some(chest) = bot.open_container_at(chest_block).await else { - println!("Couldn't open chest"); - return Ok(()); + println!("Couldn't open chest at {chest_block:?}"); + continue; }; - println!("Getting contents"); - for (index, slot) in chest - .contents() - .expect("we just opened the chest") - .iter() - .enumerate() - { + println!("Getting contents of chest at {chest_block:?}"); + for (index, slot) in chest.contents().unwrap_or_default().iter().enumerate() { println!("Checking slot {index}: {slot:?}"); - if let ItemStack::Present(item) = slot { - if item.kind == azalea::registry::Item::Diamond { - println!("clicking slot ^"); - chest.click(QuickMoveClick::Left { slot: index as u16 }); - } + let ItemStack::Present(item) = slot else { + continue; + }; + if item.kind == azalea::registry::Item::Diamond { + println!("clicking slot ^"); + chest.click(QuickMoveClick::Left { slot: index as u16 }); } } - - println!("Done"); } + bot.chat("Done"); + + *state.is_stealing.lock() = false; + Ok(()) } diff --git a/azalea/examples/testbot/commands/movement.rs b/azalea/examples/testbot/commands/movement.rs index a400809b..1596ce89 100644 --- a/azalea/examples/testbot/commands/movement.rs +++ b/azalea/examples/testbot/commands/movement.rs @@ -28,7 +28,9 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { return 0; }; source.reply("ok"); - source.bot.goto(BlockPosGoal(BlockPos::from(position))); + source + .bot + .start_goto(BlockPosGoal(BlockPos::from(position))); 1 }) .then(literal("xz").then(argument("x", integer()).then( @@ -38,7 +40,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { let z = get_integer(ctx, "z").unwrap(); println!("goto xz {x} {z}"); source.reply("ok"); - source.bot.goto(XZGoal { x, z }); + source.bot.start_goto(XZGoal { x, z }); 1 }), ))) @@ -52,7 +54,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { let z = get_integer(ctx, "z").unwrap(); println!("goto radius {radius}, position: {x} {y} {z}"); source.reply("ok"); - source.bot.goto(RadiusGoal { + source.bot.start_goto(RadiusGoal { pos: BlockPos::new(x, y, z).center(), radius, }); @@ -68,7 +70,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { let z = get_integer(ctx, "z").unwrap(); println!("goto xyz {x} {y} {z}"); source.reply("ok"); - source.bot.goto(BlockPosGoal(BlockPos::new(x, y, z))); + source.bot.start_goto(BlockPosGoal(BlockPos::new(x, y, z))); 1 }), ))), 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<Output = ()> + Send; + /// Wait for one ECS Update. + fn wait_one_update(&self) -> impl Future<Output = ()> + 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::<UpdateBroadcast>(); + 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<Arc<dyn Goal + Send + Sync>>, + pub goal: Option<Arc<dyn Goal>>, pub successors_fn: Option<SuccessorsFn>, 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<dyn Goal + Send + Sync>, + pub goal: Arc<dyn Goal>, /// 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<Output = ()>; + 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<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. + /// + /// ``` + /// # 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::<Pathfinder, _>(|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<dyn Goal + Send + Sync>, + pub goal: Arc<dyn Goal>, pub successors_fn: SuccessorsFn, pub world_lock: Arc<RwLock<azalea_world::Instance>>, pub goto_id_atomic: Arc<AtomicUsize>, |
