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