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        fn sea_level(&self) -> i32 {
302            self.level.sea_level()
303        }
304    }
305
306    fn powered_wire(power: u8) -> BlockStateId {
307        vanilla_blocks::REDSTONE_WIRE
308            .default_state()
309            .set_value(&BlockStateProperties::POWER, power)
310    }
311
312    fn calculate_target_strength_reference(level: &dyn LevelReader, pos: BlockPos) -> i32 {
313        let block_signal =
314            get_best_neighbor_signal(level, pos, SignalQueryContext::without_wire_signals());
315        if block_signal == 15 {
316            return 15;
317        }
318
319        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
320        block_signal.max(get_incoming_wire_signal_reference(&evaluator, level, pos))
321    }
322
323    fn get_incoming_wire_signal_reference(
324        evaluator: &DefaultRedstoneWireEvaluator,
325        level: &dyn LevelReader,
326        pos: BlockPos,
327    ) -> i32 {
328        let mut signal = 0;
329        let above_pos = pos.above();
330        let above_is_conductor =
331            is_redstone_conductor(level, level.get_block_state(above_pos), above_pos);
332
333        for direction in Direction::HORIZONTAL {
334            let neighbor_pos = pos.relative(direction);
335            let neighbor_state = level.get_block_state(neighbor_pos);
336            signal = signal.max(evaluator.get_wire_signal(neighbor_state));
337
338            if is_redstone_conductor(level, neighbor_state, neighbor_pos) && !above_is_conductor {
339                signal = signal
340                    .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.above())));
341            } else if !is_redstone_conductor(level, neighbor_state, neighbor_pos) {
342                signal = signal
343                    .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.below())));
344            }
345        }
346
347        signal.saturating_sub(1)
348    }
349
350    fn assert_matches_reference(level: &dyn LevelReader, pos: BlockPos) {
351        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
352        assert_eq!(
353            evaluator.calculate_target_strength(level, pos),
354            calculate_target_strength_reference(level, pos),
355        );
356    }
357
358    const POWER: &IntProperty = &BlockStateProperties::POWER;
359
360    fn expected_positions(pos: BlockPos, labels: [&str; 7]) -> [BlockPos; 7] {
361        labels.map(|label| match label {
362            "center" => pos,
363            "down" => pos.below(),
364            "up" => pos.above(),
365            "north" => pos.north(),
366            "south" => pos.south(),
367            "west" => pos.west(),
368            "east" => pos.east(),
369            _ => panic!("invalid test direction label"),
370        })
371    }
372
373    #[test]
374    fn seven_position_order_matches_target_jdk_hash_set_fixtures() {
375        let fixtures = [
376            (
377                BlockPos::new(0, 64, 0),
378                ["center", "down", "south", "east", "up", "north", "west"],
379            ),
380            (
381                BlockPos::new(1, 64, 0),
382                ["up", "north", "west", "center", "down", "south", "east"],
383            ),
384            (
385                BlockPos::new(15, 64, 0),
386                ["down", "south", "east", "up", "north", "west", "center"],
387            ),
388            (
389                BlockPos::new(16, 64, 0),
390                ["center", "down", "south", "east", "up", "north", "west"],
391            ),
392            (
393                BlockPos::new(-16, -64, 31),
394                ["down", "south", "east", "up", "north", "west", "center"],
395            ),
396            (
397                BlockPos::new(30_000_000, 319, -30_000_000),
398                ["down", "south", "east", "center", "up", "north", "west"],
399            ),
400        ];
401
402        for (pos, labels) in fixtures {
403            assert_eq!(
404                java_hash_set_update_order(pos),
405                expected_positions(pos, labels)
406            );
407        }
408    }
409
410    #[test]
411    fn incoming_wire_power_does_not_feed_back_through_signal_queries() {
412        init_vanilla_registry();
413        init_behaviors();
414        let pos = BlockPos::new(0, 64, 0);
415        let powered_neighbor = vanilla_blocks::REDSTONE_WIRE
416            .default_state()
417            .set_value(POWER, 15);
418        let level = TestLevel::default().with_block(pos.east(), powered_neighbor);
419        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
420
421        assert_eq!(evaluator.calculate_target_strength(&level, pos), 14);
422    }
423
424    #[test]
425    fn target_strength_matches_vanilla() {
426        init_vanilla_registry();
427        init_behaviors();
428        let pos = BlockPos::new(0, 64, 0);
429        let stone = vanilla_blocks::STONE.default_state();
430        let redstone_block = vanilla_blocks::REDSTONE_BLOCK.default_state();
431
432        let cases = [
433            TestLevel::default(),
434            TestLevel::default().with_block(pos.east(), redstone_block),
435            TestLevel::default().with_block(pos.east(), powered_wire(15)),
436            TestLevel::default()
437                .with_block(pos.east(), stone)
438                .with_block(pos.east().above(), powered_wire(9)),
439            TestLevel::default()
440                .with_block(pos.east(), vanilla_blocks::GLASS.default_state())
441                .with_block(pos.east().below(), powered_wire(9)),
442            TestLevel::default()
443                .with_block(pos.above(), stone)
444                .with_block(pos.east(), stone)
445                .with_block(pos.east().above(), powered_wire(15)),
446            TestLevel::default()
447                .with_block(pos.east(), stone)
448                .with_block(pos.east().east(), redstone_block),
449            TestLevel::default()
450                .with_block(pos.north(), redstone_block)
451                .with_block(pos.east(), powered_wire(12)),
452        ];
453
454        for level in &cases {
455            assert_matches_reference(level, pos);
456        }
457    }
458
459    #[test]
460    fn target_strength_uses_fewer_block_state_reads() {
461        init_vanilla_registry();
462        init_behaviors();
463        let pos = BlockPos::new(0, 64, 0);
464        let level = TestLevel::default()
465            .with_block(pos.east(), vanilla_blocks::STONE.default_state())
466            .with_block(pos.east().above(), powered_wire(12))
467            .with_block(pos.north(), powered_wire(10));
468        let reference_level = CountingLevel::new(level);
469        let optimized_level = CountingLevel::new(
470            TestLevel::default()
471                .with_block(pos.east(), vanilla_blocks::STONE.default_state())
472                .with_block(pos.east().above(), powered_wire(12))
473                .with_block(pos.north(), powered_wire(10)),
474        );
475        let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
476
477        let reference_signal = calculate_target_strength_reference(&reference_level, pos);
478        let optimized_signal = evaluator.calculate_target_strength(&optimized_level, pos);
479
480        assert_eq!(optimized_signal, reference_signal);
481        assert!(
482            optimized_level.block_state_reads() < reference_level.block_state_reads(),
483            "optimized reads: {}, reference reads: {}",
484            optimized_level.block_state_reads(),
485            reference_level.block_state_reads(),
486        );
487    }
488
489    #[test]
490    fn live_world_wire_line_settles_after_source_toggle() {
491        init_vanilla_registry();
492        init_behaviors();
493        let world_fixture = fresh_test_world("wire_evaluator_source_toggle");
494        let world = &world_fixture.world;
495        let wire_start = BlockPos::new(8, 64, 8);
496        insert_ready_full_chunk(world, ChunkPos::from_block_pos(wire_start));
497
498        let source_pos = wire_start.west();
499        assert!(world.set_block(
500            source_pos.below(),
501            vanilla_blocks::STONE.default_state(),
502            UpdateFlags::UPDATE_NONE,
503        ));
504        for offset in 0..4 {
505            assert!(world.set_block(
506                wire_start.offset(offset, -1, 0),
507                vanilla_blocks::STONE.default_state(),
508                UpdateFlags::UPDATE_NONE,
509            ));
510            assert!(world.set_block(
511                wire_start.offset(offset, 0, 0),
512                vanilla_blocks::REDSTONE_WIRE.default_state(),
513                UpdateFlags::UPDATE_NONE,
514            ));
515        }
516
517        assert!(world.set_block(
518            source_pos,
519            vanilla_blocks::REDSTONE_BLOCK.default_state(),
520            UpdateFlags::UPDATE_ALL,
521        ));
522        for (offset, power) in [15_u8, 14, 13, 12].into_iter().enumerate() {
523            assert_eq!(
524                world
525                    .get_block_state(wire_start.offset(offset as i32, 0, 0))
526                    .get_value(&BlockStateProperties::POWER),
527                power,
528            );
529        }
530
531        assert!(world.remove_block(source_pos, false));
532        for offset in 0..4 {
533            assert_eq!(
534                world
535                    .get_block_state(wire_start.offset(offset, 0, 0))
536                    .get_value(&BlockStateProperties::POWER),
537                0,
538            );
539        }
540    }
541}