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