Skip to main content

azalea_client/plugins/
movement.rs

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