Skip to main content

steel_core/behavior/blocks/redstone/wire/
block.rs

1//! Vanilla non-experimental redstone-wire 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, EnumProperty, RedstoneSide};
9use steel_registry::{REGISTRY, vanilla_blocks};
10use steel_utils::types::UpdateFlags;
11use steel_utils::{BlockPos, BlockStateId, Direction};
12
13use super::evaluator::DefaultRedstoneWireEvaluator;
14use crate::behavior::{
15    BLOCK_BEHAVIORS, BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult,
16    InventoryAccess,
17};
18use crate::player::Player;
19use crate::world::{
20    LevelReader, ScheduledTickAccess, SignalQueryContext, World, is_redstone_conductor,
21};
22
23/// Vanilla `RedStoneWireBlock` using `DefaultRedstoneWireEvaluator`.
24///
25/// Steel intentionally does not implement the experimental redstone feature.
26/// Signal suppression is carried by [`SignalQueryContext`] instead of mutating
27/// vanilla's process-global `shouldSignal` field.
28#[block_behavior]
29pub struct RedStoneWireBlock {
30    block: BlockRef,
31    cross_state: BlockStateId,
32    evaluator: DefaultRedstoneWireEvaluator,
33}
34
35impl RedStoneWireBlock {
36    /// Creates the ordinary redstone-wire behavior and its persistent evaluator.
37    #[must_use]
38    pub fn new(block: BlockRef) -> Self {
39        let cross_state = block
40            .default_state()
41            .set_value(&BlockStateProperties::NORTH_REDSTONE, RedstoneSide::Side)
42            .set_value(&BlockStateProperties::EAST_REDSTONE, RedstoneSide::Side)
43            .set_value(&BlockStateProperties::SOUTH_REDSTONE, RedstoneSide::Side)
44            .set_value(&BlockStateProperties::WEST_REDSTONE, RedstoneSide::Side);
45        Self {
46            block,
47            cross_state,
48            evaluator: DefaultRedstoneWireEvaluator::new(block),
49        }
50    }
51
52    const fn property_for_direction(
53        direction: Direction,
54    ) -> Option<&'static EnumProperty<RedstoneSide>> {
55        match direction {
56            Direction::North => Some(&BlockStateProperties::NORTH_REDSTONE),
57            Direction::East => Some(&BlockStateProperties::EAST_REDSTONE),
58            Direction::South => Some(&BlockStateProperties::SOUTH_REDSTONE),
59            Direction::West => Some(&BlockStateProperties::WEST_REDSTONE),
60            Direction::Down | Direction::Up => None,
61        }
62    }
63
64    fn is_connected(side: RedstoneSide) -> bool {
65        side != RedstoneSide::None
66    }
67
68    fn is_cross(state: BlockStateId) -> bool {
69        Direction::HORIZONTAL.into_iter().all(|direction| {
70            Self::property_for_direction(direction)
71                .is_some_and(|property| Self::is_connected(state.get_value(property)))
72        })
73    }
74
75    fn is_dot(state: BlockStateId) -> bool {
76        Direction::HORIZONTAL.into_iter().all(|direction| {
77            Self::property_for_direction(direction)
78                .is_some_and(|property| !Self::is_connected(state.get_value(property)))
79        })
80    }
81
82    fn get_connection_state(
83        &self,
84        level: &dyn LevelReader,
85        state: BlockStateId,
86        pos: BlockPos,
87    ) -> BlockStateId {
88        let was_dot = Self::is_dot(state);
89        let mut state = self.get_missing_connections(
90            level,
91            self.block.default_state().set_value(
92                &BlockStateProperties::POWER,
93                state.get_value(&BlockStateProperties::POWER),
94            ),
95            pos,
96        );
97        if was_dot && Self::is_dot(state) {
98            return state;
99        }
100
101        let north = Self::is_connected(state.get_value(&BlockStateProperties::NORTH_REDSTONE));
102        let south = Self::is_connected(state.get_value(&BlockStateProperties::SOUTH_REDSTONE));
103        let east = Self::is_connected(state.get_value(&BlockStateProperties::EAST_REDSTONE));
104        let west = Self::is_connected(state.get_value(&BlockStateProperties::WEST_REDSTONE));
105        let north_south_empty = !north && !south;
106        let east_west_empty = !east && !west;
107
108        if !west && north_south_empty {
109            state = state.set_value(&BlockStateProperties::WEST_REDSTONE, RedstoneSide::Side);
110        }
111        if !east && north_south_empty {
112            state = state.set_value(&BlockStateProperties::EAST_REDSTONE, RedstoneSide::Side);
113        }
114        if !north && east_west_empty {
115            state = state.set_value(&BlockStateProperties::NORTH_REDSTONE, RedstoneSide::Side);
116        }
117        if !south && east_west_empty {
118            state = state.set_value(&BlockStateProperties::SOUTH_REDSTONE, RedstoneSide::Side);
119        }
120
121        state
122    }
123
124    fn get_missing_connections(
125        &self,
126        level: &dyn LevelReader,
127        mut state: BlockStateId,
128        pos: BlockPos,
129    ) -> BlockStateId {
130        let above_state = level.get_block_state(pos.above());
131        // Vanilla passes the wire position, rather than `pos.above()`, to this
132        // state predicate in `getMissingConnections`.
133        let can_connect_up = !is_redstone_conductor(level, above_state, pos);
134
135        for direction in Direction::HORIZONTAL {
136            let Some(property) = Self::property_for_direction(direction) else {
137                continue;
138            };
139            if !Self::is_connected(state.get_value(property)) {
140                state = state.set_value(
141                    property,
142                    self.get_connecting_side_with_up(level, pos, direction, can_connect_up),
143                );
144            }
145        }
146
147        state
148    }
149
150    fn get_connecting_side(
151        &self,
152        level: &dyn LevelReader,
153        pos: BlockPos,
154        direction: Direction,
155    ) -> RedstoneSide {
156        let above_pos = pos.above();
157        let can_connect_up = !is_redstone_conductor(level, level.get_block_state(above_pos), pos);
158        self.get_connecting_side_with_up(level, pos, direction, can_connect_up)
159    }
160
161    fn get_connecting_side_with_up(
162        &self,
163        level: &dyn LevelReader,
164        pos: BlockPos,
165        direction: Direction,
166        can_connect_up: bool,
167    ) -> RedstoneSide {
168        let relative_pos = pos.relative(direction);
169        let relative_state = level.get_block_state(relative_pos);
170
171        if can_connect_up {
172            let behavior = BLOCK_BEHAVIORS.get_behavior(relative_state.get_block());
173            let is_placeable_above =
174                behavior.is_trapdoor() || Self::can_survive_on(level, relative_pos, relative_state);
175            if is_placeable_above
176                && self.should_connect_to(level.get_block_state(relative_pos.above()), None)
177            {
178                if level.is_face_sturdy(relative_state, relative_pos, direction.opposite()) {
179                    return RedstoneSide::Up;
180                }
181                return RedstoneSide::Side;
182            }
183        }
184
185        if !self.should_connect_to(relative_state, Some(direction))
186            && (is_redstone_conductor(level, relative_state, relative_pos)
187                || !self.should_connect_to(level.get_block_state(relative_pos.below()), None))
188        {
189            RedstoneSide::None
190        } else {
191            RedstoneSide::Side
192        }
193    }
194
195    fn should_connect_to(&self, state: BlockStateId, direction: Option<Direction>) -> bool {
196        if state.get_block() == self.block {
197            return true;
198        }
199        if state.get_block() == &vanilla_blocks::REPEATER {
200            let facing = state.get_value(&BlockStateProperties::HORIZONTAL_FACING);
201            return direction == Some(facing) || direction == Some(facing.opposite());
202        }
203        if state.get_block() == &vanilla_blocks::OBSERVER {
204            return direction == Some(state.get_value(&BlockStateProperties::FACING));
205        }
206
207        direction.is_some()
208            && BLOCK_BEHAVIORS
209                .get_behavior(state.get_block())
210                .is_signal_source(state, SignalQueryContext::DEFAULT)
211    }
212
213    fn can_survive_on(level: &dyn LevelReader, pos: BlockPos, state: BlockStateId) -> bool {
214        level.is_face_sturdy(state, pos, Direction::Up)
215            || state.get_block() == &vanilla_blocks::HOPPER
216    }
217
218    fn check_corner_change_at(&self, world: &Arc<World>, pos: BlockPos) {
219        if world.get_block_state(pos).get_block() != self.block {
220            return;
221        }
222
223        world.update_neighbors_at(pos, self.block);
224        for direction in Direction::ALL {
225            world.update_neighbors_at(pos.relative(direction), self.block);
226        }
227    }
228
229    fn update_neighbors_of_neighboring_wires(&self, world: &Arc<World>, pos: BlockPos) {
230        for direction in Direction::HORIZONTAL {
231            self.check_corner_change_at(world, pos.relative(direction));
232        }
233
234        for direction in Direction::HORIZONTAL {
235            let target = pos.relative(direction);
236            let target_state = world.get_block_state(target);
237            if is_redstone_conductor(world.as_ref(), target_state, target) {
238                self.check_corner_change_at(world, target.above());
239            } else {
240                self.check_corner_change_at(world, target.below());
241            }
242        }
243    }
244
245    fn updates_on_shape_change(
246        world: &Arc<World>,
247        pos: BlockPos,
248        old_state: BlockStateId,
249        new_state: BlockStateId,
250    ) {
251        for direction in Direction::HORIZONTAL {
252            let Some(property) = Self::property_for_direction(direction) else {
253                continue;
254            };
255            if Self::is_connected(old_state.get_value(property))
256                == Self::is_connected(new_state.get_value(property))
257            {
258                continue;
259            }
260
261            let relative_pos = pos.relative(direction);
262            let relative_state = world.get_block_state(relative_pos);
263            if is_redstone_conductor(world.as_ref(), relative_state, relative_pos) {
264                world.update_neighbors_at_except_from_facing(
265                    relative_pos,
266                    new_state.get_block(),
267                    direction.opposite(),
268                );
269            }
270        }
271    }
272}
273
274impl BlockBehavior for RedStoneWireBlock {
275    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
276        Some(self.get_connection_state(
277            context.world.as_ref(),
278            self.cross_state,
279            context.place_pos(),
280        ))
281    }
282
283    fn update_shape(
284        &self,
285        state: BlockStateId,
286        world: &dyn ScheduledTickAccess,
287        pos: BlockPos,
288        direction: Direction,
289        neighbor_pos: BlockPos,
290        neighbor_state: BlockStateId,
291    ) -> BlockStateId {
292        if direction == Direction::Down {
293            return if Self::can_survive_on(world, neighbor_pos, neighbor_state) {
294                state
295            } else {
296                REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
297            };
298        }
299        if direction == Direction::Up {
300            return self.get_connection_state(world, state, pos);
301        }
302
303        let Some(property) = Self::property_for_direction(direction) else {
304            return state;
305        };
306        let side_connection = self.get_connecting_side(world, pos, direction);
307        if Self::is_connected(side_connection) == Self::is_connected(state.get_value(property))
308            && !Self::is_cross(state)
309        {
310            state.set_value(property, side_connection)
311        } else {
312            self.get_connection_state(
313                world,
314                self.cross_state
315                    .set_value(
316                        &BlockStateProperties::POWER,
317                        state.get_value(&BlockStateProperties::POWER),
318                    )
319                    .set_value(property, side_connection),
320                pos,
321            )
322        }
323    }
324
325    fn update_indirect_neighbour_shapes(
326        &self,
327        state: BlockStateId,
328        world: &Arc<World>,
329        pos: BlockPos,
330        flags: UpdateFlags,
331        update_limit: i32,
332    ) {
333        for direction in Direction::HORIZONTAL {
334            let Some(property) = Self::property_for_direction(direction) else {
335                continue;
336            };
337            if !Self::is_connected(state.get_value(property)) {
338                continue;
339            }
340
341            let adjacent_pos = pos.relative(direction);
342            if world.get_block_state(adjacent_pos).get_block() == self.block {
343                continue;
344            }
345
346            let below_pos = adjacent_pos.below();
347            if world.get_block_state(below_pos).get_block() == self.block {
348                let neighbor_pos = below_pos.relative(direction.opposite());
349                world.neighbor_shape_changed(
350                    direction.opposite(),
351                    below_pos,
352                    neighbor_pos,
353                    world.get_block_state(neighbor_pos),
354                    flags,
355                    update_limit,
356                );
357            }
358
359            let above_pos = adjacent_pos.above();
360            if world.get_block_state(above_pos).get_block() == self.block {
361                let neighbor_pos = above_pos.relative(direction.opposite());
362                world.neighbor_shape_changed(
363                    direction.opposite(),
364                    above_pos,
365                    neighbor_pos,
366                    world.get_block_state(neighbor_pos),
367                    flags,
368                    update_limit,
369                );
370            }
371        }
372    }
373
374    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
375        let below_pos = pos.below();
376        Self::can_survive_on(world, below_pos, world.get_block_state(below_pos))
377    }
378
379    fn on_place(
380        &self,
381        state: BlockStateId,
382        world: &Arc<World>,
383        pos: BlockPos,
384        old_state: BlockStateId,
385        _moved_by_piston: bool,
386    ) {
387        if old_state.get_block() == self.block {
388            return;
389        }
390
391        self.evaluator.update_power_strength(world, pos, state);
392        for direction in [Direction::Down, Direction::Up] {
393            world.update_neighbors_at(pos.relative(direction), self.block);
394        }
395        self.update_neighbors_of_neighboring_wires(world, pos);
396    }
397
398    fn affect_neighbors_after_removal(
399        &self,
400        state: BlockStateId,
401        world: &Arc<World>,
402        pos: BlockPos,
403        moved_by_piston: bool,
404    ) {
405        if moved_by_piston {
406            return;
407        }
408
409        for direction in Direction::ALL {
410            world.update_neighbors_at(pos.relative(direction), self.block);
411        }
412        self.evaluator.update_power_strength(world, pos, state);
413        self.update_neighbors_of_neighboring_wires(world, pos);
414    }
415
416    fn handle_neighbor_changed(
417        &self,
418        state: BlockStateId,
419        world: &Arc<World>,
420        pos: BlockPos,
421        _source_block: BlockRef,
422        _moved_by_piston: bool,
423    ) {
424        if self.can_survive(state, world.as_ref(), pos) {
425            self.evaluator.update_power_strength(world, pos, state);
426        } else {
427            world.drop_resources(state, pos);
428            world.remove_block(pos, false);
429        }
430    }
431
432    fn use_without_item(
433        &self,
434        state: BlockStateId,
435        world: &Arc<World>,
436        pos: BlockPos,
437        player: &Player,
438        _hit_result: &BlockHitResult,
439        _inv: &mut InventoryAccess,
440    ) -> InteractionResult {
441        if !player.abilities.lock().may_build {
442            return InteractionResult::Pass;
443        }
444        if !Self::is_cross(state) && !Self::is_dot(state) {
445            return InteractionResult::Pass;
446        }
447
448        let new_base_state = if Self::is_cross(state) {
449            self.block.default_state()
450        } else {
451            self.cross_state
452        };
453        let new_state = self.get_connection_state(
454            world.as_ref(),
455            new_base_state.set_value(
456                &BlockStateProperties::POWER,
457                state.get_value(&BlockStateProperties::POWER),
458            ),
459            pos,
460        );
461        if new_state == state {
462            return InteractionResult::Pass;
463        }
464
465        world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
466        Self::updates_on_shape_change(world, pos, state, new_state);
467        InteractionResult::Success
468    }
469
470    fn is_signal_source(&self, _state: BlockStateId, context: SignalQueryContext) -> bool {
471        context.wire_signals_enabled()
472    }
473
474    fn get_own_signal(
475        &self,
476        state: BlockStateId,
477        _world: &dyn LevelReader,
478        _pos: BlockPos,
479        _context: SignalQueryContext,
480    ) -> i32 {
481        i32::from(state.get_value(&BlockStateProperties::POWER))
482    }
483
484    fn get_signal(
485        &self,
486        state: BlockStateId,
487        world: &dyn LevelReader,
488        pos: BlockPos,
489        direction: Direction,
490        context: SignalQueryContext,
491    ) -> i32 {
492        if !context.wire_signals_enabled() || direction == Direction::Down {
493            return 0;
494        }
495
496        let power = self.get_own_signal(state, world, pos, context);
497        if power == 0 {
498            return 0;
499        }
500        if direction == Direction::Up {
501            return power;
502        }
503
504        let Some(property) = Self::property_for_direction(direction.opposite()) else {
505            return 0;
506        };
507        if Self::is_connected(
508            self.get_connection_state(world, state, pos)
509                .get_value(property),
510        ) {
511            power
512        } else {
513            0
514        }
515    }
516
517    fn get_direct_signal(
518        &self,
519        state: BlockStateId,
520        world: &dyn LevelReader,
521        pos: BlockPos,
522        direction: Direction,
523        context: SignalQueryContext,
524    ) -> i32 {
525        if context.wire_signals_enabled() {
526            self.get_signal(state, world, pos, direction, context)
527        } else {
528            0
529        }
530    }
531
532    // `animateTick` only creates client-local dust particles; the server has no
533    // corresponding work to perform.
534}
535
536#[cfg(test)]
537mod tests {
538    use steel_registry::init_vanilla_registry;
539
540    use super::*;
541    use crate::behavior::init_behaviors;
542    use crate::test_support::TestLevel;
543
544    fn wire() -> RedStoneWireBlock {
545        init_vanilla_registry();
546        init_behaviors();
547        RedStoneWireBlock::new(&vanilla_blocks::REDSTONE_WIRE)
548    }
549
550    #[test]
551    fn isolated_cross_and_dot_preserve_their_vanilla_shapes() {
552        let behavior = wire();
553        let level = TestLevel::default().with_block(
554            BlockPos::new(0, 63, 0),
555            vanilla_blocks::STONE.default_state(),
556        );
557        let pos = BlockPos::new(0, 64, 0);
558
559        let cross = behavior.get_connection_state(&level, behavior.cross_state, pos);
560        let dot = behavior.get_connection_state(
561            &level,
562            vanilla_blocks::REDSTONE_WIRE.default_state(),
563            pos,
564        );
565
566        assert!(RedStoneWireBlock::is_cross(cross));
567        assert!(RedStoneWireBlock::is_dot(dot));
568    }
569
570    #[test]
571    fn wire_climbs_sturdy_neighbor_only_when_above_is_connectable() {
572        let behavior = wire();
573        let pos = BlockPos::new(0, 64, 0);
574        let level = TestLevel::default()
575            .with_block(pos.below(), vanilla_blocks::STONE.default_state())
576            .with_block(pos.east(), vanilla_blocks::STONE.default_state())
577            .with_block(pos.east().above(), behavior.block.default_state());
578
579        assert_eq!(
580            behavior.get_connecting_side(&level, pos, Direction::East),
581            RedstoneSide::Up
582        );
583
584        level.set_test_block(pos.above(), vanilla_blocks::STONE.default_state());
585        assert_eq!(
586            behavior.get_connecting_side(&level, pos, Direction::East),
587            RedstoneSide::None
588        );
589    }
590
591    #[test]
592    fn powered_wire_signal_follows_recomputed_connections() {
593        let behavior = wire();
594        let pos = BlockPos::new(0, 64, 0);
595        let state = behavior
596            .block
597            .default_state()
598            .set_value(&BlockStateProperties::POWER, 9)
599            .set_value(&BlockStateProperties::NORTH_REDSTONE, RedstoneSide::Side)
600            .set_value(&BlockStateProperties::EAST_REDSTONE, RedstoneSide::Side);
601        let level = TestLevel::default()
602            .with_block(pos.below(), vanilla_blocks::STONE.default_state())
603            .with_block(pos.north(), behavior.block.default_state())
604            .with_block(pos.east(), behavior.block.default_state());
605
606        assert_eq!(
607            behavior.get_signal(
608                state,
609                &level,
610                pos,
611                Direction::West,
612                SignalQueryContext::DEFAULT,
613            ),
614            9
615        );
616        assert_eq!(
617            behavior.get_signal(
618                state,
619                &level,
620                pos,
621                Direction::East,
622                SignalQueryContext::DEFAULT,
623            ),
624            0
625        );
626        assert_eq!(
627            behavior.get_signal(
628                state,
629                &level,
630                pos,
631                Direction::Up,
632                SignalQueryContext::without_wire_signals(),
633            ),
634            0
635        );
636    }
637}