Skip to main content

steel_core/physics/
collision.rs

1//! World collision queries for physics simulation.
2
3use std::{ops::ControlFlow, sync::Arc};
4
5use glam::DVec3;
6use smallvec::SmallVec;
7use steel_registry::{blocks::block_state_ext::BlockStateExt, vanilla_blocks, vanilla_entities};
8use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId, WorldAabb};
9
10use crate::behavior::{
11    BLOCK_BEHAVIORS, BlockCollisionBoxes, BlockCollisionContext, blocks::PowderSnowBlock,
12};
13use crate::entity::Entity;
14use crate::physics::COLLISION_EPSILON;
15use crate::physics::shapes::join_is_not_empty;
16use crate::world::{BlockRegionBounds, World};
17
18const BLOCK_COLLISION_EPSILON: f64 = 1.0e-7;
19const ENTITY_COLLISION_EPSILON: f64 = 1.0e-7;
20// Bound the temporary snapshot to at most one full chunk section.
21const MAX_PREFETCHED_COLLISION_BLOCKS: usize = 4096;
22
23/// Trait for querying collision shapes from the world.
24///
25/// This abstraction allows testing physics without a full world instance.
26pub trait CollisionWorld {
27    /// Gets the block state at the given position.
28    fn get_block_state(&self, pos: BlockPos) -> BlockStateId;
29
30    /// Queries all block collision shapes that intersect with the given AABB.
31    ///
32    /// Returns a list of world-space AABBs representing solid block collisions.
33    fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb>;
34
35    /// Returns whether any block collision shape intersects with the given AABB.
36    fn has_block_collision(&self, aabb: &WorldAabb) -> bool {
37        !self.get_block_collisions(aabb).is_empty()
38    }
39
40    /// Queries all block collision shapes with a vanilla collision context.
41    fn get_block_collisions_with_context(
42        &self,
43        aabb: &WorldAabb,
44        context: BlockCollisionContext,
45    ) -> Vec<WorldAabb> {
46        let _ = context;
47        self.get_block_collisions(aabb)
48    }
49
50    /// Returns whether any block collision shape intersects with the given AABB and context.
51    fn has_block_collision_with_context(
52        &self,
53        aabb: &WorldAabb,
54        context: BlockCollisionContext,
55    ) -> bool {
56        !self
57            .get_block_collisions_with_context(aabb, context)
58            .is_empty()
59    }
60
61    /// Queries all entity collision shapes intersecting the given AABB.
62    ///
63    /// Path-navigation regions and test worlds use the default empty entity
64    /// collision list. Live entity movement supplies these through
65    /// [`WorldCollisionProvider`].
66    fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
67        let _ = aabb;
68        Vec::new()
69    }
70
71    /// Returns whether any entity collision shape intersects with the given AABB.
72    fn has_entity_collision(&self, aabb: &WorldAabb) -> bool {
73        !self.get_entity_collisions(aabb).is_empty()
74    }
75
76    /// Queries world-border collision shapes intersecting the given AABB.
77    fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
78        let _ = aabb;
79        Vec::new()
80    }
81
82    /// Returns whether any world-border collision shape intersects with the given AABB.
83    fn has_world_border_collision(&self, aabb: &WorldAabb) -> bool {
84        !self.get_world_border_collisions(aabb).is_empty()
85    }
86
87    /// Queries entity, world-border, then block collisions with a vanilla context.
88    fn get_collisions_with_context(
89        &self,
90        aabb: &WorldAabb,
91        context: BlockCollisionContext,
92    ) -> Vec<WorldAabb> {
93        let mut collisions = self.get_entity_collisions(aabb);
94        collisions.extend(self.get_world_border_collisions(aabb));
95        collisions.extend(self.get_block_collisions_with_context(aabb, context));
96        collisions
97    }
98
99    /// Returns whether any entity, world-border, or block collision shape intersects the AABB.
100    fn has_collision_with_context(&self, aabb: &WorldAabb, context: BlockCollisionContext) -> bool {
101        self.has_entity_collision(aabb)
102            || self.has_world_border_collision(aabb)
103            || self.has_block_collision_with_context(aabb, context)
104    }
105
106    /// Gets collision shapes for vanilla pre-move checks.
107    ///
108    /// # Arguments
109    /// * `aabb` - The entity's bounding box after intended movement
110    /// * `old_bottom_center` - The entity's bottom-center position before movement
111    /// * `descending` - Whether the source entity is descending.
112    ///
113    /// # Returns
114    /// Collision shapes intersecting the target box.
115    ///
116    /// Vanilla includes entity collisions and uses the old bottom-center Y as
117    /// block collision context.
118    fn get_pre_move_collisions(
119        &self,
120        aabb: &WorldAabb,
121        old_bottom_center: DVec3,
122        descending: bool,
123    ) -> Vec<WorldAabb> {
124        let mut collisions = self.get_entity_collisions(aabb);
125        collisions.extend(self.get_block_collisions_with_context(
126            aabb,
127            BlockCollisionContext::pre_move(old_bottom_center.y, descending),
128        ));
129        collisions
130    }
131}
132
133/// Implements `CollisionWorld` for the Steel World struct.
134pub struct WorldCollisionProvider<'a> {
135    world: &'a Arc<World>,
136    source: Option<&'a dyn Entity>,
137    include_entity_collisions: bool,
138}
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141struct BlockCollisionSearchBounds {
142    min_x: i32,
143    min_y: i32,
144    min_z: i32,
145    max_x: i32,
146    max_y: i32,
147    max_z: i32,
148}
149
150impl BlockCollisionSearchBounds {
151    fn from_aabb(aabb: &WorldAabb) -> Self {
152        Self {
153            min_x: (aabb.min_x() - BLOCK_COLLISION_EPSILON).floor() as i32 - 1,
154            min_y: (aabb.min_y() - BLOCK_COLLISION_EPSILON).floor() as i32 - 1,
155            min_z: (aabb.min_z() - BLOCK_COLLISION_EPSILON).floor() as i32 - 1,
156            max_x: (aabb.max_x() + BLOCK_COLLISION_EPSILON).floor() as i32 + 1,
157            max_y: (aabb.max_y() + BLOCK_COLLISION_EPSILON).floor() as i32 + 1,
158            max_z: (aabb.max_z() + BLOCK_COLLISION_EPSILON).floor() as i32 + 1,
159        }
160    }
161
162    fn cursor_type(self, x: i32, y: i32, z: i32) -> CollisionCursorType {
163        let boundary_axis_count = u8::from(x == self.min_x || x == self.max_x)
164            + u8::from(y == self.min_y || y == self.max_y)
165            + u8::from(z == self.min_z || z == self.max_z);
166
167        match boundary_axis_count {
168            0 => CollisionCursorType::Inside,
169            1 => CollisionCursorType::Face,
170            2 => CollisionCursorType::Edge,
171            _ => CollisionCursorType::Corner,
172        }
173    }
174
175    const fn region_bounds(self) -> BlockRegionBounds {
176        BlockRegionBounds::from_corners(
177            BlockPos::new(self.min_x, self.min_y, self.min_z),
178            BlockPos::new(self.max_x, self.max_y, self.max_z),
179        )
180    }
181
182    fn block_count(self) -> Option<usize> {
183        let width = usize::try_from(i64::from(self.max_x) - i64::from(self.min_x) + 1).ok()?;
184        let height = usize::try_from(i64::from(self.max_y) - i64::from(self.min_y) + 1).ok()?;
185        let depth = usize::try_from(i64::from(self.max_z) - i64::from(self.min_z) + 1).ok()?;
186        width.checked_mul(height)?.checked_mul(depth)
187    }
188
189    fn try_for_each_candidate<R>(
190        self,
191        mut visit: impl FnMut(BlockPos, CollisionCursorType) -> ControlFlow<R>,
192    ) -> ControlFlow<R> {
193        for y in self.min_y..=self.max_y {
194            for z in self.min_z..=self.max_z {
195                for x in self.min_x..=self.max_x {
196                    let cursor_type = self.cursor_type(x, y, z);
197                    if cursor_type == CollisionCursorType::Corner {
198                        continue;
199                    }
200                    visit(BlockPos::new(x, y, z), cursor_type)?;
201                }
202            }
203        }
204        ControlFlow::Continue(())
205    }
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209enum CollisionCursorType {
210    Inside,
211    Face,
212    Edge,
213    Corner,
214}
215
216type BlockCollisionCandidate = (BlockPos, BlockStateId, CollisionCursorType);
217
218struct CollisionShape {
219    boxes: BlockCollisionBoxes,
220}
221
222impl CollisionShape {
223    fn has_large_collision_shape(&self) -> bool {
224        self.boxes.iter().any(|aabb| {
225            aabb.min_x() < 0.0
226                || aabb.min_y() < 0.0
227                || aabb.min_z() < 0.0
228                || aabb.max_x() > 1.0
229                || aabb.max_y() > 1.0
230                || aabb.max_z() > 1.0
231        })
232    }
233}
234
235fn should_query_collision_shape(
236    block_state: BlockStateId,
237    collision_shape: &CollisionShape,
238    cursor_type: CollisionCursorType,
239) -> bool {
240    match cursor_type {
241        CollisionCursorType::Inside => true,
242        CollisionCursorType::Face => {
243            block_state.get_block().config.dynamic_shape
244                || collision_shape.has_large_collision_shape()
245        }
246        CollisionCursorType::Edge => block_state.get_block() == &vanilla_blocks::MOVING_PISTON,
247        CollisionCursorType::Corner => false,
248    }
249}
250
251fn translate_collision_shape(shape: &BlockLocalAabb, block_pos: BlockPos) -> WorldAabb {
252    shape.at_block(block_pos)
253}
254
255impl<'a> WorldCollisionProvider<'a> {
256    /// Creates a new collision provider for the given world.
257    pub const fn new(world: &'a Arc<World>) -> Self {
258        Self {
259            world,
260            source: None,
261            include_entity_collisions: true,
262        }
263    }
264
265    /// Creates a collision provider for movement authored by `source`.
266    pub const fn for_entity(world: &'a Arc<World>, source: &'a dyn Entity) -> Self {
267        Self {
268            world,
269            source: Some(source),
270            include_entity_collisions: true,
271        }
272    }
273
274    /// Creates a collision provider matching vanilla `PathNavigationRegion`.
275    pub const fn for_path_navigation(world: &'a Arc<World>, source: &'a dyn Entity) -> Self {
276        Self {
277            world,
278            source: Some(source),
279            include_entity_collisions: false,
280        }
281    }
282
283    fn prefetched_block_collision_candidates(
284        &self,
285        bounds: BlockCollisionSearchBounds,
286    ) -> Option<SmallVec<[BlockCollisionCandidate; 64]>> {
287        if bounds
288            .block_count()
289            .is_none_or(|count| count > MAX_PREFETCHED_COLLISION_BLOCKS)
290        {
291            return None;
292        }
293
294        self.world
295            .try_with_block_region(bounds.region_bounds(), |region| {
296                let mut candidates = SmallVec::new();
297                let _ = bounds.try_for_each_candidate(|block_pos, cursor_type| {
298                    let Some(block_state) = region.get_block_state(block_pos) else {
299                        return ControlFlow::<()>::Continue(());
300                    };
301                    if !block_state.is_air() {
302                        candidates.push((block_pos, block_state, cursor_type));
303                    }
304                    ControlFlow::<()>::Continue(())
305                });
306                candidates
307            })
308    }
309
310    fn visit_block_collision_candidates<R>(
311        &self,
312        bounds: BlockCollisionSearchBounds,
313        mut visit: impl FnMut(BlockPos, BlockStateId, CollisionCursorType) -> ControlFlow<R>,
314    ) -> ControlFlow<R> {
315        if let Some(candidates) = self.prefetched_block_collision_candidates(bounds) {
316            for (block_pos, block_state, cursor_type) in candidates {
317                visit(block_pos, block_state, cursor_type)?;
318            }
319            return ControlFlow::Continue(());
320        }
321
322        bounds.try_for_each_candidate(|block_pos, cursor_type| {
323            let block_state = self.world.get_block_state(block_pos);
324            if block_state.is_air() {
325                return ControlFlow::Continue(());
326            }
327            visit(block_pos, block_state, cursor_type)
328        })
329    }
330
331    fn get_collision_shape(
332        &self,
333        block_state: BlockStateId,
334        block_pos: BlockPos,
335        context: BlockCollisionContext,
336    ) -> CollisionShape {
337        let behavior = BLOCK_BEHAVIORS.get_behavior(block_state.get_block());
338        let boxes =
339            behavior.get_collision_boxes(block_state, self.world.as_ref(), block_pos, context);
340
341        CollisionShape { boxes }
342    }
343
344    fn entity_collision_context(
345        &self,
346        entity_bottom: f64,
347        descending: bool,
348        placement: bool,
349    ) -> BlockCollisionContext {
350        let context = if placement {
351            BlockCollisionContext::pre_move(entity_bottom, descending)
352        } else {
353            BlockCollisionContext::entity(entity_bottom, descending)
354        };
355
356        if let Some(source) = self.source {
357            context
358                .with_fall_distance(source.fall_distance())
359                .with_can_walk_on_powder_snow(PowderSnowBlock::can_entity_walk_on_powder_snow(
360                    source,
361                ))
362                .with_falling_block(source.entity_type() == &vanilla_entities::FALLING_BLOCK)
363        } else {
364            context
365        }
366    }
367
368    /// Returns whether an entity-context collision query intersects anything.
369    ///
370    /// Mirrors vanilla `Level.noCollision(entity, box)` callers by using the
371    /// source entity's normal collision context rather than a source-less check.
372    #[must_use]
373    pub fn has_entity_context_collision(
374        &self,
375        aabb: WorldAabb,
376        entity_bottom: f64,
377        descending: bool,
378    ) -> bool {
379        self.has_collision_with_context(
380            &aabb.deflate(COLLISION_EPSILON),
381            self.entity_collision_context(entity_bottom, descending, false),
382        )
383    }
384
385    /// Finds the block supporting an entity within `aabb`.
386    ///
387    /// Mirrors vanilla `CollisionGetter.findSupportingBlock`: among colliding
388    /// blocks, choose the closest block center to the entity position, then use
389    /// vanilla `BlockPos` ordering as a tie-breaker.
390    #[must_use]
391    #[expect(
392        clippy::float_cmp,
393        reason = "intentional: vanilla compares equal support distances exactly"
394    )]
395    pub fn find_supporting_block(
396        &self,
397        entity_position: DVec3,
398        aabb: &WorldAabb,
399        descending: bool,
400    ) -> Option<BlockPos> {
401        let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
402        let context = self.entity_collision_context(entity_position.y, descending, false);
403
404        let mut main_support = None;
405        let mut main_support_distance = f64::MAX;
406        let _ =
407            self.visit_block_collision_candidates(bounds, |block_pos, block_state, cursor_type| {
408                let collision_shape = self.get_collision_shape(block_state, block_pos, context);
409                if collision_shape.boxes.is_empty()
410                    || !should_query_collision_shape(block_state, &collision_shape, cursor_type)
411                    || !collision_shape
412                        .boxes
413                        .iter()
414                        .map(|shape_aabb| translate_collision_shape(shape_aabb, block_pos))
415                        .any(|world_aabb| aabb.intersects(world_aabb))
416                {
417                    return ControlFlow::<()>::Continue(());
418                }
419
420                let distance = block_pos_center_distance_sq(block_pos, entity_position);
421                let should_replace = distance < main_support_distance
422                    || distance == main_support_distance
423                        && main_support
424                            .is_none_or(|support| vanilla_block_pos_less(support, block_pos));
425                if should_replace {
426                    main_support = Some(block_pos);
427                    main_support_distance = distance;
428                }
429                ControlFlow::<()>::Continue(())
430            });
431
432        main_support
433    }
434
435    /// Returns vanilla `CollisionGetter.findFreePosition` for AABB-backed center shapes.
436    #[must_use]
437    pub fn find_free_position(
438        &self,
439        allowed_centers: &[WorldAabb],
440        preferred_center: DVec3,
441        size_x: f64,
442        size_y: f64,
443        size_z: f64,
444    ) -> Option<DVec3> {
445        let allowed_bounds = union_bounds(allowed_centers)?;
446        let search_area = allowed_bounds.inflate_xyz(size_x, size_y, size_z);
447        let context = self
448            .source
449            .map_or(BlockCollisionContext::empty(), |source| {
450                self.entity_collision_context(source.position().y, source.is_descending(), false)
451            });
452        let world_border = self.world.world_border_snapshot();
453        let expanded_collisions = self
454            .get_block_collisions_with_context(&search_area, context)
455            .into_iter()
456            .filter(|shape| world_border.is_within_bounds(*shape))
457            .map(|shape| shape.inflate_xyz(size_x / 2.0, size_y / 2.0, size_z / 2.0));
458
459        closest_free_position(allowed_centers, preferred_center, expanded_collisions)
460    }
461}
462
463fn union_bounds(boxes: &[WorldAabb]) -> Option<WorldAabb> {
464    let mut boxes = boxes.iter().copied().filter(|aabb| !aabb.is_empty());
465    let first = boxes.next()?;
466    Some(boxes.fold(first, |bounds, aabb| {
467        WorldAabb::encapsulating(&bounds, &aabb)
468    }))
469}
470
471fn closest_free_position(
472    allowed_centers: &[WorldAabb],
473    preferred_center: DVec3,
474    expanded_collisions: impl IntoIterator<Item = WorldAabb>,
475) -> Option<DVec3> {
476    let mut free_boxes = allowed_centers
477        .iter()
478        .copied()
479        .filter(|aabb| !aabb.is_empty())
480        .collect::<Vec<_>>();
481
482    if free_boxes.is_empty() {
483        return None;
484    }
485
486    for collision in expanded_collisions {
487        if collision.is_empty() {
488            continue;
489        }
490
491        let mut next_boxes = Vec::new();
492        for free_box in free_boxes {
493            subtract_aabb(free_box, collision, &mut next_boxes);
494        }
495        free_boxes = next_boxes;
496        if free_boxes.is_empty() {
497            return None;
498        }
499    }
500
501    closest_point_to_boxes(&free_boxes, preferred_center)
502}
503
504fn subtract_aabb(free: WorldAabb, blocked: WorldAabb, output: &mut Vec<WorldAabb>) {
505    if free.is_empty() {
506        return;
507    }
508
509    if !free.intersects(blocked) {
510        output.push(free);
511        return;
512    }
513
514    let min_x = free.min_x().max(blocked.min_x());
515    let max_x = free.max_x().min(blocked.max_x());
516    let min_y = free.min_y().max(blocked.min_y());
517    let max_y = free.max_y().min(blocked.max_y());
518    let min_z = free.min_z().max(blocked.min_z());
519    let max_z = free.max_z().min(blocked.max_z());
520
521    push_non_empty_aabb(
522        output,
523        free.min_x(),
524        free.min_y(),
525        free.min_z(),
526        min_x,
527        free.max_y(),
528        free.max_z(),
529    );
530    push_non_empty_aabb(
531        output,
532        max_x,
533        free.min_y(),
534        free.min_z(),
535        free.max_x(),
536        free.max_y(),
537        free.max_z(),
538    );
539    push_non_empty_aabb(
540        output,
541        min_x,
542        free.min_y(),
543        free.min_z(),
544        max_x,
545        min_y,
546        free.max_z(),
547    );
548    push_non_empty_aabb(
549        output,
550        min_x,
551        max_y,
552        free.min_z(),
553        max_x,
554        free.max_y(),
555        free.max_z(),
556    );
557    push_non_empty_aabb(output, min_x, min_y, free.min_z(), max_x, max_y, min_z);
558    push_non_empty_aabb(output, min_x, min_y, max_z, max_x, max_y, free.max_z());
559}
560
561fn push_non_empty_aabb(
562    output: &mut Vec<WorldAabb>,
563    min_x: f64,
564    min_y: f64,
565    min_z: f64,
566    max_x: f64,
567    max_y: f64,
568    max_z: f64,
569) {
570    let aabb = WorldAabb::new(min_x, min_y, min_z, max_x, max_y, max_z);
571    if !aabb.is_empty() {
572        output.push(aabb);
573    }
574}
575
576fn closest_point_to_boxes(boxes: &[WorldAabb], preferred_center: DVec3) -> Option<DVec3> {
577    let mut closest = None;
578    let mut closest_distance = f64::MAX;
579    for aabb in boxes {
580        let point = aabb.closest_point_to(preferred_center);
581        let distance = point.distance_squared(preferred_center);
582        if closest.is_none() || distance < closest_distance {
583            closest = Some(point);
584            closest_distance = distance;
585        }
586    }
587    closest
588}
589
590fn block_pos_center_distance_sq(pos: BlockPos, point: DVec3) -> f64 {
591    let dx = f64::from(pos.x()) + 0.5 - point.x;
592    let dy = f64::from(pos.y()) + 0.5 - point.y;
593    let dz = f64::from(pos.z()) + 0.5 - point.z;
594    dx * dx + dy * dy + dz * dz
595}
596
597const fn vanilla_block_pos_less(left: BlockPos, right: BlockPos) -> bool {
598    left.y() < right.y()
599        || left.y() == right.y()
600            && (left.z() < right.z() || left.z() == right.z() && left.x() < right.x())
601}
602
603#[must_use]
604const fn bottom_center(aabb: WorldAabb) -> DVec3 {
605    DVec3::new(
606        f64::midpoint(aabb.min_x(), aabb.max_x()),
607        aabb.min_y(),
608        f64::midpoint(aabb.min_z(), aabb.max_z()),
609    )
610}
611
612/// Returns whether an entity box intersects any block collision shape.
613#[must_use]
614pub fn has_block_collision(world: &impl CollisionWorld, aabb: WorldAabb) -> bool {
615    world.has_block_collision(&aabb.deflate(COLLISION_EPSILON))
616}
617
618/// Returns whether an entity box intersects any entity or block collision shape.
619#[must_use]
620pub fn has_collision(world: &impl CollisionWorld, aabb: WorldAabb) -> bool {
621    world.has_collision_with_context(
622        &aabb.deflate(COLLISION_EPSILON),
623        BlockCollisionContext::empty(),
624    )
625}
626
627/// Returns whether `new_aabb` collides with shapes that `old_aabb` did not.
628///
629/// Matches vanilla `ServerGamePacketListenerImpl.isEntityCollidingWithAnythingNew()`.
630#[must_use]
631pub fn is_colliding_with_new_shapes(
632    world: &impl CollisionWorld,
633    old_aabb: WorldAabb,
634    new_aabb: WorldAabb,
635    descending: bool,
636) -> bool {
637    let old_shape = old_aabb.deflate(COLLISION_EPSILON);
638    for collision_aabb in world.get_pre_move_collisions(
639        &new_aabb.deflate(COLLISION_EPSILON),
640        bottom_center(old_aabb),
641        descending,
642    ) {
643        if !join_is_not_empty(&collision_aabb, &old_shape) {
644            return true;
645        }
646    }
647
648    false
649}
650
651impl CollisionWorld for WorldCollisionProvider<'_> {
652    fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
653        self.world.get_block_state(pos)
654    }
655
656    fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
657        self.get_block_collisions_with_context(aabb, BlockCollisionContext::empty())
658    }
659
660    fn get_block_collisions_with_context(
661        &self,
662        aabb: &WorldAabb,
663        context: BlockCollisionContext,
664    ) -> Vec<WorldAabb> {
665        let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
666        let mut collisions = Vec::new();
667        let _ =
668            self.visit_block_collision_candidates(bounds, |block_pos, block_state, cursor_type| {
669                let collision_shape = self.get_collision_shape(block_state, block_pos, context);
670                if collision_shape.boxes.is_empty()
671                    || !should_query_collision_shape(block_state, &collision_shape, cursor_type)
672                {
673                    return ControlFlow::<()>::Continue(());
674                }
675
676                collisions.extend(
677                    collision_shape
678                        .boxes
679                        .iter()
680                        .map(|shape| translate_collision_shape(shape, block_pos))
681                        .filter(|shape| aabb.intersects(*shape)),
682                );
683                ControlFlow::<()>::Continue(())
684            });
685        collisions
686    }
687
688    fn has_block_collision_with_context(
689        &self,
690        aabb: &WorldAabb,
691        context: BlockCollisionContext,
692    ) -> bool {
693        let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
694        self.visit_block_collision_candidates(bounds, |block_pos, block_state, cursor_type| {
695            let collision_shape = self.get_collision_shape(block_state, block_pos, context);
696            if collision_shape.boxes.is_empty()
697                || !should_query_collision_shape(block_state, &collision_shape, cursor_type)
698            {
699                return ControlFlow::Continue(());
700            }
701
702            if collision_shape
703                .boxes
704                .iter()
705                .map(|shape| translate_collision_shape(shape, block_pos))
706                .any(|shape| aabb.intersects(shape))
707            {
708                ControlFlow::Break(())
709            } else {
710                ControlFlow::Continue(())
711            }
712        })
713        .is_break()
714    }
715
716    fn get_pre_move_collisions(
717        &self,
718        aabb: &WorldAabb,
719        old_bottom_center: DVec3,
720        descending: bool,
721    ) -> Vec<WorldAabb> {
722        let mut collisions = self.get_entity_collisions(aabb);
723        collisions.extend(self.get_block_collisions_with_context(
724            aabb,
725            self.entity_collision_context(old_bottom_center.y, descending, true),
726        ));
727        collisions
728    }
729
730    fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
731        if !self.include_entity_collisions {
732            return Vec::new();
733        }
734        if aabb.size() < ENTITY_COLLISION_EPSILON {
735            return Vec::new();
736        }
737
738        let query = aabb.inflate(ENTITY_COLLISION_EPSILON);
739        self.world
740            .get_entity_bounding_boxes_in_aabb_matching(&query, |entity| match self.source {
741                Some(source) => {
742                    entity.id() != source.id()
743                        && !entity.is_removed()
744                        && !entity.is_spectator()
745                        && source.can_collide_with(entity)
746                }
747                None => {
748                    !entity.is_removed()
749                        && !entity.is_spectator()
750                        && entity.can_be_collided_with(None)
751                }
752            })
753    }
754
755    fn has_entity_collision(&self, aabb: &WorldAabb) -> bool {
756        if !self.include_entity_collisions {
757            return false;
758        }
759        if aabb.size() < ENTITY_COLLISION_EPSILON {
760            return false;
761        }
762
763        let query = aabb.inflate(ENTITY_COLLISION_EPSILON);
764        self.world
765            .has_entity_in_aabb_matching(&query, |entity| match self.source {
766                Some(source) => {
767                    entity.id() != source.id()
768                        && !entity.is_removed()
769                        && !entity.is_spectator()
770                        && source.can_collide_with(entity)
771                }
772                None => {
773                    !entity.is_removed()
774                        && !entity.is_spectator()
775                        && entity.can_be_collided_with(None)
776                }
777            })
778    }
779
780    fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
781        let Some(source) = self.source else {
782            return Vec::new();
783        };
784
785        let border = self.world.world_border_snapshot();
786        let source_position = source.position();
787        if !border.is_inside_close_to_border(source_position.x, source_position.z, *aabb) {
788            return Vec::new();
789        }
790
791        border.collision_shapes_for(*aabb)
792    }
793
794    fn has_world_border_collision(&self, aabb: &WorldAabb) -> bool {
795        let Some(source) = self.source else {
796            return false;
797        };
798
799        let border = self.world.world_border_snapshot();
800        let source_position = source.position();
801        border.is_inside_close_to_border(source_position.x, source_position.z, *aabb)
802            && !border.collision_shapes_for(*aabb).is_empty()
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use std::iter;
809
810    use super::*;
811    use steel_registry::blocks::shapes::VoxelShape;
812    use steel_registry::init_vanilla_registry;
813    use steel_utils::{BlockLocalAabb, ChunkPos, types::UpdateFlags};
814
815    use crate::{
816        behavior::init_behaviors,
817        test_support::{fresh_test_world, insert_ready_full_chunk},
818    };
819
820    const LARGE_COLLISION_SHAPE: &[BlockLocalAabb] =
821        &[BlockLocalAabb::new(-0.25, 0.0, 0.0, 1.0, 1.0, 1.0)];
822
823    struct TestCollisionWorld {
824        block_collisions: Vec<WorldAabb>,
825        entity_collisions: Vec<WorldAabb>,
826        pre_move_collisions: Vec<WorldAabb>,
827    }
828
829    struct BorderPreMoveWorld {
830        entity_collisions: Vec<WorldAabb>,
831        border_collisions: Vec<WorldAabb>,
832    }
833
834    impl CollisionWorld for TestCollisionWorld {
835        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
836            vanilla_blocks::AIR.default_state()
837        }
838
839        fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
840            self.block_collisions
841                .iter()
842                .copied()
843                .filter(|collision| collision.intersects(*aabb))
844                .collect()
845        }
846
847        fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
848            self.entity_collisions
849                .iter()
850                .copied()
851                .filter(|collision| collision.intersects(*aabb))
852                .collect()
853        }
854
855        fn get_pre_move_collisions(
856            &self,
857            _aabb: &WorldAabb,
858            _old_bottom_center: DVec3,
859            _descending: bool,
860        ) -> Vec<WorldAabb> {
861            self.pre_move_collisions.clone()
862        }
863    }
864
865    impl CollisionWorld for BorderPreMoveWorld {
866        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
867            vanilla_blocks::AIR.default_state()
868        }
869
870        fn get_block_collisions(&self, _aabb: &WorldAabb) -> Vec<WorldAabb> {
871            Vec::new()
872        }
873
874        fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
875            self.entity_collisions
876                .iter()
877                .copied()
878                .filter(|collision| collision.intersects(*aabb))
879                .collect()
880        }
881
882        fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
883            self.border_collisions
884                .iter()
885                .copied()
886                .filter(|collision| collision.intersects(*aabb))
887                .collect()
888        }
889    }
890
891    #[test]
892    fn test_intersects_aabb() {
893        let aabb1 = WorldAabb::new(0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
894        let aabb2 = WorldAabb::new(1.0, 1.0, 1.0, 3.0, 3.0, 3.0);
895
896        assert!(aabb1.intersects(aabb2));
897
898        let aabb3 = WorldAabb::new(5.0, 5.0, 5.0, 6.0, 6.0, 6.0);
899
900        assert!(!aabb1.intersects(aabb3));
901    }
902
903    #[test]
904    fn closest_free_position_returns_preferred_center_without_collisions() {
905        let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
906        let preferred = DVec3::new(2.0, 0.5, 0.5);
907
908        assert_eq!(
909            closest_free_position(&allowed, preferred, iter::empty()),
910            Some(preferred)
911        );
912    }
913
914    #[test]
915    fn closest_free_position_excludes_expanded_collisions() {
916        let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
917        let collision = WorldAabb::new(0.0, -1.0, -1.0, 3.0, 2.0, 2.0);
918
919        assert_eq!(
920            closest_free_position(&allowed, DVec3::new(1.5, 0.5, 0.5), [collision].into_iter()),
921            Some(DVec3::new(3.0, 0.5, 0.5))
922        );
923    }
924
925    #[test]
926    fn closest_free_position_returns_none_when_fully_blocked() {
927        let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
928        let collision = WorldAabb::new(-1.0, -1.0, -1.0, 5.0, 2.0, 2.0);
929
930        assert_eq!(
931            closest_free_position(&allowed, DVec3::new(1.5, 0.5, 0.5), [collision].into_iter()),
932            None
933        );
934    }
935
936    #[test]
937    fn block_collision_helper_reports_intersecting_collision_shape() {
938        let world = TestCollisionWorld {
939            block_collisions: vec![WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)],
940            entity_collisions: Vec::new(),
941            pre_move_collisions: Vec::new(),
942        };
943
944        assert!(has_block_collision(
945            &world,
946            WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
947        ));
948        assert!(!has_block_collision(
949            &world,
950            WorldAabb::new(2.0, 2.0, 2.0, 3.0, 3.0, 3.0)
951        ));
952    }
953
954    #[test]
955    fn live_block_collisions_use_bounded_region_reads() {
956        init_vanilla_registry();
957        init_behaviors();
958        let world = fresh_test_world("bounded_collision_reads");
959        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
960        let block_pos = BlockPos::new(0, 64, 0);
961        assert!(world.set_block(
962            block_pos,
963            vanilla_blocks::STONE.default_state(),
964            UpdateFlags::UPDATE_NONE,
965        ));
966
967        let collisions = WorldCollisionProvider::new(&world)
968            .get_block_collisions(&WorldAabb::new(0.25, 64.0, 0.25, 0.75, 65.0, 0.75));
969
970        assert!(collisions.contains(&WorldAabb::new(0.0, 64.0, 0.0, 1.0, 65.0, 1.0)));
971    }
972
973    #[test]
974    fn collision_helper_reports_intersecting_entity_shape() {
975        let world = TestCollisionWorld {
976            block_collisions: Vec::new(),
977            entity_collisions: vec![WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)],
978            pre_move_collisions: Vec::new(),
979        };
980
981        assert!(has_collision(
982            &world,
983            WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
984        ));
985        assert!(!has_block_collision(
986            &world,
987            WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
988        ));
989    }
990
991    #[test]
992    fn new_shape_collision_helper_ignores_collision_already_touching_old_box() {
993        let already_overlapped = WorldAabb::new(0.25, 0.0, 0.25, 0.75, 1.0, 0.75);
994        let new_collision = WorldAabb::new(2.0, 0.0, 0.0, 3.0, 1.0, 1.0);
995        let old_aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
996        let new_aabb = WorldAabb::new(2.0, 0.0, 0.0, 3.0, 1.0, 1.0);
997
998        let already_stuck_world = TestCollisionWorld {
999            block_collisions: Vec::new(),
1000            entity_collisions: Vec::new(),
1001            pre_move_collisions: vec![already_overlapped],
1002        };
1003        assert!(!is_colliding_with_new_shapes(
1004            &already_stuck_world,
1005            old_aabb,
1006            new_aabb,
1007            false
1008        ));
1009
1010        let newly_blocked_world = TestCollisionWorld {
1011            block_collisions: Vec::new(),
1012            entity_collisions: Vec::new(),
1013            pre_move_collisions: vec![new_collision],
1014        };
1015        assert!(is_colliding_with_new_shapes(
1016            &newly_blocked_world,
1017            old_aabb,
1018            new_aabb,
1019            false
1020        ));
1021    }
1022
1023    #[test]
1024    fn pre_move_collisions_exclude_world_border_collisions() {
1025        let entity_collision = WorldAabb::new(0.25, 0.0, 0.25, 0.75, 1.0, 0.75);
1026        let border_collision = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
1027        let world = BorderPreMoveWorld {
1028            entity_collisions: vec![entity_collision],
1029            border_collisions: vec![border_collision],
1030        };
1031        let aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
1032
1033        assert_eq!(
1034            world.get_pre_move_collisions(&aabb, DVec3::ZERO, false),
1035            vec![entity_collision]
1036        );
1037        assert!(world.has_world_border_collision(&aabb));
1038    }
1039
1040    #[test]
1041    fn supporting_block_tie_breaker_matches_vanilla_ordering() {
1042        assert!(vanilla_block_pos_less(
1043            BlockPos::new(0, 0, 0),
1044            BlockPos::new(0, 1, 0)
1045        ));
1046        assert!(vanilla_block_pos_less(
1047            BlockPos::new(0, 1, 0),
1048            BlockPos::new(0, 1, 1)
1049        ));
1050        assert!(vanilla_block_pos_less(
1051            BlockPos::new(0, 1, 1),
1052            BlockPos::new(1, 1, 1)
1053        ));
1054        assert!(!vanilla_block_pos_less(
1055            BlockPos::new(1, 1, 1),
1056            BlockPos::new(0, 1, 1)
1057        ));
1058    }
1059
1060    #[test]
1061    fn supporting_block_distance_uses_block_center() {
1062        let distance =
1063            block_pos_center_distance_sq(BlockPos::new(1, 2, 3), DVec3::new(1.5, 1.5, 5.5));
1064
1065        assert!((distance - 5.0).abs() < f64::EPSILON);
1066    }
1067
1068    #[test]
1069    fn block_collision_search_bounds_match_vanilla_epsilon_range() {
1070        let bounds =
1071            BlockCollisionSearchBounds::from_aabb(&WorldAabb::new(0.0, 0.25, 0.0, 1.0, 1.0, 1.0));
1072
1073        assert_eq!(bounds.min_x, -2);
1074        assert_eq!(bounds.max_x, 2);
1075        assert_eq!(bounds.min_y, -1);
1076        assert_eq!(bounds.max_y, 2);
1077        assert_eq!(bounds.min_z, -2);
1078        assert_eq!(bounds.max_z, 2);
1079    }
1080
1081    #[test]
1082    fn collision_cursor_type_matches_vanilla_boundary_count() {
1083        let bounds = BlockCollisionSearchBounds::from_aabb(&WorldAabb::new(
1084            0.25, 0.25, 0.25, 0.75, 0.75, 0.75,
1085        ));
1086
1087        assert_eq!(bounds.cursor_type(0, 0, 0), CollisionCursorType::Inside);
1088        assert_eq!(
1089            bounds.cursor_type(bounds.min_x, 0, 0),
1090            CollisionCursorType::Face
1091        );
1092        assert_eq!(
1093            bounds.cursor_type(bounds.min_x, bounds.min_y, 0),
1094            CollisionCursorType::Edge
1095        );
1096        assert_eq!(
1097            bounds.cursor_type(bounds.min_x, bounds.min_y, bounds.min_z),
1098            CollisionCursorType::Corner
1099        );
1100    }
1101
1102    #[test]
1103    fn collision_shape_filter_matches_vanilla_cursor_rules() {
1104        init_vanilla_registry();
1105
1106        let stone = vanilla_blocks::STONE.default_state();
1107        let moving_piston = vanilla_blocks::MOVING_PISTON.default_state();
1108        let large_shape = VoxelShape::from_boxes(LARGE_COLLISION_SHAPE);
1109        let shape = |shape: VoxelShape| CollisionShape {
1110            boxes: shape.into_iter().copied().collect(),
1111        };
1112
1113        assert!(should_query_collision_shape(
1114            stone,
1115            &shape(VoxelShape::FULL_BLOCK),
1116            CollisionCursorType::Inside
1117        ));
1118        assert!(!should_query_collision_shape(
1119            stone,
1120            &shape(VoxelShape::FULL_BLOCK),
1121            CollisionCursorType::Face
1122        ));
1123        assert!(should_query_collision_shape(
1124            stone,
1125            &shape(large_shape),
1126            CollisionCursorType::Face
1127        ));
1128        assert!(!should_query_collision_shape(
1129            stone,
1130            &shape(large_shape),
1131            CollisionCursorType::Edge
1132        ));
1133        assert!(should_query_collision_shape(
1134            moving_piston,
1135            &shape(VoxelShape::FULL_BLOCK),
1136            CollisionCursorType::Edge
1137        ));
1138        assert!(!should_query_collision_shape(
1139            moving_piston,
1140            &shape(large_shape),
1141            CollisionCursorType::Corner
1142        ));
1143    }
1144
1145    #[test]
1146    fn collision_shape_filter_uses_position_resolved_offset_bounds() {
1147        init_vanilla_registry();
1148
1149        let stone = vanilla_blocks::STONE.default_state();
1150        let shifted_full_block = CollisionShape {
1151            boxes: VoxelShape::FULL_BLOCK
1152                .into_iter()
1153                .map(|aabb| aabb.translate(DVec3::new(0.25, 0.0, 0.0)))
1154                .collect(),
1155        };
1156
1157        assert!(should_query_collision_shape(
1158            stone,
1159            &shifted_full_block,
1160            CollisionCursorType::Face
1161        ));
1162    }
1163}