steel_core/behavior/blocks/redstone/
tnt_block.rs1use 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#[block_behavior]
32pub struct TntBlock {
33 block: BlockRef,
34}
35
36impl TntBlock {
37 #[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 pub(crate) fn prime(world: &Arc<World>, pos: BlockPos) -> bool {
63 Self::prime_with_source(world, pos, None, || false)
64 }
65
66 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 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;