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#[derive(Default, Component)]
65pub struct Bot {
66 jumping_once: bool,
67}
68
69#[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 fn jump(&self);
92 fn look_at(&self, pos: Vec3);
94 fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
96 fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>;
98 fn wait_ticks(&self, n: usize) -> impl Future<Output = ()> + Send;
100 fn wait_updates(&self, n: usize) -> impl Future<Output = ()> + Send;
102 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 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 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 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 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#[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#[derive(Message)]
225pub struct LookAtEvent {
226 pub entity: Entity,
227 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
245pub fn direction_looking_at(current: Vec3, target: Vec3) -> LookDirection {
248 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
257pub 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}