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