1use std::{backtrace::Backtrace, io};
2
3use azalea_core::{
4 game_type::GameMode,
5 position::{Vec2, Vec3},
6 tick::GameTick,
7};
8use azalea_entity::{
9 Attributes, Crouching, HasClientLoaded, Jumping, LastSentPosition, LocalEntity, LookDirection,
10 Physics, PlayerAbilities, Pose, Position,
11 dimensions::calculate_dimensions,
12 metadata::{self, Sprinting},
13 update_bounding_box,
14};
15use azalea_physics::{
16 PhysicsSystems, ai_step,
17 collision::entity_collisions::{AabbQuery, CollidableEntityQuery, update_last_bounding_box},
18 local_player::{PhysicsState, SprintDirection, WalkDirection},
19 travel::{no_collision, travel},
20};
21use azalea_protocol::{
22 common::movements::MoveFlags,
23 packets::{
24 Packet,
25 game::{
26 ServerboundPlayerCommand, ServerboundPlayerInput,
27 s_move_player_pos::ServerboundMovePlayerPos,
28 s_move_player_pos_rot::ServerboundMovePlayerPosRot,
29 s_move_player_rot::ServerboundMovePlayerRot,
30 s_move_player_status_only::ServerboundMovePlayerStatusOnly,
31 },
32 },
33};
34use azalea_registry::EntityKind;
35use azalea_world::{Instance, MinecraftEntityId, MoveEntityError};
36use bevy_app::{App, Plugin, Update};
37use bevy_ecs::prelude::*;
38use thiserror::Error;
39
40use crate::{
41 client::Client,
42 local_player::{Hunger, InstanceHolder, LocalGameMode},
43 packet::game::SendGamePacketEvent,
44};
45
46#[derive(Error, Debug)]
47pub enum MovePlayerError {
48 #[error("Player is not in world")]
49 PlayerNotInWorld(Backtrace),
50 #[error("{0}")]
51 Io(#[from] io::Error),
52}
53
54impl From<MoveEntityError> for MovePlayerError {
55 fn from(err: MoveEntityError) -> Self {
56 match err {
57 MoveEntityError::EntityDoesNotExist(backtrace) => {
58 MovePlayerError::PlayerNotInWorld(backtrace)
59 }
60 }
61 }
62}
63
64pub struct MovementPlugin;
65
66impl Plugin for MovementPlugin {
67 fn build(&self, app: &mut App) {
68 app.add_message::<StartWalkEvent>()
69 .add_message::<StartSprintEvent>()
70 .add_message::<KnockbackEvent>()
71 .add_systems(
72 Update,
73 (handle_sprint, handle_walk, handle_knockback)
74 .chain()
75 .in_set(MoveEventsSystems)
76 .after(update_bounding_box)
77 .after(update_last_bounding_box),
78 )
79 .add_systems(
80 GameTick,
81 (
82 (tick_controls, local_player_ai_step, update_pose)
83 .chain()
84 .in_set(PhysicsSystems)
85 .before(ai_step)
86 .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
87 send_player_input_packet,
88 send_sprinting_if_needed
89 .after(azalea_entity::update_in_loaded_chunk)
90 .after(travel),
91 send_position.after(PhysicsSystems),
92 )
93 .chain(),
94 );
95 }
96}
97
98#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
99pub struct MoveEventsSystems;
100
101impl Client {
102 pub fn set_jumping(&self, jumping: bool) {
110 self.query_self::<&mut Jumping, _>(|mut j| **j = jumping);
111 }
112
113 pub fn jumping(&self) -> bool {
115 *self.component::<Jumping>()
116 }
117
118 pub fn set_crouching(&self, crouching: bool) {
119 self.query_self::<&mut PhysicsState, _>(|mut p| p.trying_to_crouch = crouching);
120 }
121
122 pub fn crouching(&self) -> bool {
126 self.query_self::<&PhysicsState, _>(|p| p.trying_to_crouch)
127 }
128
129 pub fn set_direction(&self, y_rot: f32, x_rot: f32) {
136 self.query_self::<&mut LookDirection, _>(|mut ld| {
137 ld.update(LookDirection::new(y_rot, x_rot));
138 });
139 }
140
141 pub fn direction(&self) -> (f32, f32) {
145 let look_direction: LookDirection = self.component::<LookDirection>();
146 (look_direction.y_rot(), look_direction.x_rot())
147 }
148}
149
150#[derive(Debug, Component, Clone, Default)]
153pub struct LastSentLookDirection {
154 pub x_rot: f32,
155 pub y_rot: f32,
156}
157
158#[allow(clippy::type_complexity)]
159pub fn send_position(
160 mut query: Query<
161 (
162 Entity,
163 &Position,
164 &LookDirection,
165 &mut PhysicsState,
166 &mut LastSentPosition,
167 &mut Physics,
168 &mut LastSentLookDirection,
169 ),
170 With<HasClientLoaded>,
171 >,
172 mut commands: Commands,
173) {
174 for (
175 entity,
176 position,
177 direction,
178 mut physics_state,
179 mut last_sent_position,
180 mut physics,
181 mut last_direction,
182 ) in query.iter_mut()
183 {
184 let packet = {
185 let x_delta = position.x - last_sent_position.x;
189 let y_delta = position.y - last_sent_position.y;
190 let z_delta = position.z - last_sent_position.z;
191 let y_rot_delta = (direction.y_rot() - last_direction.y_rot) as f64;
192 let x_rot_delta = (direction.x_rot() - last_direction.x_rot) as f64;
193
194 physics_state.position_remainder += 1;
195
196 let is_delta_large_enough =
199 (x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) > 2.0e-4f64.powi(2);
200 let sending_position = is_delta_large_enough || physics_state.position_remainder >= 20;
201 let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
202
203 let flags = MoveFlags {
207 on_ground: physics.on_ground(),
208 horizontal_collision: physics.horizontal_collision,
209 };
210 let packet = if sending_position && sending_direction {
211 Some(
212 ServerboundMovePlayerPosRot {
213 pos: **position,
214 look_direction: *direction,
215 flags,
216 }
217 .into_variant(),
218 )
219 } else if sending_position {
220 Some(
221 ServerboundMovePlayerPos {
222 pos: **position,
223 flags,
224 }
225 .into_variant(),
226 )
227 } else if sending_direction {
228 Some(
229 ServerboundMovePlayerRot {
230 look_direction: *direction,
231 flags,
232 }
233 .into_variant(),
234 )
235 } else if physics.last_on_ground() != physics.on_ground() {
236 Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
237 } else {
238 None
239 };
240
241 if sending_position {
242 **last_sent_position = **position;
243 physics_state.position_remainder = 0;
244 }
245 if sending_direction {
246 last_direction.y_rot = direction.y_rot();
247 last_direction.x_rot = direction.x_rot();
248 }
249
250 let on_ground = physics.on_ground();
251 physics.set_last_on_ground(on_ground);
252 packet
255 };
256
257 if let Some(packet) = packet {
258 commands.trigger(SendGamePacketEvent {
259 sent_by: entity,
260 packet,
261 });
262 }
263 }
264}
265
266#[derive(Debug, Default, Component, Clone, PartialEq, Eq)]
267pub struct LastSentInput(pub ServerboundPlayerInput);
268pub fn send_player_input_packet(
269 mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
270 mut commands: Commands,
271) {
272 for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
273 let dir = physics_state.move_direction;
274 type D = WalkDirection;
275 let input = ServerboundPlayerInput {
276 forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
277 backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
278 left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
279 right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
280 jump: **jumping,
281 shift: physics_state.trying_to_crouch,
282 sprint: physics_state.trying_to_sprint,
283 };
284
285 let last_sent_input = last_sent_input.cloned().unwrap_or_default();
288
289 if input != last_sent_input.0 {
290 commands.trigger(SendGamePacketEvent {
291 sent_by: entity,
292 packet: input.clone().into_variant(),
293 });
294 commands.entity(entity).insert(LastSentInput(input));
295 }
296 }
297}
298
299pub fn send_sprinting_if_needed(
300 mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
301 mut commands: Commands,
302) {
303 for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
304 let was_sprinting = physics_state.was_sprinting;
305 if **sprinting != was_sprinting {
306 let sprinting_action = if **sprinting {
307 azalea_protocol::packets::game::s_player_command::Action::StartSprinting
308 } else {
309 azalea_protocol::packets::game::s_player_command::Action::StopSprinting
310 };
311 commands.trigger(SendGamePacketEvent::new(
312 entity,
313 ServerboundPlayerCommand {
314 id: *minecraft_entity_id,
315 action: sprinting_action,
316 data: 0,
317 },
318 ));
319 physics_state.was_sprinting = **sprinting;
320 }
321 }
322}
323
324pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
327 for mut physics_state in query.iter_mut() {
328 let mut forward_impulse: f32 = 0.;
329 let mut left_impulse: f32 = 0.;
330 let move_direction = physics_state.move_direction;
331 match move_direction {
332 WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
333 forward_impulse += 1.;
334 }
335 WalkDirection::Backward
336 | WalkDirection::BackwardRight
337 | WalkDirection::BackwardLeft => {
338 forward_impulse -= 1.;
339 }
340 _ => {}
341 };
342 match move_direction {
343 WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
344 left_impulse += 1.;
345 }
346 WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
347 left_impulse -= 1.;
348 }
349 _ => {}
350 };
351
352 let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
353 physics_state.move_vector = move_vector;
354 }
355}
356
357#[allow(clippy::type_complexity)]
361pub fn local_player_ai_step(
362 mut query: Query<
363 (
364 Entity,
365 &PhysicsState,
366 &PlayerAbilities,
367 &metadata::Swimming,
368 &metadata::SleepingPos,
369 &InstanceHolder,
370 &Position,
371 Option<&Hunger>,
372 Option<&LastSentInput>,
373 &mut Physics,
374 &mut Sprinting,
375 &mut Crouching,
376 &mut Attributes,
377 ),
378 (With<HasClientLoaded>, With<LocalEntity>),
379 >,
380 aabb_query: AabbQuery,
381 collidable_entity_query: CollidableEntityQuery,
382) {
383 for (
384 entity,
385 physics_state,
386 abilities,
387 swimming,
388 sleeping_pos,
389 instance_holder,
390 position,
391 hunger,
392 last_sent_input,
393 mut physics,
394 mut sprinting,
395 mut crouching,
396 mut attributes,
397 ) in query.iter_mut()
398 {
399 let is_swimming = **swimming;
402 let is_passenger = false;
404 let is_sleeping = sleeping_pos.is_some();
405
406 let world = instance_holder.instance.read();
407 let ctx = CanPlayerFitCtx {
408 world: &world,
409 entity,
410 position: *position,
411 aabb_query: &aabb_query,
412 collidable_entity_query: &collidable_entity_query,
413 physics: &physics,
414 };
415
416 let new_crouching = !abilities.flying
417 && !is_swimming
418 && !is_passenger
419 && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching)
420 && (last_sent_input.is_some_and(|i| i.0.shift)
421 || !is_sleeping
422 && !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing));
423 if **crouching != new_crouching {
424 **crouching = new_crouching;
425 }
426
427 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
431
432 let trying_to_sprint = physics_state.trying_to_sprint;
435
436 let is_underwater = false;
438 let is_in_water = physics.is_in_water();
439 let is_fall_flying = false;
441 let is_passenger = false;
443 let using_item = false;
445 let has_blindness = false;
447
448 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
449
450 let can_start_sprinting = !**sprinting
452 && has_enough_impulse
453 && has_enough_food_to_sprint
454 && !using_item
455 && !has_blindness
456 && (!is_passenger || is_underwater)
457 && (!is_fall_flying || is_underwater)
458 && (!is_moving_slowly(&crouching) || is_underwater)
459 && (!is_in_water || is_underwater);
460 if trying_to_sprint && can_start_sprinting {
461 set_sprinting(true, &mut sprinting, &mut attributes);
462 }
463
464 if **sprinting {
465 let vehicle_can_sprint = false;
468 let should_stop_sprinting = has_blindness
470 || (is_passenger && !vehicle_can_sprint)
471 || !has_enough_impulse
472 || !has_enough_food_to_sprint
473 || (physics.horizontal_collision && !physics.minor_horizontal_collision)
474 || (is_in_water && !is_underwater);
475 if should_stop_sprinting {
476 set_sprinting(false, &mut sprinting, &mut attributes);
477 }
478 }
479
480 let move_vector = modify_input(
483 physics_state.move_vector,
484 false,
485 false,
486 **crouching,
487 &attributes,
488 );
489 physics.x_acceleration = move_vector.x;
490 physics.z_acceleration = move_vector.y;
491 }
492}
493
494fn is_moving_slowly(crouching: &Crouching) -> bool {
495 **crouching
496}
497
498fn modify_input(
500 mut move_vector: Vec2,
501 is_using_item: bool,
502 is_passenger: bool,
503 moving_slowly: bool,
504 attributes: &Attributes,
505) -> Vec2 {
506 if move_vector.length_squared() == 0. {
507 return move_vector;
508 }
509
510 move_vector *= 0.98;
511 if is_using_item && !is_passenger {
512 move_vector *= 0.2;
513 }
514
515 if moving_slowly {
516 let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
517 move_vector *= sneaking_speed;
518 }
519
520 modify_input_speed_for_square_movement(move_vector)
521}
522fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
523 let length = move_vector.length();
524 if length == 0. {
525 return move_vector;
526 }
527 let scaled_to_inverse_length = move_vector * (1. / length);
528 let dist = distance_to_unit_square(scaled_to_inverse_length);
529 let scale = (length * dist).min(1.);
530 scaled_to_inverse_length * scale
531}
532fn distance_to_unit_square(v: Vec2) -> f32 {
533 let x = v.x.abs();
534 let y = v.y.abs();
535 let ratio = if y > x { x / y } else { y / x };
536 (1. + ratio * ratio).sqrt()
537}
538
539impl Client {
540 pub fn walk(&self, direction: WalkDirection) {
558 let mut ecs = self.ecs.lock();
559 ecs.write_message(StartWalkEvent {
560 entity: self.entity,
561 direction,
562 });
563 }
564
565 pub fn sprint(&self, direction: SprintDirection) {
582 let mut ecs = self.ecs.lock();
583 ecs.write_message(StartSprintEvent {
584 entity: self.entity,
585 direction,
586 });
587 }
588}
589
590#[derive(Message, Debug)]
596pub struct StartWalkEvent {
597 pub entity: Entity,
598 pub direction: WalkDirection,
599}
600
601pub fn handle_walk(
604 mut events: MessageReader<StartWalkEvent>,
605 mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
606) {
607 for event in events.read() {
608 if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
609 {
610 physics_state.move_direction = event.direction;
611 physics_state.trying_to_sprint = false;
612 set_sprinting(false, &mut sprinting, &mut attributes);
613 }
614 }
615}
616
617#[derive(Message)]
621pub struct StartSprintEvent {
622 pub entity: Entity,
623 pub direction: SprintDirection,
624}
625pub fn handle_sprint(
628 mut query: Query<&mut PhysicsState>,
629 mut events: MessageReader<StartSprintEvent>,
630) {
631 for event in events.read() {
632 if let Ok(mut physics_state) = query.get_mut(event.entity) {
633 physics_state.move_direction = WalkDirection::from(event.direction);
634 physics_state.trying_to_sprint = true;
635 }
636 }
637}
638
639fn set_sprinting(
647 sprinting: bool,
648 currently_sprinting: &mut Sprinting,
649 attributes: &mut Attributes,
650) -> bool {
651 **currently_sprinting = sprinting;
652 if sprinting {
653 attributes
654 .movement_speed
655 .try_insert(azalea_entity::attributes::sprinting_modifier())
656 .is_ok()
657 } else {
658 attributes
659 .movement_speed
660 .remove(&azalea_entity::attributes::sprinting_modifier().id)
661 .is_none()
662 }
663}
664
665fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
667 physics_state.move_vector.y > 0.8
671 }
673
674#[derive(Message)]
680pub struct KnockbackEvent {
681 pub entity: Entity,
682 pub knockback: KnockbackType,
683}
684
685pub enum KnockbackType {
686 Set(Vec3),
687 Add(Vec3),
688}
689
690pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: MessageReader<KnockbackEvent>) {
691 for event in events.read() {
692 if let Ok(mut physics) = query.get_mut(event.entity) {
693 match event.knockback {
694 KnockbackType::Set(velocity) => {
695 physics.velocity = velocity;
696 }
697 KnockbackType::Add(velocity) => {
698 physics.velocity += velocity;
699 }
700 }
701 }
702 }
703}
704
705pub fn update_pose(
706 mut query: Query<(
707 Entity,
708 &mut Pose,
709 &Physics,
710 &PhysicsState,
711 &LocalGameMode,
712 &InstanceHolder,
713 &Position,
714 )>,
715 aabb_query: AabbQuery,
716 collidable_entity_query: CollidableEntityQuery,
717) {
718 for (entity, mut pose, physics, physics_state, game_mode, instance_holder, position) in
719 query.iter_mut()
720 {
721 let world = instance_holder.instance.read();
722 let world = &*world;
723 let ctx = CanPlayerFitCtx {
724 world,
725 entity,
726 position: *position,
727 aabb_query: &aabb_query,
728 collidable_entity_query: &collidable_entity_query,
729 physics,
730 };
731
732 if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
733 continue;
734 }
735
736 let desired_pose = if physics_state.trying_to_crouch {
739 Pose::Crouching
740 } else {
741 Pose::Standing
742 };
743
744 let is_passenger = false;
746
747 let new_pose = if game_mode.current == GameMode::Spectator
749 || is_passenger
750 || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
751 {
752 desired_pose
753 } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
754 Pose::Crouching
755 } else {
756 Pose::Swimming
757 };
758
759 if new_pose != *pose {
761 *pose = new_pose;
762 }
763 }
764}
765
766struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
767 world: &'a Instance,
768 entity: Entity,
769 position: Position,
770 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
771 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
772 physics: &'a Physics,
773}
774fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
775 no_collision(
778 ctx.world,
779 Some(ctx.entity),
780 ctx.aabb_query,
781 ctx.collidable_entity_query,
782 ctx.physics,
783 &calculate_dimensions(EntityKind::Player, pose).make_bounding_box(*ctx.position),
784 false,
785 )
786}