azalea_physics/collision/
mod.rs

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