1mod input;
7mod state;
8mod teleport;
9
10pub use input::PlayerInput;
11pub(super) use state::MovementState;
12pub(super) use teleport::TeleportState;
13
14use glam::DVec3;
15use steel_protocol::packets::game::{
16 CMoveVehicle, CPlayerPosition, PlayerCommandAction, RelativeMovement, SAcceptTeleportation,
17 SMovePlayer, SMoveVehicle, SPlayerCommand, SPlayerInput,
18};
19use steel_registry::vanilla_game_rules::{ELYTRA_MOVEMENT_CHECK, PLAYER_MOVEMENT_CHECK};
20use steel_registry::vanilla_mob_effects;
21use steel_utils::translations;
22use steel_utils::types::GameType;
23
24use crate::entity::{
25 AcceptedClientMovement, AcceptedClientMovementOutcome, Entity, EntityMoveError, LivingEntity,
26};
27use crate::physics::{
28 MOVEMENT_ERROR_THRESHOLD, MovementCollisionValidation, MoverType, WorldCollisionProvider,
29 is_colliding_with_new_shapes, movement_error_delta,
30};
31use crate::player::Player;
32use crate::player::food_data::food_constants;
33use crate::world::World;
34
35pub const DEFAULT_GRAVITY: f64 = 0.08;
37
38pub const SPEED_THRESHOLD_NORMAL: f64 = 100.0;
40pub const SPEED_THRESHOLD_FLYING: f64 = 300.0;
42
43pub const CLAMP_HORIZONTAL: f64 = 3.0E7;
45pub const CLAMP_VERTICAL: f64 = 2.0E7;
47
48#[must_use]
50pub fn clamp_horizontal(value: f64) -> f64 {
51 value.clamp(-CLAMP_HORIZONTAL, CLAMP_HORIZONTAL)
52}
53
54#[must_use]
56pub fn clamp_vertical(value: f64) -> f64 {
57 value.clamp(-CLAMP_VERTICAL, CLAMP_VERTICAL)
58}
59
60#[must_use]
61pub(crate) fn wrap_degrees(mut degrees: f32) -> f32 {
62 degrees %= 360.0;
63 if degrees >= 180.0 {
64 degrees -= 360.0;
65 }
66 if degrees < -180.0 {
67 degrees += 360.0;
68 }
69 degrees
70}
71
72#[derive(Debug, Clone, Copy)]
73struct PlayerFloatingValidation {
74 y_dist: f64,
75 player_stands_on_something: bool,
76 is_spectator: bool,
77 server_allows_flight: bool,
78 may_fly: bool,
79 has_levitation: bool,
80 is_fall_flying: bool,
81}
82
83impl PlayerFloatingValidation {
84 fn can_violate(self) -> bool {
85 self.y_dist >= -0.03125
86 && !self.player_stands_on_something
87 && !self.is_spectator
88 && !self.server_allows_flight
89 && !self.may_fly
90 && !self.has_levitation
91 && !self.is_fall_flying
92 }
93}
94
95impl Player {
96 const fn is_invalid_position(x: f64, y: f64, z: f64, rot_x: f32, rot_y: f32) -> bool {
97 if x.is_nan() || y.is_nan() || z.is_nan() {
98 return true;
99 }
100
101 if !rot_x.is_finite() || !rot_y.is_finite() {
102 return true;
103 }
104
105 false
106 }
107
108 fn move_vehicle_packet_from_entity(entity: &dyn Entity) -> CMoveVehicle {
109 let rotation = entity.rotation();
110 CMoveVehicle {
111 position: entity.position(),
112 y_rot: rotation.0,
113 x_rot: rotation.1,
114 }
115 }
116
117 fn update_awaiting_teleport(&self) -> bool {
122 let mut tp = self.teleport_state.lock();
123 let Some(pos) = tp.awaiting_position else {
124 tp.teleport_time = self.tick_count();
125 return false;
126 };
127
128 let current_tick = self.tick_count();
129
130 if current_tick.wrapping_sub(tp.teleport_time) > 20 {
132 drop(tp);
133
134 let (yaw, pitch) = self.rotation();
135 if let Err(error) = self.teleport(pos, yaw, pitch) {
136 log::warn!(
137 "Failed to resend pending teleport for player {}: {error}",
138 self.id()
139 );
140 }
141 }
142 true
143 }
144
145 pub fn apply_post_impulse_grace_time(&self, ticks: i32) {
147 LivingEntity::apply_post_impulse_grace_time(self, ticks);
148 }
149
150 pub(super) fn reset_vehicle_movement_for_tick(&self) {
152 let Some(vehicle) = self.root_vehicle() else {
153 self.movement.lock().clear_vehicle_for_tick();
154 return;
155 };
156
157 let controlled_by_player = vehicle
158 .controlling_passenger()
159 .is_some_and(|controller| controller.id() == self.id());
160 if !controlled_by_player {
161 self.movement.lock().clear_vehicle_for_tick();
162 return;
163 }
164
165 self.movement
166 .lock()
167 .reset_vehicle_for_tick(vehicle.id(), vehicle.position());
168 }
169
170 fn should_validate_movement(world: &World, is_fall_flying: bool) -> bool {
177 let player_check = world.get_game_rule(&PLAYER_MOVEMENT_CHECK);
178 if !player_check {
179 return false;
180 }
181
182 if is_fall_flying {
183 let elytra_check = world.get_game_rule(&ELYTRA_MOVEMENT_CHECK);
184 return elytra_check;
185 }
186
187 true
188 }
189
190 #[expect(
199 clippy::too_many_lines,
200 reason = "matches vanilla handleMovePlayer; splitting would hurt readability"
201 )]
202 pub fn handle_move_player(&self, packet: SMovePlayer) {
203 if Self::is_invalid_position(
204 packet.get_x(0.0),
205 packet.get_y(0.0),
206 packet.get_z(0.0),
207 packet.get_x_rot(0.0),
208 packet.get_y_rot(0.0),
209 ) {
210 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT.msg());
211 return;
212 }
213 if self.has_won_game() {
214 return;
215 }
216
217 if self.is_world_change_pending() {
218 return;
219 }
220
221 let current_rotation = self.rotation();
222 let target_yaw = wrap_degrees(packet.get_y_rot(current_rotation.0));
223 let target_pitch = wrap_degrees(packet.get_x_rot(current_rotation.1));
224
225 if self.update_awaiting_teleport() {
226 self.set_rotation((target_yaw, target_pitch));
227 return;
228 }
229
230 if !self.has_client_loaded() {
231 return;
232 }
233
234 let start_pos = self.position();
235 let target_pos = DVec3::new(
236 clamp_horizontal(packet.get_x(start_pos.x)),
237 clamp_vertical(packet.get_y(start_pos.y)),
238 clamp_horizontal(packet.get_z(start_pos.z)),
239 );
240 let game_mode = self.game_mode();
241 let is_sleeping = self.is_sleeping();
242 let is_fall_flying = self.is_fall_flying();
243 let was_on_ground = self.on_ground();
244 let is_spectator = game_mode == GameType::Spectator;
245 let is_creative = game_mode == GameType::Creative;
246 let world = self.get_world();
247 let tick_runs_normally = world.tick_runs_normally();
248 if self.is_passenger() {
249 let passenger_pos = self.position();
250 if let Err(error) = self.try_set_position(passenger_pos) {
251 log::warn!(
252 "Failed to refresh passenger player {} position during movement: {error}",
253 self.id()
254 );
255 return;
256 }
257 self.set_rotation((target_yaw, target_pitch));
258 world.chunk_map.update_player_status(self);
259 return;
260 }
261
262 let (first_good, last_good) = self.movement.lock().good_positions();
263
264 if is_sleeping {
265 let dx = target_pos.x - first_good.x;
266 let dy = target_pos.y - first_good.y;
267 let dz = target_pos.z - first_good.z;
268 let moved_dist_sq = dx * dx + dy * dy + dz * dz;
269
270 if moved_dist_sq > 1.0 {
271 if let Err(error) = self.teleport(start_pos, target_yaw, target_pitch) {
272 log::warn!(
273 "Failed to correct sleeping player {} movement: {error}",
274 self.id()
275 );
276 }
277 return;
278 }
279 return;
280 }
281
282 let dx = target_pos.x - first_good.x;
283 let dy = target_pos.y - first_good.y;
284 let dz = target_pos.z - first_good.z;
285 let moved_dist_sq = dx * dx + dy * dy + dz * dz;
286
287 if tick_runs_normally {
288 let mut delta_packets = {
289 let mut mv = self.movement.lock();
290 mv.record_move_packet_delta()
291 };
292
293 if delta_packets > 5 {
294 delta_packets = 1;
295 }
296
297 if Self::should_validate_movement(&world, is_fall_flying) {
298 let threshold = if is_fall_flying {
299 SPEED_THRESHOLD_FLYING
300 } else {
301 SPEED_THRESHOLD_NORMAL
302 } * f64::from(delta_packets);
303
304 if moved_dist_sq - self.velocity().length_squared() > threshold {
305 if let Err(error) =
306 self.teleport(start_pos, current_rotation.0, current_rotation.1)
307 {
308 log::warn!(
309 "Failed to correct too-fast player {} movement: {error}",
310 self.id()
311 );
312 }
313 return;
314 }
315 }
316 }
317
318 let old_aabb = self.bounding_box();
319 let move_delta = target_pos - last_good;
320 let moved_upwards = move_delta.y > 0.0;
321 let player_stands_on_something = self.vertical_collision_below();
322
323 if was_on_ground && !packet.on_ground && moved_upwards {
324 self.jump_from_ground();
325 }
326
327 if self.move_entity(MoverType::Player, move_delta).is_none() {
328 if let Err(error) = self.teleport(start_pos, target_yaw, target_pitch) {
329 panic!(
330 "failed to correct rejected player {} movement: {error}",
331 self.id()
332 );
333 }
334 return;
335 }
336
337 let error_delta = movement_error_delta(target_pos, self.position());
338 let error_dist_sq = error_delta.length_squared();
339 let in_impulse_grace = self.is_in_post_impulse_grace_time();
340 let fail = error_dist_sq > MOVEMENT_ERROR_THRESHOLD
341 && !is_creative
342 && !is_spectator
343 && !in_impulse_grace;
344
345 let new_aabb = self.bounding_box().translate(target_pos - self.position());
346 let collision_world = WorldCollisionProvider::for_entity(&world, self);
347 let old_collision = collision_world.has_entity_context_collision(
348 old_aabb,
349 self.position().y,
350 self.is_descending(),
351 );
352 let new_collision =
353 is_colliding_with_new_shapes(&collision_world, old_aabb, new_aabb, self.is_crouching());
354
355 if (MovementCollisionValidation {
356 no_physics: self.no_physics(),
357 moved_wrongly: fail,
358 old_collision,
359 new_collision,
360 })
361 .rejects()
362 {
363 if let Err(error) = self.teleport(start_pos, target_yaw, target_pitch) {
364 log::warn!(
365 "Failed to correct collided player {} movement: {error}",
366 self.id()
367 );
368 }
369 self.refresh_supporting_block_for_fall_damage(DVec3::ZERO, packet.on_ground);
370 self.do_check_fall_damage(DVec3::ZERO, packet.on_ground, &world);
371 self.remove_latest_movement_recording();
372 return;
373 }
374
375 let floating_check = Some((player_stands_on_something, move_delta.y));
378
379 if packet.on_ground && self.is_sprinting() {
380 let dx = move_delta.x;
381 let dz = move_delta.z;
382
383 let cm = ((dx * dx + dz * dz).sqrt() as f32 * 100.0).round() as i32;
384 if cm > 0 {
385 self.cause_food_exhaustion(food_constants::EXHAUSTION_SPRINT * cm as f32 * 0.01);
386 }
387 }
388
389 let client_delta = target_pos - start_pos;
390 match self.apply_accepted_client_movement(
391 &world,
392 AcceptedClientMovement {
393 position: Some(target_pos),
394 rotation: (target_yaw, target_pitch),
395 on_ground: packet.on_ground,
396 horizontal_collision: packet.horizontal_collision,
397 movement: client_delta,
398 reset_fall_distance: moved_upwards,
399 },
400 ) {
401 Ok(AcceptedClientMovementOutcome::Applied) => {}
402 Ok(AcceptedClientMovementOutcome::Handled) => return,
403 Err(error) => {
404 log::warn!(
405 "Rejected accepted player movement for entity {}: {error}",
406 self.id()
407 );
408 if let Err(teleport_error) = self.teleport(start_pos, target_yaw, target_pitch) {
409 log::warn!(
410 "Failed to correct rejected player movement for entity {}: {teleport_error}",
411 self.id()
412 );
413 }
414 self.remove_latest_movement_recording();
415 return;
416 }
417 }
418 world.chunk_map.update_player_status(self);
419
420 if let Some((player_stands_on_something, y_dist)) = floating_check {
421 self.record_client_floating(
422 &world,
423 y_dist,
424 player_stands_on_something,
425 is_spectator,
426 is_fall_flying,
427 );
428 }
429 self.movement
430 .lock()
431 .mark_last_good_position(self.position());
432
433 self.movement
434 .lock()
435 .set_last_known_client_movement(client_delta);
436 }
437
438 #[expect(
442 clippy::too_many_lines,
443 reason = "matches vanilla handleMoveVehicle; splitting would hurt readability"
444 )]
445 pub fn handle_move_vehicle(&self, packet: SMoveVehicle) {
446 if Self::is_invalid_position(
447 packet.pos.x,
448 packet.pos.y,
449 packet.pos.z,
450 packet.x_rot,
451 packet.y_rot,
452 ) {
453 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_VEHICLE_MOVEMENT.msg());
454 return;
455 }
456
457 if self.is_world_change_pending() {
458 return;
459 }
460
461 if self.update_awaiting_teleport() || !self.has_client_loaded() {
462 return;
463 }
464
465 let Some(vehicle) = self.root_vehicle() else {
466 return;
467 };
468 if vehicle.is_world_change_pending() {
469 return;
470 }
471 let controlled_by_player = vehicle
472 .controlling_passenger()
473 .is_some_and(|controller| controller.id() == self.id());
474 if !controlled_by_player {
475 return;
476 }
477 let Some((first_good, last_good)) =
478 self.movement.lock().vehicle_good_positions(vehicle.id())
479 else {
480 return;
481 };
482
483 let world = self.get_world();
484 let old_position = vehicle.position();
485 let target_pos = DVec3::new(
486 clamp_horizontal(packet.pos.x),
487 clamp_vertical(packet.pos.y),
488 clamp_horizontal(packet.pos.z),
489 );
490 let target_yaw = wrap_degrees(packet.y_rot);
491 let target_pitch = wrap_degrees(packet.x_rot);
492 let first_good_delta = target_pos - first_good;
493 let moved_dist_sq = first_good_delta.length_squared();
494 let expected_dist_sq = vehicle.velocity().length_squared();
495 if moved_dist_sq - expected_dist_sq > SPEED_THRESHOLD_NORMAL {
496 log::warn!(
497 "{} (vehicle of {}) moved too quickly! {},{},{}",
498 vehicle.id(),
499 self.gameprofile.name,
500 first_good_delta.x,
501 first_good_delta.y,
502 first_good_delta.z
503 );
504 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
505 return;
506 }
507
508 let old_aabb = vehicle.bounding_box();
509 let move_delta = target_pos - last_good;
510 let vehicle_rests_on_something = vehicle.vertical_collision_below();
511 if vehicle.is_living_entity() && vehicle.on_climbable() {
512 vehicle.reset_fall_distance();
513 }
514
515 if vehicle.move_entity(MoverType::Player, move_delta).is_none() {
516 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
517 return;
518 }
519
520 let error_delta = movement_error_delta(target_pos, vehicle.position());
521 let error_dist_sq = error_delta.length_squared();
522 let fail = error_dist_sq > MOVEMENT_ERROR_THRESHOLD;
523 if fail {
524 log::warn!(
525 "{} (vehicle of {}) moved wrongly! {}",
526 vehicle.id(),
527 self.gameprofile.name,
528 error_dist_sq.sqrt()
529 );
530 }
531
532 let new_aabb = vehicle
533 .bounding_box()
534 .translate(target_pos - vehicle.position());
535 let collision_world = WorldCollisionProvider::for_entity(&world, vehicle.as_ref());
536 let old_collision = collision_world.has_entity_context_collision(
537 old_aabb,
538 vehicle.position().y,
539 vehicle.is_descending(),
540 );
541 let new_collision = is_colliding_with_new_shapes(
542 &collision_world,
543 old_aabb,
544 new_aabb,
545 vehicle.is_descending(),
546 );
547
548 if (MovementCollisionValidation {
549 no_physics: false,
550 moved_wrongly: fail,
551 old_collision,
552 new_collision,
553 })
554 .rejects()
555 {
556 if let Err(error) = vehicle.try_set_position(old_position) {
557 log::warn!(
558 "Failed to roll vehicle {} back after rejected movement: {error}",
559 vehicle.id()
560 );
561 }
562 vehicle.refresh_fluid_contact();
563 vehicle.set_rotation((target_yaw, target_pitch));
564 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
565 vehicle.remove_latest_movement_recording();
566 return;
567 }
568
569 let client_delta = target_pos - old_position;
570 match vehicle.apply_accepted_client_vehicle_movement(
571 &world,
572 AcceptedClientMovement {
573 position: Some(target_pos),
574 rotation: (target_yaw, target_pitch),
575 on_ground: packet.on_ground,
576 horizontal_collision: vehicle.horizontal_collision(),
577 movement: client_delta,
578 reset_fall_distance: false,
579 },
580 ) {
581 Ok(AcceptedClientMovementOutcome::Applied) => {}
582 Ok(AcceptedClientMovementOutcome::Handled) => return,
583 Err(error) => {
584 log::warn!(
585 "Rejected accepted vehicle movement for entity {}: {error}",
586 vehicle.id()
587 );
588 if let Err(rollback_error) = vehicle.try_set_position(old_position) {
589 log::warn!(
590 "Failed to roll vehicle {} back after rejected movement: {rollback_error}",
591 vehicle.id()
592 );
593 }
594 vehicle.refresh_fluid_contact();
595 vehicle.set_rotation((target_yaw, target_pitch));
596 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
597 vehicle.remove_latest_movement_recording();
598 return;
599 }
600 }
601 self.movement
602 .lock()
603 .set_last_known_client_movement(client_delta);
604 world.chunk_map.update_player_status(self);
605 self.record_client_vehicle_floating(
606 &world,
607 vehicle.as_ref(),
608 move_delta.y,
609 vehicle_rests_on_something,
610 );
611 self.movement
612 .lock()
613 .mark_vehicle_last_good_position(vehicle.id(), vehicle.position());
614 }
615
616 fn record_client_floating(
617 &self,
618 world: &World,
619 y_dist: f64,
620 player_stands_on_something: bool,
621 is_spectator: bool,
622 is_fall_flying: bool,
623 ) {
624 let can_violate_floating = PlayerFloatingValidation {
626 y_dist,
627 player_stands_on_something,
628 is_spectator,
629 server_allows_flight: self.config.allow_flight,
630 may_fly: self.abilities.lock().may_fly,
631 has_levitation: self.has_mob_effect(vanilla_mob_effects::LEVITATION),
632 is_fall_flying,
633 }
634 .can_violate();
635
636 let client_is_floating = can_violate_floating && Self::no_blocks_around_entity(world, self);
637 self.movement
638 .lock()
639 .record_client_floating(client_is_floating);
640 }
641
642 fn record_client_vehicle_floating(
643 &self,
644 world: &World,
645 vehicle: &dyn Entity,
646 y_dist: f64,
647 vehicle_rests_on_something: bool,
648 ) {
649 let client_is_floating = y_dist >= -0.03125
650 && !vehicle_rests_on_something
651 && !self.config.allow_flight
652 && !vehicle.is_flying_vehicle()
653 && !vehicle.is_no_gravity()
654 && Self::no_blocks_around_entity(world, vehicle);
655 self.movement
656 .lock()
657 .record_vehicle_client_floating(vehicle.id(), client_is_floating);
658 }
659
660 fn no_blocks_around_entity(world: &World, entity: &dyn Entity) -> bool {
661 let block_query = entity
662 .bounding_box()
663 .inflate(0.0625)
664 .expand_towards(DVec3::new(0.0, -0.55, 0.0));
665 world.block_states_in_aabb_are_air(block_query)
666 }
667
668 pub(super) fn maximum_flying_ticks(&self) -> i32 {
670 Self::maximum_flying_ticks_for_gravity(self.get_gravity())
671 }
672
673 #[expect(
675 clippy::cast_possible_truncation,
676 reason = "gravity threshold bounds the result far below i32::MAX"
677 )]
678 fn maximum_flying_ticks_for_gravity(gravity: f64) -> i32 {
679 if gravity < 1.0E-5 {
680 return i32::MAX;
681 }
682
683 let gravity_modifier = DEFAULT_GRAVITY / gravity;
684 (80.0 * gravity_modifier.max(1.0)).ceil() as i32
685 }
686
687 pub(super) fn disconnect_if_floating_too_long(&self) -> bool {
689 let should_count = !self.is_sleeping() && !self.is_passenger() && !self.is_dead_or_dying();
690 let maximum_flying_ticks = self.maximum_flying_ticks();
691 let should_disconnect = self
692 .movement
693 .lock()
694 .tick_client_floating(should_count, maximum_flying_ticks);
695
696 if should_disconnect {
697 log::warn!(
698 "{} was kicked for floating too long!",
699 self.gameprofile.name
700 );
701 self.disconnect(translations::MULTIPLAYER_DISCONNECT_FLYING.msg());
702 }
703
704 should_disconnect
705 }
706
707 pub(super) fn disconnect_if_vehicle_floating_too_long(&self) -> bool {
709 let Some(vehicle) = self.root_vehicle() else {
710 self.movement.lock().clear_vehicle_for_tick();
711 return false;
712 };
713 let controlled_by_player = vehicle
714 .controlling_passenger()
715 .is_some_and(|controller| controller.id() == self.id());
716 if !controlled_by_player {
717 self.movement.lock().clear_vehicle_for_tick();
718 return false;
719 }
720
721 let maximum_flying_ticks = Self::maximum_flying_ticks_for_gravity(vehicle.get_gravity());
722 let should_disconnect = self
723 .movement
724 .lock()
725 .tick_vehicle_client_floating(vehicle.id(), maximum_flying_ticks);
726
727 if should_disconnect {
728 log::warn!(
729 "{} was kicked for floating a vehicle too long!",
730 self.gameprofile.name
731 );
732 self.disconnect(translations::MULTIPLAYER_DISCONNECT_FLYING.msg());
733 }
734
735 should_disconnect
736 }
737
738 #[must_use]
740 pub fn is_awaiting_teleport(&self) -> bool {
741 self.teleport_state.lock().is_awaiting()
742 }
743
744 pub fn teleport(&self, pos: DVec3, yaw: f32, pitch: f32) -> Result<(), EntityMoveError> {
751 self.teleport_with_velocity(pos, DVec3::ZERO, yaw, pitch)
752 }
753
754 pub(crate) fn teleport_with_velocity(
757 &self,
758 pos: DVec3,
759 velocity: DVec3,
760 yaw: f32,
761 pitch: f32,
762 ) -> Result<(), EntityMoveError> {
763 self.teleport_with_velocity_packet(
764 pos,
765 velocity,
766 (yaw, pitch),
767 pos,
768 velocity,
769 (yaw, pitch),
770 RelativeMovement::NONE,
771 )
772 }
773
774 #[expect(
776 clippy::too_many_arguments,
777 reason = "packet-relative teleports must keep resolved and protocol values separate"
778 )]
779 pub(crate) fn teleport_with_velocity_packet(
780 &self,
781 pos: DVec3,
782 velocity: DVec3,
783 rotation: (f32, f32),
784 packet_pos: DVec3,
785 packet_velocity: DVec3,
786 packet_rotation: (f32, f32),
787 relatives: RelativeMovement,
788 ) -> Result<(), EntityMoveError> {
789 let world = self.get_world();
790 self.try_set_position(pos)?;
791 if world.entity_manager().get_by_id(self.id()).is_some() {
792 world.chunk_map.update_player_status(self);
793 }
794 self.set_velocity(velocity);
795
796 let new_id = {
797 let mut tp = self.teleport_state.lock();
798 tp.teleport_time = self.tick_count();
799 let id = tp.next_id();
800 tp.awaiting_position = Some(pos);
801 id
802 };
803
804 self.set_rotation(rotation);
805 self.set_old_position_to_current();
806 {
807 let mut movement = self.movement.lock();
808 movement.reset_last_known_client_movement();
809 }
810
811 self.send_packet(CPlayerPosition::new(
812 new_id,
813 packet_pos,
814 packet_velocity,
815 packet_rotation.0,
816 packet_rotation.1,
817 relatives,
818 ));
819 Ok(())
820 }
821
822 pub(crate) fn reset_flying_ticks(&self) {
824 self.movement.lock().reset_flying_ticks();
825 }
826
827 pub fn handle_accept_teleportation(&self, packet: SAcceptTeleportation) {
831 let mut tp = self.teleport_state.lock();
832
833 if let Some(pos) = tp.try_accept(packet.teleport_id) {
834 drop(tp);
835 if let Err(error) = self.try_set_position(pos) {
836 log::warn!(
837 "Failed to commit accepted teleport for player entity {}: {error}",
838 self.id()
839 );
840 self.teleport_state.lock().awaiting_position = Some(pos);
841 return;
842 }
843 self.set_old_position_to_current();
844 let mut movement = self.movement.lock();
845 movement.mark_last_good_position(pos);
846 movement.reset_last_known_client_movement();
847 } else if packet.teleport_id == tp.teleport_id && tp.awaiting_position.is_none() {
848 drop(tp);
849 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT.msg());
850 }
851 }
852
853 #[must_use]
855 pub fn last_client_input(&self) -> PlayerInput {
856 self.movement.lock().last_client_input()
857 }
858
859 pub fn handle_player_input(&self, packet: SPlayerInput) {
861 let input = PlayerInput::from_flags(packet.flags);
863 self.movement.lock().set_last_client_input(input);
864
865 if !self.has_client_loaded() {
866 return;
867 }
868
869 self.set_crouching(input.shift());
873 }
874
875 pub fn handle_player_command(&self, packet: SPlayerCommand) {
877 if !self.has_client_loaded() {
878 return;
879 }
880
881 if packet.entity_id != self.id() {
882 log::warn!(
883 "Player {} (eid {}) sent SPlayerCommand with mismatched entity_id {}",
884 self.gameprofile.name,
885 self.id(),
886 packet.entity_id
887 );
888 return;
889 }
890
891 match packet.action {
895 PlayerCommandAction::StartSprinting => {
896 self.set_sprinting(true);
897 }
898 PlayerCommandAction::StopSprinting => {
899 self.set_sprinting(false);
900 }
901 PlayerCommandAction::StartFallFlying => {
902 if !self.try_to_start_fall_flying() {
903 self.stop_fall_flying();
904 }
905 }
906 PlayerCommandAction::LeaveBed => {
907 if self.is_sleeping() {
908 self.stop_sleeping();
909 }
919 }
920 PlayerCommandAction::StartRidingJump
921 | PlayerCommandAction::StopRidingJump
922 | PlayerCommandAction::OpenVehicleInventory => {
923 }
925 }
926
927 }
929}
930
931#[cfg(test)]
932#[expect(clippy::float_cmp, reason = "exact match against vanilla test vectors")]
933mod tests {
934 use super::*;
935
936 #[test]
937 fn test_clamp_horizontal() {
938 assert_eq!(clamp_horizontal(0.0), 0.0);
939 assert_eq!(clamp_horizontal(1e8), CLAMP_HORIZONTAL);
940 assert_eq!(clamp_horizontal(-1e8), -CLAMP_HORIZONTAL);
941 }
942
943 #[test]
944 fn test_clamp_vertical() {
945 assert_eq!(clamp_vertical(0.0), 0.0);
946 assert_eq!(clamp_vertical(1e8), CLAMP_VERTICAL);
947 assert_eq!(clamp_vertical(-1e8), -CLAMP_VERTICAL);
948 }
949
950 #[test]
951 fn test_wrap_degrees() {
952 assert_eq!(wrap_degrees(181.0), -179.0);
953 assert_eq!(wrap_degrees(-181.0), 179.0);
954 assert_eq!(wrap_degrees(90.0), 90.0);
955 }
956
957 #[test]
958 fn floating_validation_exempts_levitation_like_vanilla() {
959 let validation = PlayerFloatingValidation {
960 y_dist: 0.0,
961 player_stands_on_something: false,
962 is_spectator: false,
963 server_allows_flight: false,
964 may_fly: false,
965 has_levitation: false,
966 is_fall_flying: false,
967 };
968
969 assert!(validation.can_violate());
970 assert!(
971 !PlayerFloatingValidation {
972 y_dist: -0.0784,
973 ..validation
974 }
975 .can_violate()
976 );
977 assert!(
978 !PlayerFloatingValidation {
979 has_levitation: true,
980 ..validation
981 }
982 .can_violate()
983 );
984 }
985}