azalea/
bot.rs

1use std::f64::consts::PI;
2
3use azalea_client::{
4    mining::Mining,
5    tick_broadcast::{TickBroadcast, UpdateBroadcast},
6};
7use azalea_core::{
8    position::{BlockPos, Vec3},
9    tick::GameTick,
10};
11use azalea_entity::{
12    Jumping, LocalEntity, LookDirection, Position, clamp_look_direction,
13    dimensions::EntityDimensions, metadata::Player, update_dimensions,
14};
15use azalea_physics::PhysicsSystems;
16use bevy_app::Update;
17use bevy_ecs::prelude::*;
18use futures_lite::Future;
19use tracing::trace;
20
21use crate::{
22    accept_resource_packs::AcceptResourcePacksPlugin,
23    app::{App, Plugin, PluginGroup, PluginGroupBuilder},
24    auto_respawn::AutoRespawnPlugin,
25    container::ContainerPlugin,
26    ecs::{
27        component::Component,
28        entity::Entity,
29        query::{With, Without},
30        system::{Commands, Query},
31    },
32    pathfinder::PathfinderPlugin,
33};
34
35#[derive(Clone, Default)]
36pub struct BotPlugin;
37impl Plugin for BotPlugin {
38    fn build(&self, app: &mut App) {
39        app.add_message::<LookAtEvent>()
40            .add_message::<JumpEvent>()
41            .add_systems(
42                Update,
43                (
44                    insert_bot,
45                    look_at_listener
46                        .before(clamp_look_direction)
47                        .after(update_dimensions),
48                    jump_listener,
49                ),
50            )
51            .add_systems(
52                GameTick,
53                stop_jumping
54                    .after(PhysicsSystems)
55                    .after(azalea_client::movement::send_player_input_packet),
56            );
57    }
58}
59
60/// A component that clients with [`BotPlugin`] will have.
61///
62/// If you just want to check if an entity is one of our bots, you should use
63/// [`LocalEntity`].
64#[derive(Default, Component)]
65pub struct Bot {
66    jumping_once: bool,
67}
68
69/// Insert the [`Bot`] component for any local players that don't have it.
70#[allow(clippy::type_complexity)]
71fn insert_bot(
72    mut commands: Commands,
73    mut query: Query<Entity, (Without<Bot>, With<LocalEntity>, With<Player>)>,
74) {
75    for entity in &mut query {
76        commands.entity(entity).insert(Bot::default());
77    }
78}
79
80fn stop_jumping(mut query: Query<(&mut Jumping, &mut Bot)>) {
81    for (mut jumping, mut bot) in &mut query {
82        if bot.jumping_once && **jumping {
83            bot.jumping_once = false;
84            **jumping = false;
85        }
86    }
87}
88
89pub trait BotClientExt {
90    /// Queue a jump for the next tick.
91    fn jump(&self);
92    /// Turn the bot's head to look at the coordinate in the world.
93    fn look_at(&self, pos: Vec3);
94    /// Get a receiver that will receive a message every tick.
95    fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
96    /// Get a receiver that will receive a message every ECS Update.
97    fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
98    /// Wait for the specified number of game ticks.
99    fn wait_ticks(&self, n: usize) -> impl Future<Output = ()> + Send;
100    /// Wait for the specified number of ECS `Update`s.
101    fn wait_updates(&self, n: usize) -> impl Future<Output = ()> + Send;
102    /// Mine a block.
103    ///
104    /// This won't turn the bot's head towards the block, so if that's necessary
105    /// you'll have to do that yourself with [`look_at`].
106    ///
107    /// [`look_at`]: crate::prelude::BotClientExt::look_at
108    fn mine(&self, position: BlockPos) -> impl Future<Output = ()> + Send;
109}
110
111impl BotClientExt for azalea_client::Client {
112    fn jump(&self) {
113        let mut ecs = self.ecs.lock();
114        ecs.write_message(JumpEvent {
115            entity: self.entity,
116        });
117    }
118
119    fn look_at(&self, position: Vec3) {
120        let mut ecs = self.ecs.lock();
121        ecs.write_message(LookAtEvent {
122            entity: self.entity,
123            position,
124        });
125    }
126
127    /// Returns a Receiver that receives a message every game tick.
128    ///
129    /// This is useful if you want to efficiently loop until a certain condition
130    /// is met.
131    ///
132    /// ```
133    /// # use azalea::prelude::*;
134    /// # use azalea::container::WaitingForInventoryOpen;
135    /// # async fn example(bot: &mut azalea::Client) {
136    /// let mut ticks = bot.get_tick_broadcaster();
137    /// while ticks.recv().await.is_ok() {
138    ///     let ecs = bot.ecs.lock();
139    ///     if ecs.get::<WaitingForInventoryOpen>(bot.entity).is_none() {
140    ///         break;
141    ///     }
142    /// }
143    /// # }
144    /// ```
145    fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> {
146        let ecs = self.ecs.lock();
147        let tick_broadcast = ecs.resource::<TickBroadcast>();
148        tick_broadcast.subscribe()
149    }
150
151    /// Returns a Receiver that receives a message every ECS Update.
152    ///
153    /// ECS Updates happen at least at the frequency of game ticks, usually
154    /// faster.
155    ///
156    /// This is useful if you're sending an ECS event and want to make sure it's
157    /// been handled before continuing.
158    fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> {
159        let ecs = self.ecs.lock();
160        let update_broadcast = ecs.resource::<UpdateBroadcast>();
161        update_broadcast.subscribe()
162    }
163
164    /// Wait for the specified number of ticks using
165    /// [`Self::get_tick_broadcaster`].
166    ///
167    /// If you're going to run this in a loop, you may want to use that function
168    /// instead and use the `Receiver` from it to avoid accidentally skipping
169    /// ticks and having to wait longer.
170    async fn wait_ticks(&self, n: usize) {
171        let mut receiver = self.get_tick_broadcaster();
172        for _ in 0..n {
173            let _ = receiver.recv().await;
174        }
175    }
176    /// Waits for the specified number of ECS `Update`s using
177    /// [`Self::get_update_broadcaster`].
178    ///
179    /// These are basically equivalent to frames because even though we have no
180    /// rendering, some game mechanics depend on frames.
181    ///
182    /// If you're going to run this in a loop, you may want to use that function
183    /// instead and use the `Receiver` from it to avoid accidentally skipping
184    /// ticks and having to wait longer.
185    async fn wait_updates(&self, n: usize) {
186        let mut receiver = self.get_update_broadcaster();
187        for _ in 0..n {
188            let _ = receiver.recv().await;
189        }
190    }
191
192    async fn mine(&self, position: BlockPos) {
193        self.start_mining(position);
194
195        let mut receiver = self.get_tick_broadcaster();
196        while receiver.recv().await.is_ok() {
197            let ecs = self.ecs.lock();
198            if ecs.get::<Mining>(self.entity).is_none() {
199                break;
200            }
201        }
202    }
203}
204
205/// Event to jump once.
206#[derive(Message)]
207pub struct JumpEvent {
208    pub entity: Entity,
209}
210
211pub fn jump_listener(
212    mut query: Query<(&mut Jumping, &mut Bot)>,
213    mut events: MessageReader<JumpEvent>,
214) {
215    for event in events.read() {
216        if let Ok((mut jumping, mut bot)) = query.get_mut(event.entity) {
217            **jumping = true;
218            bot.jumping_once = true;
219        }
220    }
221}
222
223/// Make an entity look towards a certain position in the world.
224#[derive(Message)]
225pub struct LookAtEvent {
226    pub entity: Entity,
227    /// The position we want the entity to be looking at.
228    pub position: Vec3,
229}
230fn look_at_listener(
231    mut events: MessageReader<LookAtEvent>,
232    mut query: Query<(&Position, &EntityDimensions, &mut LookDirection)>,
233) {
234    for event in events.read() {
235        if let Ok((position, dimensions, mut look_direction)) = query.get_mut(event.entity) {
236            let new_look_direction =
237                direction_looking_at(position.up(dimensions.eye_height.into()), event.position);
238
239            trace!("look at {} (currently at {})", event.position, **position);
240            look_direction.update(new_look_direction);
241        }
242    }
243}
244
245/// Return the look direction that would make a client at `current` be
246/// looking at `target`.
247pub fn direction_looking_at(current: Vec3, target: Vec3) -> LookDirection {
248    // borrowed from mineflayer's Bot.lookAt because i didn't want to do math
249    let delta = target - current;
250    let y_rot = (PI - f64::atan2(-delta.x, -delta.z)) * (180.0 / PI);
251    let ground_distance = f64::sqrt(delta.x * delta.x + delta.z * delta.z);
252    let x_rot = f64::atan2(delta.y, ground_distance) * -(180.0 / PI);
253
254    LookDirection::new(y_rot as f32, x_rot as f32)
255}
256
257/// A [`PluginGroup`] for the plugins that add extra bot functionality to the
258/// client.
259pub struct DefaultBotPlugins;
260
261impl PluginGroup for DefaultBotPlugins {
262    fn build(self) -> PluginGroupBuilder {
263        PluginGroupBuilder::start::<Self>()
264            .add(BotPlugin)
265            .add(PathfinderPlugin)
266            .add(ContainerPlugin)
267            .add(AutoRespawnPlugin)
268            .add(AcceptResourcePacksPlugin)
269    }
270}