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