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