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::EPSILON,
15    position::{BlockPos, Vec3},
16};
17use azalea_world::{ChunkStorage, Instance, MoveEntityError};
18use bevy_ecs::{entity::Entity, world::Mut};
19pub use blocks::BlockWithShape;
20pub use discrete_voxel_shape::*;
21use entity_collisions::{CollidableEntityQuery, PhysicsQuery, get_entity_collisions};
22pub use shape::*;
23use tracing::warn;
24
25use self::world_collisions::get_block_collisions;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum MoverType {
29    Own,
30    Player,
31    Piston,
32    ShulkerBox,
33    Shulker,
34}
35
36// Entity.collide
37fn collide(
38    movement: &Vec3,
39    world: &Instance,
40    physics: &azalea_entity::Physics,
41    source_entity: Option<Entity>,
42    physics_query: &PhysicsQuery,
43    collidable_entity_query: &CollidableEntityQuery,
44) -> Vec3 {
45    let entity_bounding_box = physics.bounding_box;
46    let entity_collisions = get_entity_collisions(
47        world,
48        &entity_bounding_box.expand_towards(movement),
49        source_entity,
50        physics_query,
51        collidable_entity_query,
52    );
53    let collided_delta = if movement.length_squared() == 0.0 {
54        *movement
55    } else {
56        collide_bounding_box(movement, &entity_bounding_box, world, &entity_collisions)
57    };
58
59    let x_collision = movement.x != collided_delta.x;
60    let y_collision = movement.y != collided_delta.y;
61    let z_collision = movement.z != collided_delta.z;
62
63    let on_ground = physics.on_ground() || y_collision && movement.y < 0.;
64
65    let max_up_step = 0.6;
66    if max_up_step > 0. && on_ground && (x_collision || z_collision) {
67        let mut step_to_delta = collide_bounding_box(
68            &movement.with_y(max_up_step),
69            &entity_bounding_box,
70            world,
71            &entity_collisions,
72        );
73        let directly_up_delta = collide_bounding_box(
74            &Vec3::ZERO.with_y(max_up_step),
75            &entity_bounding_box.expand_towards(&Vec3::new(movement.x, 0., movement.z)),
76            world,
77            &entity_collisions,
78        );
79        if directly_up_delta.y < max_up_step {
80            let target_movement = collide_bounding_box(
81                &movement.with_y(0.),
82                &entity_bounding_box.move_relative(directly_up_delta),
83                world,
84                &entity_collisions,
85            )
86            .add(directly_up_delta);
87            if target_movement.horizontal_distance_squared()
88                > step_to_delta.horizontal_distance_squared()
89            {
90                step_to_delta = target_movement;
91            }
92        }
93
94        if step_to_delta.horizontal_distance_squared()
95            > collided_delta.horizontal_distance_squared()
96        {
97            return step_to_delta.add(collide_bounding_box(
98                &Vec3::ZERO.with_y(-step_to_delta.y + movement.y),
99                &entity_bounding_box.move_relative(step_to_delta),
100                world,
101                &entity_collisions,
102            ));
103        }
104    }
105
106    collided_delta
107}
108
109/// Move an entity by a given delta, checking for collisions.
110///
111/// In Mojmap, this is `Entity.move`.
112#[allow(clippy::too_many_arguments)]
113pub fn move_colliding(
114    _mover_type: MoverType,
115    movement: &Vec3,
116    world: &Instance,
117    position: &mut Mut<azalea_entity::Position>,
118    physics: &mut azalea_entity::Physics,
119    source_entity: Option<Entity>,
120    physics_query: &PhysicsQuery,
121    collidable_entity_query: &CollidableEntityQuery,
122) -> Result<(), MoveEntityError> {
123    // TODO: do all these
124
125    // if self.no_physics {
126    //     return;
127    // };
128
129    // if (var1 == MoverType.PISTON) {
130    //     var2 = this.limitPistonMovement(var2);
131    //     if (var2.equals(Vec3.ZERO)) {
132    //        return;
133    //     }
134    // }
135
136    // if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) {
137    //     var2 = var2.multiply(this.stuckSpeedMultiplier);
138    //     this.stuckSpeedMultiplier = Vec3.ZERO;
139    //     this.setDeltaMovement(Vec3.ZERO);
140    // }
141
142    // movement = this.maybeBackOffFromEdge(movement, moverType);
143
144    let collide_result = collide(
145        movement,
146        world,
147        physics,
148        source_entity,
149        physics_query,
150        collidable_entity_query,
151    );
152
153    let move_distance = collide_result.length_squared();
154
155    if move_distance > EPSILON {
156        // TODO: fall damage
157
158        let new_pos = {
159            Vec3 {
160                x: position.x + collide_result.x,
161                y: position.y + collide_result.y,
162                z: position.z + collide_result.z,
163            }
164        };
165
166        if new_pos != ***position {
167            ***position = new_pos;
168        }
169    }
170
171    let x_collision = movement.x != collide_result.x;
172    let z_collision = movement.z != collide_result.z;
173    let horizontal_collision = x_collision || z_collision;
174    let vertical_collision = movement.y != collide_result.y;
175    let on_ground = vertical_collision && movement.y < 0.;
176
177    physics.horizontal_collision = horizontal_collision;
178    physics.vertical_collision = vertical_collision;
179    physics.set_on_ground(on_ground);
180
181    // TODO: minecraft checks for a "minor" horizontal collision here
182
183    let _block_pos_below = azalea_entity::on_pos_legacy(&world.chunks, position);
184    // let _block_state_below = self
185    //     .world
186    //     .get_block_state(&block_pos_below)
187    //     .expect("Couldn't get block state below");
188
189    // self.check_fall_damage(collide_result.y, on_ground, block_state_below,
190    // block_pos_below);
191
192    // if self.isRemoved() { return; }
193
194    if horizontal_collision {
195        let delta_movement = &physics.velocity;
196        physics.velocity = Vec3 {
197            x: if x_collision { 0. } else { delta_movement.x },
198            y: delta_movement.y,
199            z: if z_collision { 0. } else { delta_movement.z },
200        }
201    }
202
203    if vertical_collision {
204        // blockBelow.updateEntityAfterFallOn(this.level, this);
205        // the default implementation of updateEntityAfterFallOn sets the y movement to
206        // 0
207        physics.velocity.y = 0.;
208    }
209
210    if on_ground {
211        // blockBelow.stepOn(this.level, blockPosBelow, blockStateBelow,
212        // this);
213    }
214
215    // sounds
216
217    // this.tryCheckInsideBlocks();
218
219    // float var25 = this.getBlockSpeedFactor();
220    // this.setDeltaMovement(this.getDeltaMovement().multiply((double)var25, 1.0D,
221    // (double)var25)); if (this.level.getBlockStatesIfLoaded(this.
222    // getBoundingBox().deflate(1.0E-6D)).noneMatch((var0) -> {
223    //    return var0.is(BlockTags.FIRE) || var0.is(Blocks.LAVA);
224    // })) {
225    //    if (this.remainingFireTicks <= 0) {
226    //       this.setRemainingFireTicks(-this.getFireImmuneTicks());
227    //    }
228
229    //    if (this.wasOnFire && (this.isInPowderSnow ||
230    // this.isInWaterRainOrBubble())) {       this.
231    // playEntityOnFireExtinguishedSound();    }
232    // }
233
234    // if (this.isOnFire() && (this.isInPowderSnow || this.isInWaterRainOrBubble()))
235    // {    this.setRemainingFireTicks(-this.getFireImmuneTicks());
236    // }
237
238    Ok(())
239}
240
241fn collide_bounding_box(
242    movement: &Vec3,
243    entity_bounding_box: &AABB,
244    world: &Instance,
245    entity_collisions: &[VoxelShape],
246) -> Vec3 {
247    let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
248
249    if !entity_collisions.is_empty() {
250        collision_boxes.extend_from_slice(entity_collisions);
251    }
252
253    // TODO: world border
254
255    let block_collisions =
256        get_block_collisions(world, &entity_bounding_box.expand_towards(movement));
257    collision_boxes.extend(block_collisions);
258    collide_with_shapes(movement, *entity_bounding_box, &collision_boxes)
259}
260
261fn collide_with_shapes(
262    movement: &Vec3,
263    mut entity_box: AABB,
264    collision_boxes: &Vec<VoxelShape>,
265) -> Vec3 {
266    if collision_boxes.is_empty() {
267        return *movement;
268    }
269
270    let mut x_movement = movement.x;
271    let mut y_movement = movement.y;
272    let mut z_movement = movement.z;
273    if y_movement != 0. {
274        y_movement = Shapes::collide(&Axis::Y, &entity_box, collision_boxes, y_movement);
275        if y_movement != 0. {
276            entity_box = entity_box.move_relative(Vec3 {
277                x: 0.,
278                y: y_movement,
279                z: 0.,
280            });
281        }
282    }
283
284    // whether the player is moving more in the z axis than x
285    // this is done to fix a movement bug, minecraft does this too
286    let more_z_movement = x_movement.abs() < z_movement.abs();
287
288    if more_z_movement && z_movement != 0. {
289        z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement);
290        if z_movement != 0. {
291            entity_box = entity_box.move_relative(Vec3 {
292                x: 0.,
293                y: 0.,
294                z: z_movement,
295            });
296        }
297    }
298
299    if x_movement != 0. {
300        x_movement = Shapes::collide(&Axis::X, &entity_box, collision_boxes, x_movement);
301        if x_movement != 0. {
302            entity_box = entity_box.move_relative(Vec3 {
303                x: x_movement,
304                y: 0.,
305                z: 0.,
306            });
307        }
308    }
309
310    if !more_z_movement && z_movement != 0. {
311        z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement);
312    }
313
314    Vec3 {
315        x: x_movement,
316        y: y_movement,
317        z: z_movement,
318    }
319}
320
321/// Get the [`VoxelShape`] for the given fluid state.
322///
323/// The instance and position are required so it can check if the block above is
324/// also the same fluid type.
325pub fn fluid_shape(
326    fluid: &FluidState,
327    world: &ChunkStorage,
328    pos: &BlockPos,
329) -> &'static VoxelShape {
330    if fluid.amount == 9 {
331        let fluid_state_above = world.get_fluid_state(&pos.up(1)).unwrap_or_default();
332        if fluid_state_above.kind == fluid.kind {
333            return &BLOCK_SHAPE;
334        }
335    }
336    if fluid.amount > 9 {
337        warn!("Tried to calculate shape for fluid with height > 9: {fluid:?} at {pos}");
338        return &EMPTY_SHAPE;
339    }
340
341    // pre-calculate these in a LazyLock so this function can return a
342    // reference instead
343
344    static FLUID_SHAPES: LazyLock<[VoxelShape; 10]> = LazyLock::new(|| {
345        [
346            calculate_shape_for_fluid(0),
347            calculate_shape_for_fluid(1),
348            calculate_shape_for_fluid(2),
349            calculate_shape_for_fluid(3),
350            calculate_shape_for_fluid(4),
351            calculate_shape_for_fluid(5),
352            calculate_shape_for_fluid(6),
353            calculate_shape_for_fluid(7),
354            calculate_shape_for_fluid(8),
355            calculate_shape_for_fluid(9),
356        ]
357    });
358
359    &FLUID_SHAPES[fluid.amount as usize]
360}
361fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
362    box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0)
363}
364
365/// Whether the block is treated as "motion blocking".
366///
367/// This is marked as deprecated in Minecraft.
368pub fn legacy_blocks_motion(block: BlockState) -> bool {
369    if block == BlockState::AIR {
370        // fast path
371        return false;
372    }
373
374    let registry_block = azalea_registry::Block::from(block);
375    legacy_calculate_solid(block)
376        && registry_block != azalea_registry::Block::Cobweb
377        && registry_block != azalea_registry::Block::BambooSapling
378}
379
380pub fn legacy_calculate_solid(block: BlockState) -> bool {
381    // force_solid has to be checked before anything else
382    let block_trait = Box::<dyn azalea_block::Block>::from(block);
383    if let Some(solid) = block_trait.behavior().force_solid {
384        return solid;
385    }
386
387    let shape = block.collision_shape();
388    if shape.is_empty() {
389        return false;
390    }
391    let bounds = shape.bounds();
392    bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0
393}