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, s_player_command,
29 },
30 },
31};
32use azalea_registry::builtin::EntityKind;
33use azalea_world::{Instance, MinecraftEntityId};
34use bevy_app::{App, Plugin, Update};
35use bevy_ecs::prelude::*;
36
37use crate::{
38 local_player::{Hunger, InstanceHolder, LocalGameMode},
39 packet::game::SendGamePacketEvent,
40};
41
42pub struct MovementPlugin;
43
44impl Plugin for MovementPlugin {
45 fn build(&self, app: &mut App) {
46 app.add_message::<StartWalkEvent>()
47 .add_message::<StartSprintEvent>()
48 .add_systems(
49 Update,
50 (handle_sprint, handle_walk)
51 .chain()
52 .in_set(MoveEventsSystems)
53 .after(update_bounding_box)
54 .after(update_last_bounding_box),
55 )
56 .add_systems(
57 GameTick,
58 (
59 (tick_controls, local_player_ai_step, update_pose)
60 .chain()
61 .in_set(PhysicsSystems)
62 .before(ai_step)
63 .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
64 send_player_input_packet,
65 send_sprinting_if_needed
66 .after(azalea_entity::update_in_loaded_chunk)
67 .after(travel),
68 send_position.after(PhysicsSystems),
69 )
70 .chain(),
71 )
72 .add_observer(handle_knockback);
73 }
74}
75
76#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
77pub struct MoveEventsSystems;
78
79#[derive(Clone, Component, Debug, Default)]
82pub struct LastSentLookDirection {
83 pub x_rot: f32,
84 pub y_rot: f32,
85}
86
87#[allow(clippy::type_complexity)]
88pub fn send_position(
89 mut query: Query<
90 (
91 Entity,
92 &Position,
93 &LookDirection,
94 &mut PhysicsState,
95 &mut LastSentPosition,
96 &mut Physics,
97 &mut LastSentLookDirection,
98 ),
99 With<HasClientLoaded>,
100 >,
101 mut commands: Commands,
102) {
103 for (
104 entity,
105 position,
106 direction,
107 mut physics_state,
108 mut last_sent_position,
109 mut physics,
110 mut last_direction,
111 ) in query.iter_mut()
112 {
113 let packet = {
114 let x_delta = position.x - last_sent_position.x;
118 let y_delta = position.y - last_sent_position.y;
119 let z_delta = position.z - last_sent_position.z;
120 let y_rot_delta = (direction.y_rot() - last_direction.y_rot) as f64;
121 let x_rot_delta = (direction.x_rot() - last_direction.x_rot) as f64;
122
123 physics_state.position_remainder += 1;
124
125 let is_delta_large_enough =
128 (x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) > 2.0e-4f64.powi(2);
129 let sending_position = is_delta_large_enough || physics_state.position_remainder >= 20;
130 let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
131
132 let flags = MoveFlags {
136 on_ground: physics.on_ground(),
137 horizontal_collision: physics.horizontal_collision,
138 };
139 let packet = if sending_position && sending_direction {
140 Some(
141 ServerboundMovePlayerPosRot {
142 pos: **position,
143 look_direction: *direction,
144 flags,
145 }
146 .into_variant(),
147 )
148 } else if sending_position {
149 Some(
150 ServerboundMovePlayerPos {
151 pos: **position,
152 flags,
153 }
154 .into_variant(),
155 )
156 } else if sending_direction {
157 Some(
158 ServerboundMovePlayerRot {
159 look_direction: *direction,
160 flags,
161 }
162 .into_variant(),
163 )
164 } else if physics.last_on_ground() != physics.on_ground() {
165 Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
166 } else {
167 None
168 };
169
170 if sending_position {
171 **last_sent_position = **position;
172 physics_state.position_remainder = 0;
173 }
174 if sending_direction {
175 last_direction.y_rot = direction.y_rot();
176 last_direction.x_rot = direction.x_rot();
177 }
178
179 let on_ground = physics.on_ground();
180 physics.set_last_on_ground(on_ground);
181 packet
184 };
185
186 if let Some(packet) = packet {
187 commands.trigger(SendGamePacketEvent {
188 sent_by: entity,
189 packet,
190 });
191 }
192 }
193}
194
195#[derive(Clone, Component, Debug, Default, Eq, PartialEq)]
196pub struct LastSentInput(pub ServerboundPlayerInput);
197pub fn send_player_input_packet(
198 mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
199 mut commands: Commands,
200) {
201 for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
202 let dir = physics_state.move_direction;
203 type D = WalkDirection;
204 let input = ServerboundPlayerInput {
205 forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
206 backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
207 left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
208 right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
209 jump: **jumping,
210 shift: physics_state.trying_to_crouch,
211 sprint: physics_state.trying_to_sprint,
212 };
213
214 let last_sent_input = last_sent_input.cloned().unwrap_or_default();
217
218 if input != last_sent_input.0 {
219 commands.trigger(SendGamePacketEvent {
220 sent_by: entity,
221 packet: input.clone().into_variant(),
222 });
223 commands.entity(entity).insert(LastSentInput(input));
224 }
225 }
226}
227
228pub fn send_sprinting_if_needed(
229 mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
230 mut commands: Commands,
231) {
232 for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
233 let was_sprinting = physics_state.was_sprinting;
234 if **sprinting != was_sprinting {
235 let sprinting_action = if **sprinting {
236 s_player_command::Action::StartSprinting
237 } else {
238 s_player_command::Action::StopSprinting
239 };
240 commands.trigger(SendGamePacketEvent::new(
241 entity,
242 ServerboundPlayerCommand {
243 id: *minecraft_entity_id,
244 action: sprinting_action,
245 data: 0,
246 },
247 ));
248 physics_state.was_sprinting = **sprinting;
249 }
250 }
251}
252
253pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
256 for mut physics_state in query.iter_mut() {
257 let mut forward_impulse: f32 = 0.;
258 let mut left_impulse: f32 = 0.;
259 let move_direction = physics_state.move_direction;
260 match move_direction {
261 WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
262 forward_impulse += 1.;
263 }
264 WalkDirection::Backward
265 | WalkDirection::BackwardRight
266 | WalkDirection::BackwardLeft => {
267 forward_impulse -= 1.;
268 }
269 _ => {}
270 };
271 match move_direction {
272 WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
273 left_impulse += 1.;
274 }
275 WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
276 left_impulse -= 1.;
277 }
278 _ => {}
279 };
280
281 let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
282 physics_state.move_vector = move_vector;
283 }
284}
285
286#[allow(clippy::type_complexity)]
290pub fn local_player_ai_step(
291 mut query: Query<
292 (
293 Entity,
294 &PhysicsState,
295 &PlayerAbilities,
296 &metadata::Swimming,
297 &metadata::SleepingPos,
298 &InstanceHolder,
299 &Position,
300 Option<&Hunger>,
301 Option<&LastSentInput>,
302 &mut Physics,
303 &mut Sprinting,
304 &mut Crouching,
305 &mut Attributes,
306 ),
307 (With<HasClientLoaded>, With<LocalEntity>),
308 >,
309 aabb_query: AabbQuery,
310 collidable_entity_query: CollidableEntityQuery,
311) {
312 for (
313 entity,
314 physics_state,
315 abilities,
316 swimming,
317 sleeping_pos,
318 instance_holder,
319 position,
320 hunger,
321 last_sent_input,
322 mut physics,
323 mut sprinting,
324 mut crouching,
325 mut attributes,
326 ) in query.iter_mut()
327 {
328 let is_swimming = **swimming;
331 let is_passenger = false;
333 let is_sleeping = sleeping_pos.is_some();
334
335 let world = instance_holder.instance.read();
336 let ctx = CanPlayerFitCtx {
337 world: &world,
338 entity,
339 position: *position,
340 aabb_query: &aabb_query,
341 collidable_entity_query: &collidable_entity_query,
342 physics: &physics,
343 };
344
345 let new_crouching = !abilities.flying
346 && !is_swimming
347 && !is_passenger
348 && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching)
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 if **crouching != new_crouching {
353 **crouching = new_crouching;
354 }
355
356 let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
360
361 let trying_to_sprint = physics_state.trying_to_sprint;
364
365 let is_underwater = false;
367 let is_in_water = physics.is_in_water();
368 let is_fall_flying = false;
370 let is_passenger = false;
372 let using_item = false;
374 let has_blindness = false;
376
377 let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
378
379 let can_start_sprinting = !**sprinting
381 && has_enough_impulse
382 && has_enough_food_to_sprint
383 && !using_item
384 && !has_blindness
385 && (!is_passenger || is_underwater)
386 && (!is_fall_flying || is_underwater)
387 && (!is_moving_slowly(&crouching) || is_underwater)
388 && (!is_in_water || is_underwater);
389 if trying_to_sprint && can_start_sprinting {
390 set_sprinting(true, &mut sprinting, &mut attributes);
391 }
392
393 if **sprinting {
394 let vehicle_can_sprint = false;
397 let should_stop_sprinting = has_blindness
399 || (is_passenger && !vehicle_can_sprint)
400 || !has_enough_impulse
401 || !has_enough_food_to_sprint
402 || (physics.horizontal_collision && !physics.minor_horizontal_collision)
403 || (is_in_water && !is_underwater);
404 if should_stop_sprinting {
405 set_sprinting(false, &mut sprinting, &mut attributes);
406 }
407 }
408
409 let move_vector = modify_input(
412 physics_state.move_vector,
413 false,
414 false,
415 **crouching,
416 &attributes,
417 );
418 physics.x_acceleration = move_vector.x;
419 physics.z_acceleration = move_vector.y;
420 }
421}
422
423fn is_moving_slowly(crouching: &Crouching) -> bool {
424 **crouching
425}
426
427fn modify_input(
429 mut move_vector: Vec2,
430 is_using_item: bool,
431 is_passenger: bool,
432 moving_slowly: bool,
433 attributes: &Attributes,
434) -> Vec2 {
435 if move_vector.length_squared() == 0. {
436 return move_vector;
437 }
438
439 move_vector *= 0.98;
440 if is_using_item && !is_passenger {
441 move_vector *= 0.2;
442 }
443
444 if moving_slowly {
445 let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
446 move_vector *= sneaking_speed;
447 }
448
449 modify_input_speed_for_square_movement(move_vector)
450}
451fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
452 let length = move_vector.length();
453 if length == 0. {
454 return move_vector;
455 }
456 let scaled_to_inverse_length = move_vector * (1. / length);
457 let dist = distance_to_unit_square(scaled_to_inverse_length);
458 let scale = (length * dist).min(1.);
459 scaled_to_inverse_length * scale
460}
461fn distance_to_unit_square(v: Vec2) -> f32 {
462 let x = v.x.abs();
463 let y = v.y.abs();
464 let ratio = if y > x { x / y } else { y / x };
465 (1. + ratio * ratio).sqrt()
466}
467
468#[derive(Debug, Message)]
474pub struct StartWalkEvent {
475 pub entity: Entity,
476 pub direction: WalkDirection,
477}
478
479pub fn handle_walk(
482 mut events: MessageReader<StartWalkEvent>,
483 mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
484) {
485 for event in events.read() {
486 if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
487 {
488 physics_state.move_direction = event.direction;
489 physics_state.trying_to_sprint = false;
490 set_sprinting(false, &mut sprinting, &mut attributes);
491 }
492 }
493}
494
495#[derive(Message)]
499pub struct StartSprintEvent {
500 pub entity: Entity,
501 pub direction: SprintDirection,
502}
503pub fn handle_sprint(
506 mut query: Query<&mut PhysicsState>,
507 mut events: MessageReader<StartSprintEvent>,
508) {
509 for event in events.read() {
510 if let Ok(mut physics_state) = query.get_mut(event.entity) {
511 physics_state.move_direction = WalkDirection::from(event.direction);
512 physics_state.trying_to_sprint = true;
513 }
514 }
515}
516
517fn set_sprinting(
525 sprinting: bool,
526 currently_sprinting: &mut Sprinting,
527 attributes: &mut Attributes,
528) -> bool {
529 **currently_sprinting = sprinting;
530 if sprinting {
531 attributes
532 .movement_speed
533 .try_insert(azalea_entity::attributes::sprinting_modifier())
534 .is_ok()
535 } else {
536 attributes
537 .movement_speed
538 .remove(&azalea_entity::attributes::sprinting_modifier().id)
539 .is_none()
540 }
541}
542
543fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
545 physics_state.move_vector.y > 0.8
549 }
551
552#[derive(EntityEvent, Debug, Clone)]
558pub struct KnockbackEvent {
559 pub entity: Entity,
560 pub data: KnockbackData,
561}
562
563#[derive(Debug, Clone)]
564pub enum KnockbackData {
565 Set(Vec3),
566 Add(Vec3),
567}
568
569pub fn handle_knockback(knockback: On<KnockbackEvent>, mut query: Query<&mut Physics>) {
570 if let Ok(mut physics) = query.get_mut(knockback.entity) {
571 match knockback.data {
572 KnockbackData::Set(velocity) => {
573 physics.velocity = velocity;
574 }
575 KnockbackData::Add(velocity) => {
576 physics.velocity += velocity;
577 }
578 }
579 }
580}
581
582pub fn update_pose(
583 mut query: Query<(
584 Entity,
585 &mut Pose,
586 &Physics,
587 &PhysicsState,
588 &LocalGameMode,
589 &InstanceHolder,
590 &Position,
591 )>,
592 aabb_query: AabbQuery,
593 collidable_entity_query: CollidableEntityQuery,
594) {
595 for (entity, mut pose, physics, physics_state, game_mode, instance_holder, position) in
596 query.iter_mut()
597 {
598 let world = instance_holder.instance.read();
599 let world = &*world;
600 let ctx = CanPlayerFitCtx {
601 world,
602 entity,
603 position: *position,
604 aabb_query: &aabb_query,
605 collidable_entity_query: &collidable_entity_query,
606 physics,
607 };
608
609 if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
610 continue;
611 }
612
613 let desired_pose = if physics_state.trying_to_crouch {
616 Pose::Crouching
617 } else {
618 Pose::Standing
619 };
620
621 let is_passenger = false;
623
624 let new_pose = if game_mode.current == GameMode::Spectator
626 || is_passenger
627 || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
628 {
629 desired_pose
630 } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
631 Pose::Crouching
632 } else {
633 Pose::Swimming
634 };
635
636 if new_pose != *pose {
638 *pose = new_pose;
639 }
640 }
641}
642
643struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
644 world: &'a Instance,
645 entity: Entity,
646 position: Position,
647 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
648 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
649 physics: &'a Physics,
650}
651fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
652 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).make_bounding_box(*ctx.position),
661 false,
662 )
663}