Skip to main content

azalea_client/plugins/
movement.rs

1use azalea_core::{
2    entity_id::MinecraftEntityId,
3    game_type::GameMode,
4    position::{Vec2, Vec3},
5    tick::GameTick,
6};
7use azalea_entity::{
8    ActiveEffects, Attributes, Crouching, EntityGeometryUpdateSystems, HasClientLoaded, Jumping,
9    LastSentPosition, LocalEntity, LookDirection, OnClimbable, Physics, PlayerAbilities, Pose,
10    Position,
11    dimensions::calculate_dimensions,
12    inventory::Inventory,
13    metadata::{self, FallFlying, Sprinting},
14    update_bounding_box,
15};
16use azalea_inventory::{
17    ItemStack,
18    components::{self, EquipmentSlot},
19};
20use azalea_physics::{
21    PhysicsSystems, ai_step,
22    client_movement::{ClientMovementState, SprintDirection, WalkDirection},
23    collision::entity_collisions::{AabbQuery, CollidableEntityQuery, update_last_bounding_box},
24    travel::{no_collision, travel},
25};
26use azalea_protocol::{
27    common::movements::MoveFlags,
28    packets::{
29        Packet,
30        game::{
31            ServerboundPlayerCommand, ServerboundPlayerInput,
32            s_move_player_pos::ServerboundMovePlayerPos,
33            s_move_player_pos_rot::ServerboundMovePlayerPosRot,
34            s_move_player_rot::ServerboundMovePlayerRot,
35            s_move_player_status_only::ServerboundMovePlayerStatusOnly, s_player_command,
36        },
37    },
38};
39use azalea_registry::builtin::{EntityKind, MobEffect};
40use azalea_world::World;
41use bevy_app::{App, Plugin, Update};
42use bevy_ecs::prelude::*;
43
44use crate::{
45    local_player::{Hunger, WorldHolder},
46    packet::game::SendGamePacketEvent,
47};
48
49pub struct MovementPlugin;
50
51impl Plugin for MovementPlugin {
52    fn build(&self, app: &mut App) {
53        app.add_message::<StartWalkEvent>()
54            .add_message::<StartSprintEvent>()
55            .add_systems(
56                Update,
57                (handle_sprint, handle_walk)
58                    .chain()
59                    .in_set(MoveEventsSystems)
60                    .after(update_bounding_box)
61                    .after(update_last_bounding_box),
62            )
63            .add_systems(
64                GameTick,
65                (
66                    (
67                        tick_controls,
68                        local_player_ai_step,
69                        process_fall_flying_activation,
70                    )
71                        .chain()
72                        .in_set(PhysicsSystems)
73                        .before(ai_step)
74                        .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
75                    send_player_input_packet,
76                    update_pose.before(EntityGeometryUpdateSystems),
77                    send_sprinting_if_needed
78                        .after(azalea_entity::update_in_loaded_chunk)
79                        .after(travel)
80                        .after(EntityGeometryUpdateSystems),
81                    send_position,
82                )
83                    .chain(),
84            )
85            .add_observer(handle_knockback);
86    }
87}
88
89#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
90pub struct MoveEventsSystems;
91
92/// A component that contains the look direction that was last sent over the
93/// network.
94#[derive(Clone, Component, Debug, Default)]
95pub struct LastSentLookDirection {
96    pub x_rot: f32,
97    pub y_rot: f32,
98}
99
100#[allow(clippy::type_complexity)]
101pub fn send_position(
102    mut query: Query<
103        (
104            Entity,
105            &Position,
106            &LookDirection,
107            &mut ClientMovementState,
108            &mut LastSentPosition,
109            &mut Physics,
110            &mut LastSentLookDirection,
111        ),
112        With<HasClientLoaded>,
113    >,
114    mut commands: Commands,
115) {
116    for (
117        entity,
118        position,
119        direction,
120        mut physics_state,
121        mut last_sent_position,
122        mut physics,
123        mut last_direction,
124    ) in query.iter_mut()
125    {
126        let packet = {
127            // TODO: the camera being able to be controlled by other entities isn't
128            // implemented yet if !self.is_controlled_camera() { return };
129
130            let x_delta = position.x - last_sent_position.x;
131            let y_delta = position.y - last_sent_position.y;
132            let z_delta = position.z - last_sent_position.z;
133            let y_rot_delta = (direction.y_rot() - last_direction.y_rot) as f64;
134            let x_rot_delta = (direction.x_rot() - last_direction.x_rot) as f64;
135
136            physics_state.position_remainder += 1;
137
138            // boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) >
139            // Mth.square(2.0E-4D) || this.positionReminder >= 20;
140            let is_delta_large_enough =
141                (x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) > 2.0e-4f64.powi(2);
142            let sending_position = is_delta_large_enough || physics_state.position_remainder >= 20;
143            let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
144
145            // if self.is_passenger() {
146            //   TODO: posrot packet for being a passenger
147            // }
148            let flags = MoveFlags {
149                on_ground: physics.on_ground(),
150                horizontal_collision: physics.horizontal_collision,
151            };
152            let packet = if sending_position && sending_direction {
153                Some(
154                    ServerboundMovePlayerPosRot {
155                        pos: **position,
156                        look_direction: *direction,
157                        flags,
158                    }
159                    .into_variant(),
160                )
161            } else if sending_position {
162                Some(
163                    ServerboundMovePlayerPos {
164                        pos: **position,
165                        flags,
166                    }
167                    .into_variant(),
168                )
169            } else if sending_direction {
170                Some(
171                    ServerboundMovePlayerRot {
172                        look_direction: *direction,
173                        flags,
174                    }
175                    .into_variant(),
176                )
177            } else if physics.last_on_ground() != physics.on_ground() {
178                Some(ServerboundMovePlayerStatusOnly { flags }.into_variant())
179            } else {
180                None
181            };
182
183            if sending_position {
184                **last_sent_position = **position;
185                physics_state.position_remainder = 0;
186            }
187            if sending_direction {
188                last_direction.y_rot = direction.y_rot();
189                last_direction.x_rot = direction.x_rot();
190            }
191
192            let on_ground = physics.on_ground();
193            physics.set_last_on_ground(on_ground);
194            // minecraft checks for autojump here, but also autojump is bad so
195
196            packet
197        };
198
199        if let Some(packet) = packet {
200            commands.trigger(SendGamePacketEvent {
201                sent_by: entity,
202                packet,
203            });
204        }
205    }
206}
207
208#[derive(Clone, Component, Debug, Default, Eq, PartialEq)]
209pub struct LastSentInput(pub ServerboundPlayerInput);
210pub fn send_player_input_packet(
211    mut query: Query<(
212        Entity,
213        &ClientMovementState,
214        &Jumping,
215        Option<&LastSentInput>,
216    )>,
217    mut commands: Commands,
218) {
219    for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
220        let dir = physics_state.move_direction;
221        let input = ServerboundPlayerInput {
222            forward: dir.forward(),
223            backward: dir.backward(),
224            left: dir.left(),
225            right: dir.right(),
226            jump: **jumping,
227            shift: physics_state.trying_to_crouch,
228            sprint: physics_state.trying_to_sprint,
229        };
230
231        // if LastSentInput isn't present, we default to assuming we're not pressing any
232        // keys and insert it anyways every time it changes
233        let last_sent_input = last_sent_input.cloned().unwrap_or_default();
234
235        if input != last_sent_input.0 {
236            commands.trigger(SendGamePacketEvent {
237                sent_by: entity,
238                packet: input.clone().into_variant(),
239            });
240            commands.entity(entity).insert(LastSentInput(input));
241        }
242    }
243}
244
245pub fn send_sprinting_if_needed(
246    mut query: Query<(
247        Entity,
248        &MinecraftEntityId,
249        &Sprinting,
250        &mut ClientMovementState,
251    )>,
252    mut commands: Commands,
253) {
254    for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
255        let was_sprinting = physics_state.was_sprinting;
256        if **sprinting != was_sprinting {
257            let sprinting_action = if **sprinting {
258                s_player_command::Action::StartSprinting
259            } else {
260                s_player_command::Action::StopSprinting
261            };
262            commands.trigger(SendGamePacketEvent::new(
263                entity,
264                ServerboundPlayerCommand {
265                    id: *minecraft_entity_id,
266                    action: sprinting_action,
267                    data: 0,
268                },
269            ));
270            physics_state.was_sprinting = **sprinting;
271        }
272    }
273}
274
275/// Updates the [`PhysicsState::move_vector`] based on the
276/// [`PhysicsState::move_direction`].
277pub(crate) fn tick_controls(mut query: Query<&mut ClientMovementState>) {
278    for mut physics_state in query.iter_mut() {
279        let mut forward_impulse: f32 = 0.;
280        let mut left_impulse: f32 = 0.;
281        let move_direction = physics_state.move_direction;
282
283        if move_direction.forward() {
284            forward_impulse += 1.;
285        } else if move_direction.backward() {
286            forward_impulse -= 1.;
287        }
288
289        if move_direction.left() {
290            left_impulse += 1.;
291        } else if move_direction.right() {
292            left_impulse -= 1.;
293        }
294
295        let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
296        physics_state.move_vector = move_vector;
297    }
298}
299
300/// Makes the bot do one physics tick.
301///
302/// This is handled automatically by the client.
303#[allow(clippy::type_complexity)]
304pub fn local_player_ai_step(
305    mut query: Query<
306        (
307            Entity,
308            &ClientMovementState,
309            &PlayerAbilities,
310            &metadata::Swimming,
311            &metadata::SleepingPos,
312            &WorldHolder,
313            &Position,
314            Option<&Hunger>,
315            Option<&LastSentInput>,
316            &FallFlying,
317            &Pose,
318            &mut Physics,
319            &mut Sprinting,
320            &mut Crouching,
321            &mut Attributes,
322        ),
323        (With<HasClientLoaded>, With<LocalEntity>),
324    >,
325    aabb_query: AabbQuery,
326    collidable_entity_query: CollidableEntityQuery,
327) {
328    for (
329        entity,
330        physics_state,
331        abilities,
332        swimming,
333        sleeping_pos,
334        world_holder,
335        position,
336        hunger,
337        last_sent_input,
338        fall_flying,
339        pose,
340        mut physics,
341        mut sprinting,
342        mut crouching,
343        mut attributes,
344    ) in query.iter_mut()
345    {
346        // server ai step
347
348        let is_swimming = **swimming;
349        // TODO: implement passengers
350        let is_passenger = false;
351        let is_sleeping = sleeping_pos.is_some();
352
353        let world = world_holder.shared.read();
354        let ctx = CanPlayerFitCtx {
355            world: &world,
356            entity,
357            position: *position,
358            aabb_query: &aabb_query,
359            collidable_entity_query: &collidable_entity_query,
360            physics: &physics,
361        };
362
363        let new_crouching = !abilities.flying
364            && !is_swimming
365            && !is_passenger
366            && (last_sent_input.is_some_and(|i| i.0.shift)
367                || !is_sleeping
368                    && !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing))
369            && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching);
370        if **crouching != new_crouching {
371            **crouching = new_crouching;
372        }
373
374        // TODO: food data and abilities
375        // let has_enough_food_to_sprint = self.food_data().food_level ||
376        // self.abilities().may_fly;
377        let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
378
379        // TODO: double tapping w to sprint i think
380
381        let trying_to_sprint = physics_state.trying_to_sprint;
382
383        // TODO: swimming
384        let is_underwater = false;
385        let is_in_water = physics.is_in_water();
386
387        let is_fall_flying = **fall_flying;
388        // TODO: passenger
389        let is_passenger = false;
390        // TODO: using items
391        let using_item = false;
392        // TODO: status effects
393        let has_blindness = false;
394
395        let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
396
397        // LocalPlayer.canStartSprinting
398        let can_start_sprinting = !**sprinting
399            && has_enough_impulse
400            && has_enough_food_to_sprint
401            && !using_item
402            && !has_blindness
403            && (!is_passenger || is_underwater)
404            && (!is_fall_flying || is_underwater)
405            && (!is_moving_slowly(&crouching, fall_flying, pose, is_in_water) || is_underwater)
406            && (!is_in_water || is_underwater);
407        if trying_to_sprint && can_start_sprinting {
408            set_sprinting(true, &mut sprinting, &mut attributes);
409        }
410
411        if **sprinting {
412            // TODO: swimming
413
414            let vehicle_can_sprint = false;
415            // shouldStopRunSprinting
416            let should_stop_sprinting = has_blindness
417                || (is_passenger && !vehicle_can_sprint)
418                || !has_enough_impulse
419                || !has_enough_food_to_sprint
420                || (physics.horizontal_collision && !physics.minor_horizontal_collision)
421                || (is_in_water && !is_underwater);
422            if should_stop_sprinting {
423                set_sprinting(false, &mut sprinting, &mut attributes);
424            }
425        }
426
427        // TODO: replace those booleans when using items and passengers are properly
428        // implemented
429        let move_vector = modify_input(
430            physics_state.move_vector,
431            false,
432            false,
433            is_moving_slowly(&crouching, fall_flying, pose, is_in_water),
434            &attributes,
435        );
436        physics.x_acceleration = move_vector.x;
437        physics.z_acceleration = move_vector.y;
438    }
439}
440
441// this should technically be a step within local_player_ai_step, but
442// 1. adds too much new query parameters if not extracted
443// 2. is very local to interact with the elytra shared flag
444// therefore I think it's safe to isolate into a separate system
445pub fn process_fall_flying_activation(
446    mut query: Query<
447        (
448            Entity,
449            &MinecraftEntityId,
450            &PlayerAbilities,
451            Option<&LastSentInput>,
452            &Jumping,
453            &Inventory,
454            &Physics,
455            &ActiveEffects,
456            &OnClimbable,
457            &mut FallFlying,
458        ),
459        (With<HasClientLoaded>, With<LocalEntity>),
460    >,
461    mut commands: Commands,
462) {
463    for (
464        entity,
465        minecraft_entity_id,
466        abilities,
467        last_sent_input,
468        jumping,
469        inv,
470        physics,
471        active_effects,
472        onclimbable,
473        mut fall_flying,
474    ) in query.iter_mut()
475    {
476        // TODO: creative fly toggle
477        let creative_flight_toggled = false;
478
479        if **jumping
480            && !creative_flight_toggled
481            && last_sent_input.is_some_and(|input| !input.0.jump)
482            && !**onclimbable
483            && can_start_fall_flying(&fall_flying, abilities, inv, physics, active_effects)
484        {
485            // split `tryToStartFallFlying` into condition check
486            **fall_flying = true; // Player.startFallFlying()
487            commands.trigger(SendGamePacketEvent::new(
488                entity,
489                s_player_command::ServerboundPlayerCommand {
490                    id: *minecraft_entity_id,
491                    action: s_player_command::Action::StartFallFlying,
492                    data: 0,
493                },
494            ));
495        }
496    }
497}
498
499// Player.tryToStartFallFlying()
500fn can_start_fall_flying(
501    already_fall_flying: &FallFlying,
502    abilities: &PlayerAbilities,
503    inv: &Inventory,
504    physics: &Physics,
505    active_effects: &ActiveEffects,
506) -> bool {
507    (!**already_fall_flying)
508        && (!abilities.flying)
509
510        // LivingEntity.canGlide()
511        && !physics.on_ground()
512        // TODO: && !isPassenger()
513        && !active_effects.0.contains_key(&MobEffect::Levitation)
514        && EquipmentSlot::values().iter().any(|slot| {
515            inv.get_equipment(*slot).is_some_and(|stack| {
516                // LivingEntity.canGlideUsing()
517                stack.get_component::<components::Glider>().is_some()
518                    && stack.get_component::<components::Equippable>().is_some_and(
519                        |equippable| equippable.slot == *slot
520                    )
521                    && !next_damage_will_break(stack)
522            })
523        })
524
525        && !physics.is_in_water()
526}
527
528// ItemStack.nextDamageWillBreak()
529fn next_damage_will_break(stack: &ItemStack) -> bool {
530    let Some(max_damage) = stack.get_component::<components::MaxDamage>() else {
531        return false;
532    };
533    let Some(damage) = stack.get_component::<components::Damage>() else {
534        return false;
535    };
536    // ItemStack.isDamageableItem()
537    if stack.get_component::<components::Unbreakable>().is_some() {
538        return false;
539    }
540    damage.amount >= max_damage.amount - 1
541}
542
543// LocalPlayer.isMovingSlowly
544fn is_moving_slowly(
545    crouching: &Crouching,
546    fall_flying: &FallFlying,
547    pose: &Pose,
548    is_in_water: bool,
549) -> bool {
550    if **crouching {
551        return true;
552    }
553
554    // Entity.isVisuallyCrawling
555    if is_in_water {
556        return false;
557    }
558
559    // LivingEntity.isVisuallySwimming override
560    match *pose {
561        Pose::Swimming => true,
562        Pose::FallFlying => !**fall_flying,
563        // There is going to be a slowdown for a tick when the server sets the Fallflying shared
564        // flag while the client is still in the FallFlying Pose And that is totally
565        // intended
566        _ => false,
567    }
568}
569
570// LocalPlayer.modifyInput
571fn modify_input(
572    mut move_vector: Vec2,
573    is_using_item: bool,
574    is_passenger: bool,
575    moving_slowly: bool,
576    attributes: &Attributes,
577) -> Vec2 {
578    if move_vector.length_squared() == 0. {
579        return move_vector;
580    }
581
582    move_vector *= 0.98;
583    if is_using_item && !is_passenger {
584        move_vector *= 0.2;
585    }
586
587    if moving_slowly {
588        let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
589        move_vector *= sneaking_speed;
590    }
591
592    modify_input_speed_for_square_movement(move_vector)
593}
594fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
595    let length = move_vector.length();
596    if length == 0. {
597        return move_vector;
598    }
599    let scaled_to_inverse_length = move_vector * (1. / length);
600    let dist = distance_to_unit_square(scaled_to_inverse_length);
601    let scale = (length * dist).min(1.);
602    scaled_to_inverse_length * scale
603}
604fn distance_to_unit_square(v: Vec2) -> f32 {
605    let x = v.x.abs();
606    let y = v.y.abs();
607    let ratio = if y > x { x / y } else { y / x };
608    (1. + ratio * ratio).sqrt()
609}
610
611/// An event sent when the client starts walking.
612///
613/// This does not get sent for non-local entities.
614///
615/// To stop walking or sprinting, send this event with `WalkDirection::None`.
616#[derive(Debug, Message)]
617pub struct StartWalkEvent {
618    pub entity: Entity,
619    pub direction: WalkDirection,
620}
621
622/// The system that makes the player start walking when they receive a
623/// [`StartWalkEvent`].
624pub fn handle_walk(
625    mut events: MessageReader<StartWalkEvent>,
626    mut query: Query<(&mut ClientMovementState, &mut Sprinting, &mut Attributes)>,
627) {
628    for event in events.read() {
629        if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
630        {
631            physics_state.move_direction = event.direction;
632            physics_state.trying_to_sprint = false;
633            set_sprinting(false, &mut sprinting, &mut attributes);
634        }
635    }
636}
637
638/// An event sent when the client starts sprinting.
639///
640/// This does not get sent for non-local entities.
641#[derive(Message)]
642pub struct StartSprintEvent {
643    pub entity: Entity,
644    pub direction: SprintDirection,
645}
646/// The system that makes the player start sprinting when they receive a
647/// [`StartSprintEvent`].
648pub fn handle_sprint(
649    mut query: Query<&mut ClientMovementState>,
650    mut events: MessageReader<StartSprintEvent>,
651) {
652    for event in events.read() {
653        if let Ok(mut physics_state) = query.get_mut(event.entity) {
654            physics_state.move_direction = WalkDirection::from(event.direction);
655            physics_state.trying_to_sprint = true;
656        }
657    }
658}
659
660/// Change whether we're sprinting by adding an attribute modifier to the
661/// player.
662///
663/// You should use the [`Client::walk`] and [`Client::sprint`] functions
664/// instead.
665///
666/// Returns true if the operation was successful.
667fn set_sprinting(
668    sprinting: bool,
669    currently_sprinting: &mut Sprinting,
670    attributes: &mut Attributes,
671) -> bool {
672    **currently_sprinting = sprinting;
673    if sprinting {
674        attributes
675            .movement_speed
676            .try_insert(azalea_entity::attributes::sprinting_modifier())
677            .is_ok()
678    } else {
679        attributes
680            .movement_speed
681            .remove(&azalea_entity::attributes::sprinting_modifier().id)
682            .is_none()
683    }
684}
685
686// Whether the player is moving fast enough to be able to start sprinting.
687fn has_enough_impulse_to_start_sprinting(physics_state: &ClientMovementState) -> bool {
688    // if self.underwater() {
689    //     self.has_forward_impulse()
690    // } else {
691    physics_state.move_vector.y > 0.8
692    // }
693}
694
695/// An event sent by the server that sets or adds to our velocity.
696///
697/// Usually `KnockbackKind::Set` is used for normal knockback and
698/// `KnockbackKind::Add` is used for explosions, but some servers (notably
699/// Hypixel) use explosions for knockback.
700#[derive(EntityEvent, Debug, Clone)]
701pub struct KnockbackEvent {
702    pub entity: Entity,
703    pub data: KnockbackData,
704}
705
706#[derive(Debug, Clone)]
707pub enum KnockbackData {
708    Set(Vec3),
709    Add(Vec3),
710}
711
712pub fn handle_knockback(knockback: On<KnockbackEvent>, mut query: Query<&mut Physics>) {
713    if let Ok(mut physics) = query.get_mut(knockback.entity) {
714        match knockback.data {
715            KnockbackData::Set(velocity) => {
716                physics.velocity = velocity;
717            }
718            KnockbackData::Add(velocity) => {
719                physics.velocity += velocity;
720            }
721        }
722    }
723}
724
725pub fn update_pose(
726    mut query: Query<(
727        Entity,
728        &mut Pose,
729        &Physics,
730        &ClientMovementState,
731        &FallFlying,
732        &GameMode,
733        &WorldHolder,
734        &Position,
735    )>,
736    aabb_query: AabbQuery,
737    collidable_entity_query: CollidableEntityQuery,
738) {
739    for (
740        entity,
741        mut pose,
742        physics,
743        physics_state,
744        fall_flying,
745        &game_mode,
746        world_holder,
747        position,
748    ) in query.iter_mut()
749    {
750        let world = world_holder.shared.read();
751        let world = &*world;
752        let ctx = CanPlayerFitCtx {
753            world,
754            entity,
755            position: *position,
756            aabb_query: &aabb_query,
757            collidable_entity_query: &collidable_entity_query,
758            physics,
759        };
760
761        if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
762            continue;
763        }
764
765        // TODO: implement everything else from getDesiredPose: sleeping, swimming,
766        // fallFlying, spinAttack
767        let desired_pose = if physics_state.trying_to_crouch {
768            Pose::Crouching
769        } else if **fall_flying {
770            Pose::FallFlying
771        } else {
772            Pose::Standing
773        };
774
775        // TODO: passengers
776        let is_passenger = false;
777
778        // canPlayerFitWithinBlocksAndEntitiesWhen
779        let new_pose = if game_mode == GameMode::Spectator
780            || is_passenger
781            || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
782        {
783            desired_pose
784        } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
785            Pose::Crouching
786        } else {
787            Pose::Swimming
788        };
789
790        // avoid triggering change detection
791        if new_pose != *pose {
792            *pose = new_pose;
793        }
794    }
795}
796
797struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
798    world: &'a World,
799    entity: Entity,
800    position: Position,
801    aabb_query: &'a AabbQuery<'world, 'state, 'b>,
802    collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
803    physics: &'a Physics,
804}
805fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
806    no_collision(
807        ctx.world,
808        Some(ctx.entity),
809        ctx.aabb_query,
810        ctx.collidable_entity_query,
811        ctx.physics,
812        &calculate_dimensions(EntityKind::Player, pose)
813            .make_bounding_box(*ctx.position)
814            .deflate_all(1.0e-7),
815        false,
816    )
817}