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, MoveEntityError};
22use bevy_ecs::{entity::Entity, world::Mut};
23pub use blocks::BlockWithShape;
24pub use discrete_voxel_shape::*;
25use entity_collisions::{CollidableEntityQuery, PhysicsQuery, get_entity_collisions};
26pub use shape::*;
27use tracing::warn;
28
29use self::world_collisions::get_block_collisions;
30use crate::{local_player::PhysicsState, travel::no_collision};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum MoverType {
34 Own,
35 Player,
36 Piston,
37 ShulkerBox,
38 Shulker,
39}
40
41fn collide(ctx: &MoveCtx, movement: Vec3) -> Vec3 {
43 let entity_bounding_box = ctx.physics.bounding_box;
44 let entity_collisions = get_entity_collisions(
45 ctx.world,
46 &entity_bounding_box.expand_towards(movement),
47 Some(ctx.source_entity),
48 ctx.physics_query,
49 ctx.collidable_entity_query,
50 );
51 let world = ctx.world;
52 let collided_delta = if movement.length_squared() == 0.0 {
53 movement
54 } else {
55 collide_bounding_box(movement, &entity_bounding_box, world, &entity_collisions)
56 };
57
58 let x_collision = movement.x != collided_delta.x;
59 let y_collision = movement.y != collided_delta.y;
60 let z_collision = movement.z != collided_delta.z;
61
62 let on_ground = ctx.physics.on_ground() || y_collision && movement.y < 0.;
63
64 let max_up_step = 0.6;
65 if max_up_step > 0. && on_ground && (x_collision || z_collision) {
66 let mut step_to_delta = collide_bounding_box(
67 movement.with_y(max_up_step),
68 &entity_bounding_box,
69 world,
70 &entity_collisions,
71 );
72 let directly_up_delta = collide_bounding_box(
73 Vec3::ZERO.with_y(max_up_step),
74 &entity_bounding_box.expand_towards(Vec3::new(movement.x, 0., movement.z)),
75 world,
76 &entity_collisions,
77 );
78 if directly_up_delta.y < max_up_step {
79 let target_movement = collide_bounding_box(
80 movement.with_y(0.),
81 &entity_bounding_box.move_relative(directly_up_delta),
82 world,
83 &entity_collisions,
84 )
85 .add(directly_up_delta);
86 if target_movement.horizontal_distance_squared()
87 > step_to_delta.horizontal_distance_squared()
88 {
89 step_to_delta = target_movement;
90 }
91 }
92
93 if step_to_delta.horizontal_distance_squared()
94 > collided_delta.horizontal_distance_squared()
95 {
96 return step_to_delta.add(collide_bounding_box(
97 Vec3::ZERO.with_y(-step_to_delta.y + movement.y),
98 &entity_bounding_box.move_relative(step_to_delta),
99 world,
100 &entity_collisions,
101 ));
102 }
103 }
104
105 collided_delta
106}
107
108pub struct MoveCtx<'world, 'state, 'a, 'b> {
109 pub mover_type: MoverType,
110 pub world: &'a Instance,
111 pub position: Mut<'a, Position>,
112 pub physics: &'a mut Physics,
113 pub source_entity: Entity,
114 pub physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
115 pub collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
116 pub physics_state: Option<&'a PhysicsState>,
117 pub attributes: &'a Attributes,
118 pub abilities: Option<&'a PlayerAbilities>,
119
120 pub direction: LookDirection,
121 pub sprinting: Sprinting,
122 pub on_climbable: OnClimbable,
123 pub pose: Option<Pose>,
124 pub jumping: Jumping,
125}
126
127#[allow(clippy::too_many_arguments)]
131pub fn move_colliding(ctx: &mut MoveCtx, mut movement: Vec3) -> Result<(), MoveEntityError> {
132 movement = maybe_back_off_from_edge(ctx, movement);
152 let collide_result = collide(ctx, movement);
153
154 let move_distance_sqr = collide_result.length_squared();
155
156 let position = &mut ctx.position;
157 let physics = &mut *ctx.physics;
158 let world = ctx.world;
159
160 if move_distance_sqr > EPSILON || movement.length_squared() - move_distance_sqr < EPSILON {
161 let new_pos = {
164 Vec3 {
165 x: position.x + collide_result.x,
166 y: position.y + collide_result.y,
167 z: position.z + collide_result.z,
168 }
169 };
170
171 if new_pos != ***position {
172 ***position = new_pos;
173 }
174 }
175
176 let x_collision = !math::equal(movement.x, collide_result.x);
177 let z_collision = !math::equal(movement.z, collide_result.z);
178 let horizontal_collision = x_collision || z_collision;
179 physics.horizontal_collision = horizontal_collision;
180
181 let vertical_collision = movement.y != collide_result.y;
182 physics.vertical_collision = vertical_collision;
183 let on_ground = vertical_collision && movement.y < 0.;
184 physics.set_on_ground(on_ground);
185
186 let block_pos_below = azalea_entity::on_pos_legacy(&world.chunks, **position);
189 let block_state_below = world.get_block_state(block_pos_below).unwrap_or_default();
190
191 check_fall_damage(
192 physics,
193 collide_result.y,
194 block_state_below,
195 block_pos_below,
196 );
197
198 if horizontal_collision {
201 let delta_movement = &physics.velocity;
202 physics.velocity = Vec3 {
203 x: if x_collision { 0. } else { delta_movement.x },
204 y: delta_movement.y,
205 z: if z_collision { 0. } else { delta_movement.z },
206 }
207 }
208
209 if vertical_collision {
210 physics.velocity.y = 0.;
214 }
215
216 if on_ground {
217 }
220
221 Ok(())
245}
246
247fn check_fall_damage(
248 physics: &mut Physics,
249 delta_y: f64,
250 _block_state_below: BlockState,
251 _block_pos_below: BlockPos,
252) {
253 if !physics.is_in_water() && delta_y < 0. {
254 physics.fall_distance -= delta_y as f32 as f64;
255 }
256
257 if physics.on_ground() {
258 physics.fall_distance = 0.;
261 }
262}
263
264fn maybe_back_off_from_edge(move_ctx: &mut MoveCtx, mut movement: Vec3) -> Vec3 {
265 let is_staying_on_ground_surface = move_ctx.physics_state.is_some_and(|s| s.trying_to_crouch);
266 let max_up_step = get_max_up_step(move_ctx.attributes);
267
268 let fall_ctx = CanFallAtLeastCtx {
269 physics: move_ctx.physics,
270 world: move_ctx.world,
271 source_entity: move_ctx.source_entity,
272 physics_query: move_ctx.physics_query,
273 collidable_entity_query: move_ctx.collidable_entity_query,
274 };
275
276 let Some(abilities) = move_ctx.abilities else {
277 return movement;
278 };
279
280 let is_backing_off = !abilities.flying
281 && movement.y <= 0.
282 && matches!(move_ctx.mover_type, MoverType::Own | MoverType::Player)
283 && is_staying_on_ground_surface
284 && is_above_ground(&fall_ctx, max_up_step);
285 if !is_backing_off {
286 return movement;
287 }
288
289 let min_movement = 0.05;
290 let min_movement_x = movement.x.signum() * min_movement;
291 let min_movement_z = movement.z.signum() * min_movement;
292
293 while movement.x != 0. && can_fall_at_least(&fall_ctx, movement.x, 0., max_up_step as f64) {
294 if movement.x.abs() <= min_movement {
295 movement.x = 0.;
296 break;
297 }
298
299 movement.x -= min_movement_x
300 }
301 while movement.z != 0. && can_fall_at_least(&fall_ctx, 0., movement.z, max_up_step as f64) {
302 if movement.z.abs() <= min_movement {
303 movement.z = 0.;
304 break;
305 }
306
307 movement.z -= min_movement_z
308 }
309 while movement.x != 0.0
310 && movement.z != 0.0
311 && can_fall_at_least(&fall_ctx, movement.x, movement.z, max_up_step as f64)
312 {
313 if movement.x.abs() <= min_movement {
314 movement.x = 0.;
315 } else {
316 movement.x -= min_movement_x;
317 }
318 if movement.z.abs() <= min_movement {
319 movement.z = 0.;
320 } else {
321 movement.z -= min_movement_z;
322 }
323 }
324
325 movement
326}
327
328fn get_max_up_step(attributes: &Attributes) -> f32 {
329 attributes.step_height.calculate() as f32
331}
332
333fn is_above_ground(ctx: &CanFallAtLeastCtx, max_up_step: f32) -> bool {
334 ctx.physics.on_ground()
335 && ctx.physics.fall_distance < max_up_step as f64
336 && !can_fall_at_least(ctx, 0., 0., max_up_step as f64 - ctx.physics.fall_distance)
337}
338
339pub struct CanFallAtLeastCtx<'world, 'state, 'a, 'b> {
340 physics: &'a Physics,
341 world: &'a Instance,
342 source_entity: Entity,
343 physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
344 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
345}
346
347fn can_fall_at_least(
348 ctx: &CanFallAtLeastCtx,
349 delta_x: f64,
350 delta_z: f64,
351 max_up_step: f64,
352) -> bool {
353 let aabb = ctx.physics.bounding_box;
354 let aabb = AABB {
355 min: Vec3 {
356 x: aabb.min.x + EPSILON + delta_x,
357 y: aabb.min.y - max_up_step - EPSILON,
358 z: aabb.min.z + EPSILON + delta_z,
359 },
360 max: Vec3 {
361 x: aabb.max.x - EPSILON + delta_x,
362 y: aabb.min.y,
363 z: aabb.max.z - EPSILON + delta_z,
364 },
365 };
366 no_collision(
367 ctx.world,
368 Some(ctx.source_entity),
369 ctx.physics_query,
370 ctx.collidable_entity_query,
371 ctx.physics,
372 &aabb,
373 false,
374 )
375}
376
377fn collide_bounding_box(
378 movement: Vec3,
379 entity_bounding_box: &AABB,
380 world: &Instance,
381 entity_collisions: &[VoxelShape],
382) -> Vec3 {
383 let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
384
385 if !entity_collisions.is_empty() {
386 collision_boxes.extend_from_slice(entity_collisions);
387 }
388
389 let block_collisions =
392 get_block_collisions(world, &entity_bounding_box.expand_towards(movement));
393 collision_boxes.extend(block_collisions);
394 collide_with_shapes(movement, *entity_bounding_box, &collision_boxes)
395}
396
397fn collide_with_shapes(
398 mut movement: Vec3,
399 mut entity_box: AABB,
400 collision_boxes: &[VoxelShape],
401) -> Vec3 {
402 if collision_boxes.is_empty() {
403 return movement;
404 }
405
406 if movement.y != 0. {
407 movement.y = Shapes::collide(Axis::Y, &entity_box, collision_boxes, movement.y);
408 if movement.y != 0. {
409 entity_box = entity_box.move_relative(Vec3::new(0., movement.y, 0.));
410 }
411 }
412
413 let more_z_movement = movement.x.abs() < movement.z.abs();
416
417 if more_z_movement && movement.z != 0. {
418 movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
419 if movement.z != 0. {
420 entity_box = entity_box.move_relative(Vec3::new(0., 0., movement.z));
421 }
422 }
423
424 if movement.x != 0. {
425 movement.x = Shapes::collide(Axis::X, &entity_box, collision_boxes, movement.x);
426 if movement.x != 0. {
427 entity_box = entity_box.move_relative(Vec3::new(movement.x, 0., 0.));
428 }
429 }
430
431 if !more_z_movement && movement.z != 0. {
432 movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
433 }
434
435 movement
436}
437
438pub fn fluid_shape(fluid: &FluidState, world: &ChunkStorage, pos: BlockPos) -> &'static VoxelShape {
443 if fluid.amount == 9 {
444 let fluid_state_above = world.get_fluid_state(pos.up(1)).unwrap_or_default();
445 if fluid_state_above.kind == fluid.kind {
446 return &BLOCK_SHAPE;
447 }
448 }
449 if fluid.amount > 9 {
450 warn!("Tried to calculate shape for fluid with height > 9: {fluid:?} at {pos}");
451 return &EMPTY_SHAPE;
452 }
453
454 static FLUID_SHAPES: LazyLock<[VoxelShape; 10]> = LazyLock::new(|| {
458 [
459 calculate_shape_for_fluid(0),
460 calculate_shape_for_fluid(1),
461 calculate_shape_for_fluid(2),
462 calculate_shape_for_fluid(3),
463 calculate_shape_for_fluid(4),
464 calculate_shape_for_fluid(5),
465 calculate_shape_for_fluid(6),
466 calculate_shape_for_fluid(7),
467 calculate_shape_for_fluid(8),
468 calculate_shape_for_fluid(9),
469 ]
470 });
471
472 &FLUID_SHAPES[fluid.amount as usize]
473}
474fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
475 box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0)
476}
477
478pub fn legacy_blocks_motion(block: BlockState) -> bool {
482 if block == BlockState::AIR {
483 return false;
485 }
486
487 let registry_block = azalea_registry::Block::from(block);
488 legacy_calculate_solid(block)
489 && registry_block != azalea_registry::Block::Cobweb
490 && registry_block != azalea_registry::Block::BambooSapling
491}
492
493pub fn legacy_calculate_solid(block: BlockState) -> bool {
494 let block_trait = Box::<dyn azalea_block::BlockTrait>::from(block);
496 if let Some(solid) = block_trait.behavior().force_solid {
497 return solid;
498 }
499
500 let shape = block.collision_shape();
501 if shape.is_empty() {
502 return false;
503 }
504 let bounds = shape.bounds();
505 bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0
506}