steel_core/world/explosion/server/
exposure.rs1use 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 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 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}