azalea/
bot.rs

1use std::f64::consts::PI;
2
3use azalea_client::interact::SwingArmEvent;
4use azalea_client::mining::Mining;
5use azalea_client::TickBroadcast;
6use azalea_core::position::{BlockPos, Vec3};
7use azalea_core::tick::GameTick;
8use azalea_entity::{
9    clamp_look_direction, metadata::Player, EyeHeight, Jumping, LocalEntity, LookDirection,
10    Position,
11};
12use azalea_physics::PhysicsSet;
13use bevy_app::Update;
14use bevy_ecs::prelude::Event;
15use bevy_ecs::schedule::IntoSystemConfigs;
16use futures_lite::Future;
17use tracing::trace;
18
19use crate::accept_resource_packs::AcceptResourcePacksPlugin;
20use crate::app::{App, Plugin, PluginGroup, PluginGroupBuilder};
21use crate::auto_respawn::AutoRespawnPlugin;
22use crate::container::ContainerPlugin;
23use crate::ecs::{
24    component::Component,
25    entity::Entity,
26    event::EventReader,
27    query::{With, Without},
28    system::{Commands, Query},
29};
30use crate::pathfinder::PathfinderPlugin;
31
32#[derive(Clone, Default)]
33pub struct BotPlugin;
34impl Plugin for BotPlugin {
35    fn build(&self, app: &mut App) {
36        app.add_event::<LookAtEvent>()
37            .add_event::<JumpEvent>()
38            .add_systems(
39                Update,
40                (
41                    insert_bot,
42                    look_at_listener.before(clamp_look_direction),
43                    jump_listener,
44                ),
45            )
46            .add_systems(
47                GameTick,
48                stop_jumping
49                    .after(PhysicsSet)
50                    .after(azalea_client::movement::send_player_input_packet),
51            );
52    }
53}
54
55/// A component that clients with [`BotPlugin`] will have. If you just want to
56/// check if an entity is one of our bots, you should use [`LocalEntity`].
57#[derive(Default, Component)]
58pub struct Bot {
59    jumping_once: bool,
60}
61
62/// Insert the [`Bot`] component for any local players that don't have it.
63#[allow(clippy::type_complexity)]
64fn insert_bot(
65    mut commands: Commands,
66    mut query: Query<Entity, (Without<Bot>, With<LocalEntity>, With<Player>)>,
67) {
68    for entity in &mut query {
69        commands.entity(entity).insert(Bot::default());
70    }
71}
72
73fn stop_jumping(mut query: Query<(&mut Jumping, &mut Bot)>) {
74    for (mut jumping, mut bot) in &mut query {
75        if bot.jumping_once && **jumping {
76            bot.jumping_once = false;
77            **jumping = false;
78        }
79    }
80}
81
82pub trait BotClientExt {
83    /// Queue a jump for the next tick.
84    fn jump(&mut self);
85    /// Turn the bot's head to look at the coordinate in the world.
86    fn look_at(&mut self, pos: Vec3);
87    /// Get a receiver that will receive a message every tick.
88    fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
89    /// Mine a block. This won't turn the bot's head towards the block, so if
90    /// that's necessary you'll have to do that yourself with [`look_at`].
91    ///
92    /// [`look_at`]: crate::prelude::BotClientExt::look_at
93    fn mine(&mut self, position: BlockPos) -> impl Future<Output = ()> + Send;
94}
95
96impl BotClientExt for azalea_client::Client {
97    fn jump(&mut self) {
98        let mut ecs = self.ecs.lock();
99        ecs.send_event(JumpEvent {
100            entity: self.entity,
101        });
102    }
103
104    fn look_at(&mut self, position: Vec3) {
105        let mut ecs = self.ecs.lock();
106        ecs.send_event(LookAtEvent {
107            entity: self.entity,
108            position,
109        });
110    }
111
112    /// Returns a Receiver that receives a message every game tick.
113    ///
114    /// This is useful if you want to efficiently loop until a certain condition
115    /// is met.
116    ///
117    /// ```
118    /// # use azalea::prelude::*;
119    /// # use azalea::container::WaitingForInventoryOpen;
120    /// # async fn example(bot: &mut azalea::Client) {
121    /// let mut ticks = bot.get_tick_broadcaster();
122    /// while ticks.recv().await.is_ok() {
123    ///     let ecs = bot.ecs.lock();
124    ///     if ecs.get::<WaitingForInventoryOpen>(bot.entity).is_none() {
125    ///         break;
126    ///     }
127    /// }
128    /// # }
129    /// ```
130    fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> {
131        let ecs = self.ecs.lock();
132        let tick_broadcast = ecs.resource::<TickBroadcast>();
133        tick_broadcast.subscribe()
134    }
135
136    async fn mine(&mut self, position: BlockPos) {
137        self.start_mining(position);
138        // vanilla sends an extra swing arm packet when we start mining
139        self.ecs.lock().send_event(SwingArmEvent {
140            entity: self.entity,
141        });
142
143        let mut receiver = self.get_tick_broadcaster();
144        while receiver.recv().await.is_ok() {
145            let ecs = self.ecs.lock();
146            if ecs.get::<Mining>(self.entity).is_none() {
147                break;
148            }
149        }
150    }
151}
152
153/// Event to jump once.
154#[derive(Event)]
155pub struct JumpEvent {
156    pub entity: Entity,
157}
158
159pub fn jump_listener(
160    mut query: Query<(&mut Jumping, &mut Bot)>,
161    mut events: EventReader<JumpEvent>,
162) {
163    for event in events.read() {
164        if let Ok((mut jumping, mut bot)) = query.get_mut(event.entity) {
165            **jumping = true;
166            bot.jumping_once = true;
167        }
168    }
169}
170
171/// Make an entity look towards a certain position in the world.
172#[derive(Event)]
173pub struct LookAtEvent {
174    pub entity: Entity,
175    /// The position we want the entity to be looking at.
176    pub position: Vec3,
177}
178fn look_at_listener(
179    mut events: EventReader<LookAtEvent>,
180    mut query: Query<(&Position, &EyeHeight, &mut LookDirection)>,
181) {
182    for event in events.read() {
183        if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
184            let new_look_direction =
185                direction_looking_at(&position.up(eye_height.into()), &event.position);
186            trace!(
187                "look at {:?} (currently at {:?})",
188                event.position,
189                **position
190            );
191            *look_direction = new_look_direction;
192        }
193    }
194}
195
196/// Return the look direction that would make a client at `current` be
197/// looking at `target`.
198pub fn direction_looking_at(current: &Vec3, target: &Vec3) -> LookDirection {
199    // borrowed from mineflayer's Bot.lookAt because i didn't want to do math
200    let delta = target - current;
201    let y_rot = (PI - f64::atan2(-delta.x, -delta.z)) * (180.0 / PI);
202    let ground_distance = f64::sqrt(delta.x * delta.x + delta.z * delta.z);
203    let x_rot = f64::atan2(delta.y, ground_distance) * -(180.0 / PI);
204
205    // clamp
206    let y_rot = y_rot.rem_euclid(360.0);
207    let x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
208
209    LookDirection {
210        x_rot: x_rot as f32,
211        y_rot: y_rot as f32,
212    }
213}
214
215/// A [`PluginGroup`] for the plugins that add extra bot functionality to the
216/// client.
217pub struct DefaultBotPlugins;
218
219impl PluginGroup for DefaultBotPlugins {
220    fn build(self) -> PluginGroupBuilder {
221        PluginGroupBuilder::start::<Self>()
222            .add(BotPlugin)
223            .add(PathfinderPlugin)
224            .add(ContainerPlugin)
225            .add(AutoRespawnPlugin)
226            .add(AcceptResourcePacksPlugin)
227    }
228}