Skip to main content

steel_core/world/explosion/
server.rs

1//! Vanilla server explosion orchestration.
2
3use std::sync::Arc;
4
5use glam::DVec3;
6use rustc_hash::FxHashMap;
7use steel_registry::vanilla_game_rules::MOB_GRIEFING;
8use steel_registry::{vanilla_damage_types, vanilla_entities, vanilla_game_events};
9use steel_utils::random::{Random as _, legacy_random::LegacyRandom};
10
11use crate::entity::damage::DamageSource;
12use crate::entity::{Entity, SharedEntity};
13use crate::world::World;
14use crate::world::game_event::GameEventContext;
15
16use super::{
17    BlockInteraction, Explosion, ExplosionDamageCalculator, ImmutableExplosionBlockCalculator,
18    SelectedDamageCalculator,
19};
20
21mod block_effects;
22mod block_rays;
23mod entity_effects;
24mod exposure;
25
26const SMALL_EXPLOSION_RADIUS: f32 = 2.0;
27
28pub(super) struct ServerExplosion<'a> {
29    world: &'a Arc<World>,
30    fire: bool,
31    block_interaction: BlockInteraction,
32    center: DVec3,
33    source: Option<SharedEntity>,
34    indirect_source: Option<SharedEntity>,
35    radius: f32,
36    damage_source: DamageSource,
37    damage_calculator: SelectedDamageCalculator<'a>,
38    immutable_block_calculator: Option<&'a dyn ImmutableExplosionBlockCalculator>,
39    pub(super) hit_players: FxHashMap<i32, DVec3>,
40}
41
42impl<'a> ServerExplosion<'a> {
43    #[expect(
44        clippy::too_many_arguments,
45        reason = "mirrors the Vanilla ServerExplosion construction boundary"
46    )]
47    pub(super) fn new(
48        world: &'a Arc<World>,
49        source: Option<SharedEntity>,
50        damage_source: Option<DamageSource>,
51        damage_calculator: Option<&'a dyn ExplosionDamageCalculator>,
52        immutable_block_calculator: Option<&'a dyn ImmutableExplosionBlockCalculator>,
53        center: DVec3,
54        radius: f32,
55        fire: bool,
56        block_interaction: BlockInteraction,
57    ) -> Self {
58        let indirect_source = source.as_ref().and_then(|source| {
59            if source.as_living_entity().is_some() {
60                Some(Arc::clone(source))
61            } else {
62                source.explosion_indirect_source()
63            }
64        });
65        let damage_source = damage_source
66            .unwrap_or_else(|| default_damage_source(source.clone(), indirect_source.clone()));
67        let damage_calculator = match (damage_calculator, &source) {
68            (Some(calculator), _) => SelectedDamageCalculator::Custom(calculator),
69            (None, Some(source)) => SelectedDamageCalculator::Entity(Arc::clone(source)),
70            (None, None) => SelectedDamageCalculator::Default,
71        };
72        Self {
73            world,
74            fire,
75            block_interaction,
76            center,
77            source,
78            indirect_source,
79            radius,
80            damage_source,
81            damage_calculator,
82            immutable_block_calculator,
83            hit_players: FxHashMap::default(),
84        }
85    }
86
87    pub(super) fn explode(&mut self) -> usize {
88        self.world.game_event_at(
89            &vanilla_game_events::EXPLODE,
90            self.center,
91            &GameEventContext::new(self.source.as_deref(), None),
92        );
93        // Live randomness is scoped to each blast rather than stored on the world.
94        let mut random = LegacyRandom::from_seed(rand::random());
95        let mut affected = self.calculate_exploded_positions(|| random.next_f32());
96        self.hurt_entities();
97        if self.interacts_with_blocks() {
98            self.interact_with_blocks(&mut affected, &mut random);
99        }
100        if self.fire {
101            self.create_fire(&affected, &mut random);
102        }
103        affected.len()
104    }
105
106    fn interacts_with_blocks(&self) -> bool {
107        self.block_interaction != BlockInteraction::Keep
108    }
109
110    pub(super) fn is_small(&self) -> bool {
111        self.radius < SMALL_EXPLOSION_RADIUS || !self.interacts_with_blocks()
112    }
113}
114
115impl Explosion for ServerExplosion<'_> {
116    fn world(&self) -> &Arc<World> {
117        self.world
118    }
119
120    fn damage_source(&self) -> &DamageSource {
121        &self.damage_source
122    }
123
124    fn block_interaction(&self) -> BlockInteraction {
125        self.block_interaction
126    }
127
128    fn indirect_source_entity(&self) -> Option<&dyn Entity> {
129        self.indirect_source.as_deref()
130    }
131
132    fn direct_source_entity(&self) -> Option<&dyn Entity> {
133        self.source.as_deref()
134    }
135
136    fn radius(&self) -> f32 {
137        self.radius
138    }
139
140    fn center(&self) -> DVec3 {
141        self.center
142    }
143
144    fn should_affect_blocklike_entities(&self) -> bool {
145        let is_wind_charge = self.source.as_ref().is_some_and(|source| {
146            source.entity_type() == &vanilla_entities::BREEZE_WIND_CHARGE
147                || source.entity_type() == &vanilla_entities::WIND_CHARGE
148        });
149        !is_wind_charge
150            && (self.world.get_game_rule(&MOB_GRIEFING)
151                || self.block_interaction.should_affect_blocklike_entities())
152    }
153}
154
155/// Vanilla `DamageSources.explosion(direct, indirect)`.
156fn default_damage_source(
157    direct: Option<SharedEntity>,
158    indirect: Option<SharedEntity>,
159) -> DamageSource {
160    let damage_type = if direct.is_some() && indirect.is_some() {
161        &vanilla_damage_types::PLAYER_EXPLOSION
162    } else {
163        &vanilla_damage_types::EXPLOSION
164    };
165    let mut damage_source = DamageSource::environment(damage_type);
166    if let Some(direct) = direct {
167        damage_source = damage_source.with_direct_entity(direct);
168    }
169    if let Some(indirect) = indirect {
170        damage_source = damage_source.with_causing_entity(indirect);
171    }
172    damage_source
173}
174
175#[cfg(test)]
176mod tests;