Skip to main content

steel_core/world/
signal_getter.rs

1//! Vanilla redstone signal queries shared by live and test level readers.
2
3use steel_registry::blocks::block_state_ext::BlockStateExt as _;
4use steel_registry::blocks::properties::BlockStateProperties;
5use steel_registry::vanilla_blocks;
6use steel_utils::{BlockPos, BlockStateId, Direction};
7
8use super::LevelReader;
9use crate::behavior::BLOCK_BEHAVIORS;
10
11/// State carried through one synchronous redstone signal query.
12///
13/// Vanilla's default wire evaluator temporarily disables the singleton wire block's
14/// signal output while it measures non-wire input. Steel carries that exclusion in
15/// the query instead, avoiding mutable global behavior shared across worlds.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct SignalQueryContext {
18    wire_signals_enabled: bool,
19}
20
21impl SignalQueryContext {
22    pub(crate) const DEFAULT: Self = Self {
23        wire_signals_enabled: true,
24    };
25
26    /// Returns whether redstone wire may emit signal during this query.
27    #[must_use]
28    pub const fn wire_signals_enabled(self) -> bool {
29        self.wire_signals_enabled
30    }
31
32    /// Returns a query context that excludes redstone-wire output.
33    ///
34    /// Vanilla temporarily clears `RedStoneWireBlock.shouldSignal` while its
35    /// default evaluator measures power supplied by non-wire neighbors.
36    pub(crate) const fn without_wire_signals() -> Self {
37        Self {
38            wire_signals_enabled: false,
39        }
40    }
41}
42
43/// Read-only redstone signal queries matching vanilla `SignalGetter`.
44pub trait SignalGetter: LevelReader {
45    /// Returns whether `state` conducts direct power at this level position.
46    fn is_redstone_conductor(&self, state: BlockStateId, pos: BlockPos) -> bool;
47
48    /// Returns the direct signal emitted by the block at `pos` toward `direction`.
49    fn get_direct_signal(&self, pos: BlockPos, direction: Direction) -> i32;
50
51    /// Returns the strongest direct signal entering `pos` from its six neighbors.
52    fn get_direct_signal_to(&self, pos: BlockPos) -> i32;
53
54    /// Returns the side input used by vanilla diode blocks.
55    fn get_control_input_signal(
56        &self,
57        pos: BlockPos,
58        direction: Direction,
59        only_diodes: bool,
60    ) -> i32;
61
62    /// Returns whether the block at `pos` supplies a signal toward `direction`.
63    fn has_signal(&self, pos: BlockPos, direction: Direction) -> bool;
64
65    /// Returns the signal supplied by the block at `pos` toward `direction`.
66    fn get_signal(&self, pos: BlockPos, direction: Direction) -> i32;
67
68    /// Returns the strongest signal at `pos`, including the block's own source value.
69    fn get_best_own_or_neighbour_signal(&self, pos: BlockPos) -> i32;
70
71    /// Returns whether any of the six neighbors supplies signal to `pos`.
72    fn has_neighbor_signal(&self, pos: BlockPos) -> bool;
73
74    /// Returns the strongest signal supplied to `pos` by its six neighbors.
75    fn get_best_neighbor_signal(&self, pos: BlockPos) -> i32;
76}
77
78impl<T: LevelReader> SignalGetter for T {
79    fn is_redstone_conductor(&self, state: BlockStateId, pos: BlockPos) -> bool {
80        is_redstone_conductor(self, state, pos)
81    }
82
83    fn get_direct_signal(&self, pos: BlockPos, direction: Direction) -> i32 {
84        get_direct_signal(self, pos, direction, SignalQueryContext::DEFAULT)
85    }
86
87    fn get_direct_signal_to(&self, pos: BlockPos) -> i32 {
88        get_direct_signal_to(self, pos, SignalQueryContext::DEFAULT)
89    }
90
91    fn get_control_input_signal(
92        &self,
93        pos: BlockPos,
94        direction: Direction,
95        only_diodes: bool,
96    ) -> i32 {
97        get_control_input_signal(self, pos, direction, only_diodes)
98    }
99
100    fn has_signal(&self, pos: BlockPos, direction: Direction) -> bool {
101        get_signal(self, pos, direction, SignalQueryContext::DEFAULT) > 0
102    }
103
104    fn get_signal(&self, pos: BlockPos, direction: Direction) -> i32 {
105        get_signal(self, pos, direction, SignalQueryContext::DEFAULT)
106    }
107
108    fn get_best_own_or_neighbour_signal(&self, pos: BlockPos) -> i32 {
109        get_best_own_or_neighbour_signal(self, pos, SignalQueryContext::DEFAULT)
110    }
111
112    fn has_neighbor_signal(&self, pos: BlockPos) -> bool {
113        has_neighbor_signal(self, pos, SignalQueryContext::DEFAULT)
114    }
115
116    fn get_best_neighbor_signal(&self, pos: BlockPos) -> i32 {
117        get_best_neighbor_signal(self, pos, SignalQueryContext::DEFAULT)
118    }
119}
120
121pub(crate) fn is_redstone_conductor(
122    level: &dyn LevelReader,
123    state: BlockStateId,
124    pos: BlockPos,
125) -> bool {
126    BLOCK_BEHAVIORS
127        .get_behavior(state.get_block())
128        .is_redstone_conductor(state, level, pos)
129}
130
131pub(crate) fn get_direct_signal(
132    level: &dyn LevelReader,
133    pos: BlockPos,
134    direction: Direction,
135    context: SignalQueryContext,
136) -> i32 {
137    let state = level.get_block_state(pos);
138    BLOCK_BEHAVIORS
139        .get_behavior(state.get_block())
140        .get_direct_signal(state, level, pos, direction, context)
141}
142
143pub(crate) fn get_direct_signal_to(
144    level: &dyn LevelReader,
145    pos: BlockPos,
146    context: SignalQueryContext,
147) -> i32 {
148    let mut result = 0;
149    for direction in Direction::ALL {
150        result = result.max(get_direct_signal(
151            level,
152            direction.relative(pos),
153            direction,
154            context,
155        ));
156        if result >= 15 {
157            return result;
158        }
159    }
160    result
161}
162
163pub(crate) fn get_control_input_signal(
164    level: &dyn LevelReader,
165    pos: BlockPos,
166    direction: Direction,
167    only_diodes: bool,
168) -> i32 {
169    let state = level.get_block_state(pos);
170    let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
171    if only_diodes {
172        return if behavior.is_diode() {
173            get_direct_signal(level, pos, direction, SignalQueryContext::DEFAULT)
174        } else {
175            0
176        };
177    }
178    if state.get_block() == &vanilla_blocks::REDSTONE_BLOCK {
179        return 15;
180    }
181    if state.get_block() == &vanilla_blocks::REDSTONE_WIRE {
182        return i32::from(state.get_value(&BlockStateProperties::POWER));
183    }
184    if behavior.is_signal_source(state, SignalQueryContext::DEFAULT) {
185        get_direct_signal(level, pos, direction, SignalQueryContext::DEFAULT)
186    } else {
187        0
188    }
189}
190
191pub(crate) fn get_signal(
192    level: &dyn LevelReader,
193    pos: BlockPos,
194    direction: Direction,
195    context: SignalQueryContext,
196) -> i32 {
197    let state = level.get_block_state(pos);
198    let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
199    let signal = behavior.get_signal(state, level, pos, direction, context);
200    if behavior.is_redstone_conductor(state, level, pos) {
201        signal.max(get_direct_signal_to(level, pos, context))
202    } else {
203        signal
204    }
205}
206
207pub(crate) fn get_best_own_or_neighbour_signal(
208    level: &dyn LevelReader,
209    pos: BlockPos,
210    context: SignalQueryContext,
211) -> i32 {
212    let state = level.get_block_state(pos);
213    let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
214    let own_signal = if behavior.is_signal_source(state, context) {
215        behavior.get_own_signal(state, level, pos, context)
216    } else {
217        0
218    };
219    get_best_neighbor_signal(level, pos, context).max(own_signal)
220}
221
222pub(crate) fn has_neighbor_signal(
223    level: &dyn LevelReader,
224    pos: BlockPos,
225    context: SignalQueryContext,
226) -> bool {
227    Direction::ALL
228        .into_iter()
229        .any(|direction| get_signal(level, direction.relative(pos), direction, context) > 0)
230}
231
232pub(crate) fn get_best_neighbor_signal(
233    level: &dyn LevelReader,
234    pos: BlockPos,
235    context: SignalQueryContext,
236) -> i32 {
237    let mut best = 0;
238    for direction in Direction::ALL {
239        let signal = get_signal(level, direction.relative(pos), direction, context);
240        if signal >= 15 {
241            return 15;
242        }
243        best = best.max(signal);
244    }
245    best
246}
247
248#[cfg(test)]
249mod tests {
250    use steel_registry::blocks::properties::{AttachFace, BlockStateProperties};
251    use steel_registry::{init_vanilla_registry, vanilla_blocks};
252
253    use super::*;
254    use crate::behavior::init_behaviors;
255
256    struct SignalTestLevel {
257        states: Vec<(BlockPos, BlockStateId)>,
258    }
259
260    impl SignalTestLevel {
261        fn new(states: Vec<(BlockPos, BlockStateId)>) -> Self {
262            Self { states }
263        }
264    }
265
266    impl LevelReader for SignalTestLevel {
267        fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
268            self.states
269                .iter()
270                .find_map(|(state_pos, state)| (*state_pos == pos).then_some(*state))
271                .unwrap_or_else(|| vanilla_blocks::AIR.default_state())
272        }
273
274        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
275            0
276        }
277
278        fn min_y(&self) -> i32 {
279            -64
280        }
281
282        fn height(&self) -> i32 {
283            384
284        }
285    }
286
287    #[test]
288    fn powered_button_directly_powers_its_support_block() {
289        init_vanilla_registry();
290        init_behaviors();
291        let target = BlockPos::new(4, 64, -3);
292        let button = vanilla_blocks::STONE_BUTTON
293            .default_state()
294            .set_value(&BlockStateProperties::ATTACH_FACE, AttachFace::Ceiling)
295            .set_value(&BlockStateProperties::POWERED, true);
296        let level = SignalTestLevel::new(vec![
297            (target, vanilla_blocks::STONE.default_state()),
298            (target.below(), button),
299        ]);
300
301        assert_eq!(level.get_direct_signal_to(target), 15);
302        assert_eq!(level.get_signal(target, Direction::East), 15);
303    }
304
305    #[test]
306    fn non_conductor_does_not_relay_direct_signal() {
307        init_vanilla_registry();
308        init_behaviors();
309        let target = BlockPos::new(4, 64, -3);
310        let button = vanilla_blocks::STONE_BUTTON
311            .default_state()
312            .set_value(&BlockStateProperties::ATTACH_FACE, AttachFace::Ceiling)
313            .set_value(&BlockStateProperties::POWERED, true);
314        let level = SignalTestLevel::new(vec![
315            (target, vanilla_blocks::GLASS.default_state()),
316            (target.below(), button),
317        ]);
318
319        assert_eq!(level.get_direct_signal_to(target), 15);
320        assert_eq!(level.get_signal(target, Direction::East), 0);
321    }
322
323    #[test]
324    fn control_input_special_cases_redstone_block_and_wire() {
325        init_vanilla_registry();
326        init_behaviors();
327        let redstone_block_pos = BlockPos::new(0, 64, 0);
328        let wire_pos = redstone_block_pos.east();
329        let wire = vanilla_blocks::REDSTONE_WIRE
330            .default_state()
331            .set_value(&BlockStateProperties::POWER, 7_u8);
332        let level = SignalTestLevel::new(vec![
333            (
334                redstone_block_pos,
335                vanilla_blocks::REDSTONE_BLOCK.default_state(),
336            ),
337            (wire_pos, wire),
338        ]);
339
340        assert_eq!(
341            level.get_control_input_signal(redstone_block_pos, Direction::North, false),
342            15
343        );
344        assert_eq!(level.get_signal(redstone_block_pos, Direction::Down), 15);
345        assert_eq!(
346            level.get_best_own_or_neighbour_signal(redstone_block_pos),
347            15
348        );
349        assert_eq!(
350            level.get_control_input_signal(wire_pos, Direction::North, false),
351            7
352        );
353        assert_eq!(
354            level.get_control_input_signal(redstone_block_pos, Direction::North, true),
355            0
356        );
357    }
358}