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    collision::entity_collisions::{AabbQuery, CollidableEntityQuery, update_last_bounding_box},
17    local_player::{PhysicsState, SprintDirection, WalkDirection},
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 PhysicsState,
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<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
200    mut commands: Commands,
201) {
202    for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
203        let dir = physics_state.move_direction;
204        type D = WalkDirection;
205        let input = ServerboundPlayerInput {
206            forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
207            backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
208            left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
209            right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
210            jump: **jumping,
211            shift: physics_state.trying_to_crouch,
212            sprint: physics_state.trying_to_sprint,
213        };
214
215        // if LastSentInput isn't present, we default to assuming we're not pressing any
216        // keys and insert it anyways every time it changes
217        let last_sent_input = last_sent_input.cloned().unwrap_or_default();
218
219        if input != last_sent_input.0 {
220            commands.trigger(SendGamePacketEvent {
221                sent_by: entity,
222                packet: input.clone().into_variant(),
223            });
224            commands.entity(entity).insert(LastSentInput(input));
225        }
226    }
227}
228
229pub fn send_sprinting_if_needed(
230    mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
231    mut commands: Commands,
232) {
233    for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
234        let was_sprinting = physics_state.was_sprinting;
235        if **sprinting != was_sprinting {
236            let sprinting_action = if **sprinting {
237                s_player_command::Action::StartSprinting
238            } else {
239                s_player_command::Action::StopSprinting
240            };
241            commands.trigger(SendGamePacketEvent::new(
242                entity,
243                ServerboundPlayerCommand {
244                    id: *minecraft_entity_id,
245                    action: sprinting_action,
246                    data: 0,
247                },
248            ));
249            physics_state.was_sprinting = **sprinting;
250        }
251    }
252}
253
254/// Updates the [`PhysicsState::move_vector`] based on the
255/// [`PhysicsState::move_direction`].
256pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
257    for mut physics_state in query.iter_mut() {
258        let mut forward_impulse: f32 = 0.;
259        let mut left_impulse: f32 = 0.;
260        let move_direction = physics_state.move_direction;
261        match move_direction {
262            WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
263                forward_impulse += 1.;
264            }
265            WalkDirection::Backward
266            | WalkDirection::BackwardRight
267            | WalkDirection::BackwardLeft => {
268                forward_impulse -= 1.;
269            }
270            _ => {}
271        };
272        match move_direction {
273            WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
274                left_impulse += 1.;
275            }
276            WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
277                left_impulse -= 1.;
278            }
279            _ => {}
280        };
281
282        let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
283        physics_state.move_vector = move_vector;
284    }
285}
286
287/// Makes the bot do one physics tick.
288///
289/// This is handled automatically by the client.
290#[allow(clippy::type_complexity)]
291pub fn local_player_ai_step(
292    mut query: Query<
293        (
294            Entity,
295            &PhysicsState,
296            &PlayerAbilities,
297            &metadata::Swimming,
298            &metadata::SleepingPos,
299            &WorldHolder,
300            &Position,
301            Option<&Hunger>,
302            Option<&LastSentInput>,
303            &mut Physics,
304            &mut Sprinting,
305            &mut Crouching,
306            &mut Attributes,
307        ),
308        (With<HasClientLoaded>, With<LocalEntity>),
309    >,
310    aabb_query: AabbQuery,
311    collidable_entity_query: CollidableEntityQuery,
312) {
313    for (
314        entity,
315        physics_state,
316        abilities,
317        swimming,
318        sleeping_pos,
319        world_holder,
320        position,
321        hunger,
322        last_sent_input,
323        mut physics,
324        mut sprinting,
325        mut crouching,
326        mut attributes,
327    ) in query.iter_mut()
328    {
329        // server ai step
330
331        let is_swimming = **swimming;
332        // TODO: implement passengers
333        let is_passenger = false;
334        let is_sleeping = sleeping_pos.is_some();
335
336        let world = world_holder.shared.read();
337        let ctx = CanPlayerFitCtx {
338            world: &world,
339            entity,
340            position: *position,
341            aabb_query: &aabb_query,
342            collidable_entity_query: &collidable_entity_query,
343            physics: &physics,
344        };
345
346        let new_crouching = !abilities.flying
347            && !is_swimming
348            && !is_passenger
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            && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching);
353        if **crouching != new_crouching {
354            **crouching = new_crouching;
355        }
356
357        // TODO: food data and abilities
358        // let has_enough_food_to_sprint = self.food_data().food_level ||
359        // self.abilities().may_fly;
360        let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
361
362        // TODO: double tapping w to sprint i think
363
364        let trying_to_sprint = physics_state.trying_to_sprint;
365
366        // TODO: swimming
367        let is_underwater = false;
368        let is_in_water = physics.is_in_water();
369        // TODO: elytra
370        let is_fall_flying = false;
371        // TODO: passenger
372        let is_passenger = false;
373        // TODO: using items
374        let using_item = false;
375        // TODO: status effects
376        let has_blindness = false;
377
378        let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
379
380        // LocalPlayer.canStartSprinting
381        let can_start_sprinting = !**sprinting
382            && has_enough_impulse
383            && has_enough_food_to_sprint
384            && !using_item
385            && !has_blindness
386            && (!is_passenger || is_underwater)
387            && (!is_fall_flying || is_underwater)
388            && (!is_moving_slowly(&crouching) || is_underwater)
389            && (!is_in_water || is_underwater);
390        if trying_to_sprint && can_start_sprinting {
391            set_sprinting(true, &mut sprinting, &mut attributes);
392        }
393
394        if **sprinting {
395            // TODO: swimming
396
397            let vehicle_can_sprint = false;
398            // shouldStopRunSprinting
399            let should_stop_sprinting = has_blindness
400                || (is_passenger && !vehicle_can_sprint)
401                || !has_enough_impulse
402                || !has_enough_food_to_sprint
403                || (physics.horizontal_collision && !physics.minor_horizontal_collision)
404                || (is_in_water && !is_underwater);
405            if should_stop_sprinting {
406                set_sprinting(false, &mut sprinting, &mut attributes);
407            }
408        }
409
410        // TODO: replace those booleans when using items and passengers are properly
411        // implemented
412        let move_vector = modify_input(
413            physics_state.move_vector,
414            false,
415            false,
416            **crouching,
417            &attributes,
418        );
419        physics.x_acceleration = move_vector.x;
420        physics.z_acceleration = move_vector.y;
421    }
422}
423
424fn is_moving_slowly(crouching: &Crouching) -> bool {
425    **crouching
426}
427
428// LocalPlayer.modifyInput
429fn modify_input(
430    mut move_vector: Vec2,
431    is_using_item: bool,
432    is_passenger: bool,
433    moving_slowly: bool,
434    attributes: &Attributes,
435) -> Vec2 {
436    if move_vector.length_squared() == 0. {
437        return move_vector;
438    }
439
440    move_vector *= 0.98;
441    if is_using_item && !is_passenger {
442        move_vector *= 0.2;
443    }
444
445    if moving_slowly {
446        let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
447        move_vector *= sneaking_speed;
448    }
449
450    modify_input_speed_for_square_movement(move_vector)
451}
452fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
453    let length = move_vector.length();
454    if length == 0. {
455        return move_vector;
456    }
457    let scaled_to_inverse_length = move_vector * (1. / length);
458    let dist = distance_to_unit_square(scaled_to_inverse_length);
459    let scale = (length * dist).min(1.);
460    scaled_to_inverse_length * scale
461}
462fn distance_to_unit_square(v: Vec2) -> f32 {
463    let x = v.x.abs();
464    let y = v.y.abs();
465    let ratio = if y > x { x / y } else { y / x };
466    (1. + ratio * ratio).sqrt()
467}
468
469/// An event sent when the client starts walking.
470///
471/// This does not get sent for non-local entities.
472///
473/// To stop walking or sprinting, send this event with `WalkDirection::None`.
474#[derive(Debug, Message)]
475pub struct StartWalkEvent {
476    pub entity: Entity,
477    pub direction: WalkDirection,
478}
479
480/// The system that makes the player start walking when they receive a
481/// [`StartWalkEvent`].
482pub fn handle_walk(
483    mut events: MessageReader<StartWalkEvent>,
484    mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
485) {
486    for event in events.read() {
487        if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
488        {
489            physics_state.move_direction = event.direction;
490            physics_state.trying_to_sprint = false;
491            set_sprinting(false, &mut sprinting, &mut attributes);
492        }
493    }
494}
495
496/// An event sent when the client starts sprinting.
497///
498/// This does not get sent for non-local entities.
499#[derive(Message)]
500pub struct StartSprintEvent {
501    pub entity: Entity,
502    pub direction: SprintDirection,
503}
504/// The system that makes the player start sprinting when they receive a
505/// [`StartSprintEvent`].
506pub fn handle_sprint(
507    mut query: Query<&mut PhysicsState>,
508    mut events: MessageReader<StartSprintEvent>,
509) {
510    for event in events.read() {
511        if let Ok(mut physics_state) = query.get_mut(event.entity) {
512            physics_state.move_direction = WalkDirection::from(event.direction);
513            physics_state.trying_to_sprint = true;
514        }
515    }
516}
517
518/// Change whether we're sprinting by adding an attribute modifier to the
519/// player.
520///
521/// You should use the [`Client::walk`] and [`Client::sprint`] functions
522/// instead.
523///
524/// Returns true if the operation was successful.
525fn set_sprinting(
526    sprinting: bool,
527    currently_sprinting: &mut Sprinting,
528    attributes: &mut Attributes,
529) -> bool {
530    **currently_sprinting = sprinting;
531    if sprinting {
532        attributes
533            .movement_speed
534            .try_insert(azalea_entity::attributes::sprinting_modifier())
535            .is_ok()
536    } else {
537        attributes
538            .movement_speed
539            .remove(&azalea_entity::attributes::sprinting_modifier().id)
540            .is_none()
541    }
542}
543
544// Whether the player is moving fast enough to be able to start sprinting.
545fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
546    // if self.underwater() {
547    //     self.has_forward_impulse()
548    // } else {
549    physics_state.move_vector.y > 0.8
550    // }
551}
552
553/// An event sent by the server that sets or adds to our velocity.
554///
555/// Usually `KnockbackKind::Set` is used for normal knockback and
556/// `KnockbackKind::Add` is used for explosions, but some servers (notably
557/// Hypixel) use explosions for knockback.
558#[derive(EntityEvent, Debug, Clone)]
559pub struct KnockbackEvent {
560    pub entity: Entity,
561    pub data: KnockbackData,
562}
563
564#[derive(Debug, Clone)]
565pub enum KnockbackData {
566    Set(Vec3),
567    Add(Vec3),
568}
569
570pub fn handle_knockback(knockback: On<KnockbackEvent>, mut query: Query<&mut Physics>) {
571    if let Ok(mut physics) = query.get_mut(knockback.entity) {
572        match knockback.data {
573            KnockbackData::Set(velocity) => {
574                physics.velocity = velocity;
575            }
576            KnockbackData::Add(velocity) => {
577                physics.velocity += velocity;
578            }
579        }
580    }
581}
582
583pub fn update_pose(
584    mut query: Query<(
585        Entity,
586        &mut Pose,
587        &Physics,
588        &PhysicsState,
589        &LocalGameMode,
590        &WorldHolder,
591        &Position,
592    )>,
593    aabb_query: AabbQuery,
594    collidable_entity_query: CollidableEntityQuery,
595) {
596    for (entity, mut pose, physics, physics_state, game_mode, world_holder, position) in
597        query.iter_mut()
598    {
599        let world = world_holder.shared.read();
600        let world = &*world;
601        let ctx = CanPlayerFitCtx {
602            world,
603            entity,
604            position: *position,
605            aabb_query: &aabb_query,
606            collidable_entity_query: &collidable_entity_query,
607            physics,
608        };
609
610        if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
611            continue;
612        }
613
614        // TODO: implement everything else from getDesiredPose: sleeping, swimming,
615        // fallFlying, spinAttack
616        let desired_pose = if physics_state.trying_to_crouch {
617            Pose::Crouching
618        } else {
619            Pose::Standing
620        };
621
622        // TODO: passengers
623        let is_passenger = false;
624
625        // canPlayerFitWithinBlocksAndEntitiesWhen
626        let new_pose = if game_mode.current == GameMode::Spectator
627            || is_passenger
628            || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
629        {
630            desired_pose
631        } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
632            Pose::Crouching
633        } else {
634            Pose::Swimming
635        };
636
637        // avoid triggering change detection
638        if new_pose != *pose {
639            *pose = new_pose;
640        }
641    }
642}
643
644struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
645    world: &'a World,
646    entity: Entity,
647    position: Position,
648    aabb_query: &'a AabbQuery<'world, 'state, 'b>,
649    collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
650    physics: &'a Physics,
651}
652fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
653    no_collision(
654        ctx.world,
655        Some(ctx.entity),
656        ctx.aabb_query,
657        ctx.collidable_entity_query,
658        ctx.physics,
659        &calculate_dimensions(EntityKind::Player, pose)
660            .make_bounding_box(*ctx.position)
661            .deflate_all(1.0e-7),
662        false,
663    )
664}