1use azalea_core::{
2 game_type::GameMode,
3 position::{Vec2, Vec3},
4 tick::GameTick,
5};
6use azalea_entity::{
7 Attributes, Crouching, HasClientLoaded, Jumping, LastSentPosition, LocalEntity, LookDirection,
8 Physics, PlayerAbilities, Pose, Position,
9 dimensions::calculate_dimensions,
10 metadata::{self, Sprinting},
11 update_bounding_box,
12};
13use azalea_physics::{
14 PhysicsSystems, ai_step,
15 collision::entity_collisions::{AabbQuery, CollidableEntityQuery, update_last_bounding_box},
16 local_player::{PhysicsState, SprintDirection, WalkDirection},
17 travel::{no_collision, travel},
18};
19use azalea_protocol::{
20 common::movements::MoveFlags,
21 packets::{
22 Packet,
23 game::{
24 ServerboundPlayerCommand, ServerboundPlayerInput,
25 s_move_player_pos::ServerboundMovePlayerPos,
26 s_move_player_pos_rot::ServerboundMovePlayerPosRot,
27 s_move_player_rot::ServerboundMovePlayerRot,
28 s_move_player_status_only::ServerboundMovePlayerStatusOnly,
29 },
30 },
31};
32use azalea_registry::EntityKind;
33use azalea_world::{Instance, MinecraftEntityId};
34use bevy_app::{App, Plugin, Update};
35use bevy_ecs::prelude::*;
36
37use crate::{
38 client::Client,
39 local_player::{Hunger, InstanceHolder, LocalGameMode},
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_message::<KnockbackEvent>()
50 .add_systems(
51 Update,
52 (handle_sprint, handle_walk, handle_knockback)
53 .chain()
54 .in_set(MoveEventsSystems)
55 .after(update_bounding_box)
56 .after(update_last_bounding_box),
57 )
58 .add_systems(
59 GameTick,
60 (
61 (tick_controls, local_player_ai_step, update_pose)
62 .chain()
63 .in_set(PhysicsSystems)
64 .before(ai_step)
65 .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
66 send_player_input_packet,
67 send_sprinting_if_needed
68 .after(azalea_entity::update_in_loaded_chunk)
69 .after(travel),
70 send_position.after(PhysicsSystems),
71 )
72 .chain(),
73 );
74 }
75}
76
77#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
78pub struct MoveEventsSystems;
79
80impl Client {
81 pub fn set_jumping(&self, jumping: bool) {
89 self.query_self::<&mut Jumping, _>(|mut j| **j = jumping);
90 }
91
92 pub fn jumping(&self) -> bool {
94 *self.component::<Jumping>()
95 }
96
97 pub fn set_crouching(&self, crouching: bool) {
98 self.query_self::<&mut PhysicsState, _>(|mut p| p.trying_to_crouch = crouching);
99 }
100
101 pub fn crouching(&self) -> bool {
105 self.query_self::<&PhysicsState, _>(|p| p.trying_to_crouch)
106 }
107
108 pub fn set_direction(&self, y_rot: f32, x_rot: f32) {
115 self.query_self::<&mut LookDirection, _>(|mut ld| {
116 ld.update(LookDirection::new(y_rot, x_rot));
117 });
118 }
119
120 pub fn direction(&self) -> (f32, f32) {
124 let look_direction: LookDirection = self.component::<LookDirection>();
125 (look_direction.y_rot(), look_direction.x_rot())
126 }
127}
128
129#[derive(Debug, Component, Clone, Default)]
132pub struct LastSentLookDirection {
133 pub x_rot: f32,
134 pub y_rot: f32,
135}
136
137#[allow(clippy::type_complexity)]
138pub fn send_position(
139 mut query: Query<
140 (
141 Entity,
142 &Position,
143 &LookDirection,
144 &mut PhysicsState,
145 &mut LastSentPosition,
146 &mut Physics,
147 &mut LastSentLookDirection,
148 ),
149 With<HasClientLoaded>,
150 >,
151 mut commands: Commands,
152) {
153 for (
154 entity,
155 position,
156 direction,
157 mut physics_state,
158 mut last_sent_position,
159 mut physics,
160 mut last_direction,
161 ) in query.iter_mut()
162 {
163 let packet = {
164 let x_delta = position.x - last_sent_position.x;
168 let y_delta = position.y - last_sent_position.y;
169 let z_delta = position.z - last_sent_position.z;
170 let y_rot_delta = (direction.y_rot() - last_direction.y_rot) as f64;
171 let x_rot_delta = (direction.x_rot() - last_direction.x_rot) as f64;
172
173 physics_state.position_remainder += 1;
174
175 let is_delta_large_enough =
178 (x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) > 2.0e-4f64.powi(2);
179 let sending_position = is_delta_large_enough || physics_state.position_remainder >= 20;
180 let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
181
182 let flags = MoveFlags {
186 on_ground: physics.on_ground(),
187 horizontal_collision: physics.horizontal_collision,
188 };
189 let packet = if sending_position && sending_direction {
190 Some(
191 ServerboundMovePlayerPosRot {
192 pos: **position,
193 look_direction: *direction,
194 flags,
195 }
196 .into_variant(),
197 )
198 } else if sending_position {
199 Some(
200 ServerboundMovePlayerPos {
201 pos: **position,
202 flags,
203 }
204 .into_variant(),
205 )
206 } else if sending_direction {
207 Some(
208 ServerboundMovePlayerRot {
209 look_direction: *direction,
210 flags,
211 }
212 .into_variant(),
213 )
214 } else if physics.last_on_ground() != physics.on_ground() {
215 Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
216 } else {
217 None
218 };
219
220 if sending_position {
221 **last_sent_position = **position;
222 physics_state.position_remainder = 0;
223 }
224 if sending_direction {
225 last_direction.y_rot = direction.y_rot();
226 last_direction.x_rot = direction.x_rot();
227 }
228
229 let on_ground = physics.on_ground();
230 physics.set_last_on_ground(on_ground);
231 packet
234 };
235
236 if let Some(packet) = packet {
237 commands.trigger(SendGamePacketEvent {
238 sent_by: entity,
239 packet,
240 });
241 }
242 }
243}
244
245#[derive(Debug, Default, Component, Clone, PartialEq, Eq)]
246pub struct LastSentInput(pub ServerboundPlayerInput);
247pub fn send_player_input_packet(
248 mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
249 mut commands: Commands,
250) {
251 for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
252 let dir = physics_state.move_direction;
253 type D = WalkDirection;
254 let input = ServerboundPlayerInput {
255 forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
256 backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
257 left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
258 right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
259 jump: **jumping,
260 shift: physics_state.trying_to_crouch,
261 sprint: physics_state.trying_to_sprint,
262 };
263
264 let last_sent_input = last_sent_input.cloned().unwrap_or_default();
267
268 if input != last_sent_input.0 {
269 commands.trigger(SendGamePacketEvent {
270 sent_by: entity,
271 packet: input.clone().into_variant(),
272 });
273 commands.entity(entity).insert(LastSentInput(input));
274 }
275 }
276}
277
278pub fn send_sprinting_if_needed(
279 mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
280 mut commands: Commands,
281) {
282 for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
283 let was_sprinting = physics_state.was_sprinting;
284 if **sprinting != was_sprinting {
285 let sprinting_action = if **sprinting {
286 azalea_protocol::packets::game::s_player_command::Action::StartSprinting
287 } else {
288 azalea_protocol::packets::game::s_player_command::Action::StopSprinting
289 };
290 commands.trigger(SendGamePacketEvent::new(
291 entity,
292 ServerboundPlayerCommand {
293 id: *minecraft_entity_id,
294 action: sprinting_action,
295 data: 0,
296 },
297 ));
298 physics_state.was_sprinting = **sprinting;
299 }
300 }
301}
302
303pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
306 for mut physics_state in query.iter_mut() {
307 let mut forward_impulse: f32 = 0.;
308 let mut left_impulse: f32 = 0.;
309 let move_direction = physics_state.move_direction;
310 match move_direction {
311 WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
312 forward_impulse += 1.;
313 }
314 WalkDirection::Backward
315 | WalkDirection::BackwardRight
316 | WalkDirection::BackwardLeft => {
317 forward_impulse -= 1.;
318 }
319 _ => {}
320 };
321 match move_direction {
322 WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
323 left_impulse += 1.;
324 }
325 WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
326 left_impulse -= 1.;
327 }
328 _ => {}
329 };
330
331 let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
332 physics_state.move_vector = move_vector;
333 }
334}
335
336#[allow(clippy::type_complexity)]
340pub fn local_player_ai_step(
341 mut query: Query<
342 (
343 Entity,
344 &PhysicsState,
345 &PlayerAbilities,
346 &metadata::Swimming,
347 &metadata::SleepingPos,
348 &InstanceHolder,
349 &Position,
350 Option<&Hunger>,
351 Option<&LastSentInput>,
352 &mut Physics,
353 &mut Sprinting,
354 &mut Crouching,
355 &mut Attributes,
356 ),
357 (With<HasClientLoaded>, With<LocalEntity>),
358 >,
359 aabb_query: AabbQuery,
360 collidable_entity_query: CollidableEntityQuery,
361) {
362 for (
363 entity,
364 physics_state,
365 abilities,
366 swimming,
367 sleeping_pos,
368 instance_holder,
369 position,
370 hunger,
371 last_sent_input,
372 mut physics,
373 mut sprinting,
374 mut crouching,
375 mut attributes,
376 ) in query.iter_mut()
377 {
378 let is_swimming = **swimming;
381 let is_passenger = false;
383 let is_sleeping = sleeping_pos.is_some();
384
385 let world = instance_holder.instance.read();
386 let ctx = CanPlayerFitCtx {
387 world: &world,
388 entity,
389 position: *position,
390 aabb_query: &aabb_query,
391 collidable_entity_query: &collidable_entity_query,
392 physics: &physics,
393 };
394
395 let new_crouching = !abilities.flying
396 && !is_swimming
397 && !is_passenger
398 && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching)
399 && (last_sent_input.is_some_and(|i| i.0.shift)
400 || !is_sleeping
401 && !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing));
402 if **crouching != new_crouching {
403 **crouching = new_crouching;
404 }
405
406 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
410
411 let trying_to_sprint = physics_state.trying_to_sprint;
414
415 let is_underwater = false;
417 let is_in_water = physics.is_in_water();
418 let is_fall_flying = false;
420 let is_passenger = false;
422 let using_item = false;
424 let has_blindness = false;
426
427 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
428
429 let can_start_sprinting = !**sprinting
431 && has_enough_impulse
432 && has_enough_food_to_sprint
433 && !using_item
434 && !has_blindness
435 && (!is_passenger || is_underwater)
436 && (!is_fall_flying || is_underwater)
437 && (!is_moving_slowly(&crouching) || is_underwater)
438 && (!is_in_water || is_underwater);
439 if trying_to_sprint && can_start_sprinting {
440 set_sprinting(true, &mut sprinting, &mut attributes);
441 }
442
443 if **sprinting {
444 let vehicle_can_sprint = false;
447 let should_stop_sprinting = has_blindness
449 || (is_passenger && !vehicle_can_sprint)
450 || !has_enough_impulse
451 || !has_enough_food_to_sprint
452 || (physics.horizontal_collision && !physics.minor_horizontal_collision)
453 || (is_in_water && !is_underwater);
454 if should_stop_sprinting {
455 set_sprinting(false, &mut sprinting, &mut attributes);
456 }
457 }
458
459 let move_vector = modify_input(
462 physics_state.move_vector,
463 false,
464 false,
465 **crouching,
466 &attributes,
467 );
468 physics.x_acceleration = move_vector.x;
469 physics.z_acceleration = move_vector.y;
470 }
471}
472
473fn is_moving_slowly(crouching: &Crouching) -> bool {
474 **crouching
475}
476
477fn modify_input(
479 mut move_vector: Vec2,
480 is_using_item: bool,
481 is_passenger: bool,
482 moving_slowly: bool,
483 attributes: &Attributes,
484) -> Vec2 {
485 if move_vector.length_squared() == 0. {
486 return move_vector;
487 }
488
489 move_vector *= 0.98;
490 if is_using_item && !is_passenger {
491 move_vector *= 0.2;
492 }
493
494 if moving_slowly {
495 let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
496 move_vector *= sneaking_speed;
497 }
498
499 modify_input_speed_for_square_movement(move_vector)
500}
501fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
502 let length = move_vector.length();
503 if length == 0. {
504 return move_vector;
505 }
506 let scaled_to_inverse_length = move_vector * (1. / length);
507 let dist = distance_to_unit_square(scaled_to_inverse_length);
508 let scale = (length * dist).min(1.);
509 scaled_to_inverse_length * scale
510}
511fn distance_to_unit_square(v: Vec2) -> f32 {
512 let x = v.x.abs();
513 let y = v.y.abs();
514 let ratio = if y > x { x / y } else { y / x };
515 (1. + ratio * ratio).sqrt()
516}
517
518impl Client {
519 pub fn walk(&self, direction: WalkDirection) {
537 let mut ecs = self.ecs.lock();
538 ecs.write_message(StartWalkEvent {
539 entity: self.entity,
540 direction,
541 });
542 }
543
544 pub fn sprint(&self, direction: SprintDirection) {
561 let mut ecs = self.ecs.lock();
562 ecs.write_message(StartSprintEvent {
563 entity: self.entity,
564 direction,
565 });
566 }
567}
568
569#[derive(Message, Debug)]
575pub struct StartWalkEvent {
576 pub entity: Entity,
577 pub direction: WalkDirection,
578}
579
580pub fn handle_walk(
583 mut events: MessageReader<StartWalkEvent>,
584 mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
585) {
586 for event in events.read() {
587 if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
588 {
589 physics_state.move_direction = event.direction;
590 physics_state.trying_to_sprint = false;
591 set_sprinting(false, &mut sprinting, &mut attributes);
592 }
593 }
594}
595
596#[derive(Message)]
600pub struct StartSprintEvent {
601 pub entity: Entity,
602 pub direction: SprintDirection,
603}
604pub fn handle_sprint(
607 mut query: Query<&mut PhysicsState>,
608 mut events: MessageReader<StartSprintEvent>,
609) {
610 for event in events.read() {
611 if let Ok(mut physics_state) = query.get_mut(event.entity) {
612 physics_state.move_direction = WalkDirection::from(event.direction);
613 physics_state.trying_to_sprint = true;
614 }
615 }
616}
617
618fn set_sprinting(
626 sprinting: bool,
627 currently_sprinting: &mut Sprinting,
628 attributes: &mut Attributes,
629) -> bool {
630 **currently_sprinting = sprinting;
631 if sprinting {
632 attributes
633 .movement_speed
634 .try_insert(azalea_entity::attributes::sprinting_modifier())
635 .is_ok()
636 } else {
637 attributes
638 .movement_speed
639 .remove(&azalea_entity::attributes::sprinting_modifier().id)
640 .is_none()
641 }
642}
643
644fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
646 physics_state.move_vector.y > 0.8
650 }
652
653#[derive(Message)]
659pub struct KnockbackEvent {
660 pub entity: Entity,
661 pub knockback: KnockbackType,
662}
663
664pub enum KnockbackType {
665 Set(Vec3),
666 Add(Vec3),
667}
668
669pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: MessageReader<KnockbackEvent>) {
670 for event in events.read() {
671 if let Ok(mut physics) = query.get_mut(event.entity) {
672 match event.knockback {
673 KnockbackType::Set(velocity) => {
674 physics.velocity = velocity;
675 }
676 KnockbackType::Add(velocity) => {
677 physics.velocity += velocity;
678 }
679 }
680 }
681 }
682}
683
684pub fn update_pose(
685 mut query: Query<(
686 Entity,
687 &mut Pose,
688 &Physics,
689 &PhysicsState,
690 &LocalGameMode,
691 &InstanceHolder,
692 &Position,
693 )>,
694 aabb_query: AabbQuery,
695 collidable_entity_query: CollidableEntityQuery,
696) {
697 for (entity, mut pose, physics, physics_state, game_mode, instance_holder, position) in
698 query.iter_mut()
699 {
700 let world = instance_holder.instance.read();
701 let world = &*world;
702 let ctx = CanPlayerFitCtx {
703 world,
704 entity,
705 position: *position,
706 aabb_query: &aabb_query,
707 collidable_entity_query: &collidable_entity_query,
708 physics,
709 };
710
711 if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
712 continue;
713 }
714
715 let desired_pose = if physics_state.trying_to_crouch {
718 Pose::Crouching
719 } else {
720 Pose::Standing
721 };
722
723 let is_passenger = false;
725
726 let new_pose = if game_mode.current == GameMode::Spectator
728 || is_passenger
729 || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
730 {
731 desired_pose
732 } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
733 Pose::Crouching
734 } else {
735 Pose::Swimming
736 };
737
738 if new_pose != *pose {
740 *pose = new_pose;
741 }
742 }
743}
744
745struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
746 world: &'a Instance,
747 entity: Entity,
748 position: Position,
749 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
750 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
751 physics: &'a Physics,
752}
753fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
754 no_collision(
757 ctx.world,
758 Some(ctx.entity),
759 ctx.aabb_query,
760 ctx.collidable_entity_query,
761 ctx.physics,
762 &calculate_dimensions(EntityKind::Player, pose).make_bounding_box(*ctx.position),
763 false,
764 )
765}