1use 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;
20const MAX_PREFETCHED_COLLISION_BLOCKS: usize = 4096;
22
23pub trait CollisionWorld {
27 fn get_block_state(&self, pos: BlockPos) -> BlockStateId;
29
30 fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb>;
34
35 fn has_block_collision(&self, aabb: &WorldAabb) -> bool {
37 !self.get_block_collisions(aabb).is_empty()
38 }
39
40 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 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 fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
67 let _ = aabb;
68 Vec::new()
69 }
70
71 fn has_entity_collision(&self, aabb: &WorldAabb) -> bool {
73 !self.get_entity_collisions(aabb).is_empty()
74 }
75
76 fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
78 let _ = aabb;
79 Vec::new()
80 }
81
82 fn has_world_border_collision(&self, aabb: &WorldAabb) -> bool {
84 !self.get_world_border_collisions(aabb).is_empty()
85 }
86
87 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 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 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
133pub 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 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 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 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 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 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 #[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 #[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 #[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#[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#[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#[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}