Skip to main content

azalea_physics/collision/
mod.rs

1mod blocks;
2mod discrete_voxel_shape;
3pub mod entity_collisions;
4mod mergers;
5mod shape;
6mod shape_offset;
7pub mod world_collisions;
8
9use std::{ops::Add, sync::LazyLock};
10
11use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState};
12use azalea_core::{
13    aabb::Aabb,
14    direction::Axis,
15    math::{self, EPSILON},
16    position::{BlockPos, Vec3},
17};
18use azalea_entity::{
19    Attributes, Jumping, LookDirection, OnClimbable, Physics, PlayerAbilities, Pose, Position,
20    metadata::Sprinting,
21};
22use azalea_registry::builtin::BlockKind;
23use azalea_world::{ChunkStorage, World};
24use bevy_ecs::{entity::Entity, world::Mut};
25pub use blocks::BlockWithShape;
26pub use discrete_voxel_shape::*;
27use entity_collisions::{CollidableEntityQuery, get_entity_collisions};
28pub use shape::*;
29use tracing::warn;
30
31use self::world_collisions::get_block_collisions;
32use crate::{
33    client_movement::ClientMovementState, collision::entity_collisions::AabbQuery,
34    travel::no_collision,
35};
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum MoverType {
39    Own,
40    Player,
41    Piston,
42    ShulkerBox,
43    Shulker,
44}
45
46// Entity.collide
47fn collide(ctx: &MoveCtx, movement: Vec3) -> Vec3 {
48    let entity_bounding_box = ctx.physics.bounding_box;
49    let entity_collisions = get_entity_collisions(
50        ctx.world,
51        &entity_bounding_box.expand_towards(movement),
52        Some(ctx.source_entity),
53        ctx.aabb_query,
54        ctx.collidable_entity_query,
55    );
56    let world = ctx.world;
57    let collided_delta = if movement.length_squared() == 0.0 {
58        movement
59    } else {
60        collide_bounding_box(movement, &entity_bounding_box, world, &entity_collisions)
61    };
62
63    let x_collision = movement.x != collided_delta.x;
64    let y_collision = movement.y != collided_delta.y;
65    let z_collision = movement.z != collided_delta.z;
66
67    let on_ground = ctx.physics.on_ground() || y_collision && movement.y < 0.;
68
69    let max_up_step = 0.6;
70    if max_up_step > 0. && on_ground && (x_collision || z_collision) {
71        let mut step_to_delta = collide_bounding_box(
72            movement.with_y(max_up_step),
73            &entity_bounding_box,
74            world,
75            &entity_collisions,
76        );
77        let directly_up_delta = collide_bounding_box(
78            Vec3::ZERO.with_y(max_up_step),
79            &entity_bounding_box.expand_towards(Vec3::new(movement.x, 0., movement.z)),
80            world,
81            &entity_collisions,
82        );
83        if directly_up_delta.y < max_up_step {
84            let target_movement = collide_bounding_box(
85                movement.with_y(0.),
86                &entity_bounding_box.move_relative(directly_up_delta),
87                world,
88                &entity_collisions,
89            )
90            .add(directly_up_delta);
91            if target_movement.horizontal_distance_squared()
92                > step_to_delta.horizontal_distance_squared()
93            {
94                step_to_delta = target_movement;
95            }
96        }
97
98        if step_to_delta.horizontal_distance_squared()
99            > collided_delta.horizontal_distance_squared()
100        {
101            return step_to_delta.add(collide_bounding_box(
102                Vec3::ZERO.with_y(-step_to_delta.y + movement.y),
103                &entity_bounding_box.move_relative(step_to_delta),
104                world,
105                &entity_collisions,
106            ));
107        }
108    }
109
110    collided_delta
111}
112
113pub struct MoveCtx<'world, 'state, 'a, 'b> {
114    pub mover_type: MoverType,
115    pub world: &'a World,
116    pub position: Mut<'a, Position>,
117    pub physics: &'a mut Physics,
118    pub source_entity: Entity,
119    pub aabb_query: &'a AabbQuery<'world, 'state, 'b>,
120    pub collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
121    pub physics_state: Option<&'a ClientMovementState>,
122    pub attributes: &'a Attributes,
123    pub abilities: Option<&'a PlayerAbilities>,
124
125    pub direction: LookDirection,
126    pub sprinting: Sprinting,
127    pub on_climbable: OnClimbable,
128    pub pose: Option<Pose>,
129    pub jumping: Jumping,
130}
131
132/// Move an entity by a given delta, checking for collisions.
133///
134/// In Mojmap, this is `Entity.move`.
135pub fn move_colliding(ctx: &mut MoveCtx, mut movement: Vec3) {
136    // TODO: do all these
137
138    // if self.no_physics {
139    //     return;
140    // };
141
142    // if (var1 == MoverType.PISTON) {
143    //     var2 = this.limitPistonMovement(var2);
144    //     if (var2.equals(Vec3.ZERO)) {
145    //        return;
146    //     }
147    // }
148
149    // if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) {
150    //     var2 = var2.multiply(this.stuckSpeedMultiplier);
151    //     this.stuckSpeedMultiplier = Vec3.ZERO;
152    //     this.setDeltaMovement(Vec3.ZERO);
153    // }
154
155    movement = maybe_back_off_from_edge(ctx, movement);
156    let collide_result = collide(ctx, movement);
157
158    let move_distance_sqr = collide_result.length_squared();
159
160    let position = &mut ctx.position;
161    let physics = &mut *ctx.physics;
162    let world = ctx.world;
163
164    if move_distance_sqr > EPSILON || movement.length_squared() - move_distance_sqr < EPSILON {
165        // TODO: fall damage
166
167        let new_pos = {
168            Vec3 {
169                x: position.x + collide_result.x,
170                y: position.y + collide_result.y,
171                z: position.z + collide_result.z,
172            }
173        };
174
175        if new_pos != ***position {
176            ***position = new_pos;
177        }
178    }
179
180    let x_collision = !math::equal(movement.x, collide_result.x);
181    let z_collision = !math::equal(movement.z, collide_result.z);
182    let horizontal_collision = x_collision || z_collision;
183    physics.horizontal_collision = horizontal_collision;
184
185    let vertical_collision = movement.y != collide_result.y;
186    physics.vertical_collision = vertical_collision;
187    let on_ground = vertical_collision && movement.y < 0.;
188    physics.set_on_ground(on_ground);
189
190    // TODO: minecraft checks for a "minor" horizontal collision here
191
192    let block_pos_below = azalea_entity::on_pos_legacy(&world.chunks, **position);
193    let block_state_below = world.get_block_state(block_pos_below).unwrap_or_default();
194
195    check_fall_damage(
196        physics,
197        collide_result.y,
198        block_state_below,
199        block_pos_below,
200    );
201
202    // if self.isRemoved() { return; }
203
204    if horizontal_collision {
205        let delta_movement = &physics.velocity;
206        physics.velocity = Vec3 {
207            x: if x_collision { 0. } else { delta_movement.x },
208            y: delta_movement.y,
209            z: if z_collision { 0. } else { delta_movement.z },
210        }
211    }
212
213    if vertical_collision {
214        // blockBelow.updateEntityAfterFallOn(this.level, this);
215        // the default implementation of updateEntityAfterFallOn sets the y movement to
216        // 0
217        physics.velocity.y = 0.;
218    }
219
220    if on_ground {
221        // blockBelow.stepOn(this.level, blockPosBelow, blockStateBelow,
222        // this);
223    }
224
225    // sounds
226
227    // this.tryCheckInsideBlocks();
228
229    // float var25 = this.getBlockSpeedFactor();
230    // this.setDeltaMovement(this.getDeltaMovement().multiply((double)var25,
231    // 1.0D, (double)var25)); if (this.level.getBlockStatesIfLoaded(this.
232    // getBoundingBox().deflate(1.0E-6D)).noneMatch((var0) -> {
233    //    return var0.is(BlockTags.FIRE) || var0.is(Blocks.LAVA);
234    // })) {
235    //    if (this.remainingFireTicks <= 0) {
236    //       this.setRemainingFireTicks(-this.getFireImmuneTicks());
237    //    }
238
239    //    if (this.wasOnFire && (this.isInPowderSnow ||
240    // this.isInWaterRainOrBubble())) {       this.
241    // playEntityOnFireExtinguishedSound();    }
242    // }
243
244    // if (this.isOnFire() && (this.isInPowderSnow ||
245    // this.isInWaterRainOrBubble())) {    this.setRemainingFireTicks(-this.
246    // getFireImmuneTicks()); }
247}
248
249fn check_fall_damage(
250    physics: &mut Physics,
251    delta_y: f64,
252    _block_state_below: BlockState,
253    _block_pos_below: BlockPos,
254) {
255    if !physics.is_in_water() && delta_y < 0. {
256        physics.fall_distance -= delta_y as f32 as f64;
257    }
258
259    if physics.on_ground() {
260        // vanilla calls block.fallOn here but it's not relevant for us
261
262        physics.fall_distance = 0.;
263    }
264}
265
266fn maybe_back_off_from_edge(move_ctx: &mut MoveCtx, mut movement: Vec3) -> Vec3 {
267    let is_staying_on_ground_surface = move_ctx.physics_state.is_some_and(|s| s.trying_to_crouch);
268    let max_up_step = get_max_up_step(move_ctx.attributes);
269
270    let fall_ctx = CanFallAtLeastCtx {
271        physics: move_ctx.physics,
272        world: move_ctx.world,
273        source_entity: move_ctx.source_entity,
274        aabb_query: move_ctx.aabb_query,
275        collidable_entity_query: move_ctx.collidable_entity_query,
276    };
277
278    let Some(abilities) = move_ctx.abilities else {
279        return movement;
280    };
281
282    let is_backing_off = !abilities.flying
283        && movement.y <= 0.
284        && matches!(move_ctx.mover_type, MoverType::Own | MoverType::Player)
285        && is_staying_on_ground_surface
286        && is_above_ground(&fall_ctx, max_up_step);
287    if !is_backing_off {
288        return movement;
289    }
290
291    let min_movement = 0.05;
292    let min_movement_x = movement.x.signum() * min_movement;
293    let min_movement_z = movement.z.signum() * min_movement;
294
295    while movement.x != 0. && can_fall_at_least(&fall_ctx, movement.x, 0., max_up_step as f64) {
296        if movement.x.abs() <= min_movement {
297            movement.x = 0.;
298            break;
299        }
300
301        movement.x -= min_movement_x
302    }
303    while movement.z != 0. && can_fall_at_least(&fall_ctx, 0., movement.z, max_up_step as f64) {
304        if movement.z.abs() <= min_movement {
305            movement.z = 0.;
306            break;
307        }
308
309        movement.z -= min_movement_z
310    }
311    while movement.x != 0.0
312        && movement.z != 0.0
313        && can_fall_at_least(&fall_ctx, movement.x, movement.z, max_up_step as f64)
314    {
315        if movement.x.abs() <= min_movement {
316            movement.x = 0.;
317        } else {
318            movement.x -= min_movement_x;
319        }
320        if movement.z.abs() <= min_movement {
321            movement.z = 0.;
322        } else {
323            movement.z -= min_movement_z;
324        }
325    }
326
327    movement
328}
329
330fn get_max_up_step(attributes: &Attributes) -> f32 {
331    // this would be different if we were riding an entity
332    attributes.step_height.calculate() as f32
333}
334
335fn is_above_ground(ctx: &CanFallAtLeastCtx, max_up_step: f32) -> bool {
336    ctx.physics.on_ground()
337        && ctx.physics.fall_distance < max_up_step as f64
338        && !can_fall_at_least(ctx, 0., 0., max_up_step as f64 - ctx.physics.fall_distance)
339}
340
341pub struct CanFallAtLeastCtx<'world, 'state, 'a, 'b> {
342    physics: &'a Physics,
343    world: &'a World,
344    source_entity: Entity,
345    aabb_query: &'a AabbQuery<'world, 'state, 'b>,
346    collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
347}
348
349fn can_fall_at_least(
350    ctx: &CanFallAtLeastCtx,
351    delta_x: f64,
352    delta_z: f64,
353    max_up_step: f64,
354) -> bool {
355    let aabb = ctx.physics.bounding_box;
356    let aabb = Aabb {
357        min: Vec3 {
358            x: aabb.min.x + EPSILON + delta_x,
359            y: aabb.min.y - max_up_step - EPSILON,
360            z: aabb.min.z + EPSILON + delta_z,
361        },
362        max: Vec3 {
363            x: aabb.max.x - EPSILON + delta_x,
364            y: aabb.min.y,
365            z: aabb.max.z - EPSILON + delta_z,
366        },
367    };
368    no_collision(
369        ctx.world,
370        Some(ctx.source_entity),
371        ctx.aabb_query,
372        ctx.collidable_entity_query,
373        ctx.physics,
374        &aabb,
375        false,
376    )
377}
378
379fn collide_bounding_box(
380    movement: Vec3,
381    entity_bounding_box: &Aabb,
382    world: &World,
383    entity_collisions: &[VoxelShape],
384) -> Vec3 {
385    let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
386
387    if !entity_collisions.is_empty() {
388        collision_boxes.extend_from_slice(entity_collisions);
389    }
390
391    // TODO: world border
392
393    let block_collisions =
394        get_block_collisions(world, &entity_bounding_box.expand_towards(movement));
395    collision_boxes.extend(block_collisions);
396    collide_with_shapes(movement, *entity_bounding_box, &collision_boxes)
397}
398
399fn collide_with_shapes(
400    mut movement: Vec3,
401    mut entity_box: Aabb,
402    collision_boxes: &[VoxelShape],
403) -> Vec3 {
404    if collision_boxes.is_empty() {
405        return movement;
406    }
407
408    if movement.y != 0. {
409        movement.y = Shapes::collide(Axis::Y, &entity_box, collision_boxes, movement.y);
410        if movement.y != 0. {
411            entity_box = entity_box.move_relative(Vec3::new(0., movement.y, 0.));
412        }
413    }
414
415    // whether the player is moving more in the z axis than x
416    // this is done to fix a movement bug, minecraft does this too
417    let more_z_movement = movement.x.abs() < movement.z.abs();
418
419    if more_z_movement && movement.z != 0. {
420        movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
421        if movement.z != 0. {
422            entity_box = entity_box.move_relative(Vec3::new(0., 0., movement.z));
423        }
424    }
425
426    if movement.x != 0. {
427        movement.x = Shapes::collide(Axis::X, &entity_box, collision_boxes, movement.x);
428        if movement.x != 0. {
429            entity_box = entity_box.move_relative(Vec3::new(movement.x, 0., 0.));
430        }
431    }
432
433    if !more_z_movement && movement.z != 0. {
434        movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
435    }
436
437    movement
438}
439
440/// Get the [`VoxelShape`] for the given fluid state.
441///
442/// The world and position are required so it can check if the block above is
443/// also the same fluid type.
444pub fn fluid_shape(fluid: &FluidState, world: &ChunkStorage, pos: BlockPos) -> &'static VoxelShape {
445    if fluid.amount == 9 {
446        let fluid_state_above = world.get_fluid_state(pos.up(1)).unwrap_or_default();
447        if fluid_state_above.kind == fluid.kind {
448            return &BLOCK_SHAPE;
449        }
450    }
451    if fluid.amount > 9 {
452        warn!("Tried to calculate shape for fluid with height > 9: {fluid:?} at {pos}");
453        return &EMPTY_SHAPE;
454    }
455
456    // pre-calculate these in a LazyLock so this function can return a
457    // reference instead
458
459    static FLUID_SHAPES: LazyLock<[VoxelShape; 10]> = LazyLock::new(|| {
460        [
461            calculate_shape_for_fluid(0),
462            calculate_shape_for_fluid(1),
463            calculate_shape_for_fluid(2),
464            calculate_shape_for_fluid(3),
465            calculate_shape_for_fluid(4),
466            calculate_shape_for_fluid(5),
467            calculate_shape_for_fluid(6),
468            calculate_shape_for_fluid(7),
469            calculate_shape_for_fluid(8),
470            calculate_shape_for_fluid(9),
471        ]
472    });
473
474    &FLUID_SHAPES[fluid.amount as usize]
475}
476fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
477    box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0)
478}
479
480/// Whether the block is treated as "motion blocking".
481///
482/// This is marked as deprecated in Minecraft.
483pub fn legacy_blocks_motion(block: BlockState) -> bool {
484    if block == BlockState::AIR {
485        // fast path
486        return false;
487    }
488
489    let registry_block = BlockKind::from(block);
490    legacy_calculate_solid(block)
491        && registry_block != BlockKind::Cobweb
492        && registry_block != BlockKind::BambooSapling
493}
494
495pub fn legacy_calculate_solid(block: BlockState) -> bool {
496    // force_solid has to be checked before anything else
497    let block_trait = Box::<dyn BlockTrait>::from(block);
498    if let Some(solid) = block_trait.behavior().force_solid {
499        return solid;
500    }
501
502    let shape = block.base_collision_shape();
503    if shape.is_empty() {
504        return false;
505    }
506    let bounds = shape.bounds();
507    bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0
508}