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, 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
129#[allow(clippy::too_many_arguments)]
133pub fn move_colliding(ctx: &mut MoveCtx, mut movement: Vec3) -> Result<(), MoveEntityError> {
134 movement = maybe_back_off_from_edge(ctx, movement);
154 let collide_result = collide(ctx, movement);
155
156 let move_distance_sqr = collide_result.length_squared();
157
158 let position = &mut ctx.position;
159 let physics = &mut *ctx.physics;
160 let world = ctx.world;
161
162 if move_distance_sqr > EPSILON || movement.length_squared() - move_distance_sqr < EPSILON {
163 let new_pos = {
166 Vec3 {
167 x: position.x + collide_result.x,
168 y: position.y + collide_result.y,
169 z: position.z + collide_result.z,
170 }
171 };
172
173 if new_pos != ***position {
174 ***position = new_pos;
175 }
176 }
177
178 let x_collision = !math::equal(movement.x, collide_result.x);
179 let z_collision = !math::equal(movement.z, collide_result.z);
180 let horizontal_collision = x_collision || z_collision;
181 physics.horizontal_collision = horizontal_collision;
182
183 let vertical_collision = movement.y != collide_result.y;
184 physics.vertical_collision = vertical_collision;
185 let on_ground = vertical_collision && movement.y < 0.;
186 physics.set_on_ground(on_ground);
187
188 let block_pos_below = azalea_entity::on_pos_legacy(&world.chunks, **position);
191 let block_state_below = world.get_block_state(block_pos_below).unwrap_or_default();
192
193 check_fall_damage(
194 physics,
195 collide_result.y,
196 block_state_below,
197 block_pos_below,
198 );
199
200 if horizontal_collision {
203 let delta_movement = &physics.velocity;
204 physics.velocity = Vec3 {
205 x: if x_collision { 0. } else { delta_movement.x },
206 y: delta_movement.y,
207 z: if z_collision { 0. } else { delta_movement.z },
208 }
209 }
210
211 if vertical_collision {
212 physics.velocity.y = 0.;
216 }
217
218 if on_ground {
219 }
222
223 Ok(())
247}
248
249fn check_fall_damage(
250 physics: &mut Physics,
251 delta_y: f64,
252 _block_state_below: BlockState,
253 _block_pos_below: BlockPos,
254) {
255 if !physics.is_in_water() && delta_y < 0. {
256 physics.fall_distance -= delta_y as f32 as f64;
257 }
258
259 if physics.on_ground() {
260 physics.fall_distance = 0.;
263 }
264}
265
266fn maybe_back_off_from_edge(move_ctx: &mut MoveCtx, mut movement: Vec3) -> Vec3 {
267 let is_staying_on_ground_surface = move_ctx.physics_state.is_some_and(|s| s.trying_to_crouch);
268 let max_up_step = get_max_up_step(move_ctx.attributes);
269
270 let fall_ctx = CanFallAtLeastCtx {
271 physics: move_ctx.physics,
272 world: move_ctx.world,
273 source_entity: move_ctx.source_entity,
274 aabb_query: move_ctx.aabb_query,
275 collidable_entity_query: move_ctx.collidable_entity_query,
276 };
277
278 let Some(abilities) = move_ctx.abilities else {
279 return movement;
280 };
281
282 let is_backing_off = !abilities.flying
283 && movement.y <= 0.
284 && matches!(move_ctx.mover_type, MoverType::Own | MoverType::Player)
285 && is_staying_on_ground_surface
286 && is_above_ground(&fall_ctx, max_up_step);
287 if !is_backing_off {
288 return movement;
289 }
290
291 let min_movement = 0.05;
292 let min_movement_x = movement.x.signum() * min_movement;
293 let min_movement_z = movement.z.signum() * min_movement;
294
295 while movement.x != 0. && can_fall_at_least(&fall_ctx, movement.x, 0., max_up_step as f64) {
296 if movement.x.abs() <= min_movement {
297 movement.x = 0.;
298 break;
299 }
300
301 movement.x -= min_movement_x
302 }
303 while movement.z != 0. && can_fall_at_least(&fall_ctx, 0., movement.z, max_up_step as f64) {
304 if movement.z.abs() <= min_movement {
305 movement.z = 0.;
306 break;
307 }
308
309 movement.z -= min_movement_z
310 }
311 while movement.x != 0.0
312 && movement.z != 0.0
313 && can_fall_at_least(&fall_ctx, movement.x, movement.z, max_up_step as f64)
314 {
315 if movement.x.abs() <= min_movement {
316 movement.x = 0.;
317 } else {
318 movement.x -= min_movement_x;
319 }
320 if movement.z.abs() <= min_movement {
321 movement.z = 0.;
322 } else {
323 movement.z -= min_movement_z;
324 }
325 }
326
327 movement
328}
329
330fn get_max_up_step(attributes: &Attributes) -> f32 {
331 attributes.step_height.calculate() as f32
333}
334
335fn is_above_ground(ctx: &CanFallAtLeastCtx, max_up_step: f32) -> bool {
336 ctx.physics.on_ground()
337 && ctx.physics.fall_distance < max_up_step as f64
338 && !can_fall_at_least(ctx, 0., 0., max_up_step as f64 - ctx.physics.fall_distance)
339}
340
341pub struct CanFallAtLeastCtx<'world, 'state, 'a, 'b> {
342 physics: &'a Physics,
343 world: &'a Instance,
344 source_entity: Entity,
345 aabb_query: &'a AabbQuery<'world, 'state, 'b>,
346 collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
347}
348
349fn can_fall_at_least(
350 ctx: &CanFallAtLeastCtx,
351 delta_x: f64,
352 delta_z: f64,
353 max_up_step: f64,
354) -> bool {
355 let aabb = ctx.physics.bounding_box;
356 let aabb = Aabb {
357 min: Vec3 {
358 x: aabb.min.x + EPSILON + delta_x,
359 y: aabb.min.y - max_up_step - EPSILON,
360 z: aabb.min.z + EPSILON + delta_z,
361 },
362 max: Vec3 {
363 x: aabb.max.x - EPSILON + delta_x,
364 y: aabb.min.y,
365 z: aabb.max.z - EPSILON + delta_z,
366 },
367 };
368 no_collision(
369 ctx.world,
370 Some(ctx.source_entity),
371 ctx.aabb_query,
372 ctx.collidable_entity_query,
373 ctx.physics,
374 &aabb,
375 false,
376 )
377}
378
379fn collide_bounding_box(
380 movement: Vec3,
381 entity_bounding_box: &Aabb,
382 world: &Instance,
383 entity_collisions: &[VoxelShape],
384) -> Vec3 {
385 let mut collision_boxes: Vec<VoxelShape> = Vec::with_capacity(entity_collisions.len() + 1);
386
387 if !entity_collisions.is_empty() {
388 collision_boxes.extend_from_slice(entity_collisions);
389 }
390
391 let block_collisions =
394 get_block_collisions(world, &entity_bounding_box.expand_towards(movement));
395 collision_boxes.extend(block_collisions);
396 collide_with_shapes(movement, *entity_bounding_box, &collision_boxes)
397}
398
399fn collide_with_shapes(
400 mut movement: Vec3,
401 mut entity_box: Aabb,
402 collision_boxes: &[VoxelShape],
403) -> Vec3 {
404 if collision_boxes.is_empty() {
405 return movement;
406 }
407
408 if movement.y != 0. {
409 movement.y = Shapes::collide(Axis::Y, &entity_box, collision_boxes, movement.y);
410 if movement.y != 0. {
411 entity_box = entity_box.move_relative(Vec3::new(0., movement.y, 0.));
412 }
413 }
414
415 let more_z_movement = movement.x.abs() < movement.z.abs();
418
419 if more_z_movement && movement.z != 0. {
420 movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
421 if movement.z != 0. {
422 entity_box = entity_box.move_relative(Vec3::new(0., 0., movement.z));
423 }
424 }
425
426 if movement.x != 0. {
427 movement.x = Shapes::collide(Axis::X, &entity_box, collision_boxes, movement.x);
428 if movement.x != 0. {
429 entity_box = entity_box.move_relative(Vec3::new(movement.x, 0., 0.));
430 }
431 }
432
433 if !more_z_movement && movement.z != 0. {
434 movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z);
435 }
436
437 movement
438}
439
440pub fn fluid_shape(fluid: &FluidState, world: &ChunkStorage, pos: BlockPos) -> &'static VoxelShape {
445 if fluid.amount == 9 {
446 let fluid_state_above = world.get_fluid_state(pos.up(1)).unwrap_or_default();
447 if fluid_state_above.kind == fluid.kind {
448 return &BLOCK_SHAPE;
449 }
450 }
451 if fluid.amount > 9 {
452 warn!("Tried to calculate shape for fluid with height > 9: {fluid:?} at {pos}");
453 return &EMPTY_SHAPE;
454 }
455
456 static FLUID_SHAPES: LazyLock<[VoxelShape; 10]> = LazyLock::new(|| {
460 [
461 calculate_shape_for_fluid(0),
462 calculate_shape_for_fluid(1),
463 calculate_shape_for_fluid(2),
464 calculate_shape_for_fluid(3),
465 calculate_shape_for_fluid(4),
466 calculate_shape_for_fluid(5),
467 calculate_shape_for_fluid(6),
468 calculate_shape_for_fluid(7),
469 calculate_shape_for_fluid(8),
470 calculate_shape_for_fluid(9),
471 ]
472 });
473
474 &FLUID_SHAPES[fluid.amount as usize]
475}
476fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
477 box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0)
478}
479
480pub fn legacy_blocks_motion(block: BlockState) -> bool {
484 if block == BlockState::AIR {
485 return false;
487 }
488
489 let registry_block = azalea_registry::Block::from(block);
490 legacy_calculate_solid(block)
491 && registry_block != azalea_registry::Block::Cobweb
492 && registry_block != azalea_registry::Block::BambooSapling
493}
494
495pub fn legacy_calculate_solid(block: BlockState) -> bool {
496 let block_trait = Box::<dyn azalea_block::BlockTrait>::from(block);
498 if let Some(solid) = block_trait.behavior().force_solid {
499 return solid;
500 }
501
502 let shape = block.collision_shape();
503 if shape.is_empty() {
504 return false;
505 }
506 let bounds = shape.bounds();
507 bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0
508}