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, 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#[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 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 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 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 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 type D = WalkDirection;
210 let input = ServerboundPlayerInput {
211 forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
212 backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
213 left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
214 right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
215 jump: **jumping,
216 shift: physics_state.trying_to_crouch,
217 sprint: physics_state.trying_to_sprint,
218 };
219
220 let last_sent_input = last_sent_input.cloned().unwrap_or_default();
223
224 if input != last_sent_input.0 {
225 commands.trigger(SendGamePacketEvent {
226 sent_by: entity,
227 packet: input.clone().into_variant(),
228 });
229 commands.entity(entity).insert(LastSentInput(input));
230 }
231 }
232}
233
234pub fn send_sprinting_if_needed(
235 mut query: Query<(
236 Entity,
237 &MinecraftEntityId,
238 &Sprinting,
239 &mut ClientMovementState,
240 )>,
241 mut commands: Commands,
242) {
243 for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
244 let was_sprinting = physics_state.was_sprinting;
245 if **sprinting != was_sprinting {
246 let sprinting_action = if **sprinting {
247 s_player_command::Action::StartSprinting
248 } else {
249 s_player_command::Action::StopSprinting
250 };
251 commands.trigger(SendGamePacketEvent::new(
252 entity,
253 ServerboundPlayerCommand {
254 id: *minecraft_entity_id,
255 action: sprinting_action,
256 data: 0,
257 },
258 ));
259 physics_state.was_sprinting = **sprinting;
260 }
261 }
262}
263
264pub(crate) fn tick_controls(mut query: Query<&mut ClientMovementState>) {
267 for mut physics_state in query.iter_mut() {
268 let mut forward_impulse: f32 = 0.;
269 let mut left_impulse: f32 = 0.;
270 let move_direction = physics_state.move_direction;
271 match move_direction {
272 WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
273 forward_impulse += 1.;
274 }
275 WalkDirection::Backward
276 | WalkDirection::BackwardRight
277 | WalkDirection::BackwardLeft => {
278 forward_impulse -= 1.;
279 }
280 _ => {}
281 };
282 match move_direction {
283 WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
284 left_impulse += 1.;
285 }
286 WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
287 left_impulse -= 1.;
288 }
289 _ => {}
290 };
291
292 let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
293 physics_state.move_vector = move_vector;
294 }
295}
296
297#[allow(clippy::type_complexity)]
301pub fn local_player_ai_step(
302 mut query: Query<
303 (
304 Entity,
305 &ClientMovementState,
306 &PlayerAbilities,
307 &metadata::Swimming,
308 &metadata::SleepingPos,
309 &WorldHolder,
310 &Position,
311 Option<&Hunger>,
312 Option<&LastSentInput>,
313 &mut Physics,
314 &mut Sprinting,
315 &mut Crouching,
316 &mut Attributes,
317 ),
318 (With<HasClientLoaded>, With<LocalEntity>),
319 >,
320 aabb_query: AabbQuery,
321 collidable_entity_query: CollidableEntityQuery,
322) {
323 for (
324 entity,
325 physics_state,
326 abilities,
327 swimming,
328 sleeping_pos,
329 world_holder,
330 position,
331 hunger,
332 last_sent_input,
333 mut physics,
334 mut sprinting,
335 mut crouching,
336 mut attributes,
337 ) in query.iter_mut()
338 {
339 let is_swimming = **swimming;
342 let is_passenger = false;
344 let is_sleeping = sleeping_pos.is_some();
345
346 let world = world_holder.shared.read();
347 let ctx = CanPlayerFitCtx {
348 world: &world,
349 entity,
350 position: *position,
351 aabb_query: &aabb_query,
352 collidable_entity_query: &collidable_entity_query,
353 physics: &physics,
354 };
355
356 let new_crouching = !abilities.flying
357 && !is_swimming
358 && !is_passenger
359 && (last_sent_input.is_some_and(|i| i.0.shift)
360 || !is_sleeping
361 && !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing))
362 && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching);
363 if **crouching != new_crouching {
364 **crouching = new_crouching;
365 }
366
367 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
371
372 let trying_to_sprint = physics_state.trying_to_sprint;
375
376 let is_underwater = false;
378 let is_in_water = physics.is_in_water();
379 let is_fall_flying = false;
381 let is_passenger = false;
383 let using_item = false;
385 let has_blindness = false;
387
388 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
389
390 let can_start_sprinting = !**sprinting
392 && has_enough_impulse
393 && has_enough_food_to_sprint
394 && !using_item
395 && !has_blindness
396 && (!is_passenger || is_underwater)
397 && (!is_fall_flying || is_underwater)
398 && (!is_moving_slowly(&crouching) || is_underwater)
399 && (!is_in_water || is_underwater);
400 if trying_to_sprint && can_start_sprinting {
401 set_sprinting(true, &mut sprinting, &mut attributes);
402 }
403
404 if **sprinting {
405 let vehicle_can_sprint = false;
408 let should_stop_sprinting = has_blindness
410 || (is_passenger && !vehicle_can_sprint)
411 || !has_enough_impulse
412 || !has_enough_food_to_sprint
413 || (physics.horizontal_collision && !physics.minor_horizontal_collision)
414 || (is_in_water && !is_underwater);
415 if should_stop_sprinting {
416 set_sprinting(false, &mut sprinting, &mut attributes);
417 }
418 }
419
420 let move_vector = modify_input(
423 physics_state.move_vector,
424 false,
425 false,
426 **crouching,
427 &attributes,
428 );
429 physics.x_acceleration = move_vector.x;
430 physics.z_acceleration = move_vector.y;
431 }
432}
433
434fn is_moving_slowly(crouching: &Crouching) -> bool {
435 **crouching
436}
437
438fn modify_input(
440 mut move_vector: Vec2,
441 is_using_item: bool,
442 is_passenger: bool,
443 moving_slowly: bool,
444 attributes: &Attributes,
445) -> Vec2 {
446 if move_vector.length_squared() == 0. {
447 return move_vector;
448 }
449
450 move_vector *= 0.98;
451 if is_using_item && !is_passenger {
452 move_vector *= 0.2;
453 }
454
455 if moving_slowly {
456 let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
457 move_vector *= sneaking_speed;
458 }
459
460 modify_input_speed_for_square_movement(move_vector)
461}
462fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
463 let length = move_vector.length();
464 if length == 0. {
465 return move_vector;
466 }
467 let scaled_to_inverse_length = move_vector * (1. / length);
468 let dist = distance_to_unit_square(scaled_to_inverse_length);
469 let scale = (length * dist).min(1.);
470 scaled_to_inverse_length * scale
471}
472fn distance_to_unit_square(v: Vec2) -> f32 {
473 let x = v.x.abs();
474 let y = v.y.abs();
475 let ratio = if y > x { x / y } else { y / x };
476 (1. + ratio * ratio).sqrt()
477}
478
479#[derive(Debug, Message)]
485pub struct StartWalkEvent {
486 pub entity: Entity,
487 pub direction: WalkDirection,
488}
489
490pub fn handle_walk(
493 mut events: MessageReader<StartWalkEvent>,
494 mut query: Query<(&mut ClientMovementState, &mut Sprinting, &mut Attributes)>,
495) {
496 for event in events.read() {
497 if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
498 {
499 physics_state.move_direction = event.direction;
500 physics_state.trying_to_sprint = false;
501 set_sprinting(false, &mut sprinting, &mut attributes);
502 }
503 }
504}
505
506#[derive(Message)]
510pub struct StartSprintEvent {
511 pub entity: Entity,
512 pub direction: SprintDirection,
513}
514pub fn handle_sprint(
517 mut query: Query<&mut ClientMovementState>,
518 mut events: MessageReader<StartSprintEvent>,
519) {
520 for event in events.read() {
521 if let Ok(mut physics_state) = query.get_mut(event.entity) {
522 physics_state.move_direction = WalkDirection::from(event.direction);
523 physics_state.trying_to_sprint = true;
524 }
525 }
526}
527
528fn set_sprinting(
536 sprinting: bool,
537 currently_sprinting: &mut Sprinting,
538 attributes: &mut Attributes,
539) -> bool {
540 **currently_sprinting = sprinting;
541 if sprinting {
542 attributes
543 .movement_speed
544 .try_insert(azalea_entity::attributes::sprinting_modifier())
545 .is_ok()
546 } else {
547 attributes
548 .movement_speed
549 .remove(&azalea_entity::attributes::sprinting_modifier().id)
550 .is_none()
551 }
552}
553
554fn has_enough_impulse_to_start_sprinting(physics_state: &ClientMovementState) -> bool {
556 physics_state.move_vector.y > 0.8
560 }
562
563#[derive(EntityEvent, Debug, Clone)]
569pub struct KnockbackEvent {
570 pub entity: Entity,
571 pub data: KnockbackData,
572}
573
574#[derive(Debug, Clone)]
575pub enum KnockbackData {
576 Set(Vec3),
577 Add(Vec3),
578}
579
580pub fn handle_knockback(knockback: On<KnockbackEvent>, mut query: Query<&mut Physics>) {
581 if let Ok(mut physics) = query.get_mut(knockback.entity) {
582 match knockback.data {
583 KnockbackData::Set(velocity) => {
584 physics.velocity = velocity;
585 }
586 KnockbackData::Add(velocity) => {
587 physics.velocity += velocity;
588 }
589 }
590 }
591}
592
593pub fn update_pose(
594 mut query: Query<(
595 Entity,
596 &mut Pose,
597 &Physics,
598 &ClientMovementState,
599 &LocalGameMode,
600 &WorldHolder,
601 &Position,
602 )>,
603 aabb_query: AabbQuery,
604 collidable_entity_query: CollidableEntityQuery,
605) {
606 for (entity, mut pose, physics, physics_state, game_mode, world_holder, position) in
607 query.iter_mut()
608 {
609 let world = world_holder.shared.read();
610 let world = &*world;
611 let ctx = CanPlayerFitCtx {
612 world,
613 entity,
614 position: *position,
615 aabb_query: &aabb_query,
616 collidable_entity_query: &collidable_entity_query,
617 physics,
618 };
619
620 if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
621 continue;
622 }
623
624 let desired_pose = if physics_state.trying_to_crouch {
627 Pose::Crouching
628 } else {
629 Pose::Standing
630 };
631
632 let is_passenger = false;
634
635 let new_pose = if game_mode.current == GameMode::Spectator
637 || is_passenger
638 || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
639 {
640 desired_pose
641 } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
642 Pose::Crouching
643 } else {
644 Pose::Swimming
645 };
646
647 if new_pose != *pose {
649 *pose = new_pose;
650 }
651 }
652}
653
654struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
655 world: &'a World,
656 entity: Entity,
657 position: Position,
658 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
659 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
660 physics: &'a Physics,
661}
662fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
663 no_collision(
664 ctx.world,
665 Some(ctx.entity),
666 ctx.aabb_query,
667 ctx.collidable_entity_query,
668 ctx.physics,
669 &calculate_dimensions(EntityKind::Player, pose)
670 .make_bounding_box(*ctx.position)
671 .deflate_all(1.0e-7),
672 false,
673 )
674}