1use steel_registry::blocks::block_state_ext::BlockStateExt as _;
4use steel_utils::{BlockPos, BlockStateId, PackedBlockPos};
5
6use crate::world::BlockRegionBounds;
7use crate::world::explosion::{ExplosionBlockReader, ImmutableExplosionBlockCalculator};
8
9use super::super::ServerExplosion;
10use super::{
11 EqualHashOrder, ExplosionRay, ExplosionRayContext, ExplosionWorldBounds, JavaBlockPosSet,
12 RAY_POWER_DECAY, RAY_STEPS, ray_power_loss_from_resistance,
13};
14
15const BLOCK_CACHE_BITS: u32 = 9;
16const BLOCK_CACHE_SIZE: usize = 1 << BLOCK_CACHE_BITS;
17const BLOCK_CACHE_MASK: usize = BLOCK_CACHE_SIZE - 1;
18const MAX_DENSE_BLOCK_CACHE_CELLS: usize = 8_192;
20const EMPTY_DENSE_BLOCK_CACHE_SLOT: u16 = u16::MAX;
21const DENSE_BLOCK_CACHE_HAS_RESISTANCE: u8 = 1;
22const DENSE_BLOCK_CACHE_AFFECTED: u8 = 1 << 1;
23const F64_INTEGER_MANTISSA_BIAS: f64 = 6_755_399_441_055_744.0;
24const LONG_HASH_PHI: u64 = 0x9e37_79b9_7f4a_7c15;
25
26#[derive(Clone, Copy)]
27struct ExplosionBlockCacheEntry {
28 tag: i64,
29 state: BlockStateId,
30 resistance: Option<f32>,
31 occupied: bool,
32 affected: bool,
33}
34
35impl ExplosionBlockCacheEntry {
36 const EMPTY: Self = Self {
37 tag: 0,
38 state: BlockStateId(0),
39 resistance: None,
40 occupied: false,
41 affected: false,
42 };
43}
44
45pub(super) struct ExplosionBlockCache {
46 entries: [ExplosionBlockCacheEntry; BLOCK_CACHE_SIZE],
47}
48
49#[derive(Clone, Copy)]
50struct DenseExplosionBlockCacheEntry {
51 resistance: f32,
52 state: BlockStateId,
53 flags: u8,
54}
55
56pub(super) struct DenseExplosionBlockCache {
57 min: BlockPos,
58 size_x: usize,
59 size_y: usize,
60 size_z: usize,
61 slots: Vec<u16>,
62 entries: Vec<DenseExplosionBlockCacheEntry>,
63}
64
65#[derive(Clone, Copy)]
66pub(super) enum ExplosionBlockCacheLookup<Miss> {
67 Hit(usize),
68 Miss(Miss),
69}
70
71pub(super) trait ExplosionRayBlockCache {
72 type Miss: Copy;
73
74 fn lookup(&self, pos: BlockPos) -> Option<ExplosionBlockCacheLookup<Self::Miss>>;
75 fn state(&self, entry_index: usize) -> BlockStateId;
76 fn resistance(&self, entry_index: usize) -> Option<f32>;
77 fn affected(&self, entry_index: usize) -> bool;
78 fn insert(&mut self, miss: Self::Miss, state: BlockStateId, resistance: Option<f32>) -> usize;
79 fn mark_affected(&mut self, entry_index: usize);
80}
81
82#[derive(Clone, Copy)]
83pub(super) struct ImmutableRayCachePolicy {
84 pub(super) resistance: bool,
85 pub(super) always_allows_block_explosion: bool,
86}
87
88impl Default for ExplosionBlockCache {
89 fn default() -> Self {
90 Self {
91 entries: [ExplosionBlockCacheEntry::EMPTY; BLOCK_CACHE_SIZE],
92 }
93 }
94}
95
96impl ExplosionRayBlockCache for ExplosionBlockCache {
97 type Miss = (usize, i64);
98
99 #[inline]
100 fn lookup(&self, pos: BlockPos) -> Option<ExplosionBlockCacheLookup<Self::Miss>> {
101 let tag = PackedBlockPos::from(pos).as_raw();
102 let cache_index = explosion_block_cache_index(tag);
103 let entry = self.entries[cache_index];
104 Some(if entry.occupied && entry.tag == tag {
105 ExplosionBlockCacheLookup::Hit(cache_index)
106 } else {
107 ExplosionBlockCacheLookup::Miss((cache_index, tag))
108 })
109 }
110
111 #[inline]
112 fn state(&self, entry_index: usize) -> BlockStateId {
113 self.entries[entry_index].state
114 }
115
116 #[inline]
117 fn resistance(&self, entry_index: usize) -> Option<f32> {
118 self.entries[entry_index].resistance
119 }
120
121 #[inline]
122 fn affected(&self, entry_index: usize) -> bool {
123 self.entries[entry_index].affected
124 }
125
126 #[inline]
127 fn insert(
128 &mut self,
129 (cache_index, tag): Self::Miss,
130 state: BlockStateId,
131 resistance: Option<f32>,
132 ) -> usize {
133 self.entries[cache_index] = ExplosionBlockCacheEntry {
134 tag,
135 state,
136 resistance,
137 occupied: true,
138 affected: false,
139 };
140 cache_index
141 }
142
143 #[inline]
144 fn mark_affected(&mut self, entry_index: usize) {
145 self.entries[entry_index].affected = true;
146 }
147}
148
149impl DenseExplosionBlockCache {
150 pub(super) fn try_new(bounds: BlockRegionBounds) -> Option<Self> {
151 let (min, max) = bounds.corners();
152 let size_x = inclusive_block_count(min.x(), max.x())?;
153 let size_y = inclusive_block_count(min.y(), max.y())?;
154 let size_z = inclusive_block_count(min.z(), max.z())?;
155 let volume = size_x.checked_mul(size_y)?.checked_mul(size_z)?;
156 if volume > MAX_DENSE_BLOCK_CACHE_CELLS
157 || volume >= usize::from(EMPTY_DENSE_BLOCK_CACHE_SLOT)
158 {
159 return None;
160 }
161
162 Some(Self {
163 min,
164 size_x,
165 size_y,
166 size_z,
167 slots: vec![EMPTY_DENSE_BLOCK_CACHE_SLOT; volume],
168 entries: Vec::with_capacity(volume),
169 })
170 }
171
172 #[inline]
173 pub(super) fn cell_index(&self, pos: BlockPos) -> Option<usize> {
174 let x = usize::try_from(i64::from(pos.x()) - i64::from(self.min.x())).ok()?;
175 let y = usize::try_from(i64::from(pos.y()) - i64::from(self.min.y())).ok()?;
176 let z = usize::try_from(i64::from(pos.z()) - i64::from(self.min.z())).ok()?;
177 if x >= self.size_x || y >= self.size_y || z >= self.size_z {
178 return None;
179 }
180 Some((y * self.size_z + z) * self.size_x + x)
181 }
182}
183
184impl ExplosionRayBlockCache for DenseExplosionBlockCache {
185 type Miss = usize;
186
187 #[inline]
188 fn lookup(&self, pos: BlockPos) -> Option<ExplosionBlockCacheLookup<Self::Miss>> {
189 let slot_index = self.cell_index(pos)?;
190 let entry_index = self.slots[slot_index];
191 Some(if entry_index == EMPTY_DENSE_BLOCK_CACHE_SLOT {
192 ExplosionBlockCacheLookup::Miss(slot_index)
193 } else {
194 ExplosionBlockCacheLookup::Hit(usize::from(entry_index))
195 })
196 }
197
198 #[inline]
199 fn state(&self, entry_index: usize) -> BlockStateId {
200 self.entries[entry_index].state
201 }
202
203 #[inline]
204 fn resistance(&self, entry_index: usize) -> Option<f32> {
205 let entry = self.entries[entry_index];
206 (entry.flags & DENSE_BLOCK_CACHE_HAS_RESISTANCE != 0).then_some(entry.resistance)
207 }
208
209 #[inline]
210 fn affected(&self, entry_index: usize) -> bool {
211 self.entries[entry_index].flags & DENSE_BLOCK_CACHE_AFFECTED != 0
212 }
213
214 #[inline]
215 fn insert(
216 &mut self,
217 slot_index: Self::Miss,
218 state: BlockStateId,
219 resistance: Option<f32>,
220 ) -> usize {
221 let entry_index = self.entries.len();
222 debug_assert!(entry_index < usize::from(EMPTY_DENSE_BLOCK_CACHE_SLOT));
223 let (resistance, flags) = resistance.map_or((0.0, 0), |resistance| {
224 (resistance, DENSE_BLOCK_CACHE_HAS_RESISTANCE)
225 });
226 self.entries.push(DenseExplosionBlockCacheEntry {
227 resistance,
228 state,
229 flags,
230 });
231 self.slots[slot_index] = entry_index as u16;
232 entry_index
233 }
234
235 #[inline]
236 fn mark_affected(&mut self, entry_index: usize) {
237 self.entries[entry_index].flags |= DENSE_BLOCK_CACHE_AFFECTED;
238 }
239}
240
241fn inclusive_block_count(min: i32, max: i32) -> Option<usize> {
242 usize::try_from(i64::from(max) - i64::from(min) + 1).ok()
243}
244
245impl ServerExplosion<'_> {
246 pub(super) fn calculate_immutable_ray_powers_with_reader<R: ExplosionBlockReader>(
247 &self,
248 powers: &[f32; super::RAY_COUNT],
249 calculator: &dyn ImmutableExplosionBlockCalculator,
250 reader: &R,
251 bounds: BlockRegionBounds,
252 ) -> Option<Vec<BlockPos>> {
253 let context = ExplosionRayContext {
254 center: self.center,
255 bounds: ExplosionWorldBounds::from_world(self.world),
256 };
257 let cache_policy = ImmutableRayCachePolicy {
258 resistance: calculator.can_cache_explosion_resistance(),
259 always_allows_block_explosion: calculator.always_allows_block_explosion(),
260 };
261 let use_bounded_floor = context.can_use_bounded_floor(bounds);
262
263 if cache_policy.resistance
264 && cache_policy.always_allows_block_explosion
265 && let Some(cache) = DenseExplosionBlockCache::try_new(bounds)
266 {
267 return Self::calculate_immutable_ray_powers_with_cache(
268 powers,
269 calculator,
270 reader,
271 context,
272 cache_policy,
273 cache,
274 use_bounded_floor,
275 );
276 }
277
278 Self::calculate_immutable_ray_powers_with_cache(
279 powers,
280 calculator,
281 reader,
282 context,
283 cache_policy,
284 ExplosionBlockCache::default(),
285 use_bounded_floor,
286 )
287 }
288
289 fn calculate_immutable_ray_powers_with_cache<
290 R: ExplosionBlockReader,
291 C: ExplosionRayBlockCache,
292 >(
293 powers: &[f32; super::RAY_COUNT],
294 calculator: &dyn ImmutableExplosionBlockCalculator,
295 reader: &R,
296 context: ExplosionRayContext,
297 cache_policy: ImmutableRayCachePolicy,
298 cache: C,
299 use_bounded_floor: bool,
300 ) -> Option<Vec<BlockPos>> {
301 if use_bounded_floor {
302 return Self::calculate_immutable_ray_powers_with_cache_mode::<R, C, true>(
303 powers,
304 calculator,
305 reader,
306 context,
307 cache_policy,
308 cache,
309 );
310 }
311 Self::calculate_immutable_ray_powers_with_cache_mode::<R, C, false>(
312 powers,
313 calculator,
314 reader,
315 context,
316 cache_policy,
317 cache,
318 )
319 }
320
321 fn calculate_immutable_ray_powers_with_cache_mode<
322 R: ExplosionBlockReader,
323 C: ExplosionRayBlockCache,
324 const USE_BOUNDED_FLOOR: bool,
325 >(
326 powers: &[f32; super::RAY_COUNT],
327 calculator: &dyn ImmutableExplosionBlockCalculator,
328 reader: &R,
329 context: ExplosionRayContext,
330 cache_policy: ImmutableRayCachePolicy,
331 mut cache: C,
332 ) -> Option<Vec<BlockPos>> {
333 let mut affected = JavaBlockPosSet::new(EqualHashOrder::Insertion);
334 for (&step, &initial_power) in RAY_STEPS.iter().zip(powers) {
335 if !visit_immutable_ray_positions_cached::<R, C, USE_BOUNDED_FLOOR>(
336 ExplosionRay {
337 step,
338 initial_power,
339 },
340 context,
341 reader,
342 calculator,
343 cache_policy,
344 &mut cache,
345 &mut affected,
346 ) {
347 return None;
348 }
349 }
350 Some(affected.into_iter().collect())
351 }
352}
353
354pub(super) fn visit_immutable_ray_positions_cached<
355 R: ExplosionBlockReader,
356 C: ExplosionRayBlockCache,
357 const USE_BOUNDED_FLOOR: bool,
358>(
359 ray: ExplosionRay,
360 context: ExplosionRayContext,
361 reader: &R,
362 calculator: &dyn ImmutableExplosionBlockCalculator,
363 cache_policy: ImmutableRayCachePolicy,
364 cache: &mut C,
365 affected: &mut JavaBlockPosSet,
366) -> bool {
367 let mut remaining_power = ray.initial_power;
368 let mut ray_pos = context.center;
369 let mut previous_cell: Option<(BlockPos, usize)> = None;
370 while remaining_power > 0.0 {
371 let pos = ray_block_pos::<USE_BOUNDED_FLOOR>(ray_pos);
372 if let Some((previous, cache_index)) = previous_cell
373 && previous == pos
374 && cache_policy.resistance
375 && cache_policy.always_allows_block_explosion
376 && cache.affected(cache_index)
377 {
378 if let Some(resistance) = cache.resistance(cache_index) {
379 remaining_power -= ray_power_loss_from_resistance(resistance);
380 }
381 ray_pos += ray.step;
382 remaining_power -= RAY_POWER_DECAY;
383 continue;
384 }
385
386 let lookup = match previous_cell {
387 Some((previous, cache_index)) if previous == pos => {
388 ExplosionBlockCacheLookup::Hit(cache_index)
389 }
390 _ => {
391 let Some(lookup) = cache.lookup(pos) else {
392 return false;
393 };
394 lookup
395 }
396 };
397 let state = match lookup {
398 ExplosionBlockCacheLookup::Hit(cache_index) => cache.state(cache_index),
399 ExplosionBlockCacheLookup::Miss(_) => {
400 let Some(state) = reader.block_state(pos) else {
401 return false;
402 };
403 state
404 }
405 };
406 if !context.bounds.contains(pos) {
407 break;
408 }
409
410 let resistance = match lookup {
411 ExplosionBlockCacheLookup::Hit(cache_index) if cache_policy.resistance => {
412 cache.resistance(cache_index)
413 }
414 _ => {
415 let fluid = state.get_fluid_state();
416 calculator.explosion_resistance(reader, pos, state, fluid)
417 }
418 };
419 let cache_index = match lookup {
420 ExplosionBlockCacheLookup::Hit(cache_index) => cache_index,
421 ExplosionBlockCacheLookup::Miss(miss) => cache.insert(
422 miss,
423 state,
424 if cache_policy.resistance {
425 resistance
426 } else {
427 None
428 },
429 ),
430 };
431 previous_cell = Some((pos, cache_index));
432
433 if let Some(resistance) = resistance {
434 remaining_power -= ray_power_loss_from_resistance(resistance);
435 }
436
437 if remaining_power > 0.0 {
438 let already_affected = cache.affected(cache_index);
439 let should_explode = cache_policy.always_allows_block_explosion
440 || calculator.should_explode(reader, pos, state, remaining_power);
441 if should_explode && !already_affected {
442 affected.insert(pos);
443 cache.mark_affected(cache_index);
444 }
445 }
446
447 ray_pos += ray.step;
448 remaining_power -= RAY_POWER_DECAY;
449 }
450 true
451}
452
453#[inline]
454fn ray_block_pos<const USE_BOUNDED_FLOOR: bool>(position: glam::DVec3) -> BlockPos {
455 if USE_BOUNDED_FLOOR {
456 BlockPos::new(
457 bounded_floor_to_i32(position.x),
458 bounded_floor_to_i32(position.y),
459 bounded_floor_to_i32(position.z),
460 )
461 } else {
462 BlockPos::from(position)
463 }
464}
465
466#[inline]
469pub(super) fn bounded_floor_to_i32(value: f64) -> i32 {
470 debug_assert!(
471 value.is_finite() && value >= f64::from(i32::MIN) && value < f64::from(i32::MAX) + 1.0
472 );
473 ((value.floor() + F64_INTEGER_MANTISSA_BIAS).to_bits() as u32) as i32
474}
475
476#[inline]
477const fn explosion_block_cache_index(tag: i64) -> usize {
478 let mut mixed = (tag as u64).wrapping_mul(LONG_HASH_PHI);
479 mixed ^= mixed >> 32;
480 mixed ^= mixed >> 16;
481 (mixed as usize) & BLOCK_CACHE_MASK
482}