azalea_client/plugins/
movement.rs

1use std::{backtrace::Backtrace, io};
2
3use azalea_core::{
4    position::{Vec2, Vec3},
5    tick::GameTick,
6};
7use azalea_entity::{
8    Attributes, InLoadedChunk, Jumping, LastSentPosition, LookDirection, Physics, Position,
9    metadata::Sprinting,
10};
11use azalea_physics::{PhysicsSet, ai_step};
12use azalea_protocol::{
13    common::movements::MoveFlags,
14    packets::{
15        Packet,
16        game::{
17            ServerboundPlayerCommand, ServerboundPlayerInput,
18            s_move_player_pos::ServerboundMovePlayerPos,
19            s_move_player_pos_rot::ServerboundMovePlayerPosRot,
20            s_move_player_rot::ServerboundMovePlayerRot,
21            s_move_player_status_only::ServerboundMovePlayerStatusOnly,
22        },
23    },
24};
25use azalea_world::{MinecraftEntityId, MoveEntityError};
26use bevy_app::{App, Plugin, Update};
27use bevy_ecs::prelude::*;
28use thiserror::Error;
29
30use crate::{client::Client, packet::game::SendPacketEvent};
31
32#[derive(Error, Debug)]
33pub enum MovePlayerError {
34    #[error("Player is not in world")]
35    PlayerNotInWorld(Backtrace),
36    #[error("{0}")]
37    Io(#[from] io::Error),
38}
39
40impl From<MoveEntityError> for MovePlayerError {
41    fn from(err: MoveEntityError) -> Self {
42        match err {
43            MoveEntityError::EntityDoesNotExist(backtrace) => {
44                MovePlayerError::PlayerNotInWorld(backtrace)
45            }
46        }
47    }
48}
49
50pub struct MovementPlugin;
51
52impl Plugin for MovementPlugin {
53    fn build(&self, app: &mut App) {
54        app.add_event::<StartWalkEvent>()
55            .add_event::<StartSprintEvent>()
56            .add_event::<KnockbackEvent>()
57            .add_systems(
58                Update,
59                (handle_sprint, handle_walk, handle_knockback)
60                    .chain()
61                    .in_set(MoveEventsSet),
62            )
63            .add_systems(
64                GameTick,
65                (
66                    (tick_controls, local_player_ai_step)
67                        .chain()
68                        .in_set(PhysicsSet)
69                        .before(ai_step)
70                        .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
71                    send_player_input_packet,
72                    send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
73                    send_position.after(PhysicsSet),
74                )
75                    .chain(),
76            );
77    }
78}
79
80#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
81pub struct MoveEventsSet;
82
83impl Client {
84    /// Set whether we're jumping. This acts as if you held space in
85    /// vanilla. If you want to jump once, use the `jump` function.
86    ///
87    /// If you're making a realistic client, calling this function every tick is
88    /// recommended.
89    pub fn set_jumping(&self, jumping: bool) {
90        let mut ecs = self.ecs.lock();
91        let mut jumping_mut = self.query::<&mut Jumping>(&mut ecs);
92        **jumping_mut = jumping;
93    }
94
95    /// Returns whether the player will try to jump next tick.
96    pub fn jumping(&self) -> bool {
97        *self.component::<Jumping>()
98    }
99
100    /// Sets the direction the client is looking. `y_rot` is yaw (looking to the
101    /// side), `x_rot` is pitch (looking up and down). You can get these
102    /// numbers from the vanilla f3 screen.
103    /// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
104    pub fn set_direction(&self, y_rot: f32, x_rot: f32) {
105        let mut ecs = self.ecs.lock();
106        let mut look_direction = self.query::<&mut LookDirection>(&mut ecs);
107
108        (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
109    }
110
111    /// Returns the direction the client is looking. The first value is the y
112    /// rotation (ie. yaw, looking to the side) and the second value is the x
113    /// rotation (ie. pitch, looking up and down).
114    pub fn direction(&self) -> (f32, f32) {
115        let look_direction = self.component::<LookDirection>();
116        (look_direction.y_rot, look_direction.x_rot)
117    }
118}
119
120/// A component that contains the look direction that was last sent over the
121/// network.
122#[derive(Debug, Component, Clone, Default)]
123pub struct LastSentLookDirection {
124    pub x_rot: f32,
125    pub y_rot: f32,
126}
127
128/// Component for entities that can move and sprint. Usually only in
129/// [`LocalEntity`]s.
130///
131/// [`LocalEntity`]: azalea_entity::LocalEntity
132#[derive(Default, Component, Clone)]
133pub struct PhysicsState {
134    /// Minecraft only sends a movement packet either after 20 ticks or if the
135    /// player moved enough. This is that tick counter.
136    pub position_remainder: u32,
137    pub was_sprinting: bool,
138    // Whether we're going to try to start sprinting this tick. Equivalent to
139    // holding down ctrl for a tick.
140    pub trying_to_sprint: bool,
141
142    pub move_direction: WalkDirection,
143    pub move_vector: Vec2,
144}
145
146#[allow(clippy::type_complexity)]
147pub fn send_position(
148    mut query: Query<
149        (
150            Entity,
151            &Position,
152            &LookDirection,
153            &mut PhysicsState,
154            &mut LastSentPosition,
155            &mut Physics,
156            &mut LastSentLookDirection,
157        ),
158        With<InLoadedChunk>,
159    >,
160    mut commands: Commands,
161) {
162    for (
163        entity,
164        position,
165        direction,
166        mut physics_state,
167        mut last_sent_position,
168        mut physics,
169        mut last_direction,
170    ) in query.iter_mut()
171    {
172        let packet = {
173            // TODO: the camera being able to be controlled by other entities isn't
174            // implemented yet if !self.is_controlled_camera() { return };
175
176            let x_delta = position.x - last_sent_position.x;
177            let y_delta = position.y - last_sent_position.y;
178            let z_delta = position.z - last_sent_position.z;
179            let y_rot_delta = (direction.y_rot - last_direction.y_rot) as f64;
180            let x_rot_delta = (direction.x_rot - last_direction.x_rot) as f64;
181
182            physics_state.position_remainder += 1;
183
184            // boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) >
185            // Mth.square(2.0E-4D) || this.positionReminder >= 20;
186            let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
187                > 2.0e-4f64.powi(2))
188                || physics_state.position_remainder >= 20;
189            let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
190
191            // if self.is_passenger() {
192            //   TODO: posrot packet for being a passenger
193            // }
194            let flags = MoveFlags {
195                on_ground: physics.on_ground(),
196                horizontal_collision: physics.horizontal_collision,
197            };
198            let packet = if sending_position && sending_direction {
199                Some(
200                    ServerboundMovePlayerPosRot {
201                        pos: **position,
202                        look_direction: *direction,
203                        flags,
204                    }
205                    .into_variant(),
206                )
207            } else if sending_position {
208                Some(
209                    ServerboundMovePlayerPos {
210                        pos: **position,
211                        flags,
212                    }
213                    .into_variant(),
214                )
215            } else if sending_direction {
216                Some(
217                    ServerboundMovePlayerRot {
218                        look_direction: *direction,
219                        flags,
220                    }
221                    .into_variant(),
222                )
223            } else if physics.last_on_ground() != physics.on_ground() {
224                Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
225            } else {
226                None
227            };
228
229            if sending_position {
230                **last_sent_position = **position;
231                physics_state.position_remainder = 0;
232            }
233            if sending_direction {
234                last_direction.y_rot = direction.y_rot;
235                last_direction.x_rot = direction.x_rot;
236            }
237
238            let on_ground = physics.on_ground();
239            physics.set_last_on_ground(on_ground);
240            // minecraft checks for autojump here, but also autojump is bad so
241
242            packet
243        };
244
245        if let Some(packet) = packet {
246            commands.trigger(SendPacketEvent {
247                sent_by: entity,
248                packet,
249            });
250        }
251    }
252}
253
254#[derive(Debug, Default, Component, Clone, PartialEq, Eq)]
255pub struct LastSentInput(pub ServerboundPlayerInput);
256pub fn send_player_input_packet(
257    mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
258    mut commands: Commands,
259) {
260    for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
261        let dir = physics_state.move_direction;
262        type D = WalkDirection;
263        let input = ServerboundPlayerInput {
264            forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
265            backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
266            left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
267            right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
268            jump: **jumping,
269            // TODO: implement sneaking
270            shift: false,
271            sprint: physics_state.trying_to_sprint,
272        };
273
274        // if LastSentInput isn't present, we default to assuming we're not pressing any
275        // keys and insert it anyways every time it changes
276        let last_sent_input = last_sent_input.cloned().unwrap_or_default();
277
278        if input != last_sent_input.0 {
279            commands.trigger(SendPacketEvent {
280                sent_by: entity,
281                packet: input.clone().into_variant(),
282            });
283            commands.entity(entity).insert(LastSentInput(input));
284        }
285    }
286}
287
288pub fn send_sprinting_if_needed(
289    mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
290    mut commands: Commands,
291) {
292    for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
293        let was_sprinting = physics_state.was_sprinting;
294        if **sprinting != was_sprinting {
295            let sprinting_action = if **sprinting {
296                azalea_protocol::packets::game::s_player_command::Action::StartSprinting
297            } else {
298                azalea_protocol::packets::game::s_player_command::Action::StopSprinting
299            };
300            commands.trigger(SendPacketEvent::new(
301                entity,
302                ServerboundPlayerCommand {
303                    id: *minecraft_entity_id,
304                    action: sprinting_action,
305                    data: 0,
306                },
307            ));
308            physics_state.was_sprinting = **sprinting;
309        }
310    }
311}
312
313/// Updates the [`PhysicsState::move_vector`] based on the
314/// [`PhysicsState::move_direction`].
315pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
316    for mut physics_state in query.iter_mut() {
317        let mut forward_impulse: f32 = 0.;
318        let mut left_impulse: f32 = 0.;
319        let move_direction = physics_state.move_direction;
320        match move_direction {
321            WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
322                forward_impulse += 1.;
323            }
324            WalkDirection::Backward
325            | WalkDirection::BackwardRight
326            | WalkDirection::BackwardLeft => {
327                forward_impulse -= 1.;
328            }
329            _ => {}
330        };
331        match move_direction {
332            WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
333                left_impulse += 1.;
334            }
335            WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
336                left_impulse -= 1.;
337            }
338            _ => {}
339        };
340
341        let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
342        physics_state.move_vector = move_vector;
343    }
344}
345
346/// Makes the bot do one physics tick. Note that this is already handled
347/// automatically by the client.
348pub fn local_player_ai_step(
349    mut query: Query<
350        (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes),
351        With<InLoadedChunk>,
352    >,
353) {
354    for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
355        // server ai step
356
357        // TODO: replace those booleans when using items, passengers, and sneaking are
358        // properly implemented
359        let move_vector = modify_input(physics_state.move_vector, false, false, false, &attributes);
360        physics.x_acceleration = move_vector.x;
361        physics.z_acceleration = move_vector.y;
362
363        // TODO: food data and abilities
364        // let has_enough_food_to_sprint = self.food_data().food_level ||
365        // self.abilities().may_fly;
366        let has_enough_food_to_sprint = true;
367
368        // TODO: double tapping w to sprint i think
369
370        let trying_to_sprint = physics_state.trying_to_sprint;
371
372        if !**sprinting
373            && (
374                // !self.is_in_water()
375                // || self.is_underwater() &&
376                has_enough_impulse_to_start_sprinting(physics_state)
377                    && has_enough_food_to_sprint
378                    // && !self.using_item()
379                    // && !self.has_effect(MobEffects.BLINDNESS)
380                    && trying_to_sprint
381            )
382        {
383            set_sprinting(true, &mut sprinting, &mut attributes);
384        }
385    }
386}
387
388// LocalPlayer.modifyInput
389fn modify_input(
390    mut move_vector: Vec2,
391    is_using_item: bool,
392    is_passenger: bool,
393    moving_slowly: bool,
394    attributes: &Attributes,
395) -> Vec2 {
396    if move_vector.length_squared() == 0. {
397        return move_vector;
398    }
399
400    move_vector *= 0.98;
401    if is_using_item && !is_passenger {
402        move_vector *= 0.2;
403    }
404
405    if moving_slowly {
406        let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
407        move_vector *= sneaking_speed;
408    }
409
410    modify_input_speed_for_square_movement(move_vector)
411}
412fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
413    let length = move_vector.length();
414    if length == 0. {
415        return move_vector;
416    }
417    let scaled_to_inverse_length = move_vector * (1. / length);
418    let dist = distance_to_unit_square(scaled_to_inverse_length);
419    let scale = (length * dist).min(1.);
420    scaled_to_inverse_length * scale
421}
422fn distance_to_unit_square(v: Vec2) -> f32 {
423    let x = v.x.abs();
424    let y = v.y.abs();
425    let ratio = if y > x { x / y } else { y / x };
426    (1.0 + ratio * ratio).sqrt()
427}
428
429impl Client {
430    /// Start walking in the given direction. To sprint, use
431    /// [`Client::sprint`]. To stop walking, call walk with
432    /// `WalkDirection::None`.
433    ///
434    /// # Examples
435    ///
436    /// Walk for 1 second
437    /// ```rust,no_run
438    /// # use azalea_client::{Client, WalkDirection};
439    /// # use std::time::Duration;
440    /// # async fn example(mut bot: Client) {
441    /// bot.walk(WalkDirection::Forward);
442    /// tokio::time::sleep(Duration::from_secs(1)).await;
443    /// bot.walk(WalkDirection::None);
444    /// # }
445    /// ```
446    pub fn walk(&self, direction: WalkDirection) {
447        let mut ecs = self.ecs.lock();
448        ecs.send_event(StartWalkEvent {
449            entity: self.entity,
450            direction,
451        });
452    }
453
454    /// Start sprinting in the given direction. To stop moving, call
455    /// [`Client::walk(WalkDirection::None)`]
456    ///
457    /// # Examples
458    ///
459    /// Sprint for 1 second
460    /// ```rust,no_run
461    /// # use azalea_client::{Client, WalkDirection, SprintDirection};
462    /// # use std::time::Duration;
463    /// # async fn example(mut bot: Client) {
464    /// bot.sprint(SprintDirection::Forward);
465    /// tokio::time::sleep(Duration::from_secs(1)).await;
466    /// bot.walk(WalkDirection::None);
467    /// # }
468    /// ```
469    pub fn sprint(&self, direction: SprintDirection) {
470        let mut ecs = self.ecs.lock();
471        ecs.send_event(StartSprintEvent {
472            entity: self.entity,
473            direction,
474        });
475    }
476}
477
478/// An event sent when the client starts walking. This does not get sent for
479/// non-local entities.
480///
481/// To stop walking or sprinting, send this event with `WalkDirection::None`.
482#[derive(Event, Debug)]
483pub struct StartWalkEvent {
484    pub entity: Entity,
485    pub direction: WalkDirection,
486}
487
488/// The system that makes the player start walking when they receive a
489/// [`StartWalkEvent`].
490pub fn handle_walk(
491    mut events: EventReader<StartWalkEvent>,
492    mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
493) {
494    for event in events.read() {
495        if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
496        {
497            physics_state.move_direction = event.direction;
498            physics_state.trying_to_sprint = false;
499            set_sprinting(false, &mut sprinting, &mut attributes);
500        }
501    }
502}
503
504/// An event sent when the client starts sprinting. This does not get sent for
505/// non-local entities.
506#[derive(Event)]
507pub struct StartSprintEvent {
508    pub entity: Entity,
509    pub direction: SprintDirection,
510}
511/// The system that makes the player start sprinting when they receive a
512/// [`StartSprintEvent`].
513pub fn handle_sprint(
514    mut query: Query<&mut PhysicsState>,
515    mut events: EventReader<StartSprintEvent>,
516) {
517    for event in events.read() {
518        if let Ok(mut physics_state) = query.get_mut(event.entity) {
519            physics_state.move_direction = WalkDirection::from(event.direction);
520            physics_state.trying_to_sprint = true;
521        }
522    }
523}
524
525/// Change whether we're sprinting by adding an attribute modifier to the
526/// player. You should use the [`walk`] and [`sprint`] methods instead.
527/// Returns if the operation was successful.
528fn set_sprinting(
529    sprinting: bool,
530    currently_sprinting: &mut Sprinting,
531    attributes: &mut Attributes,
532) -> bool {
533    **currently_sprinting = sprinting;
534    if sprinting {
535        attributes
536            .speed
537            .try_insert(azalea_entity::attributes::sprinting_modifier())
538            .is_ok()
539    } else {
540        attributes
541            .speed
542            .remove(&azalea_entity::attributes::sprinting_modifier().id)
543            .is_none()
544    }
545}
546
547// Whether the player is moving fast enough to be able to start sprinting.
548fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
549    // if self.underwater() {
550    //     self.has_forward_impulse()
551    // } else {
552    physics_state.move_vector.y > 0.8
553    // }
554}
555
556/// An event sent by the server that sets or adds to our velocity. Usually
557/// `KnockbackKind::Set` is used for normal knockback and `KnockbackKind::Add`
558/// is used for explosions, but some servers (notably Hypixel) use explosions
559/// for knockback.
560#[derive(Event)]
561pub struct KnockbackEvent {
562    pub entity: Entity,
563    pub knockback: KnockbackType,
564}
565
566pub enum KnockbackType {
567    Set(Vec3),
568    Add(Vec3),
569}
570
571pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader<KnockbackEvent>) {
572    for event in events.read() {
573        if let Ok(mut physics) = query.get_mut(event.entity) {
574            match event.knockback {
575                KnockbackType::Set(velocity) => {
576                    physics.velocity = velocity;
577                }
578                KnockbackType::Add(velocity) => {
579                    physics.velocity += velocity;
580                }
581            }
582        }
583    }
584}
585
586#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
587pub enum WalkDirection {
588    #[default]
589    None,
590    Forward,
591    Backward,
592    Left,
593    Right,
594    ForwardRight,
595    ForwardLeft,
596    BackwardRight,
597    BackwardLeft,
598}
599
600/// The directions that we can sprint in. It's a subset of [`WalkDirection`].
601#[derive(Clone, Copy, Debug)]
602pub enum SprintDirection {
603    Forward,
604    ForwardRight,
605    ForwardLeft,
606}
607
608impl From<SprintDirection> for WalkDirection {
609    fn from(d: SprintDirection) -> Self {
610        match d {
611            SprintDirection::Forward => WalkDirection::Forward,
612            SprintDirection::ForwardRight => WalkDirection::ForwardRight,
613            SprintDirection::ForwardLeft => WalkDirection::ForwardLeft,
614        }
615    }
616}