Skip to main content

steel_core/world/explosion/server/
block_rays.rs

1//! Vanilla affected-block ray traversal and immutable-calculator fast paths.
2
3use std::sync::LazyLock;
4
5use glam::DVec3;
6use steel_registry::blocks::block_state_ext::BlockStateExt as _;
7use steel_utils::{BlockPos, BlockStateId};
8
9use crate::world::explosion::{
10    ExplosionBlockReader, ExplosionDamageCalculator as _, ImmutableExplosionBlockCalculator,
11};
12use crate::world::{BlockRegionBounds, BlockRegionRead, World};
13
14use super::ServerExplosion;
15
16mod cache;
17
18use crate::java_block_pos_set::{EqualHashOrder, JavaBlockPosSet};
19
20const RAY_GRID_SIZE: i32 = 16;
21const RAY_GRID_LAST_INDEX: i32 = RAY_GRID_SIZE - 1;
22const RAY_GRID_INTERIOR_SIZE: i32 = RAY_GRID_SIZE - 2;
23pub(super) const RAY_COUNT: usize = (RAY_GRID_SIZE * RAY_GRID_SIZE * RAY_GRID_SIZE
24    - RAY_GRID_INTERIOR_SIZE * RAY_GRID_INTERIOR_SIZE * RAY_GRID_INTERIOR_SIZE)
25    as usize;
26const RAY_STEP: f64 = 0.3_f32 as f64;
27const RAY_POWER_DECAY: f32 = 0.225_000_01;
28const INITIAL_RAY_POWER_BASE: f32 = 0.7;
29const INITIAL_RAY_POWER_RANDOM_SCALE: f32 = 0.6;
30const MAX_INITIAL_RAY_POWER_SCALE: f32 = 1.3;
31const RESISTANCE_POWER_OFFSET: f32 = 0.3;
32const RESISTANCE_POWER_SCALE: f32 = 0.3;
33const RAY_REGION_BLOCK_PADDING: f64 = 1.0;
34
35#[derive(Clone, Copy)]
36struct ExplosionRay {
37    step: DVec3,
38    initial_power: f32,
39}
40
41struct RegionExplosionBlockReader<'reader, 'world> {
42    region: &'reader BlockRegionRead<'world>,
43}
44
45impl<'reader, 'world> RegionExplosionBlockReader<'reader, 'world> {
46    const fn new(region: &'reader BlockRegionRead<'world>) -> Self {
47        Self { region }
48    }
49}
50
51impl ExplosionBlockReader for RegionExplosionBlockReader<'_, '_> {
52    #[inline]
53    fn block_state(&self, pos: BlockPos) -> Option<BlockStateId> {
54        self.region.get_block_state(pos)
55    }
56}
57
58static RAY_STEPS: LazyLock<[DVec3; RAY_COUNT]> = LazyLock::new(|| {
59    let mut steps = [DVec3::ZERO; RAY_COUNT];
60    let mut index = 0;
61    // Keep Vanilla's X/Y/Z traversal order: random ray powers are consumed in this order.
62    for xx in 0..RAY_GRID_SIZE {
63        for yy in 0..RAY_GRID_SIZE {
64            for zz in 0..RAY_GRID_SIZE {
65                if is_boundary_ray(xx, yy, zz) {
66                    steps[index] = ray_direction(xx, yy, zz) * RAY_STEP;
67                    index += 1;
68                }
69            }
70        }
71    }
72    debug_assert_eq!(index, RAY_COUNT);
73    steps
74});
75
76#[derive(Clone, Copy)]
77struct ExplosionRayContext {
78    center: DVec3,
79    bounds: ExplosionWorldBounds,
80}
81
82impl ExplosionRayContext {
83    /// Proves the cached traversal's current and first out-of-region samples fit in `i32`.
84    /// Every component is bounded by [`RAY_STEP`]. One-cell headroom on each face therefore covers
85    /// the sample that makes the bounded reader request the generic fallback.
86    fn can_use_bounded_floor(self, region_bounds: BlockRegionBounds) -> bool {
87        let (min, max) = region_bounds.corners();
88        self.center.is_finite()
89            && ray_axis_has_bounded_floor(self.center.x, min.x(), max.x())
90            && ray_axis_has_bounded_floor(self.center.y, min.y(), max.y())
91            && ray_axis_has_bounded_floor(self.center.z, min.z(), max.z())
92    }
93}
94
95fn ray_axis_has_bounded_floor(center: f64, min: i32, max: i32) -> bool {
96    min > i32::MIN && max < i32::MAX && center >= f64::from(min) && center < f64::from(max) + 1.0
97}
98
99#[derive(Clone, Copy)]
100struct ExplosionWorldBounds {
101    min_y: i32,
102    max_y: i32,
103}
104
105impl ExplosionWorldBounds {
106    const fn from_world(world: &World) -> Self {
107        Self {
108            min_y: world.get_min_y(),
109            max_y: world.get_max_y(),
110        }
111    }
112
113    const fn contains(self, pos: BlockPos) -> bool {
114        pos.y() >= self.min_y && pos.y() <= self.max_y && World::is_in_world_bounds_horizontal(pos)
115    }
116}
117
118impl ServerExplosion<'_> {
119    pub(super) fn calculate_exploded_positions(
120        &self,
121        mut next_float: impl FnMut() -> f32,
122    ) -> Vec<BlockPos> {
123        let Some(calculator) = self.immutable_calculator_for_rays() else {
124            return self.calculate_exploded_positions_sequential(next_float);
125        };
126
127        let powers = self.draw_immutable_ray_powers(&mut next_float);
128        self.calculate_immutable_ray_powers(&powers, calculator)
129    }
130
131    fn immutable_calculator_for_rays(&self) -> Option<&dyn ImmutableExplosionBlockCalculator> {
132        if !self.radius.is_finite() || self.radius < 0.0 {
133            return None;
134        }
135        self.immutable_block_calculator
136    }
137
138    fn calculate_immutable_ray_powers(
139        &self,
140        powers: &[f32; RAY_COUNT],
141        calculator: &dyn ImmutableExplosionBlockCalculator,
142    ) -> Vec<BlockPos> {
143        if let Some(read_radius) = calculator.bounded_block_read_radius()
144            && let Some(bounds) = self.immutable_ray_region_bounds(read_radius)
145            && let Some(Some(affected)) = self.world.try_with_block_region(bounds, |region| {
146                if !region.has_complete_data() {
147                    return None;
148                }
149                let reader = RegionExplosionBlockReader::new(region);
150                self.calculate_immutable_ray_powers_with_reader(powers, calculator, &reader, bounds)
151            })
152        {
153            return affected;
154        }
155
156        self.calculate_immutable_ray_powers_uncached_with_reader(
157            powers,
158            calculator,
159            self.world.as_ref(),
160        )
161    }
162
163    fn calculate_immutable_ray_powers_uncached_with_reader<R: ExplosionBlockReader>(
164        &self,
165        powers: &[f32; RAY_COUNT],
166        calculator: &dyn ImmutableExplosionBlockCalculator,
167        reader: &R,
168    ) -> Vec<BlockPos> {
169        let context = ExplosionRayContext {
170            center: self.center,
171            bounds: ExplosionWorldBounds::from_world(self.world),
172        };
173        let mut affected = JavaBlockPosSet::new(EqualHashOrder::Insertion);
174        for (&step, &initial_power) in RAY_STEPS.iter().zip(powers) {
175            visit_immutable_ray_positions(
176                ExplosionRay {
177                    step,
178                    initial_power,
179                },
180                context,
181                reader,
182                calculator,
183                |pos| {
184                    affected.insert(pos);
185                },
186            );
187        }
188        affected.into_iter().collect()
189    }
190
191    fn immutable_ray_region_bounds(&self, read_radius: u32) -> Option<BlockRegionBounds> {
192        if !self.center.is_finite() {
193            return None;
194        }
195        let maximum_ray_distance = f64::from(self.radius) * f64::from(MAX_INITIAL_RAY_POWER_SCALE)
196            / f64::from(RAY_POWER_DECAY)
197            * RAY_STEP;
198        let extent = maximum_ray_distance + f64::from(read_radius) + RAY_REGION_BLOCK_PADDING;
199        if !extent.is_finite() {
200            return None;
201        }
202        let extent = DVec3::splat(extent);
203        Some(BlockRegionBounds::from_corners(
204            BlockPos::from(self.center - extent),
205            BlockPos::from(self.center + extent),
206        ))
207    }
208
209    fn calculate_exploded_positions_sequential(
210        &self,
211        mut next_float: impl FnMut() -> f32,
212    ) -> Vec<BlockPos> {
213        let mut affected = JavaBlockPosSet::new(EqualHashOrder::Insertion);
214        let bounds = ExplosionWorldBounds::from_world(self.world);
215
216        for &step in RAY_STEPS.iter() {
217            let mut remaining_power = initial_ray_power(self.radius, next_float());
218            let mut ray_pos = self.center;
219            while remaining_power > 0.0 {
220                let pos = BlockPos::from(ray_pos);
221                let state = self.world.get_block_state(pos);
222                let fluid = state.get_fluid_state();
223                if !bounds.contains(pos) {
224                    break;
225                }
226
227                if let Some(resistance) = self
228                    .damage_calculator
229                    .block_explosion_resistance(self, self.world, pos, state, fluid)
230                {
231                    remaining_power -= ray_power_loss_from_resistance(resistance);
232                }
233
234                if remaining_power > 0.0
235                    && self.damage_calculator.should_block_explode(
236                        self,
237                        self.world,
238                        pos,
239                        state,
240                        remaining_power,
241                    )
242                {
243                    affected.insert(pos);
244                }
245
246                ray_pos += step;
247                remaining_power -= RAY_POWER_DECAY;
248            }
249        }
250
251        affected.into_iter().collect()
252    }
253
254    fn draw_immutable_ray_powers(&self, mut next_float: impl FnMut() -> f32) -> [f32; RAY_COUNT] {
255        let mut powers = [0.0; RAY_COUNT];
256        for power in &mut powers {
257            *power = initial_ray_power(self.radius, next_float());
258        }
259        powers
260    }
261}
262
263fn ray_direction(xx: i32, yy: i32, zz: i32) -> DVec3 {
264    let mut xd = ray_direction_component(xx);
265    let mut yd = ray_direction_component(yy);
266    let mut zd = ray_direction_component(zz);
267    let direction_length = (xd * xd + yd * yd + zd * zd).sqrt();
268    xd /= direction_length;
269    yd /= direction_length;
270    zd /= direction_length;
271    DVec3::new(xd, yd, zd)
272}
273
274fn ray_direction_component(index: i32) -> f64 {
275    f64::from(index as f32 / RAY_GRID_LAST_INDEX as f32 * 2.0 - 1.0)
276}
277
278const fn initial_ray_power(radius: f32, random: f32) -> f32 {
279    radius * (INITIAL_RAY_POWER_BASE + random * INITIAL_RAY_POWER_RANDOM_SCALE)
280}
281
282const fn ray_power_loss_from_resistance(resistance: f32) -> f32 {
283    (resistance + RESISTANCE_POWER_OFFSET) * RESISTANCE_POWER_SCALE
284}
285
286const fn is_boundary_ray(xx: i32, yy: i32, zz: i32) -> bool {
287    xx == 0
288        || xx == RAY_GRID_LAST_INDEX
289        || yy == 0
290        || yy == RAY_GRID_LAST_INDEX
291        || zz == 0
292        || zz == RAY_GRID_LAST_INDEX
293}
294
295fn visit_immutable_ray_positions<R: ExplosionBlockReader>(
296    ray: ExplosionRay,
297    context: ExplosionRayContext,
298    reader: &R,
299    calculator: &dyn ImmutableExplosionBlockCalculator,
300    mut visit: impl FnMut(BlockPos),
301) {
302    let mut remaining_power = ray.initial_power;
303    let mut ray_pos = context.center;
304    while remaining_power > 0.0 {
305        let pos = BlockPos::from(ray_pos);
306        let Some(state) = reader.block_state(pos) else {
307            return;
308        };
309        let fluid = state.get_fluid_state();
310        if !context.bounds.contains(pos) {
311            break;
312        }
313
314        if let Some(resistance) = calculator.explosion_resistance(reader, pos, state, fluid) {
315            remaining_power -= ray_power_loss_from_resistance(resistance);
316        }
317
318        if remaining_power > 0.0 && calculator.should_explode(reader, pos, state, remaining_power) {
319            visit(pos);
320        }
321
322        ray_pos += ray.step;
323        remaining_power -= RAY_POWER_DECAY;
324    }
325}
326
327#[cfg(test)]
328mod tests;