Skip to main content

steel_core/world/explosion/server/
exposure.rs

1//! Vanilla entity exposure sampling and Steel's collision-ray reuse.
2
3use glam::DVec3;
4use steel_registry::vanilla_entities;
5use steel_utils::{BlockPos, WorldAabb};
6
7use crate::behavior::BlockCollisionContext;
8use crate::behavior::blocks::PowderSnowBlock;
9use crate::chunk::paletted_container::BlockPalette;
10use crate::entity::Entity;
11use crate::world::raycast::{ExplosionExposureRaycast, collision_path_axis_block_bounds};
12use crate::world::{BlockRegionBounds, MAX_BLOCK_REGION_WORKSET_SLOTS};
13
14const EXPOSURE_SAMPLE_DENSITY: f64 = 2.0;
15const EXPOSURE_SAMPLE_OFFSET_DIVISOR: f64 = 2.0;
16const MAX_EXPOSURE_CERTIFICATE_AXIS_WORK: usize =
17    MAX_BLOCK_REGION_WORKSET_SLOTS * BlockPalette::SIZE;
18
19#[derive(Clone, Copy)]
20pub(super) struct EntityExplosionExposure {
21    bounding_box: WorldAabb,
22    pub(super) collision_context: BlockCollisionContext,
23    x_step: f64,
24    y_step: f64,
25    z_step: f64,
26    x_offset: f64,
27    z_offset: f64,
28}
29
30impl EntityExplosionExposure {
31    pub(super) fn capture(entity: &dyn Entity) -> Self {
32        let bounding_box = entity.bounding_box();
33        let x_step = exposure_axis_step(bounding_box.width());
34        let y_step = exposure_axis_step(bounding_box.height());
35        let z_step = exposure_axis_step(bounding_box.depth());
36        let collision_context =
37            BlockCollisionContext::entity(entity.position().y, entity.is_descending())
38                .with_fall_distance(entity.fall_distance())
39                .with_can_walk_on_powder_snow(PowderSnowBlock::can_entity_walk_on_powder_snow(
40                    entity,
41                ))
42                .with_falling_block(entity.entity_type() == &vanilla_entities::FALLING_BLOCK);
43
44        Self {
45            bounding_box,
46            collision_context,
47            x_step,
48            y_step,
49            z_step,
50            x_offset: (1.0 - (1.0 / x_step).floor() * x_step) / EXPOSURE_SAMPLE_OFFSET_DIVISOR,
51            z_offset: (1.0 - (1.0 / z_step).floor() * z_step) / EXPOSURE_SAMPLE_OFFSET_DIVISOR,
52        }
53    }
54
55    const fn has_negative_step(self) -> bool {
56        self.x_step < 0.0 || self.y_step < 0.0 || self.z_step < 0.0
57    }
58
59    fn sample_position(&self, x_fraction: f64, y_fraction: f64, z_fraction: f64) -> DVec3 {
60        DVec3::new(
61            self.bounding_box.min_x()
62                + (self.bounding_box.max_x() - self.bounding_box.min_x()) * x_fraction
63                + self.x_offset,
64            self.bounding_box.min_y()
65                + (self.bounding_box.max_y() - self.bounding_box.min_y()) * y_fraction,
66            self.bounding_box.min_z()
67                + (self.bounding_box.max_z() - self.bounding_box.min_z()) * z_fraction
68                + self.z_offset,
69        )
70    }
71
72    fn axis_sample_block_bounds(
73        axis_min: f64,
74        axis_max: f64,
75        step: f64,
76        offset: f64,
77        center: f64,
78    ) -> Option<(i32, i32)> {
79        let can_sample_axis = axis_min.is_finite()
80            && axis_max.is_finite()
81            && axis_max >= axis_min
82            && step.is_finite()
83            && step > 0.0
84            && offset.is_finite()
85            && center.is_finite();
86        if !can_sample_axis {
87            return None;
88        }
89
90        let axis_length = axis_max - axis_min;
91        let mut min_block = i32::MAX;
92        let mut max_block = i32::MIN;
93        let mut fraction = 0.0;
94        for _ in 0..MAX_EXPOSURE_CERTIFICATE_AXIS_WORK {
95            if fraction > 1.0 {
96                return Some((min_block, max_block));
97            }
98            let sample = axis_min + axis_length * fraction + offset;
99            let (sample_min, sample_max) = collision_path_axis_block_bounds(
100                sample,
101                center,
102                MAX_EXPOSURE_CERTIFICATE_AXIS_WORK,
103            )?;
104            min_block = min_block.min(sample_min);
105            max_block = max_block.max(sample_max);
106            fraction += step;
107            if !fraction.is_finite() {
108                return None;
109            }
110        }
111        None
112    }
113
114    /// Builds a Cartesian envelope containing every block visited by all exposure rays.
115    ///
116    /// Each axis uses Vanilla's repeated-addition sample sequence and exact scalar DDA recurrence.
117    /// This is linear in the three axis sample counts instead of their Cartesian product.
118    pub(super) fn stable_air_certificate_bounds(self, center: DVec3) -> Option<BlockRegionBounds> {
119        let (min_x, max_x) = Self::axis_sample_block_bounds(
120            self.bounding_box.min_x(),
121            self.bounding_box.max_x(),
122            self.x_step,
123            self.x_offset,
124            center.x,
125        )?;
126        let (min_y, max_y) = Self::axis_sample_block_bounds(
127            self.bounding_box.min_y(),
128            self.bounding_box.max_y(),
129            self.y_step,
130            0.0,
131            center.y,
132        )?;
133        let (min_z, max_z) = Self::axis_sample_block_bounds(
134            self.bounding_box.min_z(),
135            self.bounding_box.max_z(),
136            self.z_step,
137            self.z_offset,
138            center.z,
139        )?;
140        Some(BlockRegionBounds::from_corners(
141            BlockPos::new(min_x, min_y, min_z),
142            BlockPos::new(max_x, max_y, max_z),
143        ))
144    }
145
146    fn for_each_sample(self, mut visit: impl FnMut(DVec3)) -> usize {
147        let mut sample_count = 0;
148        // Repeated addition and inclusive bounds intentionally mirror Vanilla's floating-point
149        // sample sequence; deriving each fraction from an integer index can change boundary rays.
150        let mut x_fraction = 0.0;
151        while x_fraction <= 1.0 {
152            let mut y_fraction = 0.0;
153            while y_fraction <= 1.0 {
154                let mut z_fraction = 0.0;
155                while z_fraction <= 1.0 {
156                    visit(self.sample_position(x_fraction, y_fraction, z_fraction));
157                    sample_count += 1;
158                    z_fraction += self.z_step;
159                }
160                y_fraction += self.y_step;
161            }
162            x_fraction += self.x_step;
163        }
164        sample_count
165    }
166
167    pub(super) fn calculate_with_visibility(
168        self,
169        mut is_visible: impl FnMut(DVec3) -> bool,
170    ) -> f32 {
171        let mut visible_samples = 0_u32;
172        let sample_count = self.for_each_sample(|from| {
173            if is_visible(from) {
174                visible_samples += 1;
175            }
176        });
177        visible_samples as f32 / sample_count as f32
178    }
179
180    pub(super) fn calculate_cached_with(
181        self,
182        raycast: &mut ExplosionExposureRaycast<'_>,
183        center: DVec3,
184    ) -> f32 {
185        if self.has_negative_step() {
186            return 0.0;
187        }
188        if let Some(bounds) = self.stable_air_certificate_bounds(center)
189            && raycast.stable_air_box_is_clear(bounds)
190        {
191            return 1.0;
192        }
193        raycast.set_collision_context(self.collision_context);
194        self.calculate_with_visibility(|from| raycast.is_path_clear(from, center))
195    }
196}
197
198const fn exposure_axis_step(axis_length: f64) -> f64 {
199    1.0 / (axis_length * EXPOSURE_SAMPLE_DENSITY + 1.0)
200}
201
202#[cfg(test)]
203pub(super) mod tests {
204    use super::*;
205    use crate::world::World;
206
207    impl EntityExplosionExposure {
208        pub(in super::super) fn calculate_uncached(self, world: &World, center: DVec3) -> f32 {
209            if self.has_negative_step() {
210                return 0.0;
211            }
212            self.calculate_with_visibility(|from| {
213                world.is_block_collision_path_clear(from, center, self.collision_context)
214            })
215        }
216    }
217
218    pub(in super::super) fn seen_percent(world: &World, center: DVec3, entity: &dyn Entity) -> f32 {
219        let exposure = EntityExplosionExposure::capture(entity);
220        let mut raycast = ExplosionExposureRaycast::new(world, exposure.collision_context);
221        exposure.calculate_cached_with(&mut raycast, center)
222    }
223}