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, Vec3Swizzles};
15use std::sync::Arc;
16use steel_math::wrap_degrees;
17use steel_protocol::packets::game::{
18 CMoveVehicle, CPlayerPosition, PlayerCommandAction, RelativeMovement, SAcceptTeleportation,
19 SMovePlayer, SMoveVehicle, SPlayerCommand, SPlayerInput,
20};
21use steel_registry::entity_type::EntityTypeRef;
22use steel_registry::stat::custom::CustomStatRef;
23use steel_registry::vanilla_game_rules::{ELYTRA_MOVEMENT_CHECK, PLAYER_MOVEMENT_CHECK};
24use steel_registry::{vanilla_custom_stats, vanilla_entities, vanilla_mob_effects};
25use steel_utils::types::GameType;
26use steel_utils::{TICKS_PER_SECOND, translations};
27
28use crate::entity::{
29 AcceptedClientMovement, AcceptedClientMovementOutcome, Entity, EntityMoveError, LivingEntity,
30 get_input_vector,
31};
32use crate::physics::{
33 MOVEMENT_ERROR_THRESHOLD, MovementCollisionValidation, MoverType, WorldCollisionProvider,
34 is_colliding_with_new_shapes, movement_error_delta,
35};
36use crate::player::Player;
37use crate::world::World;
38
39pub const DEFAULT_GRAVITY: f64 = 0.08;
41
42pub const SPEED_THRESHOLD_NORMAL: f64 = 100.0;
44pub const SPEED_THRESHOLD_FLYING: f64 = 300.0;
46
47pub const CLAMP_HORIZONTAL: f64 = 3.0E7;
49pub const CLAMP_VERTICAL: f64 = 2.0E7;
51
52pub const RESET_ACTION_TIME_DELTA_THRESHOLD: f64 = 1E-5;
54
55#[must_use]
57pub fn clamp_horizontal(value: f64) -> f64 {
58 value.clamp(-CLAMP_HORIZONTAL, CLAMP_HORIZONTAL)
59}
60
61#[must_use]
63pub fn clamp_vertical(value: f64) -> f64 {
64 value.clamp(-CLAMP_VERTICAL, CLAMP_VERTICAL)
65}
66
67#[must_use]
68pub(crate) fn custom_stat_from_riding_vehicle(
69 vehicle_entity_type: EntityTypeRef,
70) -> Option<CustomStatRef> {
71 if vehicle_entity_type.is_abstract_minecart {
72 Some(&vanilla_custom_stats::MINECART_ONE_CM)
73 } else if vehicle_entity_type.is_abstract_boat {
74 Some(&vanilla_custom_stats::BOAT_ONE_CM)
75 } else if vehicle_entity_type == &vanilla_entities::PIG {
76 Some(&vanilla_custom_stats::PIG_ONE_CM)
77 } else if vehicle_entity_type.is_abstract_horse {
78 Some(&vanilla_custom_stats::HORSE_ONE_CM)
79 } else if vehicle_entity_type == &vanilla_entities::STRIDER {
80 Some(&vanilla_custom_stats::STRIDER_ONE_CM)
81 } else if vehicle_entity_type == &vanilla_entities::HAPPY_GHAST {
82 Some(&vanilla_custom_stats::HAPPY_GHAST_ONE_CM)
83 } else if vehicle_entity_type.is_abstract_nautilus {
84 Some(&vanilla_custom_stats::NAUTILUS_ONE_CM)
85 } else {
86 None
87 }
88}
89
90const FLOATING_Y_THRESHOLD: f64 = -0.03125;
91
92#[derive(Debug, Clone, Copy)]
93struct PlayerFloatingValidation {
94 y_dist: f64,
95 player_stands_on_something: bool,
96 is_spectator: bool,
97 server_allows_flight: bool,
98 may_fly: bool,
99 has_levitation: bool,
100 is_fall_flying: bool,
101}
102
103impl PlayerFloatingValidation {
104 fn can_violate(self) -> bool {
105 self.y_dist >= FLOATING_Y_THRESHOLD
106 && !self.player_stands_on_something
107 && !self.is_spectator
108 && !self.server_allows_flight
109 && !self.may_fly
110 && !self.has_levitation
111 && !self.is_fall_flying
112 }
113}
114
115impl Player {
116 const fn is_invalid_position(x: f64, y: f64, z: f64, rot_x: f32, rot_y: f32) -> bool {
117 if x.is_nan() || y.is_nan() || z.is_nan() {
118 return true;
119 }
120
121 if !rot_x.is_finite() || !rot_y.is_finite() {
122 return true;
123 }
124
125 false
126 }
127
128 fn move_vehicle_packet_from_entity(entity: &dyn Entity) -> CMoveVehicle {
129 let rotation = entity.rotation();
130 CMoveVehicle {
131 position: entity.position(),
132 y_rot: rotation.0,
133 x_rot: rotation.1,
134 }
135 }
136
137 fn update_awaiting_teleport(&self) -> bool {
142 let mut tp = self.teleport_state.lock();
143 let Some(pos) = tp.awaiting_position else {
144 tp.teleport_time = self.tick_count();
145 return false;
146 };
147
148 let current_tick = self.tick_count();
149
150 if current_tick.wrapping_sub(tp.teleport_time) > 20 {
152 drop(tp);
153
154 let (yaw, pitch) = self.rotation();
155 if let Err(error) = self.teleport(pos, yaw, pitch) {
156 log::warn!(
157 "Failed to resend pending teleport for player {}: {error}",
158 self.id()
159 );
160 }
161 }
162 true
163 }
164
165 pub fn apply_post_impulse_grace_time(&self, ticks: i32) {
167 LivingEntity::apply_post_impulse_grace_time(self, ticks);
168 }
169
170 pub(super) fn reset_vehicle_movement_for_tick(&self) {
172 let Some(vehicle) = self.root_vehicle() else {
173 self.movement.lock().clear_vehicle_for_tick();
174 return;
175 };
176
177 let controlled_by_player = vehicle
178 .controlling_passenger()
179 .is_some_and(|controller| controller.id() == self.id());
180 if !controlled_by_player {
181 self.movement.lock().clear_vehicle_for_tick();
182 return;
183 }
184
185 self.movement
186 .lock()
187 .reset_vehicle_for_tick(vehicle.id(), vehicle.position());
188 }
189
190 fn should_validate_movement(world: &World, is_fall_flying: bool) -> bool {
196 let player_check = world.get_game_rule(&PLAYER_MOVEMENT_CHECK);
197 if !player_check {
198 return false;
199 }
200
201 if is_fall_flying {
202 let elytra_check = world.get_game_rule(&ELYTRA_MOVEMENT_CHECK);
203 return elytra_check;
204 }
205
206 true
207 }
208
209 pub fn handle_move_player(self: &Arc<Self>, packet: SMovePlayer) {
216 if Self::is_invalid_position(
217 packet.get_x(0.0),
218 packet.get_y(0.0),
219 packet.get_z(0.0),
220 packet.get_x_rot(0.0),
221 packet.get_y_rot(0.0),
222 ) {
223 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT.msg());
224 return;
225 }
226 if packet.has_pos && !self.movement.lock().record_position_packet() {
227 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT.msg());
228 return;
229 }
230
231 if self.has_won_game() {
232 return;
233 }
234
235 if self.is_world_change_pending() {
236 return;
237 }
238
239 let current_rotation = self.rotation();
240 let target_yaw = wrap_degrees(packet.get_y_rot(current_rotation.0));
241 let target_pitch = wrap_degrees(packet.get_x_rot(current_rotation.1));
242
243 if self.update_awaiting_teleport() {
244 self.set_rotation((target_yaw, target_pitch));
245 return;
246 }
247
248 if !self.has_client_loaded() {
249 return;
250 }
251
252 let current_position = self.position();
253 self.handle_player_position_change(
254 DVec3::new(
255 packet.get_x(current_position.x),
256 packet.get_y(current_position.y),
257 packet.get_z(current_position.z),
258 ),
259 packet.get_y_rot(current_rotation.0),
260 packet.get_x_rot(current_rotation.1),
261 packet.on_ground,
262 packet.horizontal_collision,
263 );
264 }
265
266 #[expect(
270 clippy::too_many_lines,
271 reason = "matches vanilla handlePlayerPositionChange; splitting would hurt readability"
272 )]
273 fn handle_player_position_change(
274 self: &Arc<Self>,
275 requested: DVec3,
276 requested_yaw: f32,
277 requested_pitch: f32,
278 is_on_ground: bool,
279 horizontal_collision: bool,
280 ) {
281 let current_rotation = self.rotation();
282 let target_yaw = wrap_degrees(requested_yaw);
283 let target_pitch = wrap_degrees(requested_pitch);
284 let start_pos = self.position();
285 let target_pos = DVec3::new(
286 clamp_horizontal(requested.x),
287 clamp_vertical(requested.y),
288 clamp_horizontal(requested.z),
289 );
290 let game_mode = self.game_mode();
291 let is_sleeping = self.is_sleeping();
292 let is_fall_flying = self.is_fall_flying();
293 let was_on_ground = self.on_ground();
294 let is_spectator = game_mode == GameType::Spectator;
295 let is_creative = game_mode == GameType::Creative;
296 let world = self.get_world();
297 let tick_runs_normally = world.tick_runs_normally();
298 if self.is_passenger() {
299 let passenger_pos = self.position();
300 if let Err(error) = self.try_set_position(passenger_pos) {
301 log::warn!(
302 "Failed to refresh passenger player {} position during movement: {error}",
303 self.id()
304 );
305 return;
306 }
307 self.set_rotation((target_yaw, target_pitch));
308 world.chunk_map.update_player_status(self);
309 return;
310 }
311
312 let (first_good, last_good) = self.movement.lock().good_positions();
313
314 if is_sleeping {
315 let dx = target_pos.x - first_good.x;
316 let dy = target_pos.y - first_good.y;
317 let dz = target_pos.z - first_good.z;
318 let moved_dist_sq = dx * dx + dy * dy + dz * dz;
319
320 if moved_dist_sq > 1.0 {
321 if let Err(error) = self.teleport(start_pos, target_yaw, target_pitch) {
322 log::warn!(
323 "Failed to correct sleeping player {} movement: {error}",
324 self.id()
325 );
326 }
327 return;
328 }
329 return;
330 }
331
332 let dx = target_pos.x - first_good.x;
333 let dy = target_pos.y - first_good.y;
334 let dz = target_pos.z - first_good.z;
335 let moved_dist_sq = dx * dx + dy * dy + dz * dz;
336
337 if tick_runs_normally {
338 let mut delta_packets = {
339 let mut mv = self.movement.lock();
340 mv.record_move_packet_delta()
341 };
342
343 if delta_packets > 5 {
344 delta_packets = 1;
345 }
346
347 if Self::should_validate_movement(&world, is_fall_flying) {
348 let threshold = if is_fall_flying {
349 SPEED_THRESHOLD_FLYING
350 } else {
351 SPEED_THRESHOLD_NORMAL
352 } * f64::from(delta_packets);
353
354 if moved_dist_sq - self.velocity().length_squared() > threshold {
355 if let Err(error) =
356 self.teleport(start_pos, current_rotation.0, current_rotation.1)
357 {
358 log::warn!(
359 "Failed to correct too-fast player {} movement: {error}",
360 self.id()
361 );
362 }
363 return;
364 }
365 }
366 }
367
368 let old_aabb = self.bounding_box();
369 let move_delta = target_pos - last_good;
370 let moved_upwards = move_delta.y > 0.0;
371 let player_stands_on_something = self.vertical_collision_below();
372
373 if was_on_ground && !is_on_ground && moved_upwards {
374 self.jump_from_ground();
375 }
376
377 if Arc::clone(self)
378 .move_entity(MoverType::Player, move_delta)
379 .is_none()
380 {
381 if let Err(error) = self.teleport(start_pos, target_yaw, target_pitch) {
382 panic!(
383 "failed to correct rejected player {} movement: {error}",
384 self.id()
385 );
386 }
387 return;
388 }
389
390 let error_delta = movement_error_delta(target_pos, self.position());
391 let error_dist_sq = error_delta.length_squared();
392 let in_impulse_grace = self.is_in_post_impulse_grace_time();
393 let fail = error_dist_sq > MOVEMENT_ERROR_THRESHOLD
394 && !is_creative
395 && !is_spectator
396 && !in_impulse_grace;
397
398 let new_aabb = self.bounding_box().translate(target_pos - self.position());
399 let collision_world = WorldCollisionProvider::for_entity(&world, self.as_ref());
400 let old_collision = collision_world.has_entity_context_collision(
401 old_aabb,
402 self.position().y,
403 self.is_descending(),
404 );
405 let new_collision =
406 is_colliding_with_new_shapes(&collision_world, old_aabb, new_aabb, self.is_crouching());
407
408 if (MovementCollisionValidation {
409 no_physics: self.no_physics(),
410 moved_wrongly: fail,
411 old_collision,
412 new_collision,
413 })
414 .rejects()
415 {
416 if let Err(error) = self.teleport(start_pos, target_yaw, target_pitch) {
417 log::warn!(
418 "Failed to correct collided player {} movement: {error}",
419 self.id()
420 );
421 }
422 self.refresh_supporting_block_for_fall_damage(DVec3::ZERO, is_on_ground);
423 Arc::clone(self).do_check_fall_damage(DVec3::ZERO, is_on_ground, &world);
424 self.remove_latest_movement_recording();
425 return;
426 }
427
428 let floating_check = Some((player_stands_on_something, move_delta.y));
431
432 let client_delta = target_pos - start_pos;
433 match Arc::clone(self).apply_accepted_client_movement(
434 &world,
435 AcceptedClientMovement {
436 position: Some(target_pos),
437 rotation: (target_yaw, target_pitch),
438 on_ground: is_on_ground,
439 horizontal_collision,
440 movement: client_delta,
441 reset_fall_distance: moved_upwards,
442 },
443 ) {
444 Ok(AcceptedClientMovementOutcome::Applied) => {}
445 Ok(AcceptedClientMovementOutcome::Handled) => return,
446 Err(error) => {
447 log::warn!(
448 "Rejected accepted player movement for entity {}: {error}",
449 self.id()
450 );
451 if let Err(teleport_error) = self.teleport(start_pos, target_yaw, target_pitch) {
452 log::warn!(
453 "Failed to correct rejected player movement for entity {}: {teleport_error}",
454 self.id()
455 );
456 }
457 self.remove_latest_movement_recording();
458 return;
459 }
460 }
461
462 self.check_movement_statistics(client_delta);
463 world.chunk_map.update_player_status(self);
464
465 if let Some((player_stands_on_something, y_dist)) = floating_check {
466 self.record_client_floating(
467 &world,
468 y_dist,
469 player_stands_on_something,
470 is_spectator,
471 is_fall_flying,
472 );
473 }
474 self.movement
475 .lock()
476 .mark_last_good_position(self.position());
477 self.handle_player_known_movement(client_delta);
478 }
479
480 #[expect(
482 clippy::too_many_lines,
483 reason = "matches vanilla handleMoveVehicle; splitting would hurt readability"
484 )]
485 pub fn handle_move_vehicle(&self, packet: SMoveVehicle) {
486 if Self::is_invalid_position(
487 packet.pos.x,
488 packet.pos.y,
489 packet.pos.z,
490 packet.x_rot,
491 packet.y_rot,
492 ) {
493 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_VEHICLE_MOVEMENT.msg());
494 return;
495 }
496
497 if self.is_world_change_pending() {
498 return;
499 }
500
501 if self.update_awaiting_teleport() || !self.has_client_loaded() {
502 return;
503 }
504
505 let Some(vehicle) = self.root_vehicle() else {
506 return;
507 };
508 if vehicle.is_world_change_pending() {
509 return;
510 }
511 let controlled_by_player = vehicle
512 .controlling_passenger()
513 .is_some_and(|controller| controller.id() == self.id());
514 if !controlled_by_player {
515 let current_tick = self.tick_count();
516 let last_reset = self.movement.lock().vehicle_position_last_reset_at;
517
518 if current_tick.wrapping_sub(last_reset) > TICKS_PER_SECOND
519 || vehicle.position().distance_squared(packet.pos) > 1.0
520 {
521 log::warn!(
522 "{} was expected to be controlling vehicle {} but was not. Resetting vehicle position.",
523 self.gameprofile.name(),
524 vehicle.id()
525 );
526
527 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
528 self.movement.lock().vehicle_position_last_reset_at = current_tick;
529 }
530
531 return;
532 }
533 let Some((first_good, last_good)) =
534 self.movement.lock().vehicle_good_positions(vehicle.id())
535 else {
536 return;
537 };
538
539 let world = self.get_world();
540 let old_position = vehicle.position();
541 let target_pos = DVec3::new(
542 clamp_horizontal(packet.pos.x),
543 clamp_vertical(packet.pos.y),
544 clamp_horizontal(packet.pos.z),
545 );
546 let target_yaw = wrap_degrees(packet.y_rot);
547 let target_pitch = wrap_degrees(packet.x_rot);
548 let first_good_delta = target_pos - first_good;
549 let moved_dist_sq = first_good_delta.length_squared();
550 let expected_dist_sq = vehicle.velocity().length_squared();
551 if moved_dist_sq - expected_dist_sq > SPEED_THRESHOLD_NORMAL {
552 log::warn!(
553 "{} (vehicle of {}) moved too quickly! {},{},{}",
554 vehicle.id(),
555 self.gameprofile.name(),
556 first_good_delta.x,
557 first_good_delta.y,
558 first_good_delta.z
559 );
560 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
561 return;
562 }
563
564 let old_aabb = vehicle.bounding_box();
565 let move_delta = target_pos - last_good;
566 let vehicle_rests_on_something = vehicle.vertical_collision_below();
567 if vehicle.is_living_entity() && vehicle.on_climbable() {
568 vehicle.reset_fall_distance();
569 }
570
571 if Arc::clone(&vehicle)
572 .move_entity(MoverType::Player, move_delta)
573 .is_none()
574 {
575 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
576 return;
577 }
578
579 let error_delta = movement_error_delta(target_pos, vehicle.position());
580 let error_dist_sq = error_delta.length_squared();
581 let fail = error_dist_sq > MOVEMENT_ERROR_THRESHOLD;
582 if fail {
583 log::warn!(
584 "{} (vehicle of {}) moved wrongly! {}",
585 vehicle.id(),
586 self.gameprofile.name(),
587 error_dist_sq.sqrt()
588 );
589 }
590
591 let new_aabb = vehicle
592 .bounding_box()
593 .translate(target_pos - vehicle.position());
594 let collision_world = WorldCollisionProvider::for_entity(&world, vehicle.as_ref());
595 let old_collision = collision_world.has_entity_context_collision(
596 old_aabb,
597 vehicle.position().y,
598 vehicle.is_descending(),
599 );
600 let new_collision = is_colliding_with_new_shapes(
601 &collision_world,
602 old_aabb,
603 new_aabb,
604 vehicle.is_descending(),
605 );
606
607 if (MovementCollisionValidation {
608 no_physics: false,
609 moved_wrongly: fail,
610 old_collision,
611 new_collision,
612 })
613 .rejects()
614 {
615 if let Err(error) = vehicle.try_set_position(old_position) {
616 log::warn!(
617 "Failed to roll vehicle {} back after rejected movement: {error}",
618 vehicle.id()
619 );
620 }
621 vehicle.set_rotation((target_yaw, target_pitch));
622 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
623 vehicle.remove_latest_movement_recording();
624 return;
625 }
626
627 let client_delta = target_pos - old_position;
628 match Arc::clone(&vehicle).apply_accepted_client_vehicle_movement(
629 &world,
630 AcceptedClientMovement {
631 position: Some(target_pos),
632 rotation: (target_yaw, target_pitch),
633 on_ground: packet.on_ground,
634 horizontal_collision: vehicle.horizontal_collision(),
635 movement: client_delta,
636 reset_fall_distance: false,
637 },
638 ) {
639 Ok(AcceptedClientMovementOutcome::Applied) => {}
640 Ok(AcceptedClientMovementOutcome::Handled) => return,
641 Err(error) => {
642 log::warn!(
643 "Rejected accepted vehicle movement for entity {}: {error}",
644 vehicle.id()
645 );
646 if let Err(rollback_error) = vehicle.try_set_position(old_position) {
647 log::warn!(
648 "Failed to roll vehicle {} back after rejected movement: {rollback_error}",
649 vehicle.id()
650 );
651 }
652 vehicle.set_rotation((target_yaw, target_pitch));
653 self.send_packet(Self::move_vehicle_packet_from_entity(vehicle.as_ref()));
654 vehicle.remove_latest_movement_recording();
655 return;
656 }
657 }
658 self.handle_player_known_movement(client_delta);
659 self.check_movement_statistics(client_delta);
660 world.chunk_map.update_player_status(self);
661 self.record_client_vehicle_floating(
662 &world,
663 vehicle.as_ref(),
664 move_delta.y,
665 vehicle_rests_on_something,
666 );
667 self.movement
668 .lock()
669 .mark_vehicle_last_good_position(vehicle.id(), vehicle.position());
670 }
671
672 fn handle_player_known_movement(&self, client_delta: DVec3) {
673 if client_delta.length_squared() > RESET_ACTION_TIME_DELTA_THRESHOLD {
674 self.reset_last_action_time();
675 }
676 self.movement
677 .lock()
678 .set_last_known_client_movement(client_delta);
679 }
680
681 fn record_client_floating(
682 &self,
683 world: &World,
684 y_dist: f64,
685 player_stands_on_something: bool,
686 is_spectator: bool,
687 is_fall_flying: bool,
688 ) {
689 let can_violate_floating = PlayerFloatingValidation {
691 y_dist,
692 player_stands_on_something,
693 is_spectator,
694 server_allows_flight: self.config.allow_flight,
695 may_fly: self.abilities.lock().may_fly,
696 has_levitation: self.has_mob_effect(vanilla_mob_effects::LEVITATION),
697 is_fall_flying,
698 }
699 .can_violate();
700
701 let client_is_floating = can_violate_floating && Self::no_blocks_around_entity(world, self);
702 self.movement
703 .lock()
704 .record_client_floating(client_is_floating);
705 }
706
707 fn record_client_vehicle_floating(
708 &self,
709 world: &World,
710 vehicle: &dyn Entity,
711 y_dist: f64,
712 vehicle_rests_on_something: bool,
713 ) {
714 let client_is_floating = y_dist >= FLOATING_Y_THRESHOLD
715 && !vehicle_rests_on_something
716 && !self.config.allow_flight
717 && !vehicle.is_flying_vehicle()
718 && !vehicle.is_no_gravity()
719 && Self::no_blocks_around_entity(world, vehicle);
720 self.movement
721 .lock()
722 .record_vehicle_client_floating(vehicle.id(), client_is_floating);
723 }
724
725 fn no_blocks_around_entity(world: &World, entity: &dyn Entity) -> bool {
726 let block_query = entity
727 .bounding_box()
728 .inflate(0.0625)
729 .expand_towards(DVec3::new(0.0, -0.55, 0.0));
730 world.block_states_in_aabb_are_air(block_query)
731 }
732
733 pub(super) fn maximum_flying_ticks(&self) -> i32 {
735 Self::maximum_flying_ticks_for_gravity(self.get_gravity())
736 }
737
738 #[expect(
740 clippy::cast_possible_truncation,
741 reason = "gravity threshold bounds the result far below i32::MAX"
742 )]
743 fn maximum_flying_ticks_for_gravity(gravity: f64) -> i32 {
744 if gravity < 1.0E-5 {
745 return i32::MAX;
746 }
747
748 let gravity_modifier = DEFAULT_GRAVITY / gravity;
749 (80.0 * gravity_modifier.max(1.0)).ceil() as i32
750 }
751
752 pub(super) fn disconnect_if_floating_too_long(&self) -> bool {
754 let should_count = !self.is_sleeping() && !self.is_passenger() && !self.is_dead_or_dying();
755 let maximum_flying_ticks = self.maximum_flying_ticks();
756 let should_disconnect = self
757 .movement
758 .lock()
759 .tick_client_floating(should_count, maximum_flying_ticks);
760
761 if should_disconnect {
762 log::warn!(
763 "{} was kicked for floating too long!",
764 self.gameprofile.name()
765 );
766 self.disconnect(translations::MULTIPLAYER_DISCONNECT_FLYING.msg());
767 }
768
769 should_disconnect
770 }
771
772 pub(super) fn disconnect_if_vehicle_floating_too_long(&self) -> bool {
774 let Some(vehicle) = self.root_vehicle() else {
775 self.movement.lock().clear_vehicle_for_tick();
776 return false;
777 };
778 let controlled_by_player = vehicle
779 .controlling_passenger()
780 .is_some_and(|controller| controller.id() == self.id());
781 if !controlled_by_player {
782 self.movement.lock().clear_vehicle_for_tick();
783 return false;
784 }
785
786 let maximum_flying_ticks = Self::maximum_flying_ticks_for_gravity(vehicle.get_gravity());
787 let should_disconnect = self
788 .movement
789 .lock()
790 .tick_vehicle_client_floating(vehicle.id(), maximum_flying_ticks);
791
792 if should_disconnect {
793 log::warn!(
794 "{} was kicked for floating a vehicle too long!",
795 self.gameprofile.name()
796 );
797 self.disconnect(translations::MULTIPLAYER_DISCONNECT_FLYING.msg());
798 }
799
800 should_disconnect
801 }
802
803 #[must_use]
805 pub fn is_awaiting_teleport(&self) -> bool {
806 self.teleport_state.lock().is_awaiting()
807 }
808
809 pub fn teleport(&self, pos: DVec3, yaw: f32, pitch: f32) -> Result<(), EntityMoveError> {
814 self.teleport_with_velocity(pos, DVec3::ZERO, yaw, pitch)
815 }
816
817 pub(crate) fn teleport_with_velocity(
820 &self,
821 pos: DVec3,
822 velocity: DVec3,
823 yaw: f32,
824 pitch: f32,
825 ) -> Result<(), EntityMoveError> {
826 self.teleport_with_velocity_packet(
827 pos,
828 velocity,
829 (yaw, pitch),
830 pos,
831 velocity,
832 (yaw, pitch),
833 RelativeMovement::NONE,
834 )
835 }
836
837 #[expect(
839 clippy::too_many_arguments,
840 reason = "packet-relative teleports must keep resolved and protocol values separate"
841 )]
842 pub(crate) fn teleport_with_velocity_packet(
843 &self,
844 pos: DVec3,
845 velocity: DVec3,
846 rotation: (f32, f32),
847 packet_pos: DVec3,
848 packet_velocity: DVec3,
849 packet_rotation: (f32, f32),
850 relatives: RelativeMovement,
851 ) -> Result<(), EntityMoveError> {
852 let world = self.get_world();
853 self.try_set_position(pos)?;
854 if world.entity_manager().get_by_id(self.id()).is_some() {
855 world.chunk_map.update_player_status(self);
856 }
857 self.set_velocity(velocity);
858
859 let new_id = {
860 let mut tp = self.teleport_state.lock();
861 tp.teleport_time = self.tick_count();
862 let id = tp.next_id();
863 tp.awaiting_position = Some(pos);
864 id
865 };
866
867 self.set_rotation(rotation);
868 self.set_old_position_to_current();
869 {
870 let mut movement = self.movement.lock();
871 movement.reset_last_known_client_movement();
872 }
873
874 self.send_packet(CPlayerPosition::new(
875 new_id,
876 packet_pos,
877 packet_velocity,
878 packet_rotation.0,
879 packet_rotation.1,
880 relatives,
881 ));
882 Ok(())
883 }
884
885 pub(crate) fn reset_flying_ticks(&self) {
887 self.movement.lock().reset_flying_ticks();
888 }
889
890 pub fn handle_accept_teleportation(self: &Arc<Self>, packet: SAcceptTeleportation) {
892 let mut tp = self.teleport_state.lock();
893
894 if packet.teleport_id == tp.teleport_id
896 && Self::is_invalid_position(packet.x, packet.y, packet.z, packet.x_rot, packet.y_rot)
897 {
898 drop(tp);
899 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT.msg());
900 return;
901 }
902
903 if let Some(pos) = tp.try_accept(packet.teleport_id) {
904 drop(tp);
905 if let Err(error) = self.try_set_position(pos) {
906 log::warn!(
907 "Failed to commit accepted teleport for player entity {}: {error}",
908 self.id()
909 );
910 self.teleport_state.lock().awaiting_position = Some(pos);
911 return;
912 }
913 self.set_old_position_to_current();
914 {
915 let mut movement = self.movement.lock();
916 movement.mark_last_good_position(pos);
917 movement.reset_last_known_client_movement();
918 }
919
920 self.handle_player_position_change(
923 DVec3::new(packet.x, packet.y, packet.z),
924 packet.y_rot,
925 packet.x_rot,
926 false,
927 false,
928 );
929 } else if packet.teleport_id == tp.teleport_id && tp.awaiting_position.is_none() {
930 drop(tp);
931 self.disconnect(translations::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT.msg());
932 }
933 }
934
935 #[must_use]
937 pub fn last_client_input(&self) -> PlayerInput {
938 self.movement.lock().last_client_input()
939 }
940
941 #[must_use]
943 pub fn last_client_move_intent(&self) -> DVec3 {
944 get_input_vector(
945 self.last_client_input().movement_input(),
946 1.0,
947 self.rotation().0,
948 )
949 }
950
951 pub fn handle_player_input(&self, packet: SPlayerInput) {
953 let input = PlayerInput::from_flags(packet.flags);
955 self.movement.lock().set_last_client_input(input);
956
957 if !self.has_client_loaded() {
958 return;
959 }
960
961 self.reset_last_action_time();
962
963 self.set_crouching(input.shift());
964 }
965
966 pub fn handle_player_command(&self, packet: SPlayerCommand) {
968 if !self.has_client_loaded() {
969 return;
970 }
971
972 if packet.entity_id != self.id() {
973 log::warn!(
974 "Player {} (eid {}) sent SPlayerCommand with mismatched entity_id {}",
975 self.gameprofile.name(),
976 self.id(),
977 packet.entity_id
978 );
979 return;
980 }
981
982 self.reset_last_action_time();
983
984 match packet.action {
985 PlayerCommandAction::StartSprinting => {
986 self.set_sprinting(true);
987 }
988 PlayerCommandAction::StopSprinting => {
989 self.set_sprinting(false);
990 }
991 PlayerCommandAction::StartFallFlying => {
992 if !self.try_to_start_fall_flying() {
993 self.stop_fall_flying();
994 }
995 }
996 PlayerCommandAction::LeaveBed => {
997 if self.is_sleeping() {
998 self.stop_sleep_in_bed(false, true);
999 }
1000 }
1001 PlayerCommandAction::StartRidingJump
1002 | PlayerCommandAction::StopRidingJump
1003 | PlayerCommandAction::OpenVehicleInventory => {
1004 }
1006 }
1007
1008 }
1010
1011 pub fn check_movement_statistics(&self, delta_movement: DVec3) {
1016 if self.is_passenger() || delta_movement == DVec3::ZERO {
1017 return;
1018 }
1019 if self.is_swimming() {
1020 self.award_3d_distance_stat(&vanilla_custom_stats::SWIM_ONE_CM, delta_movement, 0.01);
1021 } else if self.is_eye_in_water() {
1022 self.award_3d_distance_stat(
1023 &vanilla_custom_stats::WALK_UNDER_WATER_ONE_CM,
1024 delta_movement,
1025 0.01,
1026 );
1027 } else if self.is_in_water() {
1028 self.award_horizontal_distance_stat(
1029 &vanilla_custom_stats::WALK_ON_WATER_ONE_CM,
1030 delta_movement,
1031 0.01,
1032 );
1033 } else if self.on_climbable() && delta_movement.y > 0.0 {
1034 self.award_custom_stat_with_count(
1035 &vanilla_custom_stats::CLIMB_ONE_CM,
1036 (delta_movement.y * 100.0).round() as i32,
1037 );
1038 } else if self.on_ground() {
1039 if self.is_sprinting() {
1040 self.award_horizontal_distance_stat(
1041 &vanilla_custom_stats::SPRINT_ONE_CM,
1042 delta_movement,
1043 0.1,
1044 );
1045 } else if self.is_crouching() {
1046 self.award_horizontal_distance_stat(
1047 &vanilla_custom_stats::CROUCH_ONE_CM,
1048 delta_movement,
1049 0.0,
1050 );
1051 } else {
1052 self.award_horizontal_distance_stat(
1053 &vanilla_custom_stats::WALK_ONE_CM,
1054 delta_movement,
1055 0.0,
1056 );
1057 }
1058 } else if self.is_fall_flying() {
1059 self.award_3d_distance_stat(&vanilla_custom_stats::AVIATE_ONE_CM, delta_movement, 0.0);
1060 } else {
1061 self.award_horizontal_distance_stat_with_lower_bound(
1062 &vanilla_custom_stats::FLY_ONE_CM,
1063 delta_movement,
1064 0.0,
1065 25,
1066 );
1067 }
1068 }
1069
1070 pub fn check_riding_statistics(&self, delta_movement: DVec3) {
1075 if delta_movement == DVec3::ZERO {
1076 return;
1077 }
1078 let Some(vehicle) = self.vehicle() else {
1079 return;
1080 };
1081
1082 if let Some(stat) = custom_stat_from_riding_vehicle(vehicle.entity_type()) {
1083 let distance = (delta_movement.length() as f32 * 100.0).round() as i32;
1084 self.award_custom_stat_with_count(stat, distance);
1085 }
1086 }
1087
1088 fn award_3d_distance_stat(
1089 &self,
1090 stat: CustomStatRef,
1091 delta_movement: DVec3,
1092 exhaust_multiplier: f32,
1093 ) {
1094 let distance = (delta_movement.length() as f32 * 100.0).round() as i32;
1095 if distance > 0 {
1096 self.award_custom_stat_with_count(stat, distance);
1097 self.cause_food_exhaustion(exhaust_multiplier * distance as f32 * 0.01);
1098 }
1099 }
1100
1101 fn award_horizontal_distance_stat(
1102 &self,
1103 stat: CustomStatRef,
1104 delta_movement: DVec3,
1105 exhaust_multiplier: f32,
1106 ) {
1107 self.award_horizontal_distance_stat_with_lower_bound(
1108 stat,
1109 delta_movement,
1110 exhaust_multiplier,
1111 0,
1112 );
1113 }
1114
1115 fn award_horizontal_distance_stat_with_lower_bound(
1116 &self,
1117 stat: CustomStatRef,
1118 delta_movement: DVec3,
1119 exhaust_multiplier: f32,
1120 lower_bound: i32,
1121 ) {
1122 let horizontal_distance = (delta_movement.xz().length() as f32 * 100.0).round() as i32;
1123 if horizontal_distance > lower_bound {
1124 self.award_custom_stat_with_count(stat, horizontal_distance);
1125 if exhaust_multiplier != 0.0 {
1126 self.cause_food_exhaustion(exhaust_multiplier * horizontal_distance as f32 * 0.01);
1127 }
1128 }
1129 }
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135
1136 #[test]
1137 fn test_clamp_horizontal() {
1138 assert_eq!(clamp_horizontal(0.0), 0.0);
1139 assert_eq!(clamp_horizontal(1e8), CLAMP_HORIZONTAL);
1140 assert_eq!(clamp_horizontal(-1e8), -CLAMP_HORIZONTAL);
1141 }
1142
1143 #[test]
1144 fn test_clamp_vertical() {
1145 assert_eq!(clamp_vertical(0.0), 0.0);
1146 assert_eq!(clamp_vertical(1e8), CLAMP_VERTICAL);
1147 assert_eq!(clamp_vertical(-1e8), -CLAMP_VERTICAL);
1148 }
1149
1150 #[test]
1151 fn test_wrap_degrees() {
1152 assert_eq!(wrap_degrees(181.0), -179.0);
1153 assert_eq!(wrap_degrees(-181.0), 179.0);
1154 assert_eq!(wrap_degrees(90.0), 90.0);
1155 }
1156
1157 #[test]
1158 fn sprinting_charges_food_exhaustion_once_per_move() {
1159 use steel_utils::ChunkPos;
1160
1161 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
1162
1163 let world_fixture = fresh_test_world("sprint_exhaustion_single_charge");
1164 let world = &world_fixture.world;
1165 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
1166
1167 let player = TestPlayerBuilder::new(Arc::clone(world), "SprintTester", 1).build();
1168 player.set_client_loaded(true);
1169
1170 let start = DVec3::new(8.0, 64.0, 8.0);
1171 player.base().set_position_local(start);
1172 player.movement.lock().reset_for_position_sync(start);
1173 player.set_sprinting(true);
1174 player.food_data.lock().exhaustion_level = 0.0;
1175
1176 player.handle_move_player(SMovePlayer {
1180 position: start + DVec3::new(0.25, 0.0, 0.0),
1181 y_rot: 0.0,
1182 x_rot: 0.0,
1183 on_ground: true,
1184 horizontal_collision: false,
1185 has_pos: true,
1186 has_rot: false,
1187 });
1188
1189 let exhaustion = player.food_data.lock().exhaustion_level;
1190 assert!(
1191 (exhaustion - 0.025).abs() < 1e-4,
1192 "sprinting should charge exhaustion once (0.025), got {exhaustion}"
1193 );
1194 }
1195
1196 #[test]
1197 fn floating_validation_exempts_levitation_like_vanilla() {
1198 let validation = PlayerFloatingValidation {
1199 y_dist: 0.0,
1200 player_stands_on_something: false,
1201 is_spectator: false,
1202 server_allows_flight: false,
1203 may_fly: false,
1204 has_levitation: false,
1205 is_fall_flying: false,
1206 };
1207
1208 assert!(validation.can_violate());
1209 assert!(
1210 !PlayerFloatingValidation {
1211 y_dist: -0.0784,
1212 ..validation
1213 }
1214 .can_violate()
1215 );
1216 assert!(
1217 !PlayerFloatingValidation {
1218 has_levitation: true,
1219 ..validation
1220 }
1221 .can_violate()
1222 );
1223 }
1224}