Skip to main content

steel_core/world/
raycast.rs

1use super::*;
2
3/// Controls how a block position is treated during a raytrace traversal.
4///
5/// Returned by the predicate closure passed to [`World::raytrace`].
6#[derive(Debug)]
7pub enum RaytraceAction {
8    /// Skip this block and continue traversal (transparent block).
9    Pass,
10    /// Test the block's voxel shape for a precise ray intersection.
11    CheckShape,
12    /// Immediately treat this block as a hit without shape testing.
13    ImmediateHit,
14}
15
16/// Block shape channel used by vanilla-style world clipping.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ClipBlockShape {
19    /// `ClipContext.Block.COLLIDER`
20    Collider,
21    /// `ClipContext.Block.OUTLINE`
22    Outline,
23    /// `ClipContext.Block.VISUAL`
24    Visual,
25    /// `ClipContext.Block.FALLDAMAGE_RESETTING`
26    FallDamageResetting {
27        /// Whether the clip context entity is a player.
28        entity_is_player: bool,
29    },
30}
31
32/// Fluid shape filter used by vanilla-style world clipping.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ClipFluid {
35    /// `ClipContext.Fluid.NONE`
36    None,
37    /// `ClipContext.Fluid.SOURCE_ONLY`
38    SourceOnly,
39    /// `ClipContext.Fluid.ANY`
40    Any,
41    /// `ClipContext.Fluid.WATER`
42    Water,
43}
44
45/// Result of a vanilla-style world clip.
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct ClipHitResult {
48    /// Exact hit location in world coordinates.
49    pub location: DVec3,
50    /// Hit face, or the miss direction for misses.
51    pub direction: Direction,
52    /// Block position containing the hit or miss endpoint.
53    pub block_pos: BlockPos,
54    /// Whether this result is a miss.
55    pub miss: bool,
56    /// Whether the ray started inside the hit shape.
57    pub inside: bool,
58    /// Whether this hit was synthesized by the world border.
59    pub world_border_hit: bool,
60}
61
62impl ClipHitResult {
63    /// Returns whether this clip missed all selected block and fluid shapes.
64    #[must_use]
65    pub const fn is_miss(self) -> bool {
66        self.miss
67    }
68}
69
70impl World {
71    /// Checks if a ray intersects with a block's selection box.
72    pub fn ray_outline_check(
73        &self,
74        block_pos: BlockPos,
75        from: DVec3,
76        to: DVec3,
77    ) -> (bool, Option<Direction>) {
78        let state = self.get_block_state(block_pos);
79        let shape = state.get_outline_shape_at(block_pos);
80
81        match Self::clip_shape(block_pos, from, to, shape) {
82            Some(hit) => (true, Some(hit.direction)),
83            None => (false, None),
84        }
85    }
86
87    /// Performs a vanilla-style block/fluid clip in the world.
88    #[must_use]
89    pub fn clip(
90        &self,
91        start_pos: DVec3,
92        end_pos: DVec3,
93        block_shape: ClipBlockShape,
94        fluid: ClipFluid,
95    ) -> ClipHitResult {
96        if start_pos == end_pos {
97            return Self::clip_miss(start_pos, end_pos);
98        }
99
100        let adjust = -1.0e-7f64;
101        let to = end_pos.lerp(start_pos, adjust);
102        let from = start_pos.lerp(end_pos, adjust);
103
104        let mut block = BlockPos::new(
105            from.x.floor() as i32,
106            from.y.floor() as i32,
107            from.z.floor() as i32,
108        );
109
110        if let Some(hit) = self.clip_block_and_fluid(block, start_pos, end_pos, block_shape, fluid)
111        {
112            return hit;
113        }
114
115        let difference = to - from;
116
117        let step = difference.signum().as_ivec3();
118
119        let delta = DVec3::new(
120            if step.x == 0 {
121                f64::MAX
122            } else {
123                (f64::from(step.x)) / difference.x
124            },
125            if step.y == 0 {
126                f64::MAX
127            } else {
128                (f64::from(step.y)) / difference.y
129            },
130            if step.z == 0 {
131                f64::MAX
132            } else {
133                (f64::from(step.z)) / difference.z
134            },
135        );
136
137        let mut next = DVec3::new(
138            delta.x
139                * (if step.x > 0 {
140                    1.0 - (from.x - from.x.floor())
141                } else {
142                    from.x - from.x.floor()
143                }),
144            delta.y
145                * (if step.y > 0 {
146                    1.0 - (from.y - from.y.floor())
147                } else {
148                    from.y - from.y.floor()
149                }),
150            delta.z
151                * (if step.z > 0 {
152                    1.0 - (from.z - from.z.floor())
153                } else {
154                    from.z - from.z.floor()
155                }),
156        );
157
158        while next.x <= 1.0 || next.y <= 1.0 || next.z <= 1.0 {
159            if next.x < next.y && next.x < next.z {
160                block.0.x += step.x;
161                next.x += delta.x;
162            } else if next.y < next.z {
163                block.0.y += step.y;
164                next.y += delta.y;
165            } else {
166                block.0.z += step.z;
167                next.z += delta.z;
168            }
169
170            if let Some(hit) =
171                self.clip_block_and_fluid(block, start_pos, end_pos, block_shape, fluid)
172            {
173                return hit;
174            }
175        }
176
177        Self::clip_miss(start_pos, end_pos)
178    }
179
180    /// Performs vanilla `CollisionGetter.clipIncludingBorder`.
181    #[must_use]
182    pub fn clip_including_border(
183        &self,
184        start_pos: DVec3,
185        end_pos: DVec3,
186        block_shape: ClipBlockShape,
187        fluid: ClipFluid,
188    ) -> ClipHitResult {
189        let hit = self.clip(start_pos, end_pos, block_shape, fluid);
190        let border = self.world_border_snapshot();
191        if border.is_within_bounds_with_margin(start_pos.x, start_pos.z, 0.0)
192            && !border.is_within_bounds_with_margin(hit.location.x, hit.location.z, 0.0)
193        {
194            let delta = hit.location - start_pos;
195            let location = border.clamp_vec3_to_bound(hit.location);
196            return ClipHitResult {
197                location,
198                direction: Self::approximate_nearest_direction(delta),
199                block_pos: BlockPos::from(location),
200                miss: false,
201                inside: false,
202                world_border_hit: true,
203            };
204        }
205        hit
206    }
207
208    pub(super) fn clip_block_and_fluid(
209        &self,
210        pos: BlockPos,
211        from: DVec3,
212        to: DVec3,
213        block_shape: ClipBlockShape,
214        fluid: ClipFluid,
215    ) -> Option<ClipHitResult> {
216        let state = self.get_block_state(pos);
217        let block_result = Self::clip_shape(
218            pos,
219            from,
220            to,
221            self.clip_block_shape(state, pos, block_shape),
222        )
223        .map(|hit| Self::clip_with_interaction_override(pos, from, to, state, hit));
224        let fluid_result = self.clip_fluid_shape(pos, from, to, state, fluid);
225
226        match (block_result, fluid_result) {
227            (Some(block_hit), Some(fluid_hit)) => {
228                let block_distance = from.distance_squared(block_hit.location);
229                let fluid_distance = from.distance_squared(fluid_hit.location);
230                if block_distance <= fluid_distance {
231                    Some(block_hit)
232                } else {
233                    Some(fluid_hit)
234                }
235            }
236            (Some(hit), None) | (None, Some(hit)) => Some(hit),
237            (None, None) => None,
238        }
239    }
240
241    pub(super) fn clip_with_interaction_override(
242        pos: BlockPos,
243        from: DVec3,
244        to: DVec3,
245        state: BlockStateId,
246        block_hit: ClipHitResult,
247    ) -> ClipHitResult {
248        let Some(override_hit) =
249            Self::clip_shape(pos, from, to, state.get_interaction_shape_at(pos))
250        else {
251            return block_hit;
252        };
253
254        if from.distance_squared(override_hit.location) < from.distance_squared(block_hit.location)
255        {
256            ClipHitResult {
257                direction: override_hit.direction,
258                ..block_hit
259            }
260        } else {
261            block_hit
262        }
263    }
264
265    pub(super) fn clip_block_shape(
266        &self,
267        state: BlockStateId,
268        pos: BlockPos,
269        shape: ClipBlockShape,
270    ) -> OffsetVoxelShape {
271        match shape {
272            ClipBlockShape::Collider => state.get_collision_shape_at(pos),
273            ClipBlockShape::Outline => state.get_outline_shape_at(pos),
274            ClipBlockShape::Visual => state.get_visual_shape_at(pos),
275            ClipBlockShape::FallDamageResetting { entity_is_player } => {
276                OffsetVoxelShape::without_offset(
277                    self.fall_damage_resetting_shape(state, entity_is_player),
278                )
279            }
280        }
281    }
282
283    pub(super) fn fall_damage_resetting_shape(
284        &self,
285        state: BlockStateId,
286        entity_is_player: bool,
287    ) -> VoxelShape {
288        let block = state.get_block();
289        if block.has_tag(&BlockTag::FALL_DAMAGE_RESETTING) {
290            return VoxelShape::FULL_BLOCK;
291        }
292
293        if !entity_is_player {
294            return VoxelShape::EMPTY;
295        }
296
297        if block == &vanilla_blocks::END_GATEWAY || block == &vanilla_blocks::END_PORTAL {
298            return VoxelShape::FULL_BLOCK;
299        }
300
301        if block == &vanilla_blocks::NETHER_PORTAL
302            && self.get_game_rule(&PLAYERS_NETHER_PORTAL_DEFAULT_DELAY) == 0
303        {
304            return VoxelShape::FULL_BLOCK;
305        }
306
307        VoxelShape::EMPTY
308    }
309
310    pub(super) fn clip_fluid_shape(
311        &self,
312        pos: BlockPos,
313        from: DVec3,
314        to: DVec3,
315        state: BlockStateId,
316        fluid: ClipFluid,
317    ) -> Option<ClipHitResult> {
318        let fluid_state = state.get_fluid_state();
319        let can_pick = match fluid {
320            ClipFluid::None => false,
321            ClipFluid::SourceOnly => fluid_state.is_source(),
322            ClipFluid::Any => !fluid_state.is_empty(),
323            ClipFluid::Water => fluid_state.is_water(),
324        };
325        if !can_pick {
326            return None;
327        }
328
329        let height = self.fluid_clip_height(pos, fluid_state);
330        Self::clip_local_aabb(
331            pos,
332            from,
333            to,
334            BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, height, 1.0),
335        )
336    }
337
338    pub(super) fn fluid_clip_height(&self, pos: BlockPos, fluid_state: FluidState) -> f64 {
339        let above_fluid = self.get_block_state(pos.above()).get_fluid_state();
340        Self::fluid_clip_height_from_above(fluid_state, above_fluid)
341    }
342
343    pub(super) fn fluid_clip_height_from_above(
344        fluid_state: FluidState,
345        above_fluid: FluidState,
346    ) -> f64 {
347        if FLUID_BEHAVIORS
348            .get_behavior(fluid_state.fluid_id)
349            .is_same(above_fluid.fluid_id)
350        {
351            1.0
352        } else {
353            f64::from(fluid_state.own_height())
354        }
355    }
356
357    pub(super) fn clip_shape(
358        block_pos: BlockPos,
359        from: DVec3,
360        to: DVec3,
361        shape: OffsetVoxelShape,
362    ) -> Option<ClipHitResult> {
363        if shape.is_empty() {
364            return None;
365        }
366
367        if (to - from).length_squared() < 1.0e-7 {
368            return None;
369        }
370
371        let block_vec = DVec3::new(
372            f64::from(block_pos.x()),
373            f64::from(block_pos.y()),
374            f64::from(block_pos.z()),
375        );
376        let inside_test_point = from + (to - from) * 0.001;
377        if Self::shape_contains_world_point(shape, block_vec, inside_test_point) {
378            return Some(ClipHitResult {
379                location: inside_test_point,
380                direction: Self::approximate_nearest_direction(to - from).opposite(),
381                block_pos,
382                miss: false,
383                inside: true,
384                world_border_hit: false,
385            });
386        }
387
388        let mut closest: Option<(f64, Direction)> = None;
389
390        for shape in shape.iter() {
391            let world_min = DVec3::new(shape.min_x(), shape.min_y(), shape.min_z()) + block_vec;
392            let world_max = DVec3::new(shape.max_x(), shape.max_y(), shape.max_z()) + block_vec;
393
394            if let Some(hit) = Self::intersects_aabb_with_t(from, to, world_min, world_max)
395                && hit.0 > 0.0
396                && hit.0 < 1.0
397                && closest.is_none_or(|(best_t, _)| hit.0 < best_t)
398            {
399                closest = Some(hit);
400            }
401        }
402
403        closest.map(|(t, direction)| ClipHitResult {
404            location: from + (to - from) * t,
405            direction,
406            block_pos,
407            miss: false,
408            inside: false,
409            world_border_hit: false,
410        })
411    }
412
413    pub(super) fn clip_local_aabb(
414        block_pos: BlockPos,
415        from: DVec3,
416        to: DVec3,
417        aabb: BlockLocalAabb,
418    ) -> Option<ClipHitResult> {
419        if aabb.is_empty() {
420            return None;
421        }
422
423        if (to - from).length_squared() < 1.0e-7 {
424            return None;
425        }
426
427        let block_vec = DVec3::new(
428            f64::from(block_pos.x()),
429            f64::from(block_pos.y()),
430            f64::from(block_pos.z()),
431        );
432        let inside_test_point = from + (to - from) * 0.001;
433        if Self::local_aabb_contains_world_point(aabb, block_vec, inside_test_point) {
434            return Some(ClipHitResult {
435                location: inside_test_point,
436                direction: Self::approximate_nearest_direction(to - from).opposite(),
437                block_pos,
438                miss: false,
439                inside: true,
440                world_border_hit: false,
441            });
442        }
443
444        let world_min = DVec3::new(aabb.min_x(), aabb.min_y(), aabb.min_z()) + block_vec;
445        let world_max = DVec3::new(aabb.max_x(), aabb.max_y(), aabb.max_z()) + block_vec;
446        Self::intersects_aabb_with_t(from, to, world_min, world_max).and_then(|(t, direction)| {
447            if t > 0.0 && t < 1.0 {
448                Some(ClipHitResult {
449                    location: from + (to - from) * t,
450                    direction,
451                    block_pos,
452                    miss: false,
453                    inside: false,
454                    world_border_hit: false,
455                })
456            } else {
457                None
458            }
459        })
460    }
461
462    pub(super) fn shape_contains_world_point(
463        shape: OffsetVoxelShape,
464        block_vec: DVec3,
465        point: DVec3,
466    ) -> bool {
467        shape
468            .iter()
469            .any(|aabb| Self::local_aabb_contains_world_point(aabb, block_vec, point))
470    }
471
472    pub(super) fn local_aabb_contains_world_point(
473        aabb: BlockLocalAabb,
474        block_vec: DVec3,
475        point: DVec3,
476    ) -> bool {
477        let local = point - block_vec;
478        !aabb.is_empty()
479            && local.x >= aabb.min_x()
480            && local.x <= aabb.max_x()
481            && local.y >= aabb.min_y()
482            && local.y <= aabb.max_y()
483            && local.z >= aabb.min_z()
484            && local.z <= aabb.max_z()
485    }
486
487    pub(super) fn clip_miss(from: DVec3, to: DVec3) -> ClipHitResult {
488        ClipHitResult {
489            location: to,
490            direction: Self::approximate_nearest_direction(from - to),
491            block_pos: BlockPos::from(to),
492            miss: true,
493            inside: false,
494            world_border_hit: false,
495        }
496    }
497
498    pub(super) fn approximate_nearest_direction(vector: DVec3) -> Direction {
499        let mut result = Direction::North;
500        let mut highest_dot = 0.0;
501        for direction in [
502            Direction::Down,
503            Direction::Up,
504            Direction::North,
505            Direction::South,
506            Direction::West,
507            Direction::East,
508        ] {
509            let dot = vector.dot(direction.offset_vec().as_dvec3());
510            if dot > highest_dot {
511                highest_dot = dot;
512                result = direction;
513            }
514        }
515        result
516    }
517
518    /// Ray-AABB intersection returning the entry t-parameter and the hit face.
519    ///
520    /// Returns `Some((tmin, direction))` where `tmin` is the ray parameter at entry
521    /// and `direction` is the face normal pointing away from the hit surface.
522    /// Returns `None` if the AABB is missed or entirely behind the ray origin.
523    ///
524    /// Used internally by [`ray_outline_check`] to pick the *closest* hit across
525    /// a multi-box voxel shape, matching vanilla's `VoxelShape.clip()` behavior.
526    pub(super) fn intersects_aabb_with_t(
527        start: DVec3,
528        end: DVec3,
529        min: DVec3,
530        max: DVec3,
531    ) -> Option<(f64, Direction)> {
532        let dir = end - start;
533
534        let mut tmin = f64::NEG_INFINITY;
535        let mut tmax = f64::INFINITY;
536        let mut hit_dir = None;
537
538        macro_rules! slab {
539            ($start:expr, $dir:expr, $min:expr, $max:expr, $neg:expr, $pos:expr) => {{
540                if $dir.abs() < 1e-8 {
541                    if $start < $min || $start > $max {
542                        return None;
543                    }
544                } else {
545                    let inv = 1.0 / $dir;
546                    let mut t1 = ($min - $start) * inv;
547                    let mut t2 = ($max - $start) * inv;
548
549                    let dir_hit = if t1 > t2 {
550                        std::mem::swap(&mut t1, &mut t2);
551                        $pos
552                    } else {
553                        $neg
554                    };
555
556                    if t1 > tmin {
557                        tmin = t1;
558                        hit_dir = Some(dir_hit);
559                    }
560
561                    tmax = tmax.min(t2);
562                    if tmin > tmax {
563                        return None;
564                    }
565                }
566            }};
567        }
568
569        slab!(
570            start.x,
571            dir.x,
572            min.x,
573            max.x,
574            Direction::West,
575            Direction::East
576        );
577        slab!(start.y, dir.y, min.y, max.y, Direction::Down, Direction::Up);
578        slab!(
579            start.z,
580            dir.z,
581            min.z,
582            max.z,
583            Direction::North,
584            Direction::South
585        );
586
587        if tmax < 0.0 {
588            None
589        } else {
590            hit_dir.map(|d| (tmin, d))
591        }
592    }
593
594    /// Performs a raytrace in the world.
595    ///
596    /// Adapted from Pumpkin project.
597    pub fn raytrace<F>(
598        &self,
599        start_pos: DVec3,
600        end_pos: DVec3,
601        hit_check: F,
602    ) -> (Option<BlockPos>, Option<Direction>)
603    where
604        F: Fn(BlockPos, &Self) -> RaytraceAction,
605    {
606        if start_pos == end_pos {
607            return (None, None);
608        }
609
610        let adjust = -1.0e-7f64;
611        let to = end_pos.lerp(start_pos, adjust);
612        let from = start_pos.lerp(end_pos, adjust);
613
614        let mut block = BlockPos::new(
615            from.x.floor() as i32,
616            from.y.floor() as i32,
617            from.z.floor() as i32,
618        );
619
620        match hit_check(block, self) {
621            RaytraceAction::ImmediateHit => return (Some(block), None),
622            RaytraceAction::CheckShape => {
623                let (hit, face) = self.ray_outline_check(block, start_pos, end_pos);
624                if hit {
625                    return (Some(block), face);
626                }
627            }
628            RaytraceAction::Pass => {}
629        }
630
631        let difference = to - from;
632
633        let step = difference.signum().as_ivec3();
634
635        let delta = DVec3::new(
636            if step.x == 0 {
637                f64::MAX
638            } else {
639                (f64::from(step.x)) / difference.x
640            },
641            if step.y == 0 {
642                f64::MAX
643            } else {
644                (f64::from(step.y)) / difference.y
645            },
646            if step.z == 0 {
647                f64::MAX
648            } else {
649                (f64::from(step.z)) / difference.z
650            },
651        );
652
653        let mut next = DVec3::new(
654            delta.x
655                * (if step.x > 0 {
656                    1.0 - (from.x - from.x.floor())
657                } else {
658                    from.x - from.x.floor()
659                }),
660            delta.y
661                * (if step.y > 0 {
662                    1.0 - (from.y - from.y.floor())
663                } else {
664                    from.y - from.y.floor()
665                }),
666            delta.z
667                * (if step.z > 0 {
668                    1.0 - (from.z - from.z.floor())
669                } else {
670                    from.z - from.z.floor()
671                }),
672        );
673
674        while next.x <= 1.0 || next.y <= 1.0 || next.z <= 1.0 {
675            // Vanilla parity: traverseBlocks tie-breaking — Z wins on any tie.
676            // X wins only when strictly less than both Y and Z.
677            // Y wins only when strictly less than both X and Z.
678            // Everything else (including all ties) goes to Z.
679            let block_direction = if next.x < next.y && next.x < next.z {
680                block.0.x += step.x;
681                next.x += delta.x;
682                if step.x > 0 {
683                    Direction::West
684                } else {
685                    Direction::East
686                }
687            } else if next.y < next.x && next.y < next.z {
688                block.0.y += step.y;
689                next.y += delta.y;
690                if step.y > 0 {
691                    Direction::Down
692                } else {
693                    Direction::Up
694                }
695            } else {
696                block.0.z += step.z;
697                next.z += delta.z;
698                if step.z > 0 {
699                    Direction::North
700                } else {
701                    Direction::South
702                }
703            };
704
705            match hit_check(block, self) {
706                RaytraceAction::ImmediateHit => {
707                    return (Some(block), Some(block_direction));
708                }
709                RaytraceAction::CheckShape => {
710                    let (hit, face) = self.ray_outline_check(block, start_pos, end_pos);
711                    if hit {
712                        return (Some(block), face);
713                    }
714                }
715                RaytraceAction::Pass => {}
716            }
717        }
718
719        (None, None)
720    }
721}