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, IntProperty};
8use steel_utils::types::UpdateFlags;
9use steel_utils::{BlockPos, BlockStateId, Direction};
10
11use crate::behavior::BLOCK_BEHAVIORS;
12use crate::world::{LevelReader, SignalQueryContext, World};
13
14use crate::behavior::blocks::redstone::java_hash::sort_small_map_positions;
15
16/// Persistent evaluator used by `RedStoneWireBlock` when redstone experiments are disabled.
17pub(super) struct DefaultRedstoneWireEvaluator {
18    wire_block: BlockRef,
19}
20
21const POWER: &IntProperty = &BlockStateProperties::POWER;
22
23impl DefaultRedstoneWireEvaluator {
24    pub(super) const fn new(wire_block: BlockRef) -> Self {
25        Self { wire_block }
26    }
27
28    pub(super) fn update_power_strength(
29        &self,
30        world: &Arc<World>,
31        pos: BlockPos,
32        state: BlockStateId,
33    ) {
34        let target_strength = self.calculate_target_strength(world.as_ref(), pos);
35        if i32::from(state.get_value(POWER)) == target_strength {
36            return;
37        }
38
39        if world.get_block_state(pos) == state {
40            world.set_block(
41                pos,
42                state.set_value(POWER, target_strength as u8),
43                UpdateFlags::UPDATE_CLIENTS,
44            );
45        }
46
47        for update_pos in java_hash_set_update_order(pos) {
48            world.update_neighbors_at(update_pos, self.wire_block);
49        }
50    }
51
52    fn calculate_target_strength(&self, level: &dyn LevelReader, pos: BlockPos) -> i32 {
53        // This is vanilla's `calculateTargetStrength` with Lithium's
54        // `getNeighborSignal(..., false, false)` calculation inlined.
55        let context = SignalQueryContext::without_wire_signals();
56        let center_state = level.get_block_state(pos);
57        let ignore_center_signal =
58            center_state.is_air() || center_state.get_block() == self.wire_block;
59        let below_pos = pos.below();
60        let above_pos = pos.above();
61        let below_state = level.get_block_state(below_pos);
62        let above_state = level.get_block_state(above_pos);
63
64        let (below_signal, _) = self.get_signal_from_vertical(
65            level,
66            below_pos,
67            below_state,
68            Direction::Down,
69            context,
70            ignore_center_signal,
71        );
72        if below_signal == 15 {
73            return 15;
74        }
75
76        let (above_signal, above_is_conductor) = self.get_signal_from_vertical(
77            level,
78            above_pos,
79            above_state,
80            Direction::Up,
81            context,
82            ignore_center_signal,
83        );
84        let mut signal = below_signal.max(above_signal);
85        if signal == 15 {
86            return 15;
87        }
88
89        for direction in Direction::HORIZONTAL {
90            let neighbor_pos = pos.relative(direction);
91            let neighbor_state = level.get_block_state(neighbor_pos);
92            if neighbor_state.get_block() == self.wire_block {
93                signal = signal.max(self.get_wire_signal(neighbor_state) - 1);
94            } else {
95                // This branch is the non-wire part of Lithium's
96                // `getSignalFromSide`. The wire/step checks remain here so the
97                // already-fetched state and conductor result can be reused.
98                let (neighbor_signal, neighbor_is_conductor) = self.get_non_wire_signal(
99                    level,
100                    neighbor_pos,
101                    neighbor_state,
102                    direction,
103                    context,
104                    ignore_center_signal,
105                );
106                signal = signal.max(neighbor_signal);
107                if signal == 15 {
108                    return 15;
109                }
110
111                if signal < 14 {
112                    if neighbor_is_conductor && !above_is_conductor {
113                        let above_neighbor_pos = neighbor_pos.above();
114                        signal = signal.max(
115                            self.get_wire_signal(level.get_block_state(above_neighbor_pos)) - 1,
116                        );
117                    } else if !neighbor_is_conductor {
118                        let below_neighbor_pos = neighbor_pos.below();
119                        signal = signal.max(
120                            self.get_wire_signal(level.get_block_state(below_neighbor_pos)) - 1,
121                        );
122                    }
123                }
124            }
125
126            if signal == 15 {
127                return 15;
128            }
129        }
130
131        signal
132    }
133
134    /// Translation of Lithium's `getSignalFromVertical`.
135    fn get_signal_from_vertical(
136        &self,
137        level: &dyn LevelReader,
138        pos: BlockPos,
139        state: BlockStateId,
140        direction: Direction,
141        context: SignalQueryContext,
142        ignore_center_signal: bool,
143    ) -> (i32, bool) {
144        if state.get_block() == self.wire_block {
145            return (0, false);
146        }
147
148        self.get_non_wire_signal(level, pos, state, direction, context, ignore_center_signal)
149    }
150
151    /// Shared translation of the non-wire work in Lithium's
152    /// `getSignalFromVertical` and `getSignalFromSide`.
153    fn get_non_wire_signal(
154        &self,
155        level: &dyn LevelReader,
156        pos: BlockPos,
157        state: BlockStateId,
158        direction: Direction,
159        context: SignalQueryContext,
160        ignore_center_signal: bool,
161    ) -> (i32, bool) {
162        if state.is_air() {
163            return (0, false);
164        }
165
166        let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
167        let mut signal = behavior.get_signal(state, level, pos, direction, context);
168        let is_conductor = behavior.is_redstone_conductor(state, level, pos);
169        if is_conductor && signal < 15 {
170            signal = signal.max(self.get_direct_signal_to(
171                level,
172                pos,
173                direction.opposite(),
174                context,
175                ignore_center_signal,
176            ));
177        }
178
179        (signal, is_conductor)
180    }
181
182    /// Translation of Lithium's `getDirectSignalTo`.
183    ///
184    /// Lithium always ignores the direction back to the wire. Steel also runs
185    /// this path after replacement, so that direction is ignored only while the
186    /// center is still wire or air.
187    fn get_direct_signal_to(
188        &self,
189        level: &dyn LevelReader,
190        pos: BlockPos,
191        direction_to_center: Direction,
192        context: SignalQueryContext,
193        ignore_center_signal: bool,
194    ) -> i32 {
195        let mut signal = 0;
196        for direction in Direction::ALL {
197            if direction == direction_to_center && ignore_center_signal {
198                continue;
199            }
200
201            let neighbor_pos = pos.relative(direction);
202            let neighbor_state = level.get_block_state(neighbor_pos);
203            if neighbor_state.is_air() || neighbor_state.get_block() == self.wire_block {
204                continue;
205            }
206
207            signal = signal.max(
208                BLOCK_BEHAVIORS
209                    .get_behavior(neighbor_state.get_block())
210                    .get_direct_signal(neighbor_state, level, neighbor_pos, direction, context),
211            );
212            if signal == 15 {
213                return 15;
214            }
215        }
216
217        signal
218    }
219
220    fn get_wire_signal(&self, state: BlockStateId) -> i32 {
221        if state.get_block() == self.wire_block {
222            i32::from(state.get_value(POWER))
223        } else {
224            0
225        }
226    }
227}
228
229/// Returns the iteration order of the seven-entry `HashSet<BlockPos>` created by
230/// vanilla's default evaluator.
231///
232/// Seven inserts keep Java `HashMap` at its initial 16 buckets. Iteration walks
233/// buckets from low to high and retains insertion order within a collision chain.
234/// The stable insertion sort below models exactly that behavior without relying on
235/// Rust's unrelated hash-table implementation.
236fn java_hash_set_update_order(pos: BlockPos) -> [BlockPos; 7] {
237    let mut positions = [
238        pos,
239        pos.below(),
240        pos.above(),
241        pos.north(),
242        pos.south(),
243        pos.west(),
244        pos.east(),
245    ];
246
247    sort_small_map_positions(&mut positions);
248
249    positions
250}
251
252#[cfg(test)]
253mod tests {
254    use std::cell::Cell;
255
256    use steel_registry::init_vanilla_registry;
257    use steel_registry::vanilla_blocks;
258    use steel_utils::ChunkPos;
259
260    use super::*;
261    use crate::behavior::init_behaviors;
262    use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
263    use crate::world::{get_best_neighbor_signal, is_redstone_conductor};
264
265    struct CountingLevel {
266        level: TestLevel,
267        block_state_reads: Cell<usize>,
268    }
269
270    impl CountingLevel {
271        fn new(level: TestLevel) -> Self {
272            Self {
273                level,
274                block_state_reads: Cell::new(0),
275            }
276        }
277
278        fn block_state_reads(&self) -> usize {
279            self.block_state_reads.get()
280        }
281    }
282
283    impl LevelReader for CountingLevel {
284        fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
285            self.block_state_reads.set(self.block_state_reads.get() + 1);
286            self.level.get_block_state(pos)
287        }
288
289        fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
290            self.level.raw_brightness(pos, sky_darkening)
291        }
292
293        fn min_y(&self) -> i32 {
294            self.level.min_y()
295        }
296
297        fn height(&self) -> i32 {
298            self.level.height()
299        }
300    }
301
302    fn powered_wire(power: u8) -> BlockStateId {
303        vanilla_blocks::REDSTONE_WIRE
304            .default_state()
305            .set_value(&BlockStateProperties::POWER, power)
306    }
307
308    fn calculate_target_strength_reference(level: &dyn LevelReader, pos: BlockPos) -> i32 {
309        let block_signal =
310            get_best_neighbor_signal(level, pos, SignalQueryContext::without_wire_signals());
311        if block_signal == 15 {
312            return 15;
313        }
314
315        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
316        block_signal.max(get_incoming_wire_signal_reference(&evaluator, level, pos))
317    }
318
319    fn get_incoming_wire_signal_reference(
320        evaluator: &DefaultRedstoneWireEvaluator,
321        level: &dyn LevelReader,
322        pos: BlockPos,
323    ) -> i32 {
324        let mut signal = 0;
325        let above_pos = pos.above();
326        let above_is_conductor =
327            is_redstone_conductor(level, level.get_block_state(above_pos), above_pos);
328
329        for direction in Direction::HORIZONTAL {
330            let neighbor_pos = pos.relative(direction);
331            let neighbor_state = level.get_block_state(neighbor_pos);
332            signal = signal.max(evaluator.get_wire_signal(neighbor_state));
333
334            if is_redstone_conductor(level, neighbor_state, neighbor_pos) && !above_is_conductor {
335                signal = signal
336                    .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.above())));
337            } else if !is_redstone_conductor(level, neighbor_state, neighbor_pos) {
338                signal = signal
339                    .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.below())));
340            }
341        }
342
343        signal.saturating_sub(1)
344    }
345
346    fn assert_matches_reference(level: &dyn LevelReader, pos: BlockPos) {
347        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
348        assert_eq!(
349            evaluator.calculate_target_strength(level, pos),
350            calculate_target_strength_reference(level, pos),
351        );
352    }
353
354    const POWER: &IntProperty = &BlockStateProperties::POWER;
355
356    fn expected_positions(pos: BlockPos, labels: [&str; 7]) -> [BlockPos; 7] {
357        labels.map(|label| match label {
358            "center" => pos,
359            "down" => pos.below(),
360            "up" => pos.above(),
361            "north" => pos.north(),
362            "south" => pos.south(),
363            "west" => pos.west(),
364            "east" => pos.east(),
365            _ => panic!("invalid test direction label"),
366        })
367    }
368
369    #[test]
370    fn seven_position_order_matches_target_jdk_hash_set_fixtures() {
371        let fixtures = [
372            (
373                BlockPos::new(0, 64, 0),
374                ["center", "down", "south", "east", "up", "north", "west"],
375            ),
376            (
377                BlockPos::new(1, 64, 0),
378                ["up", "north", "west", "center", "down", "south", "east"],
379            ),
380            (
381                BlockPos::new(15, 64, 0),
382                ["down", "south", "east", "up", "north", "west", "center"],
383            ),
384            (
385                BlockPos::new(16, 64, 0),
386                ["center", "down", "south", "east", "up", "north", "west"],
387            ),
388            (
389                BlockPos::new(-16, -64, 31),
390                ["down", "south", "east", "up", "north", "west", "center"],
391            ),
392            (
393                BlockPos::new(30_000_000, 319, -30_000_000),
394                ["down", "south", "east", "center", "up", "north", "west"],
395            ),
396        ];
397
398        for (pos, labels) in fixtures {
399            assert_eq!(
400                java_hash_set_update_order(pos),
401                expected_positions(pos, labels)
402            );
403        }
404    }
405
406    #[test]
407    fn incoming_wire_power_does_not_feed_back_through_signal_queries() {
408        init_vanilla_registry();
409        init_behaviors();
410        let pos = BlockPos::new(0, 64, 0);
411        let powered_neighbor = vanilla_blocks::REDSTONE_WIRE
412            .default_state()
413            .set_value(POWER, 15);
414        let level = TestLevel::default().with_block(pos.east(), powered_neighbor);
415        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
416
417        assert_eq!(evaluator.calculate_target_strength(&level, pos), 14);
418    }
419
420    #[test]
421    fn target_strength_matches_vanilla() {
422        init_vanilla_registry();
423        init_behaviors();
424        let pos = BlockPos::new(0, 64, 0);
425        let stone = vanilla_blocks::STONE.default_state();
426        let redstone_block = vanilla_blocks::REDSTONE_BLOCK.default_state();
427
428        let cases = [
429            TestLevel::default(),
430            TestLevel::default().with_block(pos.east(), redstone_block),
431            TestLevel::default().with_block(pos.east(), powered_wire(15)),
432            TestLevel::default()
433                .with_block(pos.east(), stone)
434                .with_block(pos.east().above(), powered_wire(9)),
435            TestLevel::default()
436                .with_block(pos.east(), vanilla_blocks::GLASS.default_state())
437                .with_block(pos.east().below(), powered_wire(9)),
438            TestLevel::default()
439                .with_block(pos.above(), stone)
440                .with_block(pos.east(), stone)
441                .with_block(pos.east().above(), powered_wire(15)),
442            TestLevel::default()
443                .with_block(pos.east(), stone)
444                .with_block(pos.east().east(), redstone_block),
445            TestLevel::default()
446                .with_block(pos.north(), redstone_block)
447                .with_block(pos.east(), powered_wire(12)),
448        ];
449
450        for level in &cases {
451            assert_matches_reference(level, pos);
452        }
453    }
454
455    #[test]
456    fn target_strength_uses_fewer_block_state_reads() {
457        init_vanilla_registry();
458        init_behaviors();
459        let pos = BlockPos::new(0, 64, 0);
460        let level = TestLevel::default()
461            .with_block(pos.east(), vanilla_blocks::STONE.default_state())
462            .with_block(pos.east().above(), powered_wire(12))
463            .with_block(pos.north(), powered_wire(10));
464        let reference_level = CountingLevel::new(level);
465        let optimized_level = CountingLevel::new(
466            TestLevel::default()
467                .with_block(pos.east(), vanilla_blocks::STONE.default_state())
468                .with_block(pos.east().above(), powered_wire(12))
469                .with_block(pos.north(), powered_wire(10)),
470        );
471        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
472
473        let reference_signal = calculate_target_strength_reference(&reference_level, pos);
474        let optimized_signal = evaluator.calculate_target_strength(&optimized_level, pos);
475
476        assert_eq!(optimized_signal, reference_signal);
477        assert!(
478            optimized_level.block_state_reads() < reference_level.block_state_reads(),
479            "optimized reads: {}, reference reads: {}",
480            optimized_level.block_state_reads(),
481            reference_level.block_state_reads(),
482        );
483    }
484
485    #[test]
486    fn live_world_wire_line_settles_after_source_toggle() {
487        init_vanilla_registry();
488        init_behaviors();
489        let world = fresh_test_world("wire_evaluator_source_toggle");
490        let wire_start = BlockPos::new(8, 64, 8);
491        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(wire_start));
492
493        let source_pos = wire_start.west();
494        assert!(world.set_block(
495            source_pos.below(),
496            vanilla_blocks::STONE.default_state(),
497            UpdateFlags::UPDATE_NONE,
498        ));
499        for offset in 0..4 {
500            assert!(world.set_block(
501                wire_start.offset(offset, -1, 0),
502                vanilla_blocks::STONE.default_state(),
503                UpdateFlags::UPDATE_NONE,
504            ));
505            assert!(world.set_block(
506                wire_start.offset(offset, 0, 0),
507                vanilla_blocks::REDSTONE_WIRE.default_state(),
508                UpdateFlags::UPDATE_NONE,
509            ));
510        }
511
512        assert!(world.set_block(
513            source_pos,
514            vanilla_blocks::REDSTONE_BLOCK.default_state(),
515            UpdateFlags::UPDATE_ALL,
516        ));
517        for (offset, power) in [15_u8, 14, 13, 12].into_iter().enumerate() {
518            assert_eq!(
519                world
520                    .get_block_state(wire_start.offset(offset as i32, 0, 0))
521                    .get_value(&BlockStateProperties::POWER),
522                power,
523            );
524        }
525
526        assert!(world.remove_block(source_pos, false));
527        for offset in 0..4 {
528            assert_eq!(
529                world
530                    .get_block_state(wire_start.offset(offset, 0, 0))
531                    .get_value(&BlockStateProperties::POWER),
532                0,
533            );
534        }
535    }
536}