1use azalea_core::{
2 entity_id::MinecraftEntityId,
3 game_type::GameMode,
4 position::{Vec2, Vec3},
5 tick::GameTick,
6};
7use azalea_entity::{
8 ActiveEffects, Attributes, Crouching, EntityGeometryUpdateSystems, HasClientLoaded, Jumping,
9 LastSentPosition, LocalEntity, LookDirection, OnClimbable, Physics, PlayerAbilities, Pose,
10 Position,
11 dimensions::calculate_dimensions,
12 inventory::Inventory,
13 metadata::{self, FallFlying, Sprinting},
14 update_bounding_box,
15};
16use azalea_inventory::{
17 ItemStack,
18 components::{self, EquipmentSlot},
19};
20use azalea_physics::{
21 PhysicsSystems, ai_step,
22 client_movement::{ClientMovementState, SprintDirection, WalkDirection},
23 collision::entity_collisions::{AabbQuery, CollidableEntityQuery, update_last_bounding_box},
24 travel::{no_collision, travel},
25};
26use azalea_protocol::{
27 common::movements::MoveFlags,
28 packets::{
29 Packet,
30 game::{
31 ServerboundPlayerCommand, ServerboundPlayerInput,
32 s_move_player_pos::ServerboundMovePlayerPos,
33 s_move_player_pos_rot::ServerboundMovePlayerPosRot,
34 s_move_player_rot::ServerboundMovePlayerRot,
35 s_move_player_status_only::ServerboundMovePlayerStatusOnly, s_player_command,
36 },
37 },
38};
39use azalea_registry::builtin::{EntityKind, MobEffect};
40use azalea_world::World;
41use bevy_app::{App, Plugin, Update};
42use bevy_ecs::prelude::*;
43
44use crate::{
45 local_player::{Hunger, WorldHolder},
46 packet::game::SendGamePacketEvent,
47};
48
49pub struct MovementPlugin;
50
51impl Plugin for MovementPlugin {
52 fn build(&self, app: &mut App) {
53 app.add_message::<StartWalkEvent>()
54 .add_message::<StartSprintEvent>()
55 .add_systems(
56 Update,
57 (handle_sprint, handle_walk)
58 .chain()
59 .in_set(MoveEventsSystems)
60 .after(update_bounding_box)
61 .after(update_last_bounding_box),
62 )
63 .add_systems(
64 GameTick,
65 (
66 (
67 tick_controls,
68 local_player_ai_step,
69 process_fall_flying_activation,
70 )
71 .chain()
72 .in_set(PhysicsSystems)
73 .before(ai_step)
74 .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
75 send_player_input_packet,
76 update_pose.before(EntityGeometryUpdateSystems),
77 send_sprinting_if_needed
78 .after(azalea_entity::update_in_loaded_chunk)
79 .after(travel)
80 .after(EntityGeometryUpdateSystems),
81 send_position,
82 )
83 .chain(),
84 )
85 .add_observer(handle_knockback);
86 }
87}
88
89#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
90pub struct MoveEventsSystems;
91
92#[derive(Clone, Component, Debug, Default)]
95pub struct LastSentLookDirection {
96 pub x_rot: f32,
97 pub y_rot: f32,
98}
99
100#[allow(clippy::type_complexity)]
101pub fn send_position(
102 mut query: Query<
103 (
104 Entity,
105 &Position,
106 &LookDirection,
107 &mut ClientMovementState,
108 &mut LastSentPosition,
109 &mut Physics,
110 &mut LastSentLookDirection,
111 ),
112 With<HasClientLoaded>,
113 >,
114 mut commands: Commands,
115) {
116 for (
117 entity,
118 position,
119 direction,
120 mut physics_state,
121 mut last_sent_position,
122 mut physics,
123 mut last_direction,
124 ) in query.iter_mut()
125 {
126 let packet = {
127 let x_delta = position.x - last_sent_position.x;
131 let y_delta = position.y - last_sent_position.y;
132 let z_delta = position.z - last_sent_position.z;
133 let y_rot_delta = (direction.y_rot() - last_direction.y_rot) as f64;
134 let x_rot_delta = (direction.x_rot() - last_direction.x_rot) as f64;
135
136 physics_state.position_remainder += 1;
137
138 let is_delta_large_enough =
141 (x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) > 2.0e-4f64.powi(2);
142 let sending_position = is_delta_large_enough || physics_state.position_remainder >= 20;
143 let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
144
145 let flags = MoveFlags {
149 on_ground: physics.on_ground(),
150 horizontal_collision: physics.horizontal_collision,
151 };
152 let packet = if sending_position && sending_direction {
153 Some(
154 ServerboundMovePlayerPosRot {
155 pos: **position,
156 look_direction: *direction,
157 flags,
158 }
159 .into_variant(),
160 )
161 } else if sending_position {
162 Some(
163 ServerboundMovePlayerPos {
164 pos: **position,
165 flags,
166 }
167 .into_variant(),
168 )
169 } else if sending_direction {
170 Some(
171 ServerboundMovePlayerRot {
172 look_direction: *direction,
173 flags,
174 }
175 .into_variant(),
176 )
177 } else if physics.last_on_ground() != physics.on_ground() {
178 Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
179 } else {
180 None
181 };
182
183 if sending_position {
184 **last_sent_position = **position;
185 physics_state.position_remainder = 0;
186 }
187 if sending_direction {
188 last_direction.y_rot = direction.y_rot();
189 last_direction.x_rot = direction.x_rot();
190 }
191
192 let on_ground = physics.on_ground();
193 physics.set_last_on_ground(on_ground);
194 packet
197 };
198
199 if let Some(packet) = packet {
200 commands.trigger(SendGamePacketEvent {
201 sent_by: entity,
202 packet,
203 });
204 }
205 }
206}
207
208#[derive(Clone, Component, Debug, Default, Eq, PartialEq)]
209pub struct LastSentInput(pub ServerboundPlayerInput);
210pub fn send_player_input_packet(
211 mut query: Query<(
212 Entity,
213 &ClientMovementState,
214 &Jumping,
215 Option<&LastSentInput>,
216 )>,
217 mut commands: Commands,
218) {
219 for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
220 let dir = physics_state.move_direction;
221 let input = ServerboundPlayerInput {
222 forward: dir.forward(),
223 backward: dir.backward(),
224 left: dir.left(),
225 right: dir.right(),
226 jump: **jumping,
227 shift: physics_state.trying_to_crouch,
228 sprint: physics_state.trying_to_sprint,
229 };
230
231 let last_sent_input = last_sent_input.cloned().unwrap_or_default();
234
235 if input != last_sent_input.0 {
236 commands.trigger(SendGamePacketEvent {
237 sent_by: entity,
238 packet: input.clone().into_variant(),
239 });
240 commands.entity(entity).insert(LastSentInput(input));
241 }
242 }
243}
244
245pub fn send_sprinting_if_needed(
246 mut query: Query<(
247 Entity,
248 &MinecraftEntityId,
249 &Sprinting,
250 &mut ClientMovementState,
251 )>,
252 mut commands: Commands,
253) {
254 for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
255 let was_sprinting = physics_state.was_sprinting;
256 if **sprinting != was_sprinting {
257 let sprinting_action = if **sprinting {
258 s_player_command::Action::StartSprinting
259 } else {
260 s_player_command::Action::StopSprinting
261 };
262 commands.trigger(SendGamePacketEvent::new(
263 entity,
264 ServerboundPlayerCommand {
265 id: *minecraft_entity_id,
266 action: sprinting_action,
267 data: 0,
268 },
269 ));
270 physics_state.was_sprinting = **sprinting;
271 }
272 }
273}
274
275pub(crate) fn tick_controls(mut query: Query<&mut ClientMovementState>) {
278 for mut physics_state in query.iter_mut() {
279 let mut forward_impulse: f32 = 0.;
280 let mut left_impulse: f32 = 0.;
281 let move_direction = physics_state.move_direction;
282
283 if move_direction.forward() {
284 forward_impulse += 1.;
285 } else if move_direction.backward() {
286 forward_impulse -= 1.;
287 }
288
289 if move_direction.left() {
290 left_impulse += 1.;
291 } else if move_direction.right() {
292 left_impulse -= 1.;
293 }
294
295 let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
296 physics_state.move_vector = move_vector;
297 }
298}
299
300#[allow(clippy::type_complexity)]
304pub fn local_player_ai_step(
305 mut query: Query<
306 (
307 Entity,
308 &ClientMovementState,
309 &PlayerAbilities,
310 &metadata::Swimming,
311 &metadata::SleepingPos,
312 &WorldHolder,
313 &Position,
314 Option<&Hunger>,
315 Option<&LastSentInput>,
316 &FallFlying,
317 &Pose,
318 &mut Physics,
319 &mut Sprinting,
320 &mut Crouching,
321 &mut Attributes,
322 ),
323 (With<HasClientLoaded>, With<LocalEntity>),
324 >,
325 aabb_query: AabbQuery,
326 collidable_entity_query: CollidableEntityQuery,
327) {
328 for (
329 entity,
330 physics_state,
331 abilities,
332 swimming,
333 sleeping_pos,
334 world_holder,
335 position,
336 hunger,
337 last_sent_input,
338 fall_flying,
339 pose,
340 mut physics,
341 mut sprinting,
342 mut crouching,
343 mut attributes,
344 ) in query.iter_mut()
345 {
346 let is_swimming = **swimming;
349 let is_passenger = false;
351 let is_sleeping = sleeping_pos.is_some();
352
353 let world = world_holder.shared.read();
354 let ctx = CanPlayerFitCtx {
355 world: &world,
356 entity,
357 position: *position,
358 aabb_query: &aabb_query,
359 collidable_entity_query: &collidable_entity_query,
360 physics: &physics,
361 };
362
363 let new_crouching = !abilities.flying
364 && !is_swimming
365 && !is_passenger
366 && (last_sent_input.is_some_and(|i| i.0.shift)
367 || !is_sleeping
368 && !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing))
369 && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching);
370 if **crouching != new_crouching {
371 **crouching = new_crouching;
372 }
373
374 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
378
379 let trying_to_sprint = physics_state.trying_to_sprint;
382
383 let is_underwater = false;
385 let is_in_water = physics.is_in_water();
386
387 let is_fall_flying = **fall_flying;
388 let is_passenger = false;
390 let using_item = false;
392 let has_blindness = false;
394
395 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
396
397 let can_start_sprinting = !**sprinting
399 && has_enough_impulse
400 && has_enough_food_to_sprint
401 && !using_item
402 && !has_blindness
403 && (!is_passenger || is_underwater)
404 && (!is_fall_flying || is_underwater)
405 && (!is_moving_slowly(&crouching, fall_flying, pose, is_in_water) || is_underwater)
406 && (!is_in_water || is_underwater);
407 if trying_to_sprint && can_start_sprinting {
408 set_sprinting(true, &mut sprinting, &mut attributes);
409 }
410
411 if **sprinting {
412 let vehicle_can_sprint = false;
415 let should_stop_sprinting = has_blindness
417 || (is_passenger && !vehicle_can_sprint)
418 || !has_enough_impulse
419 || !has_enough_food_to_sprint
420 || (physics.horizontal_collision && !physics.minor_horizontal_collision)
421 || (is_in_water && !is_underwater);
422 if should_stop_sprinting {
423 set_sprinting(false, &mut sprinting, &mut attributes);
424 }
425 }
426
427 let move_vector = modify_input(
430 physics_state.move_vector,
431 false,
432 false,
433 is_moving_slowly(&crouching, fall_flying, pose, is_in_water),
434 &attributes,
435 );
436 physics.x_acceleration = move_vector.x;
437 physics.z_acceleration = move_vector.y;
438 }
439}
440
441pub fn process_fall_flying_activation(
446 mut query: Query<
447 (
448 Entity,
449 &MinecraftEntityId,
450 &PlayerAbilities,
451 Option<&LastSentInput>,
452 &Jumping,
453 &Inventory,
454 &Physics,
455 &ActiveEffects,
456 &OnClimbable,
457 &mut FallFlying,
458 ),
459 (With<HasClientLoaded>, With<LocalEntity>),
460 >,
461 mut commands: Commands,
462) {
463 for (
464 entity,
465 minecraft_entity_id,
466 abilities,
467 last_sent_input,
468 jumping,
469 inv,
470 physics,
471 active_effects,
472 onclimbable,
473 mut fall_flying,
474 ) in query.iter_mut()
475 {
476 let creative_flight_toggled = false;
478
479 if **jumping
480 && !creative_flight_toggled
481 && last_sent_input.is_some_and(|input| !input.0.jump)
482 && !**onclimbable
483 && can_start_fall_flying(&fall_flying, abilities, inv, physics, active_effects)
484 {
485 **fall_flying = true; commands.trigger(SendGamePacketEvent::new(
488 entity,
489 s_player_command::ServerboundPlayerCommand {
490 id: *minecraft_entity_id,
491 action: s_player_command::Action::StartFallFlying,
492 data: 0,
493 },
494 ));
495 }
496 }
497}
498
499fn can_start_fall_flying(
501 already_fall_flying: &FallFlying,
502 abilities: &PlayerAbilities,
503 inv: &Inventory,
504 physics: &Physics,
505 active_effects: &ActiveEffects,
506) -> bool {
507 (!**already_fall_flying)
508 && (!abilities.flying)
509
510 && !physics.on_ground()
512 && !active_effects.0.contains_key(&MobEffect::Levitation)
514 && EquipmentSlot::values().iter().any(|slot| {
515 inv.get_equipment(*slot).is_some_and(|stack| {
516 stack.get_component::<components::Glider>().is_some()
518 && stack.get_component::<components::Equippable>().is_some_and(
519 |equippable| equippable.slot == *slot
520 )
521 && !next_damage_will_break(stack)
522 })
523 })
524
525 && !physics.is_in_water()
526}
527
528fn next_damage_will_break(stack: &ItemStack) -> bool {
530 let Some(max_damage) = stack.get_component::<components::MaxDamage>() else {
531 return false;
532 };
533 let Some(damage) = stack.get_component::<components::Damage>() else {
534 return false;
535 };
536 if stack.get_component::<components::Unbreakable>().is_some() {
538 return false;
539 }
540 damage.amount >= max_damage.amount - 1
541}
542
543fn is_moving_slowly(
545 crouching: &Crouching,
546 fall_flying: &FallFlying,
547 pose: &Pose,
548 is_in_water: bool,
549) -> bool {
550 if **crouching {
551 return true;
552 }
553
554 if is_in_water {
556 return false;
557 }
558
559 match *pose {
561 Pose::Swimming => true,
562 Pose::FallFlying => !**fall_flying,
563 _ => false,
567 }
568}
569
570fn modify_input(
572 mut move_vector: Vec2,
573 is_using_item: bool,
574 is_passenger: bool,
575 moving_slowly: bool,
576 attributes: &Attributes,
577) -> Vec2 {
578 if move_vector.length_squared() == 0. {
579 return move_vector;
580 }
581
582 move_vector *= 0.98;
583 if is_using_item && !is_passenger {
584 move_vector *= 0.2;
585 }
586
587 if moving_slowly {
588 let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
589 move_vector *= sneaking_speed;
590 }
591
592 modify_input_speed_for_square_movement(move_vector)
593}
594fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
595 let length = move_vector.length();
596 if length == 0. {
597 return move_vector;
598 }
599 let scaled_to_inverse_length = move_vector * (1. / length);
600 let dist = distance_to_unit_square(scaled_to_inverse_length);
601 let scale = (length * dist).min(1.);
602 scaled_to_inverse_length * scale
603}
604fn distance_to_unit_square(v: Vec2) -> f32 {
605 let x = v.x.abs();
606 let y = v.y.abs();
607 let ratio = if y > x { x / y } else { y / x };
608 (1. + ratio * ratio).sqrt()
609}
610
611#[derive(Debug, Message)]
617pub struct StartWalkEvent {
618 pub entity: Entity,
619 pub direction: WalkDirection,
620}
621
622pub fn handle_walk(
625 mut events: MessageReader<StartWalkEvent>,
626 mut query: Query<(&mut ClientMovementState, &mut Sprinting, &mut Attributes)>,
627) {
628 for event in events.read() {
629 if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
630 {
631 physics_state.move_direction = event.direction;
632 physics_state.trying_to_sprint = false;
633 set_sprinting(false, &mut sprinting, &mut attributes);
634 }
635 }
636}
637
638#[derive(Message)]
642pub struct StartSprintEvent {
643 pub entity: Entity,
644 pub direction: SprintDirection,
645}
646pub fn handle_sprint(
649 mut query: Query<&mut ClientMovementState>,
650 mut events: MessageReader<StartSprintEvent>,
651) {
652 for event in events.read() {
653 if let Ok(mut physics_state) = query.get_mut(event.entity) {
654 physics_state.move_direction = WalkDirection::from(event.direction);
655 physics_state.trying_to_sprint = true;
656 }
657 }
658}
659
660fn set_sprinting(
668 sprinting: bool,
669 currently_sprinting: &mut Sprinting,
670 attributes: &mut Attributes,
671) -> bool {
672 **currently_sprinting = sprinting;
673 if sprinting {
674 attributes
675 .movement_speed
676 .try_insert(azalea_entity::attributes::sprinting_modifier())
677 .is_ok()
678 } else {
679 attributes
680 .movement_speed
681 .remove(&azalea_entity::attributes::sprinting_modifier().id)
682 .is_none()
683 }
684}
685
686fn has_enough_impulse_to_start_sprinting(physics_state: &ClientMovementState) -> bool {
688 physics_state.move_vector.y > 0.8
692 }
694
695#[derive(EntityEvent, Debug, Clone)]
701pub struct KnockbackEvent {
702 pub entity: Entity,
703 pub data: KnockbackData,
704}
705
706#[derive(Debug, Clone)]
707pub enum KnockbackData {
708 Set(Vec3),
709 Add(Vec3),
710}
711
712pub fn handle_knockback(knockback: On<KnockbackEvent>, mut query: Query<&mut Physics>) {
713 if let Ok(mut physics) = query.get_mut(knockback.entity) {
714 match knockback.data {
715 KnockbackData::Set(velocity) => {
716 physics.velocity = velocity;
717 }
718 KnockbackData::Add(velocity) => {
719 physics.velocity += velocity;
720 }
721 }
722 }
723}
724
725pub fn update_pose(
726 mut query: Query<(
727 Entity,
728 &mut Pose,
729 &Physics,
730 &ClientMovementState,
731 &FallFlying,
732 &GameMode,
733 &WorldHolder,
734 &Position,
735 )>,
736 aabb_query: AabbQuery,
737 collidable_entity_query: CollidableEntityQuery,
738) {
739 for (
740 entity,
741 mut pose,
742 physics,
743 physics_state,
744 fall_flying,
745 &game_mode,
746 world_holder,
747 position,
748 ) in query.iter_mut()
749 {
750 let world = world_holder.shared.read();
751 let world = &*world;
752 let ctx = CanPlayerFitCtx {
753 world,
754 entity,
755 position: *position,
756 aabb_query: &aabb_query,
757 collidable_entity_query: &collidable_entity_query,
758 physics,
759 };
760
761 if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
762 continue;
763 }
764
765 let desired_pose = if physics_state.trying_to_crouch {
768 Pose::Crouching
769 } else if **fall_flying {
770 Pose::FallFlying
771 } else {
772 Pose::Standing
773 };
774
775 let is_passenger = false;
777
778 let new_pose = if game_mode == GameMode::Spectator
780 || is_passenger
781 || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
782 {
783 desired_pose
784 } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
785 Pose::Crouching
786 } else {
787 Pose::Swimming
788 };
789
790 if new_pose != *pose {
792 *pose = new_pose;
793 }
794 }
795}
796
797struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
798 world: &'a World,
799 entity: Entity,
800 position: Position,
801 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
802 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
803 physics: &'a Physics,
804}
805fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
806 no_collision(
807 ctx.world,
808 Some(ctx.entity),
809 ctx.aabb_query,
810 ctx.collidable_entity_query,
811 ctx.physics,
812 &calculate_dimensions(EntityKind::Player, pose)
813 .make_bounding_box(*ctx.position)
814 .deflate_all(1.0e-7),
815 false,
816 )
817}