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 EyeHeight, Jumping, LocalEntity, LookDirection, Position, clamp_look_direction,
13 metadata::Player,
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.before(clamp_look_direction),
47 jump_listener,
48 ),
49 )
50 .add_systems(
51 GameTick,
52 stop_jumping
53 .after(PhysicsSet)
54 .after(azalea_client::movement::send_player_input_packet),
55 );
56 }
57}
58
59#[derive(Default, Component)]
62pub struct Bot {
63 jumping_once: bool,
64}
65
66#[allow(clippy::type_complexity)]
68fn insert_bot(
69 mut commands: Commands,
70 mut query: Query<Entity, (Without<Bot>, With<LocalEntity>, With<Player>)>,
71) {
72 for entity in &mut query {
73 commands.entity(entity).insert(Bot::default());
74 }
75}
76
77fn stop_jumping(mut query: Query<(&mut Jumping, &mut Bot)>) {
78 for (mut jumping, mut bot) in &mut query {
79 if bot.jumping_once && **jumping {
80 bot.jumping_once = false;
81 **jumping = false;
82 }
83 }
84}
85
86pub trait BotClientExt {
87 fn jump(&self);
89 fn look_at(&self, pos: Vec3);
91 fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
93 fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
95 fn wait_ticks(&self, n: usize) -> impl Future<Output = ()> + Send;
97 fn wait_updates(&self, n: usize) -> impl Future<Output = ()> + Send;
99 fn mine(&self, position: BlockPos) -> impl Future<Output = ()> + Send;
104}
105
106impl BotClientExt for azalea_client::Client {
107 fn jump(&self) {
108 let mut ecs = self.ecs.lock();
109 ecs.send_event(JumpEvent {
110 entity: self.entity,
111 });
112 }
113
114 fn look_at(&self, position: Vec3) {
115 let mut ecs = self.ecs.lock();
116 ecs.send_event(LookAtEvent {
117 entity: self.entity,
118 position,
119 });
120 }
121
122 fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> {
141 let ecs = self.ecs.lock();
142 let tick_broadcast = ecs.resource::<TickBroadcast>();
143 tick_broadcast.subscribe()
144 }
145
146 fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> {
154 let ecs = self.ecs.lock();
155 let update_broadcast = ecs.resource::<UpdateBroadcast>();
156 update_broadcast.subscribe()
157 }
158
159 async fn wait_ticks(&self, n: usize) {
166 let mut receiver = self.get_tick_broadcaster();
167 for _ in 0..n {
168 let _ = receiver.recv().await;
169 }
170 }
171 async fn wait_updates(&self, n: usize) {
181 let mut receiver = self.get_update_broadcaster();
182 for _ in 0..n {
183 let _ = receiver.recv().await;
184 }
185 }
186
187 async fn mine(&self, position: BlockPos) {
188 self.start_mining(position);
189
190 let mut receiver = self.get_tick_broadcaster();
191 while receiver.recv().await.is_ok() {
192 let ecs = self.ecs.lock();
193 if ecs.get::<Mining>(self.entity).is_none() {
194 break;
195 }
196 }
197 }
198}
199
200#[derive(Event)]
202pub struct JumpEvent {
203 pub entity: Entity,
204}
205
206pub fn jump_listener(
207 mut query: Query<(&mut Jumping, &mut Bot)>,
208 mut events: EventReader<JumpEvent>,
209) {
210 for event in events.read() {
211 if let Ok((mut jumping, mut bot)) = query.get_mut(event.entity) {
212 **jumping = true;
213 bot.jumping_once = true;
214 }
215 }
216}
217
218#[derive(Event)]
220pub struct LookAtEvent {
221 pub entity: Entity,
222 pub position: Vec3,
224}
225fn look_at_listener(
226 mut events: EventReader<LookAtEvent>,
227 mut query: Query<(&Position, &EyeHeight, &mut LookDirection)>,
228) {
229 for event in events.read() {
230 if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
231 let new_look_direction =
232 direction_looking_at(position.up(eye_height.into()), event.position);
233 trace!("look at {} (currently at {})", event.position, **position);
234 *look_direction = new_look_direction;
235 }
236 }
237}
238
239pub fn direction_looking_at(current: Vec3, target: Vec3) -> LookDirection {
242 let delta = target - current;
244 let y_rot = (PI - f64::atan2(-delta.x, -delta.z)) * (180.0 / PI);
245 let ground_distance = f64::sqrt(delta.x * delta.x + delta.z * delta.z);
246 let x_rot = f64::atan2(delta.y, ground_distance) * -(180.0 / PI);
247
248 let y_rot = y_rot.rem_euclid(360.0);
250 let x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
251
252 LookDirection {
253 x_rot: x_rot as f32,
254 y_rot: y_rot as f32,
255 }
256}
257
258pub struct DefaultBotPlugins;
261
262impl PluginGroup for DefaultBotPlugins {
263 fn build(self) -> PluginGroupBuilder {
264 PluginGroupBuilder::start::<Self>()
265 .add(BotPlugin)
266 .add(PathfinderPlugin)
267 .add(ContainerPlugin)
268 .add(AutoRespawnPlugin)
269 .add(AcceptResourcePacksPlugin)
270 }
271}