1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

make goto async and clean up some examples

This commit is contained in:
mat 2025-04-15 22:04:43 -04:30
parent 1a0c4e2de9
commit a9820dfd79
11 changed files with 218 additions and 95 deletions

View file

@ -46,19 +46,15 @@ use bevy_ecs::{
component::Component,
entity::Entity,
schedule::{InternedScheduleLabel, IntoSystemConfigs, LogLevel, ScheduleBuildSettings},
system::{ResMut, Resource},
system::Resource,
world::World,
};
use bevy_time::TimePlugin;
use derive_more::Deref;
use parking_lot::{Mutex, RwLock};
use simdnbt::owned::NbtCompound;
use thiserror::Error;
use tokio::{
sync::{
broadcast,
mpsc::{self, error::TrySendError},
},
sync::mpsc::{self, error::TrySendError},
time,
};
use tracing::{debug, error, info};
@ -88,6 +84,7 @@ use crate::{
raw_connection::RawConnection,
respawn::RespawnPlugin,
task_pool::TaskPoolPlugin,
tick_broadcast::TickBroadcastPlugin,
tick_end::TickEndPlugin,
};
@ -1006,39 +1003,6 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::Sender<()>) {
}
}
/// A resource that contains a [`broadcast::Sender`] that will be sent every
/// Minecraft tick.
///
/// This is useful for running code every schedule from async user code.
///
/// ```
/// use azalea_client::TickBroadcast;
/// # async fn example(client: azalea_client::Client) {
/// let mut receiver = {
/// let ecs = client.ecs.lock();
/// let tick_broadcast = ecs.resource::<TickBroadcast>();
/// tick_broadcast.subscribe()
/// };
/// while receiver.recv().await.is_ok() {
/// // do something
/// }
/// # }
/// ```
#[derive(Resource, Deref)]
pub struct TickBroadcast(broadcast::Sender<()>);
pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
let _ = tick_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
pub struct TickBroadcastPlugin;
impl Plugin for TickBroadcastPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(TickBroadcast(broadcast::channel(1).0))
.add_systems(GameTick, send_tick_broadcast);
}
}
pub struct AmbiguityLoggerPlugin;
impl Plugin for AmbiguityLoggerPlugin {
fn build(&self, app: &mut App) {

View file

@ -24,7 +24,7 @@ pub use account::{Account, AccountOpts};
pub use azalea_protocol::common::client_information::ClientInformation;
pub use client::{
Client, DefaultPlugins, InConfigState, InGameState, JoinError, JoinedClientBundle,
LocalPlayerBundle, StartClientOpts, TickBroadcast, start_ecs_runner,
LocalPlayerBundle, StartClientOpts, start_ecs_runner,
};
pub use events::Event;
pub use local_player::{GameProfileComponent, Hunger, InstanceHolder, TabList};

View file

@ -12,4 +12,5 @@ pub mod packet;
pub mod pong;
pub mod respawn;
pub mod task_pool;
pub mod tick_broadcast;
pub mod tick_end;

View file

@ -0,0 +1,45 @@
use azalea_core::tick::GameTick;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use derive_more::Deref;
use tokio::sync::broadcast;
/// A resource that contains a [`broadcast::Sender`] that will be sent every
/// Minecraft tick.
///
/// This is useful for running code every schedule from async user code.
///
/// ```
/// use azalea_client::TickBroadcast;
/// # async fn example(client: azalea_client::Client) {
/// let mut receiver = {
/// let ecs = client.ecs.lock();
/// let tick_broadcast = ecs.resource::<TickBroadcast>();
/// tick_broadcast.subscribe()
/// };
/// while receiver.recv().await.is_ok() {
/// // do something
/// }
/// # }
/// ```
#[derive(Resource, Deref)]
pub struct TickBroadcast(broadcast::Sender<()>);
#[derive(Resource, Deref)]
pub struct UpdateBroadcast(broadcast::Sender<()>);
pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
let _ = tick_broadcast.0.send(());
}
pub fn send_update_broadcast(update_broadcast: ResMut<UpdateBroadcast>) {
let _ = update_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
pub struct TickBroadcastPlugin;
impl Plugin for TickBroadcastPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(TickBroadcast(broadcast::channel(1).0))
.insert_resource(UpdateBroadcast(broadcast::channel(1).0))
.add_systems(GameTick, send_tick_broadcast)
.add_systems(Update, send_update_broadcast);
}
}

View file

@ -116,7 +116,10 @@ impl Instance {
/// Find all the coordinates of a block in the world.
///
/// This returns an iterator that yields the [`BlockPos`]s of blocks that
/// are in the given block states. It's sorted by `x+y+z`.
/// are in the given block states.
///
/// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2` for
/// optimization purposes.
pub fn find_blocks<'a>(
&'a self,
nearest_to: impl Into<BlockPos>,

View file

@ -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;
}
_ => {}
}

View file

@ -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();
}
let chest_block = bot
.world()
.read()
.find_block(bot.position(), &azalea::registry::Block::Chest.into());
// TODO: update this when find_blocks is implemented
let Some(chest_block) = chest_block else {
bot.chat("No chest found");
return Ok(());
};
// bot.goto(BlockPosGoal(chest_block));
let Some(chest) = bot.open_container_at(chest_block).await else {
println!("Couldn't open chest");
return Ok(());
};
println!("Getting contents");
for (index, slot) in chest
.contents()
.expect("we just opened the chest")
.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 });
}
}
}
println!("Done");
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_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 {
break;
};
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 at {chest_block:?}");
continue;
};
println!("Getting contents of chest at {chest_block:?}");
for (index, slot) in chest.contents().unwrap_or_default().iter().enumerate() {
println!("Checking slot {index}: {slot:?}");
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 });
}
}
}
bot.chat("Done");
*state.is_stealing.lock() = false;
Ok(())
}

View file

@ -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
}),
))),

View file

@ -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

View file

@ -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();

View file

@ -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>,