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
43fn 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
129pub fn move_colliding(ctx: &mut MoveCtx, mut movement: Vec3) {
133 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 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 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 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 physics.velocity.y = 0.;
215 }
216
217 if on_ground {
218 }
221
222 }
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 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 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 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 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
437pub 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 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
477pub fn legacy_blocks_motion(block: BlockState) -> bool {
481 if block == BlockState::AIR {
482 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 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}