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, EntityCollisionCandidates};
14use crate::physics::shapes::join_is_not_empty;
15use crate::physics::{COLLISION_EPSILON, block_state_may_expand_collision_cursor};
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::with_position(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        // Keep Vanilla's X, then Y, then Z callback order for extensible collision behavior.
194        for z in self.min_z..=self.max_z {
195            for y in self.min_y..=self.max_y {
196                for x in self.min_x..=self.max_x {
197                    let cursor_type = self.cursor_type(x, y, z);
198                    if cursor_type == CollisionCursorType::Corner {
199                        continue;
200                    }
201                    visit(BlockPos::new(x, y, z), cursor_type)?;
202                }
203            }
204        }
205        ControlFlow::Continue(())
206    }
207
208    fn try_for_each_inside_candidate<R>(
209        self,
210        mut visit: impl FnMut(BlockPos, CollisionCursorType) -> ControlFlow<R>,
211    ) -> ControlFlow<R> {
212        let Some(min_x) = self.min_x.checked_add(1) else {
213            return ControlFlow::Continue(());
214        };
215        let Some(min_y) = self.min_y.checked_add(1) else {
216            return ControlFlow::Continue(());
217        };
218        let Some(min_z) = self.min_z.checked_add(1) else {
219            return ControlFlow::Continue(());
220        };
221        let Some(max_x) = self.max_x.checked_sub(1) else {
222            return ControlFlow::Continue(());
223        };
224        let Some(max_y) = self.max_y.checked_sub(1) else {
225            return ControlFlow::Continue(());
226        };
227        let Some(max_z) = self.max_z.checked_sub(1) else {
228            return ControlFlow::Continue(());
229        };
230        if min_x > max_x || min_y > max_y || min_z > max_z {
231            return ControlFlow::Continue(());
232        }
233
234        for z in min_z..=max_z {
235            for y in min_y..=max_y {
236                for x in min_x..=max_x {
237                    visit(BlockPos::new(x, y, z), CollisionCursorType::Inside)?;
238                }
239            }
240        }
241        ControlFlow::Continue(())
242    }
243
244    fn try_for_each_region_candidate<R>(
245        self,
246        has_special_colliding_blocks: bool,
247        visit: impl FnMut(BlockPos, CollisionCursorType) -> ControlFlow<R>,
248    ) -> ControlFlow<R> {
249        if has_special_colliding_blocks {
250            self.try_for_each_candidate(visit)
251        } else {
252            self.try_for_each_inside_candidate(visit)
253        }
254    }
255}
256
257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
258enum CollisionCursorType {
259    Inside,
260    Face,
261    Edge,
262    Corner,
263}
264
265type BlockCollisionCandidate = (BlockPos, BlockStateId, CollisionCursorType);
266
267struct CollisionShape {
268    boxes: BlockCollisionBoxes,
269}
270
271fn should_resolve_collision_shape(
272    block_state: BlockStateId,
273    cursor_type: CollisionCursorType,
274) -> bool {
275    match cursor_type {
276        CollisionCursorType::Inside => true,
277        CollisionCursorType::Face => block_state_may_expand_collision_cursor(block_state),
278        CollisionCursorType::Edge => block_state.get_block() == &vanilla_blocks::MOVING_PISTON,
279        CollisionCursorType::Corner => false,
280    }
281}
282
283fn translate_collision_shape(shape: &BlockLocalAabb, block_pos: BlockPos) -> WorldAabb {
284    shape.at_block(block_pos)
285}
286
287impl<'a> WorldCollisionProvider<'a> {
288    /// Creates a new collision provider for the given world.
289    pub const fn new(world: &'a Arc<World>) -> Self {
290        Self {
291            world,
292            source: None,
293            include_entity_collisions: true,
294        }
295    }
296
297    /// Creates a collision provider for movement authored by `source`.
298    pub const fn for_entity(world: &'a Arc<World>, source: &'a dyn Entity) -> Self {
299        Self {
300            world,
301            source: Some(source),
302            include_entity_collisions: true,
303        }
304    }
305
306    /// Creates a collision provider matching vanilla `PathNavigationRegion`.
307    pub const fn for_path_navigation(world: &'a Arc<World>, source: &'a dyn Entity) -> Self {
308        Self {
309            world,
310            source: Some(source),
311            include_entity_collisions: false,
312        }
313    }
314
315    fn prefetched_block_collision_candidates(
316        &self,
317        bounds: BlockCollisionSearchBounds,
318    ) -> Option<SmallVec<[BlockCollisionCandidate; 64]>> {
319        if bounds
320            .block_count()
321            .is_none_or(|count| count > MAX_PREFETCHED_COLLISION_BLOCKS)
322        {
323            return None;
324        }
325
326        self.world
327            .try_with_block_region(bounds.region_bounds(), |region| {
328                // A plugin collision callback may mutate a later candidate. Vanilla reads each
329                // state immediately before its callback, so retain that live path whenever this
330                // region contains plugin-owned behavior.
331                if region.maybe_has_extensible_collision_behavior() {
332                    return None;
333                }
334                let mut candidates = SmallVec::new();
335                let has_special_colliding_blocks = region.maybe_has_special_colliding_blocks();
336                let _ = bounds.try_for_each_region_candidate(
337                    has_special_colliding_blocks,
338                    |block_pos, cursor_type| {
339                        let Some(block_state) = region.get_block_state(block_pos) else {
340                            return ControlFlow::<()>::Continue(());
341                        };
342                        if !block_state.is_air() {
343                            candidates.push((block_pos, block_state, cursor_type));
344                        }
345                        ControlFlow::<()>::Continue(())
346                    },
347                );
348                Some(candidates)
349            })
350            .flatten()
351    }
352
353    fn visit_block_collision_candidates<R>(
354        &self,
355        bounds: BlockCollisionSearchBounds,
356        mut visit: impl FnMut(BlockPos, BlockStateId, CollisionCursorType) -> ControlFlow<R>,
357    ) -> ControlFlow<R> {
358        if let Some(candidates) = self.prefetched_block_collision_candidates(bounds) {
359            for (block_pos, block_state, cursor_type) in candidates {
360                visit(block_pos, block_state, cursor_type)?;
361            }
362            return ControlFlow::Continue(());
363        }
364
365        bounds.try_for_each_candidate(|block_pos, cursor_type| {
366            let block_state = self.world.get_block_state(block_pos);
367            if block_state.is_air() {
368                return ControlFlow::Continue(());
369            }
370            visit(block_pos, block_state, cursor_type)
371        })
372    }
373
374    fn get_collision_shape(
375        &self,
376        block_state: BlockStateId,
377        block_pos: BlockPos,
378        context: BlockCollisionContext,
379    ) -> CollisionShape {
380        let behavior = BLOCK_BEHAVIORS.get_behavior(block_state.get_block());
381        let boxes =
382            behavior.get_collision_boxes(block_state, self.world.as_ref(), block_pos, context);
383
384        CollisionShape { boxes }
385    }
386
387    fn visit_block_collision_shapes<R>(
388        &self,
389        bounds: BlockCollisionSearchBounds,
390        context: BlockCollisionContext,
391        mut visit: impl FnMut(
392            BlockPos,
393            BlockStateId,
394            CollisionCursorType,
395            &CollisionShape,
396        ) -> ControlFlow<R>,
397    ) -> ControlFlow<R> {
398        self.visit_block_collision_candidates(bounds, |block_pos, block_state, cursor_type| {
399            if !should_resolve_collision_shape(block_state, cursor_type) {
400                return ControlFlow::Continue(());
401            }
402            let collision_shape = self.get_collision_shape(block_state, block_pos, context);
403            visit(block_pos, block_state, cursor_type, &collision_shape)
404        })
405    }
406
407    fn entity_collision_context(
408        &self,
409        entity_bottom: f64,
410        descending: bool,
411        placement: bool,
412    ) -> BlockCollisionContext {
413        let context = if placement {
414            BlockCollisionContext::with_position(entity_bottom, descending)
415        } else {
416            BlockCollisionContext::entity(entity_bottom, descending)
417        };
418
419        if let Some(source) = self.source {
420            context
421                .with_fall_distance(source.fall_distance())
422                .with_can_walk_on_powder_snow(PowderSnowBlock::can_entity_walk_on_powder_snow(
423                    source,
424                ))
425                .with_falling_block(source.entity_type() == &vanilla_entities::FALLING_BLOCK)
426        } else {
427            context
428        }
429    }
430
431    /// Returns whether an entity-context collision query intersects anything.
432    ///
433    /// Mirrors vanilla `Level.noCollision(entity, box)` callers by using the
434    /// source entity's normal collision context rather than a source-less check.
435    #[must_use]
436    pub fn has_entity_context_collision(
437        &self,
438        aabb: WorldAabb,
439        entity_bottom: f64,
440        descending: bool,
441    ) -> bool {
442        self.has_collision_with_context(
443            &aabb.deflate(COLLISION_EPSILON),
444            self.entity_collision_context(entity_bottom, descending, false),
445        )
446    }
447
448    /// Finds the block supporting an entity within `aabb`.
449    ///
450    /// Mirrors vanilla `CollisionGetter.findSupportingBlock`: among colliding
451    /// blocks, choose the closest block center to the entity position, then use
452    /// vanilla `BlockPos` ordering as a tie-breaker.
453    #[must_use]
454    #[expect(
455        clippy::float_cmp,
456        reason = "intentional: vanilla compares equal support distances exactly"
457    )]
458    pub fn find_supporting_block(
459        &self,
460        entity_position: DVec3,
461        aabb: &WorldAabb,
462        descending: bool,
463    ) -> Option<BlockPos> {
464        let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
465        let context = self.entity_collision_context(entity_position.y, descending, false);
466
467        let mut main_support = None;
468        let mut main_support_distance = f64::MAX;
469        let _ = self.visit_block_collision_shapes(
470            bounds,
471            context,
472            |block_pos, _block_state, _cursor_type, collision_shape| {
473                if collision_shape.boxes.is_empty()
474                    || !collision_shape
475                        .boxes
476                        .iter()
477                        .map(|shape_aabb| translate_collision_shape(shape_aabb, block_pos))
478                        .any(|world_aabb| aabb.intersects(world_aabb))
479                {
480                    return ControlFlow::<()>::Continue(());
481                }
482
483                let distance = block_pos_center_distance_sq(block_pos, entity_position);
484                let should_replace = distance < main_support_distance
485                    || distance == main_support_distance
486                        && main_support
487                            .is_none_or(|support| vanilla_block_pos_less(support, block_pos));
488                if should_replace {
489                    main_support = Some(block_pos);
490                    main_support_distance = distance;
491                }
492                ControlFlow::<()>::Continue(())
493            },
494        );
495
496        main_support
497    }
498
499    /// Returns vanilla `CollisionGetter.findFreePosition` for AABB-backed center shapes.
500    #[must_use]
501    pub fn find_free_position(
502        &self,
503        allowed_centers: &[WorldAabb],
504        preferred_center: DVec3,
505        size_x: f64,
506        size_y: f64,
507        size_z: f64,
508    ) -> Option<DVec3> {
509        let allowed_bounds = union_bounds(allowed_centers)?;
510        let search_area = allowed_bounds.inflate_xyz(size_x, size_y, size_z);
511        let context = self
512            .source
513            .map_or(BlockCollisionContext::empty(), |source| {
514                self.entity_collision_context(source.position().y, source.is_descending(), false)
515            });
516        let world_border = self.world.world_border_snapshot();
517        let expanded_collisions = self
518            .get_block_collisions_with_context(&search_area, context)
519            .into_iter()
520            .filter(|shape| world_border.is_within_bounds(*shape))
521            .map(|shape| shape.inflate_xyz(size_x / 2.0, size_y / 2.0, size_z / 2.0));
522
523        closest_free_position(allowed_centers, preferred_center, expanded_collisions)
524    }
525}
526
527fn union_bounds(boxes: &[WorldAabb]) -> Option<WorldAabb> {
528    let mut boxes = boxes.iter().copied().filter(|aabb| !aabb.is_empty());
529    let first = boxes.next()?;
530    Some(boxes.fold(first, |bounds, aabb| {
531        WorldAabb::encapsulating(&bounds, &aabb)
532    }))
533}
534
535fn closest_free_position(
536    allowed_centers: &[WorldAabb],
537    preferred_center: DVec3,
538    expanded_collisions: impl IntoIterator<Item = WorldAabb>,
539) -> Option<DVec3> {
540    let mut free_boxes = allowed_centers
541        .iter()
542        .copied()
543        .filter(|aabb| !aabb.is_empty())
544        .collect::<Vec<_>>();
545
546    if free_boxes.is_empty() {
547        return None;
548    }
549
550    for collision in expanded_collisions {
551        if collision.is_empty() {
552            continue;
553        }
554
555        let mut next_boxes = Vec::new();
556        for free_box in free_boxes {
557            subtract_aabb(free_box, collision, &mut next_boxes);
558        }
559        free_boxes = next_boxes;
560        if free_boxes.is_empty() {
561            return None;
562        }
563    }
564
565    closest_point_to_boxes(&free_boxes, preferred_center)
566}
567
568fn subtract_aabb(free: WorldAabb, blocked: WorldAabb, output: &mut Vec<WorldAabb>) {
569    if free.is_empty() {
570        return;
571    }
572
573    if !free.intersects(blocked) {
574        output.push(free);
575        return;
576    }
577
578    let min_x = free.min_x().max(blocked.min_x());
579    let max_x = free.max_x().min(blocked.max_x());
580    let min_y = free.min_y().max(blocked.min_y());
581    let max_y = free.max_y().min(blocked.max_y());
582    let min_z = free.min_z().max(blocked.min_z());
583    let max_z = free.max_z().min(blocked.max_z());
584
585    push_non_empty_aabb(
586        output,
587        free.min_x(),
588        free.min_y(),
589        free.min_z(),
590        min_x,
591        free.max_y(),
592        free.max_z(),
593    );
594    push_non_empty_aabb(
595        output,
596        max_x,
597        free.min_y(),
598        free.min_z(),
599        free.max_x(),
600        free.max_y(),
601        free.max_z(),
602    );
603    push_non_empty_aabb(
604        output,
605        min_x,
606        free.min_y(),
607        free.min_z(),
608        max_x,
609        min_y,
610        free.max_z(),
611    );
612    push_non_empty_aabb(
613        output,
614        min_x,
615        max_y,
616        free.min_z(),
617        max_x,
618        free.max_y(),
619        free.max_z(),
620    );
621    push_non_empty_aabb(output, min_x, min_y, free.min_z(), max_x, max_y, min_z);
622    push_non_empty_aabb(output, min_x, min_y, max_z, max_x, max_y, free.max_z());
623}
624
625fn push_non_empty_aabb(
626    output: &mut Vec<WorldAabb>,
627    min_x: f64,
628    min_y: f64,
629    min_z: f64,
630    max_x: f64,
631    max_y: f64,
632    max_z: f64,
633) {
634    let aabb = WorldAabb::new(min_x, min_y, min_z, max_x, max_y, max_z);
635    if !aabb.is_empty() {
636        output.push(aabb);
637    }
638}
639
640fn closest_point_to_boxes(boxes: &[WorldAabb], preferred_center: DVec3) -> Option<DVec3> {
641    let mut closest = None;
642    let mut closest_distance = f64::MAX;
643    for aabb in boxes {
644        let point = aabb.closest_point_to(preferred_center);
645        let distance = point.distance_squared(preferred_center);
646        if closest.is_none() || distance < closest_distance {
647            closest = Some(point);
648            closest_distance = distance;
649        }
650    }
651    closest
652}
653
654fn block_pos_center_distance_sq(pos: BlockPos, point: DVec3) -> f64 {
655    let dx = f64::from(pos.x()) + 0.5 - point.x;
656    let dy = f64::from(pos.y()) + 0.5 - point.y;
657    let dz = f64::from(pos.z()) + 0.5 - point.z;
658    dx * dx + dy * dy + dz * dz
659}
660
661const fn vanilla_block_pos_less(left: BlockPos, right: BlockPos) -> bool {
662    left.y() < right.y()
663        || left.y() == right.y()
664            && (left.z() < right.z() || left.z() == right.z() && left.x() < right.x())
665}
666
667#[must_use]
668const fn bottom_center(aabb: WorldAabb) -> DVec3 {
669    DVec3::new(
670        f64::midpoint(aabb.min_x(), aabb.max_x()),
671        aabb.min_y(),
672        f64::midpoint(aabb.min_z(), aabb.max_z()),
673    )
674}
675
676/// Returns whether an entity box intersects any block collision shape.
677#[must_use]
678pub fn has_block_collision(world: &impl CollisionWorld, aabb: WorldAabb) -> bool {
679    world.has_block_collision(&aabb.deflate(COLLISION_EPSILON))
680}
681
682/// Returns whether an entity box intersects any entity or block collision shape.
683#[must_use]
684pub fn has_collision(world: &impl CollisionWorld, aabb: WorldAabb) -> bool {
685    world.has_collision_with_context(
686        &aabb.deflate(COLLISION_EPSILON),
687        BlockCollisionContext::empty(),
688    )
689}
690
691/// Returns whether `new_aabb` collides with shapes that `old_aabb` did not.
692#[must_use]
693pub fn is_colliding_with_new_shapes(
694    world: &impl CollisionWorld,
695    old_aabb: WorldAabb,
696    new_aabb: WorldAabb,
697    descending: bool,
698) -> bool {
699    let old_shape = old_aabb.deflate(COLLISION_EPSILON);
700    for collision_aabb in world.get_pre_move_collisions(
701        &new_aabb.deflate(COLLISION_EPSILON),
702        bottom_center(old_aabb),
703        descending,
704    ) {
705        if !join_is_not_empty(&collision_aabb, &old_shape) {
706            return true;
707        }
708    }
709
710    false
711}
712
713impl CollisionWorld for WorldCollisionProvider<'_> {
714    fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
715        self.world.get_block_state(pos)
716    }
717
718    fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
719        self.get_block_collisions_with_context(aabb, BlockCollisionContext::empty())
720    }
721
722    fn get_block_collisions_with_context(
723        &self,
724        aabb: &WorldAabb,
725        context: BlockCollisionContext,
726    ) -> Vec<WorldAabb> {
727        let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
728        let mut collisions = Vec::new();
729        let _ = self.visit_block_collision_shapes(
730            bounds,
731            context,
732            |block_pos, _block_state, _cursor_type, collision_shape| {
733                if collision_shape.boxes.is_empty() {
734                    return ControlFlow::<()>::Continue(());
735                }
736
737                collisions.extend(
738                    collision_shape
739                        .boxes
740                        .iter()
741                        .map(|shape| translate_collision_shape(shape, block_pos))
742                        .filter(|shape| aabb.intersects(*shape)),
743                );
744                ControlFlow::<()>::Continue(())
745            },
746        );
747        collisions
748    }
749
750    fn has_block_collision_with_context(
751        &self,
752        aabb: &WorldAabb,
753        context: BlockCollisionContext,
754    ) -> bool {
755        let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
756        self.visit_block_collision_shapes(
757            bounds,
758            context,
759            |block_pos, _block_state, _cursor_type, collision_shape| {
760                if collision_shape.boxes.is_empty() {
761                    return ControlFlow::Continue(());
762                }
763
764                if collision_shape
765                    .boxes
766                    .iter()
767                    .map(|shape| translate_collision_shape(shape, block_pos))
768                    .any(|shape| aabb.intersects(shape))
769                {
770                    ControlFlow::Break(())
771                } else {
772                    ControlFlow::Continue(())
773                }
774            },
775        )
776        .is_break()
777    }
778
779    fn get_pre_move_collisions(
780        &self,
781        aabb: &WorldAabb,
782        old_bottom_center: DVec3,
783        descending: bool,
784    ) -> Vec<WorldAabb> {
785        let mut collisions = self.get_entity_collisions(aabb);
786        collisions.extend(self.get_block_collisions_with_context(
787            aabb,
788            self.entity_collision_context(old_bottom_center.y, descending, true),
789        ));
790        collisions
791    }
792
793    fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
794        if !self.include_entity_collisions {
795            return Vec::new();
796        }
797        if aabb.size() < ENTITY_COLLISION_EPSILON {
798            return Vec::new();
799        }
800
801        let query = aabb.inflate(ENTITY_COLLISION_EPSILON);
802        let candidates = EntityCollisionCandidates::for_source(self.source);
803        self.world
804            .get_movement_collision_boxes_in_aabb_matching(&query, candidates, |entity| match self
805                .source
806            {
807                Some(source) => {
808                    entity.id() != source.id()
809                        && !entity.is_removed()
810                        && !entity.is_spectator()
811                        && source.can_collide_with(entity)
812                }
813                None => {
814                    !entity.is_removed()
815                        && !entity.is_spectator()
816                        && entity.can_be_collided_with(None)
817                }
818            })
819    }
820
821    fn has_entity_collision(&self, aabb: &WorldAabb) -> bool {
822        if !self.include_entity_collisions {
823            return false;
824        }
825        if aabb.size() < ENTITY_COLLISION_EPSILON {
826            return false;
827        }
828
829        let query = aabb.inflate(ENTITY_COLLISION_EPSILON);
830        let candidates = EntityCollisionCandidates::for_source(self.source);
831        self.world
832            .has_movement_collision_in_aabb_matching(&query, candidates, |entity| {
833                match self.source {
834                    Some(source) => {
835                        entity.id() != source.id()
836                            && !entity.is_removed()
837                            && !entity.is_spectator()
838                            && source.can_collide_with(entity)
839                    }
840                    None => {
841                        !entity.is_removed()
842                            && !entity.is_spectator()
843                            && entity.can_be_collided_with(None)
844                    }
845                }
846            })
847    }
848
849    fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
850        let Some(source) = self.source else {
851            return Vec::new();
852        };
853
854        let border = self.world.world_border_snapshot();
855        let source_position = source.position();
856        if !border.is_inside_close_to_border(source_position.x, source_position.z, *aabb) {
857            return Vec::new();
858        }
859
860        border.collision_shapes_for(*aabb)
861    }
862
863    fn has_world_border_collision(&self, aabb: &WorldAabb) -> bool {
864        let Some(source) = self.source else {
865            return false;
866        };
867
868        let border = self.world.world_border_snapshot();
869        let source_position = source.position();
870        border.is_inside_close_to_border(source_position.x, source_position.z, *aabb)
871            && !border.collision_shapes_for(*aabb).is_empty()
872    }
873}
874
875#[cfg(test)]
876mod tests {
877    use std::iter;
878
879    use super::*;
880    use steel_registry::{
881        blocks::{Block, behavior::BlockConfig},
882        init_vanilla_registry,
883    };
884    use steel_utils::{ChunkPos, Identifier, types::UpdateFlags};
885
886    use crate::physics::block_may_expand_collision_cursor;
887    use crate::{
888        behavior::init_behaviors,
889        test_support::{fresh_test_world, insert_ready_full_chunk},
890    };
891
892    struct TestCollisionWorld {
893        block_collisions: Vec<WorldAabb>,
894        entity_collisions: Vec<WorldAabb>,
895        pre_move_collisions: Vec<WorldAabb>,
896    }
897
898    struct BorderPreMoveWorld {
899        entity_collisions: Vec<WorldAabb>,
900        border_collisions: Vec<WorldAabb>,
901    }
902
903    impl CollisionWorld for TestCollisionWorld {
904        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
905            vanilla_blocks::AIR.default_state()
906        }
907
908        fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
909            self.block_collisions
910                .iter()
911                .copied()
912                .filter(|collision| collision.intersects(*aabb))
913                .collect()
914        }
915
916        fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
917            self.entity_collisions
918                .iter()
919                .copied()
920                .filter(|collision| collision.intersects(*aabb))
921                .collect()
922        }
923
924        fn get_pre_move_collisions(
925            &self,
926            _aabb: &WorldAabb,
927            _old_bottom_center: DVec3,
928            _descending: bool,
929        ) -> Vec<WorldAabb> {
930            self.pre_move_collisions.clone()
931        }
932    }
933
934    impl CollisionWorld for BorderPreMoveWorld {
935        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
936            vanilla_blocks::AIR.default_state()
937        }
938
939        fn get_block_collisions(&self, _aabb: &WorldAabb) -> Vec<WorldAabb> {
940            Vec::new()
941        }
942
943        fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
944            self.entity_collisions
945                .iter()
946                .copied()
947                .filter(|collision| collision.intersects(*aabb))
948                .collect()
949        }
950
951        fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
952            self.border_collisions
953                .iter()
954                .copied()
955                .filter(|collision| collision.intersects(*aabb))
956                .collect()
957        }
958    }
959
960    #[test]
961    fn test_intersects_aabb() {
962        let aabb1 = WorldAabb::new(0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
963        let aabb2 = WorldAabb::new(1.0, 1.0, 1.0, 3.0, 3.0, 3.0);
964
965        assert!(aabb1.intersects(aabb2));
966
967        let aabb3 = WorldAabb::new(5.0, 5.0, 5.0, 6.0, 6.0, 6.0);
968
969        assert!(!aabb1.intersects(aabb3));
970    }
971
972    #[test]
973    fn closest_free_position_returns_preferred_center_without_collisions() {
974        let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
975        let preferred = DVec3::new(2.0, 0.5, 0.5);
976
977        assert_eq!(
978            closest_free_position(&allowed, preferred, iter::empty()),
979            Some(preferred)
980        );
981    }
982
983    #[test]
984    fn closest_free_position_excludes_expanded_collisions() {
985        let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
986        let collision = WorldAabb::new(0.0, -1.0, -1.0, 3.0, 2.0, 2.0);
987
988        assert_eq!(
989            closest_free_position(&allowed, DVec3::new(1.5, 0.5, 0.5), [collision].into_iter()),
990            Some(DVec3::new(3.0, 0.5, 0.5))
991        );
992    }
993
994    #[test]
995    fn closest_free_position_returns_none_when_fully_blocked() {
996        let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
997        let collision = WorldAabb::new(-1.0, -1.0, -1.0, 5.0, 2.0, 2.0);
998
999        assert_eq!(
1000            closest_free_position(&allowed, DVec3::new(1.5, 0.5, 0.5), [collision].into_iter()),
1001            None
1002        );
1003    }
1004
1005    #[test]
1006    fn block_collision_helper_reports_intersecting_collision_shape() {
1007        let world = TestCollisionWorld {
1008            block_collisions: vec![WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)],
1009            entity_collisions: Vec::new(),
1010            pre_move_collisions: Vec::new(),
1011        };
1012
1013        assert!(has_block_collision(
1014            &world,
1015            WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
1016        ));
1017        assert!(!has_block_collision(
1018            &world,
1019            WorldAabb::new(2.0, 2.0, 2.0, 3.0, 3.0, 3.0)
1020        ));
1021    }
1022
1023    #[test]
1024    fn live_block_collisions_use_bounded_region_reads() {
1025        init_vanilla_registry();
1026        init_behaviors();
1027        let world_fixture = fresh_test_world("bounded_collision_reads");
1028        let world = &world_fixture.world;
1029        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
1030        let block_pos = BlockPos::new(0, 64, 0);
1031        assert!(world.set_block(
1032            block_pos,
1033            vanilla_blocks::STONE.default_state(),
1034            UpdateFlags::UPDATE_NONE,
1035        ));
1036
1037        let collisions = WorldCollisionProvider::new(world)
1038            .get_block_collisions(&WorldAabb::new(0.25, 64.0, 0.25, 0.75, 65.0, 0.75));
1039
1040        assert!(collisions.contains(&WorldAabb::new(0.0, 64.0, 0.0, 1.0, 65.0, 1.0)));
1041    }
1042
1043    #[test]
1044    #[expect(
1045        clippy::too_many_lines,
1046        reason = "one scenario pins ordinary, large, dynamic, and moving-piston cursor transitions"
1047    )]
1048    fn prefetched_candidates_use_full_cursor_only_for_special_sections() {
1049        init_vanilla_registry();
1050        init_behaviors();
1051        let fixture = fresh_test_world("special_collision_section_scan");
1052        let world = &fixture.world;
1053        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
1054        let bounds = BlockCollisionSearchBounds {
1055            min_x: 1,
1056            min_y: 64,
1057            min_z: 1,
1058            max_x: 3,
1059            max_y: 66,
1060            max_z: 3,
1061        };
1062        let face = BlockPos::new(1, 65, 2);
1063        let inside = BlockPos::new(2, 65, 2);
1064        assert!(world.set_block(
1065            face,
1066            vanilla_blocks::STONE.default_state(),
1067            UpdateFlags::UPDATE_NONE,
1068        ));
1069        assert!(world.set_block(
1070            inside,
1071            vanilla_blocks::STONE.default_state(),
1072            UpdateFlags::UPDATE_NONE,
1073        ));
1074        let provider = WorldCollisionProvider::new(world);
1075
1076        let ordinary = provider
1077            .prefetched_block_collision_candidates(bounds)
1078            .expect("small loaded collision region should be prefetched");
1079        assert_eq!(
1080            ordinary
1081                .iter()
1082                .map(|(pos, _, cursor_type)| (*pos, *cursor_type))
1083                .collect::<Vec<_>>(),
1084            vec![(inside, CollisionCursorType::Inside)]
1085        );
1086        let mut ordinary_shape_callbacks = Vec::new();
1087        let _ = provider.visit_block_collision_shapes(
1088            bounds,
1089            BlockCollisionContext::empty(),
1090            |pos, _, cursor_type, _| {
1091                ordinary_shape_callbacks.push((pos, cursor_type));
1092                ControlFlow::<()>::Continue(())
1093            },
1094        );
1095        assert_eq!(
1096            ordinary_shape_callbacks,
1097            vec![(inside, CollisionCursorType::Inside)]
1098        );
1099
1100        assert!(world.set_block(
1101            BlockPos::new(15, 64, 15),
1102            vanilla_blocks::OAK_FENCE.default_state(),
1103            UpdateFlags::UPDATE_NONE,
1104        ));
1105        let special = provider
1106            .prefetched_block_collision_candidates(bounds)
1107            .expect("small loaded collision region should be prefetched");
1108        assert_eq!(
1109            special
1110                .iter()
1111                .map(|(pos, _, cursor_type)| (*pos, *cursor_type))
1112                .collect::<Vec<_>>(),
1113            vec![
1114                (face, CollisionCursorType::Face),
1115                (inside, CollisionCursorType::Inside),
1116            ]
1117        );
1118        let mut static_shape_callbacks = Vec::new();
1119        let _ = provider.visit_block_collision_shapes(
1120            bounds,
1121            BlockCollisionContext::empty(),
1122            |pos, _, cursor_type, _| {
1123                static_shape_callbacks.push((pos, cursor_type));
1124                ControlFlow::<()>::Continue(())
1125            },
1126        );
1127        assert_eq!(
1128            static_shape_callbacks,
1129            vec![(inside, CollisionCursorType::Inside)]
1130        );
1131
1132        assert!(world.set_block(
1133            BlockPos::new(15, 64, 15),
1134            vanilla_blocks::AIR.default_state(),
1135            UpdateFlags::UPDATE_NONE,
1136        ));
1137        assert!(world.set_block(
1138            face,
1139            vanilla_blocks::SCAFFOLDING.default_state(),
1140            UpdateFlags::UPDATE_NONE,
1141        ));
1142        let edge = BlockPos::new(1, 64, 2);
1143        assert!(world.set_block(
1144            edge,
1145            vanilla_blocks::MOVING_PISTON.default_state(),
1146            UpdateFlags::UPDATE_NONE,
1147        ));
1148        let dynamic = provider
1149            .prefetched_block_collision_candidates(bounds)
1150            .expect("small loaded collision region should be prefetched");
1151        assert_eq!(dynamic.len(), 3);
1152        let mut callback_sequence = Vec::new();
1153        let _ = provider.visit_block_collision_shapes(
1154            bounds,
1155            BlockCollisionContext::empty(),
1156            |pos, _, cursor_type, _| {
1157                callback_sequence.push((pos, cursor_type));
1158                ControlFlow::<()>::Continue(())
1159            },
1160        );
1161        assert_eq!(
1162            callback_sequence,
1163            vec![
1164                (edge, CollisionCursorType::Edge),
1165                (face, CollisionCursorType::Face),
1166                (inside, CollisionCursorType::Inside),
1167            ]
1168        );
1169    }
1170
1171    #[test]
1172    fn prefetched_callbacks_match_full_cursor_across_chunk_section_and_missing_data() {
1173        init_vanilla_registry();
1174        init_behaviors();
1175        let fixture = fresh_test_world("cross_boundary_collision_reads");
1176        let world = &fixture.world;
1177        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
1178        insert_ready_full_chunk(world, ChunkPos::new(1, 0));
1179
1180        let bounds = BlockCollisionSearchBounds {
1181            min_x: 14,
1182            min_y: 78,
1183            min_z: 0,
1184            max_x: 33,
1185            max_y: 81,
1186            max_z: 2,
1187        };
1188        for pos in [
1189            BlockPos::new(14, 79, 1),
1190            BlockPos::new(15, 79, 1),
1191            BlockPos::new(16, 80, 1),
1192            BlockPos::new(31, 79, 1),
1193        ] {
1194            assert!(world.set_block(
1195                pos,
1196                vanilla_blocks::STONE.default_state(),
1197                UpdateFlags::UPDATE_NONE,
1198            ));
1199        }
1200        let provider = WorldCollisionProvider::new(world);
1201
1202        let compatibility_callbacks = || {
1203            let mut callbacks = Vec::new();
1204            let _ = bounds.try_for_each_candidate(|pos, cursor_type| {
1205                let state = world.get_block_state(pos);
1206                if !state.is_air() && should_resolve_collision_shape(state, cursor_type) {
1207                    callbacks.push((pos, cursor_type));
1208                }
1209                ControlFlow::<()>::Continue(())
1210            });
1211            callbacks
1212        };
1213        let provider_callbacks = || {
1214            let mut callbacks = Vec::new();
1215            let _ = provider.visit_block_collision_shapes(
1216                bounds,
1217                BlockCollisionContext::empty(),
1218                |pos, _, cursor_type, _| {
1219                    callbacks.push((pos, cursor_type));
1220                    ControlFlow::<()>::Continue(())
1221                },
1222            );
1223            callbacks
1224        };
1225
1226        assert!(
1227            provider
1228                .prefetched_block_collision_candidates(bounds)
1229                .is_some(),
1230            "missing Full data should remain a safe air-like prefetched region"
1231        );
1232        assert_eq!(provider_callbacks(), compatibility_callbacks());
1233
1234        assert!(world.set_block(
1235            BlockPos::new(31, 79, 1),
1236            vanilla_blocks::OAK_FENCE.default_state(),
1237            UpdateFlags::UPDATE_NONE,
1238        ));
1239        assert_eq!(provider_callbacks(), compatibility_callbacks());
1240    }
1241
1242    #[test]
1243    fn collision_helper_reports_intersecting_entity_shape() {
1244        let world = TestCollisionWorld {
1245            block_collisions: Vec::new(),
1246            entity_collisions: vec![WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)],
1247            pre_move_collisions: Vec::new(),
1248        };
1249
1250        assert!(has_collision(
1251            &world,
1252            WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
1253        ));
1254        assert!(!has_block_collision(
1255            &world,
1256            WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
1257        ));
1258    }
1259
1260    #[test]
1261    fn new_shape_collision_helper_ignores_collision_already_touching_old_box() {
1262        let already_overlapped = WorldAabb::new(0.25, 0.0, 0.25, 0.75, 1.0, 0.75);
1263        let new_collision = WorldAabb::new(2.0, 0.0, 0.0, 3.0, 1.0, 1.0);
1264        let old_aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
1265        let new_aabb = WorldAabb::new(2.0, 0.0, 0.0, 3.0, 1.0, 1.0);
1266
1267        let already_stuck_world = TestCollisionWorld {
1268            block_collisions: Vec::new(),
1269            entity_collisions: Vec::new(),
1270            pre_move_collisions: vec![already_overlapped],
1271        };
1272        assert!(!is_colliding_with_new_shapes(
1273            &already_stuck_world,
1274            old_aabb,
1275            new_aabb,
1276            false
1277        ));
1278
1279        let newly_blocked_world = TestCollisionWorld {
1280            block_collisions: Vec::new(),
1281            entity_collisions: Vec::new(),
1282            pre_move_collisions: vec![new_collision],
1283        };
1284        assert!(is_colliding_with_new_shapes(
1285            &newly_blocked_world,
1286            old_aabb,
1287            new_aabb,
1288            false
1289        ));
1290    }
1291
1292    #[test]
1293    fn pre_move_collisions_exclude_world_border_collisions() {
1294        let entity_collision = WorldAabb::new(0.25, 0.0, 0.25, 0.75, 1.0, 0.75);
1295        let border_collision = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
1296        let world = BorderPreMoveWorld {
1297            entity_collisions: vec![entity_collision],
1298            border_collisions: vec![border_collision],
1299        };
1300        let aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
1301
1302        assert_eq!(
1303            world.get_pre_move_collisions(&aabb, DVec3::ZERO, false),
1304            vec![entity_collision]
1305        );
1306        assert!(world.has_world_border_collision(&aabb));
1307    }
1308
1309    #[test]
1310    fn supporting_block_tie_breaker_matches_vanilla_ordering() {
1311        assert!(vanilla_block_pos_less(
1312            BlockPos::new(0, 0, 0),
1313            BlockPos::new(0, 1, 0)
1314        ));
1315        assert!(vanilla_block_pos_less(
1316            BlockPos::new(0, 1, 0),
1317            BlockPos::new(0, 1, 1)
1318        ));
1319        assert!(vanilla_block_pos_less(
1320            BlockPos::new(0, 1, 1),
1321            BlockPos::new(1, 1, 1)
1322        ));
1323        assert!(!vanilla_block_pos_less(
1324            BlockPos::new(1, 1, 1),
1325            BlockPos::new(0, 1, 1)
1326        ));
1327    }
1328
1329    #[test]
1330    fn supporting_block_distance_uses_block_center() {
1331        let distance =
1332            block_pos_center_distance_sq(BlockPos::new(1, 2, 3), DVec3::new(1.5, 1.5, 5.5));
1333
1334        assert!((distance - 5.0).abs() < f64::EPSILON);
1335    }
1336
1337    #[test]
1338    fn block_collision_search_bounds_match_vanilla_epsilon_range() {
1339        let bounds =
1340            BlockCollisionSearchBounds::from_aabb(&WorldAabb::new(0.0, 0.25, 0.0, 1.0, 1.0, 1.0));
1341
1342        assert_eq!(bounds.min_x, -2);
1343        assert_eq!(bounds.max_x, 2);
1344        assert_eq!(bounds.min_y, -1);
1345        assert_eq!(bounds.max_y, 2);
1346        assert_eq!(bounds.min_z, -2);
1347        assert_eq!(bounds.max_z, 2);
1348    }
1349
1350    #[test]
1351    fn collision_cursor_type_matches_vanilla_boundary_count() {
1352        let bounds = BlockCollisionSearchBounds::from_aabb(&WorldAabb::new(
1353            0.25, 0.25, 0.25, 0.75, 0.75, 0.75,
1354        ));
1355
1356        assert_eq!(bounds.cursor_type(0, 0, 0), CollisionCursorType::Inside);
1357        assert_eq!(
1358            bounds.cursor_type(bounds.min_x, 0, 0),
1359            CollisionCursorType::Face
1360        );
1361        assert_eq!(
1362            bounds.cursor_type(bounds.min_x, bounds.min_y, 0),
1363            CollisionCursorType::Edge
1364        );
1365        assert_eq!(
1366            bounds.cursor_type(bounds.min_x, bounds.min_y, bounds.min_z),
1367            CollisionCursorType::Corner
1368        );
1369    }
1370
1371    #[test]
1372    fn collision_cursor_callbacks_match_vanilla_x_y_z_order() {
1373        let bounds = BlockCollisionSearchBounds {
1374            min_x: 0,
1375            min_y: 0,
1376            min_z: 0,
1377            max_x: 2,
1378            max_y: 2,
1379            max_z: 2,
1380        };
1381        let mut visited = Vec::new();
1382        let _ = bounds.try_for_each_candidate(|pos, _| {
1383            visited.push(pos);
1384            ControlFlow::<()>::Continue(())
1385        });
1386
1387        assert_eq!(
1388            visited,
1389            vec![
1390                BlockPos::new(1, 0, 0),
1391                BlockPos::new(0, 1, 0),
1392                BlockPos::new(1, 1, 0),
1393                BlockPos::new(2, 1, 0),
1394                BlockPos::new(1, 2, 0),
1395                BlockPos::new(0, 0, 1),
1396                BlockPos::new(1, 0, 1),
1397                BlockPos::new(2, 0, 1),
1398                BlockPos::new(0, 1, 1),
1399                BlockPos::new(1, 1, 1),
1400                BlockPos::new(2, 1, 1),
1401                BlockPos::new(0, 2, 1),
1402                BlockPos::new(1, 2, 1),
1403                BlockPos::new(2, 2, 1),
1404                BlockPos::new(1, 0, 2),
1405                BlockPos::new(0, 1, 2),
1406                BlockPos::new(1, 1, 2),
1407                BlockPos::new(2, 1, 2),
1408                BlockPos::new(1, 2, 2),
1409            ]
1410        );
1411    }
1412
1413    #[test]
1414    fn static_looking_plugin_block_keeps_full_cursor_callback_order() {
1415        static PLUGIN_BLOCK: Block = Block::new(
1416            Identifier::new_static("collision_test", "static_cube"),
1417            BlockConfig::new(),
1418            &[],
1419        );
1420
1421        assert!(!PLUGIN_BLOCK.config.dynamic_shape);
1422        let has_large_static_shape = false;
1423        let has_special_colliding_blocks =
1424            block_may_expand_collision_cursor(&PLUGIN_BLOCK, has_large_static_shape);
1425
1426        let bounds = BlockCollisionSearchBounds {
1427            min_x: 0,
1428            min_y: 0,
1429            min_z: 0,
1430            max_x: 2,
1431            max_y: 2,
1432            max_z: 2,
1433        };
1434        let fixture_positions = [
1435            BlockPos::new(1, 1, 0),
1436            BlockPos::new(0, 1, 1),
1437            BlockPos::new(1, 1, 1),
1438            BlockPos::new(2, 1, 1),
1439            BlockPos::new(1, 1, 2),
1440        ];
1441
1442        let mut callbacks = Vec::new();
1443        let _ = bounds.try_for_each_region_candidate(has_special_colliding_blocks, |pos, _| {
1444            if fixture_positions.contains(&pos) {
1445                callbacks.push(pos);
1446            }
1447            ControlFlow::<()>::Continue(())
1448        });
1449
1450        assert_eq!(callbacks, fixture_positions);
1451    }
1452
1453    #[test]
1454    fn collision_shape_resolution_filter_matches_vanilla_cursor_rules() {
1455        init_vanilla_registry();
1456
1457        let stone = vanilla_blocks::STONE.default_state();
1458        let fence = vanilla_blocks::OAK_FENCE.default_state();
1459        let scaffolding = vanilla_blocks::SCAFFOLDING.default_state();
1460        let moving_piston = vanilla_blocks::MOVING_PISTON.default_state();
1461
1462        assert!(should_resolve_collision_shape(
1463            stone,
1464            CollisionCursorType::Inside
1465        ));
1466        assert!(!should_resolve_collision_shape(
1467            stone,
1468            CollisionCursorType::Face
1469        ));
1470        assert!(should_resolve_collision_shape(
1471            fence,
1472            CollisionCursorType::Face
1473        ));
1474        assert!(should_resolve_collision_shape(
1475            scaffolding,
1476            CollisionCursorType::Face
1477        ));
1478        assert!(!should_resolve_collision_shape(
1479            fence,
1480            CollisionCursorType::Edge
1481        ));
1482        assert!(should_resolve_collision_shape(
1483            moving_piston,
1484            CollisionCursorType::Edge
1485        ));
1486        assert!(!should_resolve_collision_shape(
1487            moving_piston,
1488            CollisionCursorType::Corner
1489        ));
1490    }
1491}