1use std::sync::Arc;
4
5use glam::DVec3;
6use steel_protocol::packet_traits::EncodedPacket;
7use steel_protocol::packets::game::{CExplode, ExplosionParticleInfo, ExplosionParticlePalette};
8use steel_protocol::utils::ConnectionProtocol;
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::fluid::FluidState;
11use steel_registry::game_rules::GameRule;
12use steel_registry::particle_type::ParticleData;
13use steel_registry::sound_event::SoundEventHolder;
14use steel_registry::vanilla_game_rules::{
15 BLOCK_EXPLOSION_DROP_DECAY, MOB_EXPLOSION_DROP_DECAY, MOB_GRIEFING, TNT_EXPLOSION_DROP_DECAY,
16};
17use steel_registry::{sound_events, vanilla_particle_types};
18use steel_utils::{BlockPos, BlockStateId};
19
20use crate::behavior::FLUID_BEHAVIORS;
21use crate::entity::damage::DamageSource;
22use crate::entity::{Entity, SharedEntity};
23use crate::player::connection::NetworkConnection as _;
24use crate::world::World;
25
26use server::ServerExplosion;
27
28mod server;
29
30const MAX_PACKET_DISTANCE_SQUARED: f64 = 64.0 * 64.0;
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum ExplosionInteraction {
35 None,
37 Block,
39 Mob,
41 Tnt,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum BlockInteraction {
48 Keep,
50 Destroy,
52 DestroyWithDecay,
54}
55
56impl BlockInteraction {
57 #[must_use]
59 pub const fn should_affect_blocklike_entities(self) -> bool {
60 matches!(self, Self::Destroy | Self::DestroyWithDecay)
61 }
62}
63
64pub trait Explosion {
66 fn world(&self) -> &Arc<World>;
68 fn damage_source(&self) -> &DamageSource;
70 fn block_interaction(&self) -> BlockInteraction;
72 fn indirect_source_entity(&self) -> Option<&dyn Entity>;
74 fn direct_source_entity(&self) -> Option<&dyn Entity>;
76 fn radius(&self) -> f32;
78 fn center(&self) -> DVec3;
80 fn should_affect_blocklike_entities(&self) -> bool;
82}
83
84pub trait ExplosionDamageCalculator: Send + Sync {
86 fn block_explosion_resistance(
88 &self,
89 _explosion: &dyn Explosion,
90 _world: &World,
91 _pos: BlockPos,
92 state: BlockStateId,
93 fluid: FluidState,
94 ) -> Option<f32> {
95 default_block_explosion_resistance(state, fluid)
96 }
97
98 fn should_block_explode(
100 &self,
101 _explosion: &dyn Explosion,
102 _world: &World,
103 _pos: BlockPos,
104 _state: BlockStateId,
105 _power: f32,
106 ) -> bool {
107 true
108 }
109
110 fn should_damage_entity(&self, _explosion: &dyn Explosion, _entity: &dyn Entity) -> bool {
112 true
113 }
114
115 fn knockback_multiplier(&self, _entity: &dyn Entity) -> f32 {
117 1.0
118 }
119
120 #[expect(
122 clippy::manual_midpoint,
123 reason = "preserve Vanilla's exact floating-point expression"
124 )]
125 fn entity_damage_amount(
126 &self,
127 explosion: &dyn Explosion,
128 entity: &dyn Entity,
129 exposure: f32,
130 ) -> f32 {
131 let double_radius = explosion.radius() * 2.0;
132 let distance = entity.position().distance(explosion.center()) / f64::from(double_radius);
133 let impact = (1.0 - distance) * f64::from(exposure);
134 (((impact * impact + impact) / 2.0) * 7.0 * f64::from(double_radius) + 1.0) as f32
135 }
136}
137
138pub(crate) trait ExplosionBlockReader: Sync {
143 fn block_state(&self, pos: BlockPos) -> Option<BlockStateId>;
145}
146
147impl ExplosionBlockReader for World {
148 #[inline]
149 fn block_state(&self, pos: BlockPos) -> Option<BlockStateId> {
150 Some(self.get_block_state(pos))
151 }
152}
153
154pub(crate) trait ImmutableExplosionBlockCalculator: Send + Sync {
161 fn bounded_block_read_radius(&self) -> Option<u32> {
169 None
170 }
171
172 fn can_cache_explosion_resistance(&self) -> bool {
178 false
179 }
180
181 fn always_allows_block_explosion(&self) -> bool {
187 false
188 }
189
190 fn explosion_resistance(
192 &self,
193 _reader: &dyn ExplosionBlockReader,
194 _pos: BlockPos,
195 state: BlockStateId,
196 fluid: FluidState,
197 ) -> Option<f32> {
198 default_block_explosion_resistance(state, fluid)
199 }
200
201 fn should_explode(
202 &self,
203 _reader: &dyn ExplosionBlockReader,
204 _pos: BlockPos,
205 _state: BlockStateId,
206 _power: f32,
207 ) -> bool {
208 true
209 }
210}
211
212impl ImmutableExplosionBlockCalculator for DefaultExplosionDamageCalculator {
213 fn bounded_block_read_radius(&self) -> Option<u32> {
214 Some(0)
215 }
216
217 fn can_cache_explosion_resistance(&self) -> bool {
218 true
219 }
220
221 fn always_allows_block_explosion(&self) -> bool {
222 true
223 }
224}
225
226fn default_block_explosion_resistance(state: BlockStateId, fluid: FluidState) -> Option<f32> {
227 if state.is_air() && fluid.is_empty() {
228 return None;
229 }
230
231 let block_resistance = state.get_block().config.explosion_resistance;
232 let fluid_resistance = FLUID_BEHAVIORS
233 .get_behavior(fluid.fluid_id)
234 .explosion_resistance();
235 Some(block_resistance.max(fluid_resistance))
236}
237
238#[derive(Debug, Default)]
240pub struct DefaultExplosionDamageCalculator;
241
242impl ExplosionDamageCalculator for DefaultExplosionDamageCalculator {}
243
244pub struct EntityBasedExplosionDamageCalculator<'a> {
246 source: &'a dyn Entity,
247}
248
249impl<'a> EntityBasedExplosionDamageCalculator<'a> {
250 #[must_use]
252 pub const fn new(source: &'a dyn Entity) -> Self {
253 Self { source }
254 }
255}
256
257impl ExplosionDamageCalculator for EntityBasedExplosionDamageCalculator<'_> {
258 fn block_explosion_resistance(
259 &self,
260 explosion: &dyn Explosion,
261 world: &World,
262 pos: BlockPos,
263 state: BlockStateId,
264 fluid: FluidState,
265 ) -> Option<f32> {
266 DefaultExplosionDamageCalculator
267 .block_explosion_resistance(explosion, world, pos, state, fluid)
268 .map(|resistance| {
269 self.source
270 .block_explosion_resistance(explosion, world, pos, state, fluid, resistance)
271 })
272 }
273
274 fn should_block_explode(
275 &self,
276 explosion: &dyn Explosion,
277 world: &World,
278 pos: BlockPos,
279 state: BlockStateId,
280 power: f32,
281 ) -> bool {
282 self.source
283 .should_block_explode(explosion, world, pos, state, power)
284 }
285}
286
287enum SelectedDamageCalculator<'a> {
288 Default,
289 Entity(SharedEntity),
290 Custom(&'a dyn ExplosionDamageCalculator),
291}
292
293impl SelectedDamageCalculator<'_> {
294 const fn has_builtin_entity_effects(&self) -> bool {
296 matches!(self, Self::Default | Self::Entity(_))
297 }
298}
299
300impl ExplosionDamageCalculator for SelectedDamageCalculator<'_> {
301 fn block_explosion_resistance(
302 &self,
303 explosion: &dyn Explosion,
304 world: &World,
305 pos: BlockPos,
306 state: BlockStateId,
307 fluid: FluidState,
308 ) -> Option<f32> {
309 match self {
310 Self::Default => DefaultExplosionDamageCalculator
311 .block_explosion_resistance(explosion, world, pos, state, fluid),
312 Self::Entity(source) => EntityBasedExplosionDamageCalculator::new(source.as_ref())
313 .block_explosion_resistance(explosion, world, pos, state, fluid),
314 Self::Custom(calculator) => {
315 calculator.block_explosion_resistance(explosion, world, pos, state, fluid)
316 }
317 }
318 }
319
320 fn should_block_explode(
321 &self,
322 explosion: &dyn Explosion,
323 world: &World,
324 pos: BlockPos,
325 state: BlockStateId,
326 power: f32,
327 ) -> bool {
328 match self {
329 Self::Default => DefaultExplosionDamageCalculator
330 .should_block_explode(explosion, world, pos, state, power),
331 Self::Entity(source) => EntityBasedExplosionDamageCalculator::new(source.as_ref())
332 .should_block_explode(explosion, world, pos, state, power),
333 Self::Custom(calculator) => {
334 calculator.should_block_explode(explosion, world, pos, state, power)
335 }
336 }
337 }
338
339 fn should_damage_entity(&self, explosion: &dyn Explosion, entity: &dyn Entity) -> bool {
340 match self {
341 Self::Custom(calculator) => calculator.should_damage_entity(explosion, entity),
342 Self::Default | Self::Entity(_) => {
343 DefaultExplosionDamageCalculator.should_damage_entity(explosion, entity)
344 }
345 }
346 }
347
348 fn knockback_multiplier(&self, entity: &dyn Entity) -> f32 {
349 match self {
350 Self::Custom(calculator) => calculator.knockback_multiplier(entity),
351 Self::Default | Self::Entity(_) => {
352 DefaultExplosionDamageCalculator.knockback_multiplier(entity)
353 }
354 }
355 }
356
357 fn entity_damage_amount(
358 &self,
359 explosion: &dyn Explosion,
360 entity: &dyn Entity,
361 exposure: f32,
362 ) -> f32 {
363 match self {
364 Self::Custom(calculator) => {
365 calculator.entity_damage_amount(explosion, entity, exposure)
366 }
367 Self::Default | Self::Entity(_) => {
368 DefaultExplosionDamageCalculator.entity_damage_amount(explosion, entity, exposure)
369 }
370 }
371 }
372}
373
374pub struct ExplosionOptions<'a> {
376 pub source: Option<SharedEntity>,
378 pub damage_source: Option<DamageSource>,
380 pub damage_calculator: Option<&'a dyn ExplosionDamageCalculator>,
382 pub(crate) immutable_block_calculator: Option<&'a dyn ImmutableExplosionBlockCalculator>,
384 pub center: DVec3,
386 pub radius: f32,
388 pub fire: bool,
390 pub interaction: ExplosionInteraction,
392 pub small_explosion_particle: ParticleData,
394 pub large_explosion_particle: ParticleData,
396 pub block_particles: ExplosionParticlePalette,
398 pub explosion_sound: SoundEventHolder,
400}
401
402impl ExplosionOptions<'_> {
403 #[must_use]
405 pub fn new(center: DVec3, radius: f32, interaction: ExplosionInteraction) -> Self {
406 Self {
407 source: None,
408 damage_source: None,
409 damage_calculator: None,
410 immutable_block_calculator: None,
411 center,
412 radius,
413 fire: false,
414 interaction,
415 small_explosion_particle: ParticleData::simple(&vanilla_particle_types::EXPLOSION),
416 large_explosion_particle: ParticleData::simple(
417 &vanilla_particle_types::EXPLOSION_EMITTER,
418 ),
419 block_particles: default_block_particles(),
420 explosion_sound: SoundEventHolder::registry(&sound_events::ENTITY_GENERIC_EXPLODE),
421 }
422 }
423}
424
425#[derive(Clone, Copy, Debug, Eq, PartialEq)]
427pub struct ExplosionOutcome {
428 pub affected_block_count: usize,
430}
431
432impl World {
433 pub fn explode(self: &Arc<Self>, options: ExplosionOptions<'_>) -> ExplosionOutcome {
435 let block_interaction = self.resolve_block_interaction(options.interaction);
436 let mut explosion = ServerExplosion::new(
437 self,
438 options.source,
439 options.damage_source,
440 options.damage_calculator,
441 options.immutable_block_calculator,
442 options.center,
443 options.radius,
444 options.fire,
445 block_interaction,
446 );
447 let affected_block_count = explosion.explode();
448 let explosion_particle = if explosion.is_small() {
449 options.small_explosion_particle
450 } else {
451 options.large_explosion_particle
452 };
453
454 let play_sound = explosion
455 .direct_source_entity()
456 .is_none_or(|source| !source.is_silent());
457 let packet_block_count = i32::try_from(affected_block_count).unwrap_or(i32::MAX);
458
459 let packet = |player_knockback| CExplode {
460 center: options.center,
461 radius: options.radius,
462 block_count: packet_block_count,
463 player_knockback,
464 explosion_particle: explosion_particle.clone(),
465 explosion_sound: options.explosion_sound.clone(),
466 block_particles: options.block_particles.clone(),
467 play_sound,
468 };
469 let mut shared = None;
471 self.players.iter_players(|_, player| {
472 if player.position().distance_squared(options.center) >= MAX_PACKET_DISTANCE_SQUARED {
473 return true;
474 }
475 if let Some(&knockback) = explosion.hit_players.get(&player.id()) {
476 player.send_packet(packet(Some(knockback)));
477 return true;
478 }
479 let encoded = shared.get_or_insert_with(|| {
480 EncodedPacket::from_bare(packet(None), self.compression, ConnectionProtocol::Play)
481 .inspect_err(|error| {
482 tracing::warn!(?error, "Failed to encode explosion packet");
483 })
484 .ok()
485 });
486 if let Some(encoded) = encoded {
487 player.connection.send_encoded(encoded.clone());
488 }
489 true
490 });
491
492 ExplosionOutcome {
493 affected_block_count,
494 }
495 }
496
497 fn resolve_block_interaction(&self, interaction: ExplosionInteraction) -> BlockInteraction {
499 match interaction {
500 ExplosionInteraction::None => BlockInteraction::Keep,
501 ExplosionInteraction::Block => self.destroy_interaction(&BLOCK_EXPLOSION_DROP_DECAY),
502 ExplosionInteraction::Mob => {
503 if self.get_game_rule(&MOB_GRIEFING) {
504 self.destroy_interaction(&MOB_EXPLOSION_DROP_DECAY)
505 } else {
506 BlockInteraction::Keep
507 }
508 }
509 ExplosionInteraction::Tnt => self.destroy_interaction(&TNT_EXPLOSION_DROP_DECAY),
510 }
511 }
512
513 fn destroy_interaction(&self, decay_rule: &GameRule<bool>) -> BlockInteraction {
514 if self.get_game_rule(decay_rule) {
515 BlockInteraction::DestroyWithDecay
516 } else {
517 BlockInteraction::Destroy
518 }
519 }
520}
521
522fn default_block_particles() -> ExplosionParticlePalette {
523 let entries = [
524 ExplosionParticleInfo {
525 particle: ParticleData::simple(&vanilla_particle_types::POOF),
526 scaling: 0.5,
527 speed: 1.0,
528 },
529 ExplosionParticleInfo {
530 particle: ParticleData::simple(&vanilla_particle_types::SMOKE),
531 scaling: 1.0,
532 speed: 1.0,
533 },
534 ]
535 .into_iter()
536 .map(|particle| (particle, 1))
537 .collect();
538 let Ok(palette) = ExplosionParticlePalette::try_new(entries) else {
539 panic!("Vanilla explosion particle palette must be valid");
540 };
541 palette
542}