Skip to main content

steel_core/behavior/blocks/redstone/
tnt_block.rs

1//! TNT block behavior and priming entry points.
2
3use std::sync::Arc;
4
5use glam::DVec3;
6use steel_macros::block_behavior;
7use steel_protocol::packets::game::SoundSource;
8use steel_registry::blocks::BlockRef;
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::blocks::properties::BlockStateProperties;
11use steel_registry::stat::vanilla_stat_types;
12use steel_registry::vanilla_game_rules::TNT_EXPLODES;
13use steel_registry::{
14    sound_events, vanilla_blocks, vanilla_entities, vanilla_game_events, vanilla_items,
15};
16use steel_utils::types::{GameType, InteractionHand, UpdateFlags};
17use steel_utils::{BlockPos, BlockStateId, translations};
18use text_components::TextComponent;
19
20use crate::behavior::{
21    BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
22};
23use crate::entity::projectile::Projectile;
24use crate::entity::{Entity, entities::PrimedTntEntity, next_entity_id};
25use crate::player::Player;
26use crate::player::game_mode::can_break;
27use crate::world::game_event::GameEventContext;
28use crate::world::{ClipHitResult, Explosion, SignalGetter as _, World};
29
30/// Vanilla TNT block behavior.
31#[block_behavior]
32pub struct TntBlock {
33    block: BlockRef,
34}
35
36impl TntBlock {
37    /// Creates TNT block behavior.
38    #[must_use]
39    pub const fn new(block: BlockRef) -> Self {
40        Self { block }
41    }
42
43    fn primed_entity(
44        world: &Arc<World>,
45        pos: BlockPos,
46        owner: Option<&dyn Entity>,
47    ) -> Arc<PrimedTntEntity> {
48        let (x, y, z) = pos.get_bottom_center();
49        Arc::new(PrimedTntEntity::primed(
50            &vanilla_entities::TNT,
51            next_entity_id(),
52            DVec3::new(x, y, z),
53            world,
54            owner,
55        ))
56    }
57
58    /// Primes TNT at `pos` without a source, returning whether a primed entity was added.
59    ///
60    /// This is crate-visible so shared fire burn ticking can call the same entry point when that
61    /// foundation is implemented.
62    pub(crate) fn prime(world: &Arc<World>, pos: BlockPos) -> bool {
63        Self::prime_with_source(world, pos, None, || false)
64    }
65
66    /// Primes TNT for `owner`. An Adventure-mode player also needs `can_break_in_adventure`,
67    /// which checks the igniting stack's `can_break` predicate.
68    fn prime_with_source(
69        world: &Arc<World>,
70        pos: BlockPos,
71        owner: Option<&dyn Entity>,
72        can_break_in_adventure: impl FnOnce() -> bool,
73    ) -> bool {
74        if !world.get_game_rule(&TNT_EXPLODES) {
75            return false;
76        }
77        if owner
78            .and_then(|owner| owner.as_player())
79            .is_some_and(|player| player.game_mode() == GameType::Adventure)
80            && !can_break_in_adventure()
81        {
82            return false;
83        }
84
85        let entity = Self::primed_entity(world, pos, owner);
86        let position = entity.position();
87        if let Err(error) = world.try_add_entity(entity) {
88            log::debug!("failed to add primed TNT: {error}");
89            return false;
90        }
91        world.play_sound_at(
92            &sound_events::ENTITY_TNT_PRIMED,
93            SoundSource::Blocks,
94            position,
95            1.0,
96            1.0,
97            None,
98        );
99        world.game_event(
100            &vanilla_game_events::PRIME_FUSE,
101            pos,
102            &GameEventContext::new(owner, None),
103        );
104        true
105    }
106
107    fn send_disabled_message(player: &Player) {
108        player.send_overlay_message(&TextComponent::translated(
109            translations::BLOCK_MINECRAFT_TNT_DISABLED.msg(),
110        ));
111    }
112}
113
114impl BlockBehavior for TntBlock {
115    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
116        Some(self.block.default_state())
117    }
118
119    fn on_place(
120        &self,
121        state: BlockStateId,
122        world: &Arc<World>,
123        pos: BlockPos,
124        old_state: BlockStateId,
125        _moved_by_piston: bool,
126    ) {
127        if old_state.get_block() != state.get_block()
128            && world.has_neighbor_signal(pos)
129            && Self::prime(world, pos)
130        {
131            world.remove_block(pos, false);
132        }
133    }
134
135    fn handle_neighbor_changed(
136        &self,
137        _state: BlockStateId,
138        world: &Arc<World>,
139        pos: BlockPos,
140        _source_block: BlockRef,
141        _moved_by_piston: bool,
142    ) {
143        if world.has_neighbor_signal(pos) && Self::prime(world, pos) {
144            world.remove_block(pos, false);
145        }
146    }
147
148    fn player_will_destroy(
149        &self,
150        state: BlockStateId,
151        world: &Arc<World>,
152        pos: BlockPos,
153        player: &Player,
154    ) -> BlockStateId {
155        if !player.has_infinite_materials() && state.get_value(&BlockStateProperties::UNSTABLE) {
156            Self::prime(world, pos);
157        }
158        state
159    }
160
161    fn was_exploded(
162        &self,
163        _state: BlockStateId,
164        world: &Arc<World>,
165        pos: BlockPos,
166        explosion: &dyn Explosion,
167    ) {
168        if !world.get_game_rule(&TNT_EXPLODES) {
169            return;
170        }
171
172        let entity = Self::primed_entity(world, pos, explosion.indirect_source_entity());
173        entity.set_fuse(PrimedTntEntity::get_random_short_fuse(entity.fuse()));
174        if let Err(error) = world.try_add_entity(entity) {
175            log::debug!("failed to add primed TNT: {error}");
176        }
177    }
178
179    fn use_item_on(
180        &self,
181        _state: BlockStateId,
182        world: &Arc<World>,
183        pos: BlockPos,
184        player: &Player,
185        _hand: InteractionHand,
186        _hit_result: &BlockHitResult,
187        inv: &mut InventoryAccess,
188    ) -> InteractionResult {
189        let (is_flint_and_steel, is_fire_charge) = inv.with_item(|item| {
190            (
191                item.is(&vanilla_items::FLINT_AND_STEEL),
192                item.is(&vanilla_items::FIRE_CHARGE),
193            )
194        });
195        if !is_flint_and_steel && !is_fire_charge {
196            return InteractionResult::TryEmptyHandInteraction;
197        }
198
199        if !Self::prime_with_source(world, pos, Some(player), || {
200            inv.with_item(|item| can_break(item, world, pos))
201        }) {
202            if !world.get_game_rule(&TNT_EXPLODES) {
203                Self::send_disabled_message(player);
204                return InteractionResult::Pass;
205            }
206            return InteractionResult::Success;
207        }
208
209        world.set_block(
210            pos,
211            vanilla_blocks::AIR.default_state(),
212            UpdateFlags::UPDATE_ALL_IMMEDIATE,
213        );
214        let has_infinite_materials = player.has_infinite_materials();
215        let used_item = inv.with_item(|item| {
216            let used_item = item.item();
217            if is_flint_and_steel {
218                item.hurt_and_break(1, has_infinite_materials);
219            } else {
220                item.consume_one(has_infinite_materials);
221            }
222            used_item
223        });
224        player.award_stat(&vanilla_stat_types::ITEM_USED, used_item);
225        InteractionResult::Success
226    }
227
228    fn on_projectile_hit(
229        &self,
230        _state: BlockStateId,
231        world: &Arc<World>,
232        hit: &ClipHitResult,
233        projectile: &dyn Projectile,
234    ) {
235        if !projectile.is_on_fire() || !projectile.projectile_may_interact(world, hit.block_pos) {
236            return;
237        }
238
239        let owner = projectile.get_owner();
240        let living_owner = owner.as_deref().filter(|owner| owner.is_living_entity());
241        // TODO: Check an arrow's pickup stack once arrows exist. Other projectiles use an empty
242        // stack, which never permits Adventure-mode breaking.
243        if Self::prime_with_source(world, hit.block_pos, living_owner, || false) {
244            world.remove_block(hit.block_pos, false);
245        }
246    }
247
248    fn drop_from_explosion(&self, _explosion: &dyn Explosion) -> bool {
249        false
250    }
251}
252
253#[cfg(test)]
254#[path = "tnt_block/tests.rs"]
255mod tests;