Skip to main content

steel_core/behavior/blocks/redstone/diode/
comparator.rs

1//! Vanilla redstone comparator behavior.
2
3use std::sync::{Arc, Weak};
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{
9    BlockStateProperties, BoolProperty, ComparatorMode, Direction, EnumProperty,
10};
11use steel_registry::{REGISTRY, sound_events, vanilla_blocks};
12use steel_utils::types::UpdateFlags;
13use steel_utils::{BlockPos, BlockStateId, Downcast as _, WorldAabb};
14
15use super::base::DiodeBlock;
16use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
17use crate::behavior::{
18    BLOCK_BEHAVIORS, BlockBehavior, BlockEntityCreation, BlockHitResult, BlockPlaceContext,
19    InteractionResult, InventoryAccess, PlacementSource,
20};
21use crate::block_entity::entities::ComparatorBlockEntity;
22use crate::entity::{Entity, ItemFrame};
23use crate::player::Player;
24use crate::world::tick_scheduler::TickPriority;
25use crate::world::{
26    LevelReader, ScheduledTickAccess, SignalQueryContext, World, is_redstone_conductor,
27};
28
29const DELAY: i32 = 2;
30
31/// Vanilla `ComparatorBlock`, including persisted output and item-frame input.
32#[block_behavior]
33pub struct ComparatorBlock {
34    diode: DiodeBlock,
35}
36
37const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
38const MODE_COMPARATOR: &EnumProperty<ComparatorMode> = &BlockStateProperties::MODE_COMPARATOR;
39const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
40
41impl ComparatorBlock {
42    /// Creates comparator behavior for `block`.
43    #[must_use]
44    pub const fn new(block: BlockRef) -> Self {
45        Self {
46            diode: DiodeBlock::new(block),
47        }
48    }
49
50    fn output_signal(level: &dyn LevelReader, pos: BlockPos) -> i32 {
51        let Some(block_entity) = level.get_block_entity(pos) else {
52            return MIN_REDSTONE_SIGNAL;
53        };
54        block_entity
55            .downcast_ref::<ComparatorBlockEntity>()
56            .map_or(MIN_REDSTONE_SIGNAL, ComparatorBlockEntity::output_signal)
57    }
58
59    fn set_output_signal(world: &Arc<World>, pos: BlockPos, output_signal: i32) -> i32 {
60        let Some(block_entity) = world.get_block_entity(pos) else {
61            return MIN_REDSTONE_SIGNAL;
62        };
63        let Some(comparator) = block_entity.downcast_ref::<ComparatorBlockEntity>() else {
64            return MIN_REDSTONE_SIGNAL;
65        };
66        let old_output = comparator.output_signal();
67        comparator.set_output_signal(output_signal);
68        old_output
69    }
70
71    fn item_frame_signal(world: &World, direction: Direction, pos: BlockPos) -> Option<i32> {
72        let bounds = WorldAabb::new(
73            f64::from(pos.x()),
74            f64::from(pos.y()),
75            f64::from(pos.z()),
76            f64::from(pos.x() + 1),
77            f64::from(pos.y() + 1),
78            f64::from(pos.z() + 1),
79        );
80        let frames = world.get_entities_in_aabb_matching(&bounds, |entity| {
81            entity
82                .as_item_frame()
83                .is_some_and(|frame| frame.direction() == direction)
84        });
85        if frames.len() != 1 {
86            return None;
87        }
88        frames[0]
89            .as_ref()
90            .as_item_frame()
91            .map(ItemFrame::analog_output)
92    }
93
94    fn get_input_signal(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> i32 {
95        let mut result = DiodeBlock::get_input_signal(world.as_ref(), pos, state);
96        let direction = state.get_value(HORIZONTAL_FACING);
97        let mut target_pos = pos.relative(direction);
98        let mut target_state = world.get_block_state(target_pos);
99        let mut target_behavior = BLOCK_BEHAVIORS.get_behavior(target_state.get_block());
100        if target_behavior.has_analog_output_signal(target_state) {
101            return target_behavior.get_analog_output_signal(
102                target_state,
103                world.as_ref(),
104                target_pos,
105                direction.opposite(),
106            );
107        }
108
109        if result >= MAX_REDSTONE_SIGNAL
110            || !is_redstone_conductor(world.as_ref(), target_state, target_pos)
111        {
112            return result;
113        }
114
115        target_pos = target_pos.relative(direction);
116        target_state = world.get_block_state(target_pos);
117        target_behavior = BLOCK_BEHAVIORS.get_behavior(target_state.get_block());
118        let frame_signal = Self::item_frame_signal(world.as_ref(), direction, target_pos);
119        let block_signal = target_behavior
120            .has_analog_output_signal(target_state)
121            .then(|| {
122                target_behavior.get_analog_output_signal(
123                    target_state,
124                    world.as_ref(),
125                    target_pos,
126                    direction.opposite(),
127                )
128            });
129        if let Some(analog_signal) = match (frame_signal, block_signal) {
130            (Some(frame), Some(block)) => Some(frame.max(block)),
131            (Some(frame), None) => Some(frame),
132            (None, Some(block)) => Some(block),
133            (None, None) => None,
134        } {
135            result = analog_signal;
136        }
137        result
138    }
139
140    const fn calculate_output_signal(input: i32, alternate: i32, mode: ComparatorMode) -> i32 {
141        if input == MIN_REDSTONE_SIGNAL || alternate > input {
142            return MIN_REDSTONE_SIGNAL;
143        }
144        match mode {
145            ComparatorMode::Compare => input,
146            ComparatorMode::Subtract => input - alternate,
147        }
148    }
149
150    fn calculate_output(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> i32 {
151        let input = Self::get_input_signal(world, pos, state);
152        let alternate = DiodeBlock::get_alternate_signal(world.as_ref(), pos, state, false);
153        Self::calculate_output_signal(input, alternate, state.get_value(MODE_COMPARATOR))
154    }
155
156    const fn should_turn_on_from_signals(input: i32, alternate: i32, mode: ComparatorMode) -> bool {
157        input != MIN_REDSTONE_SIGNAL
158            && (input > alternate
159                || (input == alternate && matches!(mode, ComparatorMode::Compare)))
160    }
161
162    fn should_turn_on(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> bool {
163        let input = Self::get_input_signal(world, pos, state);
164        let alternate = DiodeBlock::get_alternate_signal(world.as_ref(), pos, state, false);
165        Self::should_turn_on_from_signals(input, alternate, state.get_value(MODE_COMPARATOR))
166    }
167
168    fn check_tick_on_neighbor(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
169        if world.will_tick_block_this_tick(pos, self.diode.block) {
170            return;
171        }
172        let output = Self::calculate_output(world, pos, state);
173        if output == Self::output_signal(world.as_ref(), pos)
174            && state.get_value(POWERED) == Self::should_turn_on(world, pos, state)
175        {
176            return;
177        }
178        let priority = if DiodeBlock::should_prioritize(world.as_ref(), pos, state) {
179            TickPriority::High
180        } else {
181            TickPriority::Normal
182        };
183        world.schedule_block_tick(pos, self.diode.block, DELAY, priority);
184    }
185
186    fn refresh_output_state(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
187        let output = Self::calculate_output(world, pos, state);
188        let old_output = Self::set_output_signal(world, pos, output);
189        if old_output == output && state.get_value(MODE_COMPARATOR) != ComparatorMode::Compare {
190            return;
191        }
192
193        let should_turn_on = Self::should_turn_on(world, pos, state);
194        let powered = state.get_value(POWERED);
195        if powered != should_turn_on {
196            world.set_block(
197                pos,
198                state.set_value(POWERED, should_turn_on),
199                UpdateFlags::UPDATE_CLIENTS,
200            );
201        }
202        self.diode.update_neighbors_in_front(world, pos, state);
203    }
204}
205
206impl BlockBehavior for ComparatorBlock {
207    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
208        DiodeBlock::can_survive(world, pos)
209    }
210
211    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
212        Some(self.diode.state_for_placement(context))
213    }
214
215    fn update_shape(
216        &self,
217        state: BlockStateId,
218        world: &dyn ScheduledTickAccess,
219        _pos: BlockPos,
220        direction: Direction,
221        neighbor_pos: BlockPos,
222        neighbor_state: BlockStateId,
223    ) -> BlockStateId {
224        if direction == Direction::Down
225            && !DiodeBlock::can_survive_on(world, neighbor_pos, neighbor_state)
226        {
227            REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
228        } else {
229            state
230        }
231    }
232
233    fn use_without_item(
234        &self,
235        state: BlockStateId,
236        world: &Arc<World>,
237        pos: BlockPos,
238        player: &Player,
239        _hit_result: &BlockHitResult,
240        _inv: &mut InventoryAccess,
241    ) -> InteractionResult {
242        if !player.abilities.lock().may_build {
243            return InteractionResult::Pass;
244        }
245
246        let mode = state.get_value(MODE_COMPARATOR);
247        let next_mode = if mode == ComparatorMode::Compare {
248            ComparatorMode::Subtract
249        } else {
250            ComparatorMode::Compare
251        };
252        let pitch = if next_mode == ComparatorMode::Subtract {
253            0.55
254        } else {
255            0.5
256        };
257        let next_state = state.set_value(MODE_COMPARATOR, next_mode);
258        world.play_block_sound(
259            &sound_events::BLOCK_COMPARATOR_CLICK,
260            pos,
261            0.3,
262            pitch,
263            Some(player.id()),
264        );
265        world.set_block(pos, next_state, UpdateFlags::UPDATE_CLIENTS);
266        if world.get_block_state(pos).get_block() == self.diode.block {
267            self.refresh_output_state(world, pos, next_state);
268        }
269        InteractionResult::Success
270    }
271
272    fn handle_neighbor_changed(
273        &self,
274        state: BlockStateId,
275        world: &Arc<World>,
276        pos: BlockPos,
277        _source_block: BlockRef,
278        _moved_by_piston: bool,
279    ) {
280        self.diode.handle_neighbor_changed(state, world, pos, || {
281            self.check_tick_on_neighbor(world, pos, state);
282        });
283    }
284
285    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
286        self.refresh_output_state(world, pos, state);
287    }
288
289    fn set_placed_by(
290        &self,
291        state: BlockStateId,
292        world: &Arc<World>,
293        pos: BlockPos,
294        _source: &PlacementSource<'_>,
295    ) {
296        self.diode
297            .set_placed_by(world, pos, Self::should_turn_on(world, pos, state));
298    }
299
300    fn on_place(
301        &self,
302        state: BlockStateId,
303        world: &Arc<World>,
304        pos: BlockPos,
305        _old_state: BlockStateId,
306        _moved_by_piston: bool,
307    ) {
308        self.diode.on_place(state, world, pos);
309    }
310
311    fn affect_neighbors_after_removal(
312        &self,
313        state: BlockStateId,
314        world: &Arc<World>,
315        pos: BlockPos,
316        moved_by_piston: bool,
317    ) {
318        self.diode
319            .affect_neighbors_after_removal(state, world, pos, moved_by_piston);
320    }
321
322    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
323        true
324    }
325
326    fn is_diode(&self) -> bool {
327        true
328    }
329
330    fn get_own_signal(
331        &self,
332        state: BlockStateId,
333        world: &dyn LevelReader,
334        pos: BlockPos,
335        _context: SignalQueryContext,
336    ) -> i32 {
337        DiodeBlock::own_signal(state, Self::output_signal(world, pos))
338    }
339
340    fn get_signal(
341        &self,
342        state: BlockStateId,
343        world: &dyn LevelReader,
344        pos: BlockPos,
345        direction: Direction,
346        _context: SignalQueryContext,
347    ) -> i32 {
348        DiodeBlock::signal(state, direction, Self::output_signal(world, pos))
349    }
350
351    fn get_direct_signal(
352        &self,
353        state: BlockStateId,
354        world: &dyn LevelReader,
355        pos: BlockPos,
356        direction: Direction,
357        context: SignalQueryContext,
358    ) -> i32 {
359        self.get_signal(state, world, pos, direction, context)
360    }
361
362    fn trigger_event(
363        &self,
364        _state: BlockStateId,
365        world: &Arc<World>,
366        pos: BlockPos,
367        param_a: i32,
368        param_b: i32,
369    ) -> bool {
370        let Some(block_entity) = world.get_block_entity(pos) else {
371            return false;
372        };
373        block_entity.trigger_event(param_a, param_b)
374    }
375
376    fn new_block_entity(
377        &self,
378        level: Weak<World>,
379        pos: BlockPos,
380        state: BlockStateId,
381    ) -> BlockEntityCreation {
382        BlockEntityCreation::Created(Arc::new(ComparatorBlockEntity::new(level, pos, state)))
383    }
384
385    // `animateTick` emits client-local dust particles only.
386}
387
388#[cfg(test)]
389mod tests {
390    use glam::DVec3;
391    use steel_registry::entity_type::EntityTypeRef;
392    use steel_registry::init_vanilla_registry;
393    use steel_registry::{vanilla_blocks, vanilla_entities};
394    use steel_utils::ChunkPos;
395
396    use super::*;
397    use crate::entity::{EntityBase, SharedEntity};
398    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
399
400    struct TestItemFrame {
401        base: EntityBase,
402        direction: Direction,
403        analog_output: i32,
404    }
405
406    crate::entity::impl_test_downcast_type!(TestItemFrame);
407
408    impl Entity for TestItemFrame {
409        fn base(&self) -> &EntityBase {
410            &self.base
411        }
412
413        fn entity_type(&self) -> EntityTypeRef {
414            &vanilla_entities::ITEM_FRAME
415        }
416    }
417
418    impl ItemFrame for TestItemFrame {
419        fn direction(&self) -> Direction {
420            self.direction
421        }
422
423        fn analog_output(&self) -> i32 {
424            self.analog_output
425        }
426    }
427
428    #[test]
429    fn output_calculation_matches_compare_and_subtract_modes() {
430        assert_eq!(
431            ComparatorBlock::calculate_output_signal(10, 6, ComparatorMode::Compare),
432            10
433        );
434        assert_eq!(
435            ComparatorBlock::calculate_output_signal(10, 6, ComparatorMode::Subtract),
436            4
437        );
438        assert_eq!(
439            ComparatorBlock::calculate_output_signal(6, 10, ComparatorMode::Subtract),
440            0
441        );
442        assert_eq!(
443            ComparatorBlock::calculate_output_signal(0, 0, ComparatorMode::Compare),
444            0
445        );
446    }
447
448    #[test]
449    fn equality_powers_only_compare_mode() {
450        assert!(ComparatorBlock::should_turn_on_from_signals(
451            7,
452            7,
453            ComparatorMode::Compare
454        ));
455        assert!(!ComparatorBlock::should_turn_on_from_signals(
456            7,
457            7,
458            ComparatorMode::Subtract
459        ));
460        assert!(!ComparatorBlock::should_turn_on_from_signals(
461            0,
462            0,
463            ComparatorMode::Compare
464        ));
465    }
466
467    #[test]
468    fn comparator_creates_typed_output_storage() {
469        init_vanilla_registry();
470        let behavior = ComparatorBlock::new(&vanilla_blocks::COMPARATOR);
471        let entity = behavior
472            .new_block_entity(
473                Weak::new(),
474                BlockPos::new(0, 64, 0),
475                vanilla_blocks::COMPARATOR.default_state(),
476            )
477            .into_created()
478            .expect("comparator should create its block entity");
479        assert!(entity.downcast_ref::<ComparatorBlockEntity>().is_some());
480    }
481
482    #[test]
483    fn item_frame_signal_uses_item_frame_capability() {
484        init_vanilla_registry();
485        let world = fresh_test_world("comparator_item_frame_capability");
486        let pos = BlockPos::new(8, 64, 8);
487        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
488
489        let frame: SharedEntity = Arc::new(TestItemFrame {
490            base: EntityBase::new(
491                9_001,
492                DVec3::new(8.5, 64.25, 8.5),
493                vanilla_entities::ITEM_FRAME.dimensions,
494                Arc::downgrade(&world),
495            ),
496            direction: Direction::North,
497            analog_output: 6,
498        });
499        world
500            .try_add_entity(frame)
501            .expect("test item frame should enter loaded chunk");
502
503        assert_eq!(
504            ComparatorBlock::item_frame_signal(world.as_ref(), Direction::North, pos),
505            Some(6)
506        );
507    }
508}