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#[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 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 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
263pub(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#[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 let is_swimming = **swimming;
333 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 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
362
363 let trying_to_sprint = physics_state.trying_to_sprint;
366
367 let is_underwater = false;
369 let is_in_water = physics.is_in_water();
370 let is_fall_flying = false;
372 let is_passenger = false;
374 let using_item = false;
376 let has_blindness = false;
378
379 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
380
381 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 let vehicle_can_sprint = false;
399 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 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
429fn 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#[derive(Debug, Message)]
476pub struct StartWalkEvent {
477 pub entity: Entity,
478 pub direction: WalkDirection,
479}
480
481pub 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#[derive(Message)]
501pub struct StartSprintEvent {
502 pub entity: Entity,
503 pub direction: SprintDirection,
504}
505pub 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
519fn 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
545fn has_enough_impulse_to_start_sprinting(physics_state: &ClientMovementState) -> bool {
547 physics_state.move_vector.y > 0.8
551 }
553
554#[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 let desired_pose = if physics_state.trying_to_crouch {
618 Pose::Crouching
619 } else {
620 Pose::Standing
621 };
622
623 let is_passenger = false;
625
626 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 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}