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