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    mut movement: Vec3,
263    mut entity_box: AABB,
264    collision_boxes: &[VoxelShape],
265) -> Vec3 {
266    if collision_boxes.is_empty() {
267        return movement;
268    }
269
270    if movement.y != 0. {
271        movement.y = Shapes::collide(Axis::Y, &entity_box, collision_boxes, movement.y);
272        if movement.y != 0. {
273            entity_box = entity_box.move_relative(Vec3::new(0., movement.y, 0.));
274        }
275    }
276
277    // whether the player is moving more in the z axis than x
278    // this is done to fix a movement bug, minecraft does this too
279    let more_z_movement = movement.x.abs() < movement.z.abs();
280
281    if more_z_movement && movement.z != 0. {
282        movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
283        if movement.z != 0. {
284            entity_box = entity_box.move_relative(Vec3::new(0., 0., movement.z));
285        }
286    }
287
288    if movement.x != 0. {
289        movement.x = Shapes::collide(Axis::X, &entity_box, collision_boxes, movement.x);
290        if movement.x != 0. {
291            entity_box = entity_box.move_relative(Vec3::new(movement.x, 0., 0.));
292        }
293    }
294
295    if !more_z_movement && movement.z != 0. {
296        movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
297    }
298
299    movement
300}
301
302/// Get the [`VoxelShape`] for the given fluid state.
303///
304/// The instance and position are required so it can check if the block above is
305/// also the same fluid type.
306pub fn fluid_shape(fluid: &FluidState, world: &ChunkStorage, pos: BlockPos) -> &'static VoxelShape {
307    if fluid.amount == 9 {
308        let fluid_state_above = world.get_fluid_state(pos.up(1)).unwrap_or_default();
309        if fluid_state_above.kind == fluid.kind {
310            return &BLOCK_SHAPE;
311        }
312    }
313    if fluid.amount > 9 {
314        warn!("Tried to calculate shape for fluid with height > 9: {fluid:?} at {pos}");
315        return &EMPTY_SHAPE;
316    }
317
318    // pre-calculate these in a LazyLock so this function can return a
319    // reference instead
320
321    static FLUID_SHAPES: LazyLock<[VoxelShape; 10]> = LazyLock::new(|| {
322        [
323            calculate_shape_for_fluid(0),
324            calculate_shape_for_fluid(1),
325            calculate_shape_for_fluid(2),
326            calculate_shape_for_fluid(3),
327            calculate_shape_for_fluid(4),
328            calculate_shape_for_fluid(5),
329            calculate_shape_for_fluid(6),
330            calculate_shape_for_fluid(7),
331            calculate_shape_for_fluid(8),
332            calculate_shape_for_fluid(9),
333        ]
334    });
335
336    &FLUID_SHAPES[fluid.amount as usize]
337}
338fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
339    box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0)
340}
341
342/// Whether the block is treated as "motion blocking".
343///
344/// This is marked as deprecated in Minecraft.
345pub fn legacy_blocks_motion(block: BlockState) -> bool {
346    if block == BlockState::AIR {
347        // fast path
348        return false;
349    }
350
351    let registry_block = azalea_registry::Block::from(block);
352    legacy_calculate_solid(block)
353        && registry_block != azalea_registry::Block::Cobweb
354        && registry_block != azalea_registry::Block::BambooSapling
355}
356
357pub fn legacy_calculate_solid(block: BlockState) -> bool {
358    // force_solid has to be checked before anything else
359    let block_trait = Box::<dyn azalea_block::BlockTrait>::from(block);
360    if let Some(solid) = block_trait.behavior().force_solid {
361        return solid;
362    }
363
364    let shape = block.collision_shape();
365    if shape.is_empty() {
366        return false;
367    }
368    let bounds = shape.bounds();
369    bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0
370}