azalea_physics/collision/
mod.rs

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