azalea_client/plugins/
movement.rs

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