aboutsummaryrefslogtreecommitdiff
path: root/azalea
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2025-04-15 22:04:43 -0430
committermat <git@matdoes.dev>2025-04-15 22:04:43 -0430
commita9820dfd79bf24a0a6fcb2345aad6c79a21585a5 (patch)
treea8e6290707fee0e1b18812aba599da74e7fc6eed /azalea
parent1a0c4e2de9e6d82d5efdfac2bab17a73c79fc466 (diff)
downloadazalea-drasl-a9820dfd79bf24a0a6fcb2345aad6c79a21585a5.tar.xz
make goto async and clean up some examples
Diffstat (limited to 'azalea')
-rw-r--r--azalea/README.md8
-rw-r--r--azalea/examples/steal.rs69
-rw-r--r--azalea/examples/testbot/commands/movement.rs10
-rw-r--r--azalea/src/bot.rs40
-rw-r--r--azalea/src/pathfinder/goals.rs7
-rw-r--r--azalea/src/pathfinder/mod.rs60
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>,