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#[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 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 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 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 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 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
271pub(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#[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 let is_swimming = **swimming;
345 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 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
374
375 let trying_to_sprint = physics_state.trying_to_sprint;
378
379 let is_underwater = false;
381 let is_in_water = physics.is_in_water();
382
383 let is_fall_flying = **fall_flying;
384 let is_passenger = false;
386 let using_item = false;
388 let has_blindness = false;
390
391 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
392
393 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 let vehicle_can_sprint = false;
411 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 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
437pub 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 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 **fall_flying = true; 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
493fn 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 && !physics.on_ground()
505 && 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 |equippable| equippable.slot == *slot)
514 })
515 })
516
517 && !physics.is_in_water()
518}
519
520fn 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 if is_in_water {
533 return false;
534 }
535
536 match *pose {
538 Pose::Swimming => true,
539 Pose::FallFlying => !**fall_flying,
540 _ => false,
544 }
545}
546
547fn 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#[derive(Debug, Message)]
594pub struct StartWalkEvent {
595 pub entity: Entity,
596 pub direction: WalkDirection,
597}
598
599pub 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#[derive(Message)]
619pub struct StartSprintEvent {
620 pub entity: Entity,
621 pub direction: SprintDirection,
622}
623pub 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
637fn 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
663fn has_enough_impulse_to_start_sprinting(physics_state: &ClientMovementState) -> bool {
665 physics_state.move_vector.y > 0.8
669 }
671
672#[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 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 let is_passenger = false;
754
755 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 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}