Skip to main content

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

1//! Vanilla's non-experimental redstone-wire evaluator.
2
3use std::sync::Arc;
4
5use steel_registry::blocks::BlockRef;
6use steel_registry::blocks::block_state_ext::BlockStateExt as _;
7use steel_registry::blocks::properties::BlockStateProperties;
8use steel_utils::types::UpdateFlags;
9use steel_utils::{BlockPos, BlockStateId, Direction};
10
11use crate::world::{
12    LevelReader, SignalQueryContext, World, get_best_neighbor_signal, is_redstone_conductor,
13};
14
15use crate::behavior::blocks::redstone::java_hash::sort_small_map_positions;
16
17/// Persistent evaluator used by `RedStoneWireBlock` when redstone experiments are disabled.
18pub(super) struct DefaultRedstoneWireEvaluator {
19    wire_block: BlockRef,
20}
21
22impl DefaultRedstoneWireEvaluator {
23    pub(super) const fn new(wire_block: BlockRef) -> Self {
24        Self { wire_block }
25    }
26
27    pub(super) fn update_power_strength(
28        &self,
29        world: &Arc<World>,
30        pos: BlockPos,
31        state: BlockStateId,
32    ) {
33        let target_strength = self.calculate_target_strength(world.as_ref(), pos);
34        if i32::from(state.get_value(&BlockStateProperties::POWER)) == target_strength {
35            return;
36        }
37
38        if world.get_block_state(pos) == state {
39            world.set_block(
40                pos,
41                state.set_value(&BlockStateProperties::POWER, target_strength as u8),
42                UpdateFlags::UPDATE_CLIENTS,
43            );
44        }
45
46        for update_pos in java_hash_set_update_order(pos) {
47            world.update_neighbors_at(update_pos, self.wire_block);
48        }
49    }
50
51    fn calculate_target_strength(&self, level: &dyn LevelReader, pos: BlockPos) -> i32 {
52        let block_signal =
53            get_best_neighbor_signal(level, pos, SignalQueryContext::without_wire_signals());
54        if block_signal == 15 {
55            return block_signal;
56        }
57        block_signal.max(self.get_incoming_wire_signal(level, pos))
58    }
59
60    fn get_incoming_wire_signal(&self, level: &dyn LevelReader, pos: BlockPos) -> i32 {
61        let mut wire_signal = 0;
62
63        for direction in Direction::HORIZONTAL {
64            let neighbor_pos = pos.relative(direction);
65            let neighbor_state = level.get_block_state(neighbor_pos);
66            wire_signal = wire_signal.max(self.get_wire_signal(neighbor_state));
67
68            let above_pos = pos.above();
69            if is_redstone_conductor(level, neighbor_state, neighbor_pos)
70                && !is_redstone_conductor(level, level.get_block_state(above_pos), above_pos)
71            {
72                let above_neighbor_pos = neighbor_pos.above();
73                wire_signal = wire_signal
74                    .max(self.get_wire_signal(level.get_block_state(above_neighbor_pos)));
75            } else if !is_redstone_conductor(level, neighbor_state, neighbor_pos) {
76                let below_neighbor_pos = neighbor_pos.below();
77                wire_signal = wire_signal
78                    .max(self.get_wire_signal(level.get_block_state(below_neighbor_pos)));
79            }
80        }
81
82        0.max(wire_signal - 1)
83    }
84
85    fn get_wire_signal(&self, state: BlockStateId) -> i32 {
86        if state.get_block() == self.wire_block {
87            i32::from(state.get_value(&BlockStateProperties::POWER))
88        } else {
89            0
90        }
91    }
92}
93
94/// Returns the iteration order of the seven-entry `HashSet<BlockPos>` created by
95/// vanilla's default evaluator.
96///
97/// Seven inserts keep Java `HashMap` at its initial 16 buckets. Iteration walks
98/// buckets from low to high and retains insertion order within a collision chain.
99/// The stable insertion sort below models exactly that behavior without relying on
100/// Rust's unrelated hash-table implementation.
101fn java_hash_set_update_order(pos: BlockPos) -> [BlockPos; 7] {
102    let mut positions = [
103        pos,
104        pos.below(),
105        pos.above(),
106        pos.north(),
107        pos.south(),
108        pos.west(),
109        pos.east(),
110    ];
111
112    sort_small_map_positions(&mut positions);
113
114    positions
115}
116
117#[cfg(test)]
118mod tests {
119    use steel_registry::init_vanilla_registry;
120    use steel_registry::vanilla_blocks;
121
122    use super::*;
123    use crate::behavior::init_behaviors;
124    use crate::test_support::TestLevel;
125
126    fn expected_positions(pos: BlockPos, labels: [&str; 7]) -> [BlockPos; 7] {
127        labels.map(|label| match label {
128            "center" => pos,
129            "down" => pos.below(),
130            "up" => pos.above(),
131            "north" => pos.north(),
132            "south" => pos.south(),
133            "west" => pos.west(),
134            "east" => pos.east(),
135            _ => panic!("invalid test direction label"),
136        })
137    }
138
139    #[test]
140    fn seven_position_order_matches_target_jdk_hash_set_fixtures() {
141        let fixtures = [
142            (
143                BlockPos::new(0, 64, 0),
144                ["center", "down", "south", "east", "up", "north", "west"],
145            ),
146            (
147                BlockPos::new(1, 64, 0),
148                ["up", "north", "west", "center", "down", "south", "east"],
149            ),
150            (
151                BlockPos::new(15, 64, 0),
152                ["down", "south", "east", "up", "north", "west", "center"],
153            ),
154            (
155                BlockPos::new(16, 64, 0),
156                ["center", "down", "south", "east", "up", "north", "west"],
157            ),
158            (
159                BlockPos::new(-16, -64, 31),
160                ["down", "south", "east", "up", "north", "west", "center"],
161            ),
162            (
163                BlockPos::new(30_000_000, 319, -30_000_000),
164                ["down", "south", "east", "center", "up", "north", "west"],
165            ),
166        ];
167
168        for (pos, labels) in fixtures {
169            assert_eq!(
170                java_hash_set_update_order(pos),
171                expected_positions(pos, labels)
172            );
173        }
174    }
175
176    #[test]
177    fn incoming_wire_power_does_not_feed_back_through_signal_queries() {
178        init_vanilla_registry();
179        init_behaviors();
180        let pos = BlockPos::new(0, 64, 0);
181        let powered_neighbor = vanilla_blocks::REDSTONE_WIRE
182            .default_state()
183            .set_value(&BlockStateProperties::POWER, 15);
184        let level = TestLevel::default().with_block(pos.east(), powered_neighbor);
185        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
186
187        assert_eq!(evaluator.calculate_target_strength(&level, pos), 14);
188    }
189}