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    use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
254
255    use super::*;
256    use crate::behavior::init_behaviors;
257
258    struct SignalTestLevel {
259        states: Vec<(BlockPos, BlockStateId)>,
260    }
261
262    impl SignalTestLevel {
263        fn new(states: Vec<(BlockPos, BlockStateId)>) -> Self {
264            Self { states }
265        }
266    }
267
268    impl LevelReader for SignalTestLevel {
269        fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
270            self.states
271                .iter()
272                .find_map(|(state_pos, state)| (*state_pos == pos).then_some(*state))
273                .unwrap_or_else(|| vanilla_blocks::AIR.default_state())
274        }
275
276        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
277            0
278        }
279
280        fn min_y(&self) -> i32 {
281            OverworldNoiseSettings::MIN_Y
282        }
283
284        fn height(&self) -> i32 {
285            OverworldNoiseSettings::HEIGHT
286        }
287
288        fn sea_level(&self) -> i32 {
289            OverworldNoiseSettings::SEA_LEVEL
290        }
291    }
292
293    #[test]
294    fn powered_button_directly_powers_its_support_block() {
295        init_vanilla_registry();
296        init_behaviors();
297        let target = BlockPos::new(4, 64, -3);
298        let button = vanilla_blocks::STONE_BUTTON
299            .default_state()
300            .set_value(&BlockStateProperties::ATTACH_FACE, AttachFace::Ceiling)
301            .set_value(&BlockStateProperties::POWERED, true);
302        let level = SignalTestLevel::new(vec![
303            (target, vanilla_blocks::STONE.default_state()),
304            (target.below(), button),
305        ]);
306
307        assert_eq!(level.get_direct_signal_to(target), 15);
308        assert_eq!(level.get_signal(target, Direction::East), 15);
309    }
310
311    #[test]
312    fn non_conductor_does_not_relay_direct_signal() {
313        init_vanilla_registry();
314        init_behaviors();
315        let target = BlockPos::new(4, 64, -3);
316        let button = vanilla_blocks::STONE_BUTTON
317            .default_state()
318            .set_value(&BlockStateProperties::ATTACH_FACE, AttachFace::Ceiling)
319            .set_value(&BlockStateProperties::POWERED, true);
320        let level = SignalTestLevel::new(vec![
321            (target, vanilla_blocks::GLASS.default_state()),
322            (target.below(), button),
323        ]);
324
325        assert_eq!(level.get_direct_signal_to(target), 15);
326        assert_eq!(level.get_signal(target, Direction::East), 0);
327    }
328
329    #[test]
330    fn control_input_special_cases_redstone_block_and_wire() {
331        init_vanilla_registry();
332        init_behaviors();
333        let redstone_block_pos = BlockPos::new(0, 64, 0);
334        let wire_pos = redstone_block_pos.east();
335        let wire = vanilla_blocks::REDSTONE_WIRE
336            .default_state()
337            .set_value(&BlockStateProperties::POWER, 7_u8);
338        let level = SignalTestLevel::new(vec![
339            (
340                redstone_block_pos,
341                vanilla_blocks::REDSTONE_BLOCK.default_state(),
342            ),
343            (wire_pos, wire),
344        ]);
345
346        assert_eq!(
347            level.get_control_input_signal(redstone_block_pos, Direction::North, false),
348            15
349        );
350        assert_eq!(level.get_signal(redstone_block_pos, Direction::Down), 15);
351        assert_eq!(
352            level.get_best_own_or_neighbour_signal(redstone_block_pos),
353            15
354        );
355        assert_eq!(
356            level.get_control_input_signal(wire_pos, Direction::North, false),
357            7
358        );
359        assert_eq!(
360            level.get_control_input_signal(redstone_block_pos, Direction::North, true),
361            0
362        );
363    }
364}