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;
14use crate::physics::COLLISION_EPSILON;
15use crate::physics::shapes::join_is_not_empty;
16use crate::world::{BlockRegionBounds, World};
17
18const BLOCK_COLLISION_EPSILON: f64 = 1.0e-7;
19const ENTITY_COLLISION_EPSILON: f64 = 1.0e-7;
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::pre_move(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 y in self.min_y..=self.max_y {
194 for z in self.min_z..=self.max_z {
195 for x in self.min_x..=self.max_x {
196 let cursor_type = self.cursor_type(x, y, z);
197 if cursor_type == CollisionCursorType::Corner {
198 continue;
199 }
200 visit(BlockPos::new(x, y, z), cursor_type)?;
201 }
202 }
203 }
204 ControlFlow::Continue(())
205 }
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209enum CollisionCursorType {
210 Inside,
211 Face,
212 Edge,
213 Corner,
214}
215
216type BlockCollisionCandidate = (BlockPos, BlockStateId, CollisionCursorType);
217
218struct CollisionShape {
219 boxes: BlockCollisionBoxes,
220}
221
222impl CollisionShape {
223 fn has_large_collision_shape(&self) -> bool {
224 self.boxes.iter().any(|aabb| {
225 aabb.min_x() < 0.0
226 || aabb.min_y() < 0.0
227 || aabb.min_z() < 0.0
228 || aabb.max_x() > 1.0
229 || aabb.max_y() > 1.0
230 || aabb.max_z() > 1.0
231 })
232 }
233}
234
235fn should_query_collision_shape(
236 block_state: BlockStateId,
237 collision_shape: &CollisionShape,
238 cursor_type: CollisionCursorType,
239) -> bool {
240 match cursor_type {
241 CollisionCursorType::Inside => true,
242 CollisionCursorType::Face => {
243 block_state.get_block().config.dynamic_shape
244 || collision_shape.has_large_collision_shape()
245 }
246 CollisionCursorType::Edge => block_state.get_block() == &vanilla_blocks::MOVING_PISTON,
247 CollisionCursorType::Corner => false,
248 }
249}
250
251fn translate_collision_shape(shape: &BlockLocalAabb, block_pos: BlockPos) -> WorldAabb {
252 shape.at_block(block_pos)
253}
254
255impl<'a> WorldCollisionProvider<'a> {
256 pub const fn new(world: &'a Arc<World>) -> Self {
258 Self {
259 world,
260 source: None,
261 include_entity_collisions: true,
262 }
263 }
264
265 pub const fn for_entity(world: &'a Arc<World>, source: &'a dyn Entity) -> Self {
267 Self {
268 world,
269 source: Some(source),
270 include_entity_collisions: true,
271 }
272 }
273
274 pub const fn for_path_navigation(world: &'a Arc<World>, source: &'a dyn Entity) -> Self {
276 Self {
277 world,
278 source: Some(source),
279 include_entity_collisions: false,
280 }
281 }
282
283 fn prefetched_block_collision_candidates(
284 &self,
285 bounds: BlockCollisionSearchBounds,
286 ) -> Option<SmallVec<[BlockCollisionCandidate; 64]>> {
287 if bounds
288 .block_count()
289 .is_none_or(|count| count > MAX_PREFETCHED_COLLISION_BLOCKS)
290 {
291 return None;
292 }
293
294 self.world
295 .try_with_block_region(bounds.region_bounds(), |region| {
296 let mut candidates = SmallVec::new();
297 let _ = bounds.try_for_each_candidate(|block_pos, cursor_type| {
298 let Some(block_state) = region.get_block_state(block_pos) else {
299 return ControlFlow::<()>::Continue(());
300 };
301 if !block_state.is_air() {
302 candidates.push((block_pos, block_state, cursor_type));
303 }
304 ControlFlow::<()>::Continue(())
305 });
306 candidates
307 })
308 }
309
310 fn visit_block_collision_candidates<R>(
311 &self,
312 bounds: BlockCollisionSearchBounds,
313 mut visit: impl FnMut(BlockPos, BlockStateId, CollisionCursorType) -> ControlFlow<R>,
314 ) -> ControlFlow<R> {
315 if let Some(candidates) = self.prefetched_block_collision_candidates(bounds) {
316 for (block_pos, block_state, cursor_type) in candidates {
317 visit(block_pos, block_state, cursor_type)?;
318 }
319 return ControlFlow::Continue(());
320 }
321
322 bounds.try_for_each_candidate(|block_pos, cursor_type| {
323 let block_state = self.world.get_block_state(block_pos);
324 if block_state.is_air() {
325 return ControlFlow::Continue(());
326 }
327 visit(block_pos, block_state, cursor_type)
328 })
329 }
330
331 fn get_collision_shape(
332 &self,
333 block_state: BlockStateId,
334 block_pos: BlockPos,
335 context: BlockCollisionContext,
336 ) -> CollisionShape {
337 let behavior = BLOCK_BEHAVIORS.get_behavior(block_state.get_block());
338 let boxes =
339 behavior.get_collision_boxes(block_state, self.world.as_ref(), block_pos, context);
340
341 CollisionShape { boxes }
342 }
343
344 fn entity_collision_context(
345 &self,
346 entity_bottom: f64,
347 descending: bool,
348 placement: bool,
349 ) -> BlockCollisionContext {
350 let context = if placement {
351 BlockCollisionContext::pre_move(entity_bottom, descending)
352 } else {
353 BlockCollisionContext::entity(entity_bottom, descending)
354 };
355
356 if let Some(source) = self.source {
357 context
358 .with_fall_distance(source.fall_distance())
359 .with_can_walk_on_powder_snow(PowderSnowBlock::can_entity_walk_on_powder_snow(
360 source,
361 ))
362 .with_falling_block(source.entity_type() == &vanilla_entities::FALLING_BLOCK)
363 } else {
364 context
365 }
366 }
367
368 #[must_use]
373 pub fn has_entity_context_collision(
374 &self,
375 aabb: WorldAabb,
376 entity_bottom: f64,
377 descending: bool,
378 ) -> bool {
379 self.has_collision_with_context(
380 &aabb.deflate(COLLISION_EPSILON),
381 self.entity_collision_context(entity_bottom, descending, false),
382 )
383 }
384
385 #[must_use]
391 #[expect(
392 clippy::float_cmp,
393 reason = "intentional: vanilla compares equal support distances exactly"
394 )]
395 pub fn find_supporting_block(
396 &self,
397 entity_position: DVec3,
398 aabb: &WorldAabb,
399 descending: bool,
400 ) -> Option<BlockPos> {
401 let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
402 let context = self.entity_collision_context(entity_position.y, descending, false);
403
404 let mut main_support = None;
405 let mut main_support_distance = f64::MAX;
406 let _ =
407 self.visit_block_collision_candidates(bounds, |block_pos, block_state, cursor_type| {
408 let collision_shape = self.get_collision_shape(block_state, block_pos, context);
409 if collision_shape.boxes.is_empty()
410 || !should_query_collision_shape(block_state, &collision_shape, cursor_type)
411 || !collision_shape
412 .boxes
413 .iter()
414 .map(|shape_aabb| translate_collision_shape(shape_aabb, block_pos))
415 .any(|world_aabb| aabb.intersects(world_aabb))
416 {
417 return ControlFlow::<()>::Continue(());
418 }
419
420 let distance = block_pos_center_distance_sq(block_pos, entity_position);
421 let should_replace = distance < main_support_distance
422 || distance == main_support_distance
423 && main_support
424 .is_none_or(|support| vanilla_block_pos_less(support, block_pos));
425 if should_replace {
426 main_support = Some(block_pos);
427 main_support_distance = distance;
428 }
429 ControlFlow::<()>::Continue(())
430 });
431
432 main_support
433 }
434
435 #[must_use]
437 pub fn find_free_position(
438 &self,
439 allowed_centers: &[WorldAabb],
440 preferred_center: DVec3,
441 size_x: f64,
442 size_y: f64,
443 size_z: f64,
444 ) -> Option<DVec3> {
445 let allowed_bounds = union_bounds(allowed_centers)?;
446 let search_area = allowed_bounds.inflate_xyz(size_x, size_y, size_z);
447 let context = self
448 .source
449 .map_or(BlockCollisionContext::empty(), |source| {
450 self.entity_collision_context(source.position().y, source.is_descending(), false)
451 });
452 let world_border = self.world.world_border_snapshot();
453 let expanded_collisions = self
454 .get_block_collisions_with_context(&search_area, context)
455 .into_iter()
456 .filter(|shape| world_border.is_within_bounds(*shape))
457 .map(|shape| shape.inflate_xyz(size_x / 2.0, size_y / 2.0, size_z / 2.0));
458
459 closest_free_position(allowed_centers, preferred_center, expanded_collisions)
460 }
461}
462
463fn union_bounds(boxes: &[WorldAabb]) -> Option<WorldAabb> {
464 let mut boxes = boxes.iter().copied().filter(|aabb| !aabb.is_empty());
465 let first = boxes.next()?;
466 Some(boxes.fold(first, |bounds, aabb| {
467 WorldAabb::encapsulating(&bounds, &aabb)
468 }))
469}
470
471fn closest_free_position(
472 allowed_centers: &[WorldAabb],
473 preferred_center: DVec3,
474 expanded_collisions: impl IntoIterator<Item = WorldAabb>,
475) -> Option<DVec3> {
476 let mut free_boxes = allowed_centers
477 .iter()
478 .copied()
479 .filter(|aabb| !aabb.is_empty())
480 .collect::<Vec<_>>();
481
482 if free_boxes.is_empty() {
483 return None;
484 }
485
486 for collision in expanded_collisions {
487 if collision.is_empty() {
488 continue;
489 }
490
491 let mut next_boxes = Vec::new();
492 for free_box in free_boxes {
493 subtract_aabb(free_box, collision, &mut next_boxes);
494 }
495 free_boxes = next_boxes;
496 if free_boxes.is_empty() {
497 return None;
498 }
499 }
500
501 closest_point_to_boxes(&free_boxes, preferred_center)
502}
503
504fn subtract_aabb(free: WorldAabb, blocked: WorldAabb, output: &mut Vec<WorldAabb>) {
505 if free.is_empty() {
506 return;
507 }
508
509 if !free.intersects(blocked) {
510 output.push(free);
511 return;
512 }
513
514 let min_x = free.min_x().max(blocked.min_x());
515 let max_x = free.max_x().min(blocked.max_x());
516 let min_y = free.min_y().max(blocked.min_y());
517 let max_y = free.max_y().min(blocked.max_y());
518 let min_z = free.min_z().max(blocked.min_z());
519 let max_z = free.max_z().min(blocked.max_z());
520
521 push_non_empty_aabb(
522 output,
523 free.min_x(),
524 free.min_y(),
525 free.min_z(),
526 min_x,
527 free.max_y(),
528 free.max_z(),
529 );
530 push_non_empty_aabb(
531 output,
532 max_x,
533 free.min_y(),
534 free.min_z(),
535 free.max_x(),
536 free.max_y(),
537 free.max_z(),
538 );
539 push_non_empty_aabb(
540 output,
541 min_x,
542 free.min_y(),
543 free.min_z(),
544 max_x,
545 min_y,
546 free.max_z(),
547 );
548 push_non_empty_aabb(
549 output,
550 min_x,
551 max_y,
552 free.min_z(),
553 max_x,
554 free.max_y(),
555 free.max_z(),
556 );
557 push_non_empty_aabb(output, min_x, min_y, free.min_z(), max_x, max_y, min_z);
558 push_non_empty_aabb(output, min_x, min_y, max_z, max_x, max_y, free.max_z());
559}
560
561fn push_non_empty_aabb(
562 output: &mut Vec<WorldAabb>,
563 min_x: f64,
564 min_y: f64,
565 min_z: f64,
566 max_x: f64,
567 max_y: f64,
568 max_z: f64,
569) {
570 let aabb = WorldAabb::new(min_x, min_y, min_z, max_x, max_y, max_z);
571 if !aabb.is_empty() {
572 output.push(aabb);
573 }
574}
575
576fn closest_point_to_boxes(boxes: &[WorldAabb], preferred_center: DVec3) -> Option<DVec3> {
577 let mut closest = None;
578 let mut closest_distance = f64::MAX;
579 for aabb in boxes {
580 let point = aabb.closest_point_to(preferred_center);
581 let distance = point.distance_squared(preferred_center);
582 if closest.is_none() || distance < closest_distance {
583 closest = Some(point);
584 closest_distance = distance;
585 }
586 }
587 closest
588}
589
590fn block_pos_center_distance_sq(pos: BlockPos, point: DVec3) -> f64 {
591 let dx = f64::from(pos.x()) + 0.5 - point.x;
592 let dy = f64::from(pos.y()) + 0.5 - point.y;
593 let dz = f64::from(pos.z()) + 0.5 - point.z;
594 dx * dx + dy * dy + dz * dz
595}
596
597const fn vanilla_block_pos_less(left: BlockPos, right: BlockPos) -> bool {
598 left.y() < right.y()
599 || left.y() == right.y()
600 && (left.z() < right.z() || left.z() == right.z() && left.x() < right.x())
601}
602
603#[must_use]
604const fn bottom_center(aabb: WorldAabb) -> DVec3 {
605 DVec3::new(
606 f64::midpoint(aabb.min_x(), aabb.max_x()),
607 aabb.min_y(),
608 f64::midpoint(aabb.min_z(), aabb.max_z()),
609 )
610}
611
612#[must_use]
614pub fn has_block_collision(world: &impl CollisionWorld, aabb: WorldAabb) -> bool {
615 world.has_block_collision(&aabb.deflate(COLLISION_EPSILON))
616}
617
618#[must_use]
620pub fn has_collision(world: &impl CollisionWorld, aabb: WorldAabb) -> bool {
621 world.has_collision_with_context(
622 &aabb.deflate(COLLISION_EPSILON),
623 BlockCollisionContext::empty(),
624 )
625}
626
627#[must_use]
631pub fn is_colliding_with_new_shapes(
632 world: &impl CollisionWorld,
633 old_aabb: WorldAabb,
634 new_aabb: WorldAabb,
635 descending: bool,
636) -> bool {
637 let old_shape = old_aabb.deflate(COLLISION_EPSILON);
638 for collision_aabb in world.get_pre_move_collisions(
639 &new_aabb.deflate(COLLISION_EPSILON),
640 bottom_center(old_aabb),
641 descending,
642 ) {
643 if !join_is_not_empty(&collision_aabb, &old_shape) {
644 return true;
645 }
646 }
647
648 false
649}
650
651impl CollisionWorld for WorldCollisionProvider<'_> {
652 fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
653 self.world.get_block_state(pos)
654 }
655
656 fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
657 self.get_block_collisions_with_context(aabb, BlockCollisionContext::empty())
658 }
659
660 fn get_block_collisions_with_context(
661 &self,
662 aabb: &WorldAabb,
663 context: BlockCollisionContext,
664 ) -> Vec<WorldAabb> {
665 let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
666 let mut collisions = Vec::new();
667 let _ =
668 self.visit_block_collision_candidates(bounds, |block_pos, block_state, cursor_type| {
669 let collision_shape = self.get_collision_shape(block_state, block_pos, context);
670 if collision_shape.boxes.is_empty()
671 || !should_query_collision_shape(block_state, &collision_shape, cursor_type)
672 {
673 return ControlFlow::<()>::Continue(());
674 }
675
676 collisions.extend(
677 collision_shape
678 .boxes
679 .iter()
680 .map(|shape| translate_collision_shape(shape, block_pos))
681 .filter(|shape| aabb.intersects(*shape)),
682 );
683 ControlFlow::<()>::Continue(())
684 });
685 collisions
686 }
687
688 fn has_block_collision_with_context(
689 &self,
690 aabb: &WorldAabb,
691 context: BlockCollisionContext,
692 ) -> bool {
693 let bounds = BlockCollisionSearchBounds::from_aabb(aabb);
694 self.visit_block_collision_candidates(bounds, |block_pos, block_state, cursor_type| {
695 let collision_shape = self.get_collision_shape(block_state, block_pos, context);
696 if collision_shape.boxes.is_empty()
697 || !should_query_collision_shape(block_state, &collision_shape, cursor_type)
698 {
699 return ControlFlow::Continue(());
700 }
701
702 if collision_shape
703 .boxes
704 .iter()
705 .map(|shape| translate_collision_shape(shape, block_pos))
706 .any(|shape| aabb.intersects(shape))
707 {
708 ControlFlow::Break(())
709 } else {
710 ControlFlow::Continue(())
711 }
712 })
713 .is_break()
714 }
715
716 fn get_pre_move_collisions(
717 &self,
718 aabb: &WorldAabb,
719 old_bottom_center: DVec3,
720 descending: bool,
721 ) -> Vec<WorldAabb> {
722 let mut collisions = self.get_entity_collisions(aabb);
723 collisions.extend(self.get_block_collisions_with_context(
724 aabb,
725 self.entity_collision_context(old_bottom_center.y, descending, true),
726 ));
727 collisions
728 }
729
730 fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
731 if !self.include_entity_collisions {
732 return Vec::new();
733 }
734 if aabb.size() < ENTITY_COLLISION_EPSILON {
735 return Vec::new();
736 }
737
738 let query = aabb.inflate(ENTITY_COLLISION_EPSILON);
739 self.world
740 .get_entity_bounding_boxes_in_aabb_matching(&query, |entity| match self.source {
741 Some(source) => {
742 entity.id() != source.id()
743 && !entity.is_removed()
744 && !entity.is_spectator()
745 && source.can_collide_with(entity)
746 }
747 None => {
748 !entity.is_removed()
749 && !entity.is_spectator()
750 && entity.can_be_collided_with(None)
751 }
752 })
753 }
754
755 fn has_entity_collision(&self, aabb: &WorldAabb) -> bool {
756 if !self.include_entity_collisions {
757 return false;
758 }
759 if aabb.size() < ENTITY_COLLISION_EPSILON {
760 return false;
761 }
762
763 let query = aabb.inflate(ENTITY_COLLISION_EPSILON);
764 self.world
765 .has_entity_in_aabb_matching(&query, |entity| match self.source {
766 Some(source) => {
767 entity.id() != source.id()
768 && !entity.is_removed()
769 && !entity.is_spectator()
770 && source.can_collide_with(entity)
771 }
772 None => {
773 !entity.is_removed()
774 && !entity.is_spectator()
775 && entity.can_be_collided_with(None)
776 }
777 })
778 }
779
780 fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
781 let Some(source) = self.source else {
782 return Vec::new();
783 };
784
785 let border = self.world.world_border_snapshot();
786 let source_position = source.position();
787 if !border.is_inside_close_to_border(source_position.x, source_position.z, *aabb) {
788 return Vec::new();
789 }
790
791 border.collision_shapes_for(*aabb)
792 }
793
794 fn has_world_border_collision(&self, aabb: &WorldAabb) -> bool {
795 let Some(source) = self.source else {
796 return false;
797 };
798
799 let border = self.world.world_border_snapshot();
800 let source_position = source.position();
801 border.is_inside_close_to_border(source_position.x, source_position.z, *aabb)
802 && !border.collision_shapes_for(*aabb).is_empty()
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use std::iter;
809
810 use super::*;
811 use steel_registry::blocks::shapes::VoxelShape;
812 use steel_registry::init_vanilla_registry;
813 use steel_utils::{BlockLocalAabb, ChunkPos, types::UpdateFlags};
814
815 use crate::{
816 behavior::init_behaviors,
817 test_support::{fresh_test_world, insert_ready_full_chunk},
818 };
819
820 const LARGE_COLLISION_SHAPE: &[BlockLocalAabb] =
821 &[BlockLocalAabb::new(-0.25, 0.0, 0.0, 1.0, 1.0, 1.0)];
822
823 struct TestCollisionWorld {
824 block_collisions: Vec<WorldAabb>,
825 entity_collisions: Vec<WorldAabb>,
826 pre_move_collisions: Vec<WorldAabb>,
827 }
828
829 struct BorderPreMoveWorld {
830 entity_collisions: Vec<WorldAabb>,
831 border_collisions: Vec<WorldAabb>,
832 }
833
834 impl CollisionWorld for TestCollisionWorld {
835 fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
836 vanilla_blocks::AIR.default_state()
837 }
838
839 fn get_block_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
840 self.block_collisions
841 .iter()
842 .copied()
843 .filter(|collision| collision.intersects(*aabb))
844 .collect()
845 }
846
847 fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
848 self.entity_collisions
849 .iter()
850 .copied()
851 .filter(|collision| collision.intersects(*aabb))
852 .collect()
853 }
854
855 fn get_pre_move_collisions(
856 &self,
857 _aabb: &WorldAabb,
858 _old_bottom_center: DVec3,
859 _descending: bool,
860 ) -> Vec<WorldAabb> {
861 self.pre_move_collisions.clone()
862 }
863 }
864
865 impl CollisionWorld for BorderPreMoveWorld {
866 fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
867 vanilla_blocks::AIR.default_state()
868 }
869
870 fn get_block_collisions(&self, _aabb: &WorldAabb) -> Vec<WorldAabb> {
871 Vec::new()
872 }
873
874 fn get_entity_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
875 self.entity_collisions
876 .iter()
877 .copied()
878 .filter(|collision| collision.intersects(*aabb))
879 .collect()
880 }
881
882 fn get_world_border_collisions(&self, aabb: &WorldAabb) -> Vec<WorldAabb> {
883 self.border_collisions
884 .iter()
885 .copied()
886 .filter(|collision| collision.intersects(*aabb))
887 .collect()
888 }
889 }
890
891 #[test]
892 fn test_intersects_aabb() {
893 let aabb1 = WorldAabb::new(0.0, 0.0, 0.0, 2.0, 2.0, 2.0);
894 let aabb2 = WorldAabb::new(1.0, 1.0, 1.0, 3.0, 3.0, 3.0);
895
896 assert!(aabb1.intersects(aabb2));
897
898 let aabb3 = WorldAabb::new(5.0, 5.0, 5.0, 6.0, 6.0, 6.0);
899
900 assert!(!aabb1.intersects(aabb3));
901 }
902
903 #[test]
904 fn closest_free_position_returns_preferred_center_without_collisions() {
905 let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
906 let preferred = DVec3::new(2.0, 0.5, 0.5);
907
908 assert_eq!(
909 closest_free_position(&allowed, preferred, iter::empty()),
910 Some(preferred)
911 );
912 }
913
914 #[test]
915 fn closest_free_position_excludes_expanded_collisions() {
916 let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
917 let collision = WorldAabb::new(0.0, -1.0, -1.0, 3.0, 2.0, 2.0);
918
919 assert_eq!(
920 closest_free_position(&allowed, DVec3::new(1.5, 0.5, 0.5), [collision].into_iter()),
921 Some(DVec3::new(3.0, 0.5, 0.5))
922 );
923 }
924
925 #[test]
926 fn closest_free_position_returns_none_when_fully_blocked() {
927 let allowed = [WorldAabb::new(0.0, 0.0, 0.0, 4.0, 1.0, 1.0)];
928 let collision = WorldAabb::new(-1.0, -1.0, -1.0, 5.0, 2.0, 2.0);
929
930 assert_eq!(
931 closest_free_position(&allowed, DVec3::new(1.5, 0.5, 0.5), [collision].into_iter()),
932 None
933 );
934 }
935
936 #[test]
937 fn block_collision_helper_reports_intersecting_collision_shape() {
938 let world = TestCollisionWorld {
939 block_collisions: vec![WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)],
940 entity_collisions: Vec::new(),
941 pre_move_collisions: Vec::new(),
942 };
943
944 assert!(has_block_collision(
945 &world,
946 WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
947 ));
948 assert!(!has_block_collision(
949 &world,
950 WorldAabb::new(2.0, 2.0, 2.0, 3.0, 3.0, 3.0)
951 ));
952 }
953
954 #[test]
955 fn live_block_collisions_use_bounded_region_reads() {
956 init_vanilla_registry();
957 init_behaviors();
958 let world = fresh_test_world("bounded_collision_reads");
959 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
960 let block_pos = BlockPos::new(0, 64, 0);
961 assert!(world.set_block(
962 block_pos,
963 vanilla_blocks::STONE.default_state(),
964 UpdateFlags::UPDATE_NONE,
965 ));
966
967 let collisions = WorldCollisionProvider::new(&world)
968 .get_block_collisions(&WorldAabb::new(0.25, 64.0, 0.25, 0.75, 65.0, 0.75));
969
970 assert!(collisions.contains(&WorldAabb::new(0.0, 64.0, 0.0, 1.0, 65.0, 1.0)));
971 }
972
973 #[test]
974 fn collision_helper_reports_intersecting_entity_shape() {
975 let world = TestCollisionWorld {
976 block_collisions: Vec::new(),
977 entity_collisions: vec![WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)],
978 pre_move_collisions: Vec::new(),
979 };
980
981 assert!(has_collision(
982 &world,
983 WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
984 ));
985 assert!(!has_block_collision(
986 &world,
987 WorldAabb::new(0.25, 0.25, 0.25, 0.75, 0.75, 0.75)
988 ));
989 }
990
991 #[test]
992 fn new_shape_collision_helper_ignores_collision_already_touching_old_box() {
993 let already_overlapped = WorldAabb::new(0.25, 0.0, 0.25, 0.75, 1.0, 0.75);
994 let new_collision = WorldAabb::new(2.0, 0.0, 0.0, 3.0, 1.0, 1.0);
995 let old_aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
996 let new_aabb = WorldAabb::new(2.0, 0.0, 0.0, 3.0, 1.0, 1.0);
997
998 let already_stuck_world = TestCollisionWorld {
999 block_collisions: Vec::new(),
1000 entity_collisions: Vec::new(),
1001 pre_move_collisions: vec![already_overlapped],
1002 };
1003 assert!(!is_colliding_with_new_shapes(
1004 &already_stuck_world,
1005 old_aabb,
1006 new_aabb,
1007 false
1008 ));
1009
1010 let newly_blocked_world = TestCollisionWorld {
1011 block_collisions: Vec::new(),
1012 entity_collisions: Vec::new(),
1013 pre_move_collisions: vec![new_collision],
1014 };
1015 assert!(is_colliding_with_new_shapes(
1016 &newly_blocked_world,
1017 old_aabb,
1018 new_aabb,
1019 false
1020 ));
1021 }
1022
1023 #[test]
1024 fn pre_move_collisions_exclude_world_border_collisions() {
1025 let entity_collision = WorldAabb::new(0.25, 0.0, 0.25, 0.75, 1.0, 0.75);
1026 let border_collision = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
1027 let world = BorderPreMoveWorld {
1028 entity_collisions: vec![entity_collision],
1029 border_collisions: vec![border_collision],
1030 };
1031 let aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
1032
1033 assert_eq!(
1034 world.get_pre_move_collisions(&aabb, DVec3::ZERO, false),
1035 vec![entity_collision]
1036 );
1037 assert!(world.has_world_border_collision(&aabb));
1038 }
1039
1040 #[test]
1041 fn supporting_block_tie_breaker_matches_vanilla_ordering() {
1042 assert!(vanilla_block_pos_less(
1043 BlockPos::new(0, 0, 0),
1044 BlockPos::new(0, 1, 0)
1045 ));
1046 assert!(vanilla_block_pos_less(
1047 BlockPos::new(0, 1, 0),
1048 BlockPos::new(0, 1, 1)
1049 ));
1050 assert!(vanilla_block_pos_less(
1051 BlockPos::new(0, 1, 1),
1052 BlockPos::new(1, 1, 1)
1053 ));
1054 assert!(!vanilla_block_pos_less(
1055 BlockPos::new(1, 1, 1),
1056 BlockPos::new(0, 1, 1)
1057 ));
1058 }
1059
1060 #[test]
1061 fn supporting_block_distance_uses_block_center() {
1062 let distance =
1063 block_pos_center_distance_sq(BlockPos::new(1, 2, 3), DVec3::new(1.5, 1.5, 5.5));
1064
1065 assert!((distance - 5.0).abs() < f64::EPSILON);
1066 }
1067
1068 #[test]
1069 fn block_collision_search_bounds_match_vanilla_epsilon_range() {
1070 let bounds =
1071 BlockCollisionSearchBounds::from_aabb(&WorldAabb::new(0.0, 0.25, 0.0, 1.0, 1.0, 1.0));
1072
1073 assert_eq!(bounds.min_x, -2);
1074 assert_eq!(bounds.max_x, 2);
1075 assert_eq!(bounds.min_y, -1);
1076 assert_eq!(bounds.max_y, 2);
1077 assert_eq!(bounds.min_z, -2);
1078 assert_eq!(bounds.max_z, 2);
1079 }
1080
1081 #[test]
1082 fn collision_cursor_type_matches_vanilla_boundary_count() {
1083 let bounds = BlockCollisionSearchBounds::from_aabb(&WorldAabb::new(
1084 0.25, 0.25, 0.25, 0.75, 0.75, 0.75,
1085 ));
1086
1087 assert_eq!(bounds.cursor_type(0, 0, 0), CollisionCursorType::Inside);
1088 assert_eq!(
1089 bounds.cursor_type(bounds.min_x, 0, 0),
1090 CollisionCursorType::Face
1091 );
1092 assert_eq!(
1093 bounds.cursor_type(bounds.min_x, bounds.min_y, 0),
1094 CollisionCursorType::Edge
1095 );
1096 assert_eq!(
1097 bounds.cursor_type(bounds.min_x, bounds.min_y, bounds.min_z),
1098 CollisionCursorType::Corner
1099 );
1100 }
1101
1102 #[test]
1103 fn collision_shape_filter_matches_vanilla_cursor_rules() {
1104 init_vanilla_registry();
1105
1106 let stone = vanilla_blocks::STONE.default_state();
1107 let moving_piston = vanilla_blocks::MOVING_PISTON.default_state();
1108 let large_shape = VoxelShape::from_boxes(LARGE_COLLISION_SHAPE);
1109 let shape = |shape: VoxelShape| CollisionShape {
1110 boxes: shape.into_iter().copied().collect(),
1111 };
1112
1113 assert!(should_query_collision_shape(
1114 stone,
1115 &shape(VoxelShape::FULL_BLOCK),
1116 CollisionCursorType::Inside
1117 ));
1118 assert!(!should_query_collision_shape(
1119 stone,
1120 &shape(VoxelShape::FULL_BLOCK),
1121 CollisionCursorType::Face
1122 ));
1123 assert!(should_query_collision_shape(
1124 stone,
1125 &shape(large_shape),
1126 CollisionCursorType::Face
1127 ));
1128 assert!(!should_query_collision_shape(
1129 stone,
1130 &shape(large_shape),
1131 CollisionCursorType::Edge
1132 ));
1133 assert!(should_query_collision_shape(
1134 moving_piston,
1135 &shape(VoxelShape::FULL_BLOCK),
1136 CollisionCursorType::Edge
1137 ));
1138 assert!(!should_query_collision_shape(
1139 moving_piston,
1140 &shape(large_shape),
1141 CollisionCursorType::Corner
1142 ));
1143 }
1144
1145 #[test]
1146 fn collision_shape_filter_uses_position_resolved_offset_bounds() {
1147 init_vanilla_registry();
1148
1149 let stone = vanilla_blocks::STONE.default_state();
1150 let shifted_full_block = CollisionShape {
1151 boxes: VoxelShape::FULL_BLOCK
1152 .into_iter()
1153 .map(|aabb| aabb.translate(DVec3::new(0.25, 0.0, 0.0)))
1154 .collect(),
1155 };
1156
1157 assert!(should_query_collision_shape(
1158 stone,
1159 &shifted_full_block,
1160 CollisionCursorType::Face
1161 ));
1162 }
1163}