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 PhysicsSet, ai_step,
17 collision::entity_collisions::{CollidableEntityQuery, PhysicsQuery},
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::SendPacketEvent,
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_event::<StartWalkEvent>()
69 .add_event::<StartSprintEvent>()
70 .add_event::<KnockbackEvent>()
71 .add_systems(
72 Update,
73 (handle_sprint, handle_walk, handle_knockback)
74 .chain()
75 .in_set(MoveEventsSet)
76 .after(update_bounding_box),
77 )
78 .add_systems(
79 GameTick,
80 (
81 (tick_controls, local_player_ai_step, update_pose)
82 .chain()
83 .in_set(PhysicsSet)
84 .before(ai_step)
85 .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
86 send_player_input_packet,
87 send_sprinting_if_needed
88 .after(azalea_entity::update_in_loaded_chunk)
89 .after(travel),
90 send_position.after(PhysicsSet),
91 )
92 .chain(),
93 );
94 }
95}
96
97#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
98pub struct MoveEventsSet;
99
100impl Client {
101 pub fn set_jumping(&self, jumping: bool) {
107 let mut ecs = self.ecs.lock();
108 let mut jumping_mut = self.query::<&mut Jumping>(&mut ecs);
109 **jumping_mut = jumping;
110 }
111
112 pub fn jumping(&self) -> bool {
114 *self.component::<Jumping>()
115 }
116
117 pub fn set_crouching(&self, crouching: bool) {
118 let mut ecs = self.ecs.lock();
119 let mut physics_state = self.query::<&mut PhysicsState>(&mut ecs);
120 physics_state.trying_to_crouch = crouching;
121 }
122
123 pub fn crouching(&self) -> bool {
127 let mut ecs = self.ecs.lock();
128 let physics_state = self.query::<&PhysicsState>(&mut ecs);
129 physics_state.trying_to_crouch
130 }
131
132 pub fn set_direction(&self, y_rot: f32, x_rot: f32) {
137 let mut ecs = self.ecs.lock();
138 let mut look_direction = self.query::<&mut LookDirection>(&mut ecs);
139
140 look_direction.update(LookDirection::new(y_rot, x_rot));
141 }
142
143 pub fn direction(&self) -> (f32, f32) {
147 let look_direction: LookDirection = self.component::<LookDirection>();
148 (look_direction.y_rot(), look_direction.x_rot())
149 }
150}
151
152#[derive(Debug, Component, Clone, Default)]
155pub struct LastSentLookDirection {
156 pub x_rot: f32,
157 pub y_rot: f32,
158}
159
160#[allow(clippy::type_complexity)]
161pub fn send_position(
162 mut query: Query<
163 (
164 Entity,
165 &Position,
166 &LookDirection,
167 &mut PhysicsState,
168 &mut LastSentPosition,
169 &mut Physics,
170 &mut LastSentLookDirection,
171 ),
172 With<HasClientLoaded>,
173 >,
174 mut commands: Commands,
175) {
176 for (
177 entity,
178 position,
179 direction,
180 mut physics_state,
181 mut last_sent_position,
182 mut physics,
183 mut last_direction,
184 ) in query.iter_mut()
185 {
186 let packet = {
187 let x_delta = position.x - last_sent_position.x;
191 let y_delta = position.y - last_sent_position.y;
192 let z_delta = position.z - last_sent_position.z;
193 let y_rot_delta = (direction.y_rot() - last_direction.y_rot) as f64;
194 let x_rot_delta = (direction.x_rot() - last_direction.x_rot) as f64;
195
196 physics_state.position_remainder += 1;
197
198 let is_delta_large_enough =
201 (x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) > 2.0e-4f64.powi(2);
202 let sending_position = is_delta_large_enough || physics_state.position_remainder >= 20;
203 let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
204
205 let flags = MoveFlags {
209 on_ground: physics.on_ground(),
210 horizontal_collision: physics.horizontal_collision,
211 };
212 let packet = if sending_position && sending_direction {
213 Some(
214 ServerboundMovePlayerPosRot {
215 pos: **position,
216 look_direction: *direction,
217 flags,
218 }
219 .into_variant(),
220 )
221 } else if sending_position {
222 Some(
223 ServerboundMovePlayerPos {
224 pos: **position,
225 flags,
226 }
227 .into_variant(),
228 )
229 } else if sending_direction {
230 Some(
231 ServerboundMovePlayerRot {
232 look_direction: *direction,
233 flags,
234 }
235 .into_variant(),
236 )
237 } else if physics.last_on_ground() != physics.on_ground() {
238 Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
239 } else {
240 None
241 };
242
243 if sending_position {
244 **last_sent_position = **position;
245 physics_state.position_remainder = 0;
246 }
247 if sending_direction {
248 last_direction.y_rot = direction.y_rot();
249 last_direction.x_rot = direction.x_rot();
250 }
251
252 let on_ground = physics.on_ground();
253 physics.set_last_on_ground(on_ground);
254 packet
257 };
258
259 if let Some(packet) = packet {
260 commands.trigger(SendPacketEvent {
261 sent_by: entity,
262 packet,
263 });
264 }
265 }
266}
267
268#[derive(Debug, Default, Component, Clone, PartialEq, Eq)]
269pub struct LastSentInput(pub ServerboundPlayerInput);
270pub fn send_player_input_packet(
271 mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
272 mut commands: Commands,
273) {
274 for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
275 let dir = physics_state.move_direction;
276 type D = WalkDirection;
277 let input = ServerboundPlayerInput {
278 forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
279 backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
280 left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
281 right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
282 jump: **jumping,
283 shift: physics_state.trying_to_crouch,
284 sprint: physics_state.trying_to_sprint,
285 };
286
287 let last_sent_input = last_sent_input.cloned().unwrap_or_default();
290
291 if input != last_sent_input.0 {
292 commands.trigger(SendPacketEvent {
293 sent_by: entity,
294 packet: input.clone().into_variant(),
295 });
296 commands.entity(entity).insert(LastSentInput(input));
297 }
298 }
299}
300
301pub fn send_sprinting_if_needed(
302 mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
303 mut commands: Commands,
304) {
305 for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
306 let was_sprinting = physics_state.was_sprinting;
307 if **sprinting != was_sprinting {
308 let sprinting_action = if **sprinting {
309 azalea_protocol::packets::game::s_player_command::Action::StartSprinting
310 } else {
311 azalea_protocol::packets::game::s_player_command::Action::StopSprinting
312 };
313 commands.trigger(SendPacketEvent::new(
314 entity,
315 ServerboundPlayerCommand {
316 id: *minecraft_entity_id,
317 action: sprinting_action,
318 data: 0,
319 },
320 ));
321 physics_state.was_sprinting = **sprinting;
322 }
323 }
324}
325
326pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
329 for mut physics_state in query.iter_mut() {
330 let mut forward_impulse: f32 = 0.;
331 let mut left_impulse: f32 = 0.;
332 let move_direction = physics_state.move_direction;
333 match move_direction {
334 WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
335 forward_impulse += 1.;
336 }
337 WalkDirection::Backward
338 | WalkDirection::BackwardRight
339 | WalkDirection::BackwardLeft => {
340 forward_impulse -= 1.;
341 }
342 _ => {}
343 };
344 match move_direction {
345 WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
346 left_impulse += 1.;
347 }
348 WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
349 left_impulse -= 1.;
350 }
351 _ => {}
352 };
353
354 let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
355 physics_state.move_vector = move_vector;
356 }
357}
358
359#[allow(clippy::type_complexity)]
362pub fn local_player_ai_step(
363 mut query: Query<
364 (
365 Entity,
366 &PhysicsState,
367 &PlayerAbilities,
368 &metadata::Swimming,
369 &metadata::SleepingPos,
370 &InstanceHolder,
371 &Position,
372 Option<&Hunger>,
373 Option<&LastSentInput>,
374 &mut Physics,
375 &mut Sprinting,
376 &mut Crouching,
377 &mut Attributes,
378 ),
379 (With<HasClientLoaded>, With<LocalEntity>),
380 >,
381 physics_query: PhysicsQuery,
382 collidable_entity_query: CollidableEntityQuery,
383) {
384 for (
385 entity,
386 physics_state,
387 abilities,
388 swimming,
389 sleeping_pos,
390 instance_holder,
391 position,
392 hunger,
393 last_sent_input,
394 mut physics,
395 mut sprinting,
396 mut crouching,
397 mut attributes,
398 ) in query.iter_mut()
399 {
400 let is_swimming = **swimming;
403 let is_passenger = false;
405 let is_sleeping = sleeping_pos.is_some();
406
407 let world = instance_holder.instance.read();
408 let ctx = CanPlayerFitCtx {
409 world: &world,
410 entity,
411 position: *position,
412 physics_query: &physics_query,
413 collidable_entity_query: &collidable_entity_query,
414 physics: &physics,
415 };
416
417 let new_crouching = !abilities.flying
418 && !is_swimming
419 && !is_passenger
420 && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching)
421 && (last_sent_input.is_some_and(|i| i.0.shift)
422 || !is_sleeping
423 && !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing));
424 if **crouching != new_crouching {
425 **crouching = new_crouching;
426 }
427
428 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
432
433 let trying_to_sprint = physics_state.trying_to_sprint;
436
437 let is_underwater = false;
439 let is_in_water = physics.is_in_water();
440 let is_fall_flying = false;
442 let is_passenger = false;
444 let using_item = false;
446 let has_blindness = false;
448
449 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
450
451 let can_start_sprinting = !**sprinting
453 && has_enough_impulse
454 && has_enough_food_to_sprint
455 && !using_item
456 && !has_blindness
457 && (!is_passenger || is_underwater)
458 && (!is_fall_flying || is_underwater)
459 && (!is_moving_slowly(&crouching) || is_underwater)
460 && (!is_in_water || is_underwater);
461 if trying_to_sprint && can_start_sprinting {
462 set_sprinting(true, &mut sprinting, &mut attributes);
463 }
464
465 if **sprinting {
466 let vehicle_can_sprint = false;
469 let should_stop_sprinting = has_blindness
471 || (is_passenger && !vehicle_can_sprint)
472 || !has_enough_impulse
473 || !has_enough_food_to_sprint
474 || (physics.horizontal_collision && !physics.minor_horizontal_collision)
475 || (is_in_water && !is_underwater);
476 if should_stop_sprinting {
477 set_sprinting(false, &mut sprinting, &mut attributes);
478 }
479 }
480
481 let move_vector = modify_input(
484 physics_state.move_vector,
485 false,
486 false,
487 **crouching,
488 &attributes,
489 );
490 physics.x_acceleration = move_vector.x;
491 physics.z_acceleration = move_vector.y;
492 }
493}
494
495fn is_moving_slowly(crouching: &Crouching) -> bool {
496 **crouching
497}
498
499fn modify_input(
501 mut move_vector: Vec2,
502 is_using_item: bool,
503 is_passenger: bool,
504 moving_slowly: bool,
505 attributes: &Attributes,
506) -> Vec2 {
507 if move_vector.length_squared() == 0. {
508 return move_vector;
509 }
510
511 move_vector *= 0.98;
512 if is_using_item && !is_passenger {
513 move_vector *= 0.2;
514 }
515
516 if moving_slowly {
517 let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
518 move_vector *= sneaking_speed;
519 }
520
521 modify_input_speed_for_square_movement(move_vector)
522}
523fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
524 let length = move_vector.length();
525 if length == 0. {
526 return move_vector;
527 }
528 let scaled_to_inverse_length = move_vector * (1. / length);
529 let dist = distance_to_unit_square(scaled_to_inverse_length);
530 let scale = (length * dist).min(1.);
531 scaled_to_inverse_length * scale
532}
533fn distance_to_unit_square(v: Vec2) -> f32 {
534 let x = v.x.abs();
535 let y = v.y.abs();
536 let ratio = if y > x { x / y } else { y / x };
537 (1. + ratio * ratio).sqrt()
538}
539
540impl Client {
541 pub fn walk(&self, direction: WalkDirection) {
558 let mut ecs = self.ecs.lock();
559 ecs.send_event(StartWalkEvent {
560 entity: self.entity,
561 direction,
562 });
563 }
564
565 pub fn sprint(&self, direction: SprintDirection) {
581 let mut ecs = self.ecs.lock();
582 ecs.send_event(StartSprintEvent {
583 entity: self.entity,
584 direction,
585 });
586 }
587}
588
589#[derive(Event, Debug)]
594pub struct StartWalkEvent {
595 pub entity: Entity,
596 pub direction: WalkDirection,
597}
598
599pub fn handle_walk(
602 mut events: EventReader<StartWalkEvent>,
603 mut query: Query<(&mut PhysicsState, &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(Event)]
618pub struct StartSprintEvent {
619 pub entity: Entity,
620 pub direction: SprintDirection,
621}
622pub fn handle_sprint(
625 mut query: Query<&mut PhysicsState>,
626 mut events: EventReader<StartSprintEvent>,
627) {
628 for event in events.read() {
629 if let Ok(mut physics_state) = query.get_mut(event.entity) {
630 physics_state.move_direction = WalkDirection::from(event.direction);
631 physics_state.trying_to_sprint = true;
632 }
633 }
634}
635
636fn set_sprinting(
644 sprinting: bool,
645 currently_sprinting: &mut Sprinting,
646 attributes: &mut Attributes,
647) -> bool {
648 **currently_sprinting = sprinting;
649 if sprinting {
650 attributes
651 .movement_speed
652 .try_insert(azalea_entity::attributes::sprinting_modifier())
653 .is_ok()
654 } else {
655 attributes
656 .movement_speed
657 .remove(&azalea_entity::attributes::sprinting_modifier().id)
658 .is_none()
659 }
660}
661
662fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
664 physics_state.move_vector.y > 0.8
668 }
670
671#[derive(Event)]
676pub struct KnockbackEvent {
677 pub entity: Entity,
678 pub knockback: KnockbackType,
679}
680
681pub enum KnockbackType {
682 Set(Vec3),
683 Add(Vec3),
684}
685
686pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader<KnockbackEvent>) {
687 for event in events.read() {
688 if let Ok(mut physics) = query.get_mut(event.entity) {
689 match event.knockback {
690 KnockbackType::Set(velocity) => {
691 physics.velocity = velocity;
692 }
693 KnockbackType::Add(velocity) => {
694 physics.velocity += velocity;
695 }
696 }
697 }
698 }
699}
700
701pub fn update_pose(
702 mut query: Query<(
703 Entity,
704 &mut Pose,
705 &Physics,
706 &PhysicsState,
707 &LocalGameMode,
708 &InstanceHolder,
709 &Position,
710 )>,
711 physics_query: PhysicsQuery,
712 collidable_entity_query: CollidableEntityQuery,
713) {
714 for (entity, mut pose, physics, physics_state, game_mode, instance_holder, position) in
715 query.iter_mut()
716 {
717 let world = instance_holder.instance.read();
718 let world = &*world;
719 let ctx = CanPlayerFitCtx {
720 world,
721 entity,
722 position: *position,
723 physics_query: &physics_query,
724 collidable_entity_query: &collidable_entity_query,
725 physics,
726 };
727
728 if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
729 continue;
730 }
731
732 let desired_pose = if physics_state.trying_to_crouch {
735 Pose::Crouching
736 } else {
737 Pose::Standing
738 };
739
740 let is_passenger = false;
742
743 let new_pose = if game_mode.current == GameMode::Spectator
745 || is_passenger
746 || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
747 {
748 desired_pose
749 } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
750 Pose::Crouching
751 } else {
752 Pose::Swimming
753 };
754
755 if new_pose != *pose {
757 *pose = new_pose;
758 }
759 }
760}
761
762struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
763 world: &'a Instance,
764 entity: Entity,
765 position: Position,
766 physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
767 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
768 physics: &'a Physics,
769}
770fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
771 no_collision(
774 ctx.world,
775 Some(ctx.entity),
776 ctx.physics_query,
777 ctx.collidable_entity_query,
778 ctx.physics,
779 &calculate_dimensions(EntityKind::Player, pose).make_bounding_box(*ctx.position),
780 false,
781 )
782}