Skip to main content

steel_core/behavior/blocks/redstone/
redstone_torch_block.rs

1//! Standing and wall redstone torches, including vanilla burnout behavior.
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{BlockStateProperties, Direction};
9use steel_registry::blocks::shapes::SupportType;
10use steel_registry::{REGISTRY, level_events, vanilla_blocks};
11use steel_utils::types::UpdateFlags;
12use steel_utils::{BlockPos, BlockStateId};
13
14use crate::behavior::{BlockBehavior, BlockPlaceContext};
15use crate::world::{
16    LevelReader, ScheduledTickAccess, SignalQueryContext, World, get_signal as get_redstone_signal,
17};
18
19const TOGGLE_DELAY: i32 = 2;
20const RESTART_DELAY: i32 = 160;
21
22fn notify_neighbors(block: BlockRef, world: &Arc<World>, pos: BlockPos) {
23    for direction in Direction::ALL {
24        world.update_neighbors_at(pos.relative(direction), block);
25    }
26}
27
28fn on_place(block: BlockRef, world: &Arc<World>, pos: BlockPos) {
29    notify_neighbors(block, world, pos);
30}
31
32fn affect_neighbors_after_removal(
33    block: BlockRef,
34    world: &Arc<World>,
35    pos: BlockPos,
36    moved_by_piston: bool,
37) {
38    if !moved_by_piston {
39        notify_neighbors(block, world, pos);
40    }
41}
42
43fn handle_neighbor_changed(
44    block: BlockRef,
45    state: BlockStateId,
46    world: &Arc<World>,
47    pos: BlockPos,
48    has_neighbor_signal: bool,
49) {
50    if state.get_value(&BlockStateProperties::LIT) == has_neighbor_signal
51        && !world.will_tick_block_this_tick(pos, block)
52    {
53        world.schedule_block_tick_default(pos, block, TOGGLE_DELAY);
54    }
55}
56
57fn tick_torch(state: BlockStateId, world: &Arc<World>, pos: BlockPos, has_neighbor_signal: bool) {
58    world.prune_recent_redstone_torch_toggles();
59
60    if state.get_value(&BlockStateProperties::LIT) {
61        if !has_neighbor_signal {
62            return;
63        }
64
65        world.set_block(
66            pos,
67            state.set_value(&BlockStateProperties::LIT, false),
68            UpdateFlags::UPDATE_ALL,
69        );
70        if world.redstone_torch_toggled_too_frequently(pos, true) {
71            world.level_event(level_events::REDSTONE_TORCH_BURNOUT, pos, 0, None);
72            let current_block = world.get_block_state(pos).get_block();
73            world.schedule_block_tick_default(pos, current_block, RESTART_DELAY);
74        }
75        return;
76    }
77
78    if !has_neighbor_signal && !world.redstone_torch_toggled_too_frequently(pos, false) {
79        world.set_block(
80            pos,
81            state.set_value(&BlockStateProperties::LIT, true),
82            UpdateFlags::UPDATE_ALL,
83        );
84    }
85}
86
87fn own_signal(state: BlockStateId) -> i32 {
88    if state.get_value(&BlockStateProperties::LIT) {
89        15
90    } else {
91        0
92    }
93}
94
95/// Standing redstone torch (`redstone_torch`).
96#[block_behavior]
97pub struct RedstoneTorchBlock {
98    block: BlockRef,
99}
100
101impl RedstoneTorchBlock {
102    /// Creates a standing redstone-torch behavior.
103    #[must_use]
104    pub const fn new(block: BlockRef) -> Self {
105        Self { block }
106    }
107
108    fn has_neighbor_signal(world: &dyn LevelReader, pos: BlockPos) -> bool {
109        get_redstone_signal(
110            world,
111            pos.below(),
112            Direction::Down,
113            SignalQueryContext::DEFAULT,
114        ) > 0
115    }
116}
117
118impl BlockBehavior for RedstoneTorchBlock {
119    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
120        let below_pos = pos.below();
121        world.is_face_sturdy_for(
122            world.get_block_state(below_pos),
123            below_pos,
124            Direction::Up,
125            SupportType::Center,
126        )
127    }
128
129    fn update_shape(
130        &self,
131        state: BlockStateId,
132        world: &dyn ScheduledTickAccess,
133        pos: BlockPos,
134        direction: Direction,
135        _neighbor_pos: BlockPos,
136        _neighbor_state: BlockStateId,
137    ) -> BlockStateId {
138        if direction == Direction::Down && !self.can_survive(state, world, pos) {
139            REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
140        } else {
141            state
142        }
143    }
144
145    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
146        let state = self.block.default_state();
147        self.can_survive(state, context.world.as_ref(), context.place_pos())
148            .then_some(state)
149    }
150
151    fn on_place(
152        &self,
153        _state: BlockStateId,
154        world: &Arc<World>,
155        pos: BlockPos,
156        _old_state: BlockStateId,
157        _moved_by_piston: bool,
158    ) {
159        on_place(self.block, world, pos);
160    }
161
162    fn affect_neighbors_after_removal(
163        &self,
164        _state: BlockStateId,
165        world: &Arc<World>,
166        pos: BlockPos,
167        moved_by_piston: bool,
168    ) {
169        affect_neighbors_after_removal(self.block, world, pos, moved_by_piston);
170    }
171
172    fn handle_neighbor_changed(
173        &self,
174        state: BlockStateId,
175        world: &Arc<World>,
176        pos: BlockPos,
177        _source_block: BlockRef,
178        _moved_by_piston: bool,
179    ) {
180        handle_neighbor_changed(
181            self.block,
182            state,
183            world,
184            pos,
185            Self::has_neighbor_signal(world.as_ref(), pos),
186        );
187    }
188
189    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
190        tick_torch(
191            state,
192            world,
193            pos,
194            Self::has_neighbor_signal(world.as_ref(), pos),
195        );
196    }
197
198    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
199        true
200    }
201
202    fn get_own_signal(
203        &self,
204        state: BlockStateId,
205        _world: &dyn LevelReader,
206        _pos: BlockPos,
207        _context: SignalQueryContext,
208    ) -> i32 {
209        own_signal(state)
210    }
211
212    fn get_signal(
213        &self,
214        state: BlockStateId,
215        _world: &dyn LevelReader,
216        _pos: BlockPos,
217        direction: Direction,
218        _context: SignalQueryContext,
219    ) -> i32 {
220        if direction == Direction::Up {
221            0
222        } else {
223            own_signal(state)
224        }
225    }
226
227    fn get_direct_signal(
228        &self,
229        state: BlockStateId,
230        world: &dyn LevelReader,
231        pos: BlockPos,
232        direction: Direction,
233        context: SignalQueryContext,
234    ) -> i32 {
235        if direction == Direction::Down {
236            self.get_signal(state, world, pos, direction, context)
237        } else {
238            0
239        }
240    }
241
242    // `animateTick` emits client-local dust particles only.
243}
244
245/// Wall redstone torch (`redstone_wall_torch`).
246#[block_behavior]
247pub struct RedstoneWallTorchBlock {
248    block: BlockRef,
249}
250
251impl RedstoneWallTorchBlock {
252    /// Creates a wall redstone-torch behavior.
253    #[must_use]
254    pub const fn new(block: BlockRef) -> Self {
255        Self { block }
256    }
257
258    fn has_neighbor_signal(state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
259        let opposite = state
260            .get_value(&BlockStateProperties::HORIZONTAL_FACING)
261            .opposite();
262        get_redstone_signal(
263            world,
264            pos.relative(opposite),
265            opposite,
266            SignalQueryContext::DEFAULT,
267        ) > 0
268    }
269}
270
271impl BlockBehavior for RedstoneWallTorchBlock {
272    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
273        let facing = state.get_value(&BlockStateProperties::HORIZONTAL_FACING);
274        let support_pos = pos.relative(facing.opposite());
275        world.is_face_sturdy(world.get_block_state(support_pos), support_pos, facing)
276    }
277
278    fn update_shape(
279        &self,
280        state: BlockStateId,
281        world: &dyn ScheduledTickAccess,
282        pos: BlockPos,
283        direction: Direction,
284        _neighbor_pos: BlockPos,
285        _neighbor_state: BlockStateId,
286    ) -> BlockStateId {
287        let facing = state.get_value(&BlockStateProperties::HORIZONTAL_FACING);
288        if direction.opposite() == facing && !self.can_survive(state, world, pos) {
289            REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
290        } else {
291            state
292        }
293    }
294
295    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
296        for direction in context.get_nearest_looking_directions() {
297            if !direction.is_horizontal() {
298                continue;
299            }
300            let state = self.block.default_state().set_value(
301                &BlockStateProperties::HORIZONTAL_FACING,
302                direction.opposite(),
303            );
304            if self.can_survive(state, context.world.as_ref(), context.place_pos()) {
305                return Some(state);
306            }
307        }
308        None
309    }
310
311    fn on_place(
312        &self,
313        _state: BlockStateId,
314        world: &Arc<World>,
315        pos: BlockPos,
316        _old_state: BlockStateId,
317        _moved_by_piston: bool,
318    ) {
319        on_place(self.block, world, pos);
320    }
321
322    fn affect_neighbors_after_removal(
323        &self,
324        _state: BlockStateId,
325        world: &Arc<World>,
326        pos: BlockPos,
327        moved_by_piston: bool,
328    ) {
329        affect_neighbors_after_removal(self.block, world, pos, moved_by_piston);
330    }
331
332    fn handle_neighbor_changed(
333        &self,
334        state: BlockStateId,
335        world: &Arc<World>,
336        pos: BlockPos,
337        _source_block: BlockRef,
338        _moved_by_piston: bool,
339    ) {
340        handle_neighbor_changed(
341            self.block,
342            state,
343            world,
344            pos,
345            Self::has_neighbor_signal(state, world.as_ref(), pos),
346        );
347    }
348
349    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
350        tick_torch(
351            state,
352            world,
353            pos,
354            Self::has_neighbor_signal(state, world.as_ref(), pos),
355        );
356    }
357
358    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
359        true
360    }
361
362    fn get_own_signal(
363        &self,
364        state: BlockStateId,
365        _world: &dyn LevelReader,
366        _pos: BlockPos,
367        _context: SignalQueryContext,
368    ) -> i32 {
369        own_signal(state)
370    }
371
372    fn get_signal(
373        &self,
374        state: BlockStateId,
375        _world: &dyn LevelReader,
376        _pos: BlockPos,
377        direction: Direction,
378        _context: SignalQueryContext,
379    ) -> i32 {
380        if state.get_value(&BlockStateProperties::HORIZONTAL_FACING) == direction {
381            0
382        } else {
383            own_signal(state)
384        }
385    }
386
387    fn get_direct_signal(
388        &self,
389        state: BlockStateId,
390        world: &dyn LevelReader,
391        pos: BlockPos,
392        direction: Direction,
393        context: SignalQueryContext,
394    ) -> i32 {
395        if direction == Direction::Down {
396            self.get_signal(state, world, pos, direction, context)
397        } else {
398            0
399        }
400    }
401
402    // `animateTick` emits client-local dust particles only.
403}
404
405#[cfg(test)]
406mod tests {
407    use steel_registry::init_vanilla_registry;
408
409    use super::*;
410    use crate::behavior::init_behaviors;
411    use crate::test_support::TestLevel;
412
413    #[test]
414    fn standing_torch_reads_power_from_its_support() {
415        init_vanilla_registry();
416        init_behaviors();
417        let pos = BlockPos::new(0, 64, 0);
418        let powered = TestLevel::default()
419            .with_block(pos.below(), vanilla_blocks::REDSTONE_BLOCK.default_state());
420        let unpowered =
421            TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
422
423        assert!(RedstoneTorchBlock::has_neighbor_signal(&powered, pos));
424        assert!(!RedstoneTorchBlock::has_neighbor_signal(&unpowered, pos));
425    }
426
427    #[test]
428    fn wall_torch_omits_weak_signal_toward_its_support() {
429        init_vanilla_registry();
430        init_behaviors();
431        let behavior = RedstoneWallTorchBlock::new(&vanilla_blocks::REDSTONE_WALL_TORCH);
432        let state = vanilla_blocks::REDSTONE_WALL_TORCH
433            .default_state()
434            .set_value(&BlockStateProperties::HORIZONTAL_FACING, Direction::East)
435            .set_value(&BlockStateProperties::LIT, true);
436        let level = TestLevel::default();
437        let pos = BlockPos::new(0, 64, 0);
438
439        assert_eq!(
440            behavior.get_signal(
441                state,
442                &level,
443                pos,
444                Direction::East,
445                SignalQueryContext::DEFAULT,
446            ),
447            0
448        );
449        assert_eq!(
450            behavior.get_signal(
451                state,
452                &level,
453                pos,
454                Direction::West,
455                SignalQueryContext::DEFAULT,
456            ),
457            15
458        );
459        assert_eq!(
460            behavior.get_direct_signal(
461                state,
462                &level,
463                pos,
464                Direction::Down,
465                SignalQueryContext::DEFAULT,
466            ),
467            15
468        );
469    }
470}