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