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