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