steel_core/world/explosion/server/
block_rays.rs1use 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 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 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;