Skip to main content

steel_core/behavior/blocks/redstone/
daylight_detector_block.rs

1//! Vanilla daylight-detector signal calculation and inversion.
2
3use std::f32::consts::{PI, TAU};
4use std::sync::{Arc, Weak};
5
6use steel_macros::block_behavior;
7use steel_math::trig;
8use steel_registry::block_entity_type::BlockEntityTypeRef;
9use steel_registry::blocks::BlockRef;
10use steel_registry::blocks::block_state_ext::BlockStateExt as _;
11use steel_registry::blocks::properties::BlockStateProperties;
12use steel_registry::{vanilla_block_entity_types, vanilla_game_events};
13use steel_utils::types::UpdateFlags;
14use steel_utils::{BlockPos, BlockStateId};
15
16use crate::behavior::{
17    BlockBehavior, BlockEntityCreation, BlockHitResult, BlockPlaceContext, InteractionResult,
18    InventoryAccess,
19};
20use crate::block_entity::{BlockEntityTicker, entities::DaylightDetectorBlockEntity};
21use crate::player::Player;
22use crate::world::game_event::GameEventContext;
23use crate::world::{LevelReader, SignalQueryContext, World};
24
25// `(float) (Math.PI / 180.0)` in vanilla.
26const DEGREES_TO_RADIANS: f32 = 0.017_453_292;
27
28/// Vanilla `DaylightDetectorBlock` behavior.
29#[block_behavior]
30pub struct DaylightDetectorBlock {
31    block: BlockRef,
32}
33
34impl DaylightDetectorBlock {
35    /// Creates daylight-detector behavior.
36    #[must_use]
37    pub const fn new(block: BlockRef) -> Self {
38        Self { block }
39    }
40
41    pub(crate) fn signal_strength(world: &World, pos: BlockPos, state: BlockStateId) -> u8 {
42        let sky_brightness = world.effective_sky_brightness(pos);
43        Self::calculate_signal_strength(
44            sky_brightness,
45            world.sun_angle_degrees(),
46            state.get_value(&BlockStateProperties::INVERTED),
47        )
48    }
49
50    fn calculate_signal_strength(sky_brightness: u8, sun_angle_degrees: f32, inverted: bool) -> u8 {
51        if inverted {
52            return 15 - sky_brightness;
53        }
54        if sky_brightness == 0 {
55            return 0;
56        }
57
58        let mut sun_angle = sun_angle_degrees * DEGREES_TO_RADIANS;
59        let offset = if sun_angle < PI { 0.0 } else { TAU };
60        sun_angle += (offset - sun_angle) * 0.2;
61        java_round(f32::from(sky_brightness) * trig::cos(f64::from(sun_angle))).clamp(0, 15) as u8
62    }
63
64    fn update_signal_strength(world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
65        let target = Self::signal_strength(world, pos, state);
66        if state.get_value(&BlockStateProperties::POWER) != target {
67            world.set_block(
68                pos,
69                state.set_value(&BlockStateProperties::POWER, target),
70                UpdateFlags::UPDATE_ALL,
71            );
72        }
73    }
74}
75
76#[expect(
77    clippy::cast_possible_truncation,
78    reason = "daylight detector input is bounded to [-15, 15] before Java Math.round"
79)]
80fn java_round(value: f32) -> i32 {
81    (value + 0.5).floor() as i32
82}
83
84impl BlockBehavior for DaylightDetectorBlock {
85    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
86        Some(self.block.default_state())
87    }
88
89    fn use_without_item(
90        &self,
91        state: BlockStateId,
92        world: &Arc<World>,
93        pos: BlockPos,
94        player: &Player,
95        _hit_result: &BlockHitResult,
96        _inv: &mut InventoryAccess,
97    ) -> InteractionResult {
98        if !player.abilities.lock().may_build {
99            return InteractionResult::Pass;
100        }
101
102        let new_state = state.set_value(
103            &BlockStateProperties::INVERTED,
104            !state.get_value(&BlockStateProperties::INVERTED),
105        );
106        world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
107        world.game_event(
108            &vanilla_game_events::BLOCK_CHANGE,
109            pos,
110            &GameEventContext::new(Some(player), Some(new_state)),
111        );
112        Self::update_signal_strength(world, pos, new_state);
113        InteractionResult::Success
114    }
115
116    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
117        true
118    }
119
120    fn get_own_signal(
121        &self,
122        state: BlockStateId,
123        _world: &dyn LevelReader,
124        _pos: BlockPos,
125        _context: SignalQueryContext,
126    ) -> i32 {
127        i32::from(state.get_value(&BlockStateProperties::POWER))
128    }
129
130    fn new_block_entity(
131        &self,
132        level: Weak<World>,
133        pos: BlockPos,
134        state: BlockStateId,
135    ) -> BlockEntityCreation {
136        BlockEntityCreation::Created(Arc::new(DaylightDetectorBlockEntity::new(
137            level, pos, state,
138        )))
139    }
140
141    fn get_block_entity_ticker(
142        &self,
143        world: &Arc<World>,
144        _state: BlockStateId,
145        block_entity_type: BlockEntityTypeRef,
146    ) -> Option<BlockEntityTicker> {
147        if !world.dimension_type.has_skylight {
148            return None;
149        }
150        BlockEntityTicker::for_matching_entity_tick(
151            block_entity_type,
152            &vanilla_block_entity_types::DAYLIGHT_DETECTOR,
153        )
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use std::sync::Arc;
160
161    use steel_registry::init_vanilla_registry;
162    use steel_registry::{vanilla_block_entity_types, vanilla_blocks, vanilla_world_clocks};
163
164    use super::*;
165    use crate::test_support::fresh_test_world;
166
167    #[test]
168    fn daylight_detector_selects_vanilla_server_ticker_in_skylight_dimensions() {
169        init_vanilla_registry();
170        let world = fresh_test_world("daylight_detector_block_entity");
171        let state = vanilla_blocks::DAYLIGHT_DETECTOR.default_state();
172        let behavior = DaylightDetectorBlock::new(&vanilla_blocks::DAYLIGHT_DETECTOR);
173        let entity = behavior
174            .new_block_entity(Arc::downgrade(&world), BlockPos::ZERO, state)
175            .into_created()
176            .expect("daylight detector should create block entity");
177        assert_eq!(
178            entity.get_type(),
179            &vanilla_block_entity_types::DAYLIGHT_DETECTOR
180        );
181        assert_eq!(entity.get_block_state(), state);
182        assert!(
183            behavior
184                .get_block_entity_ticker(
185                    &world,
186                    state,
187                    &vanilla_block_entity_types::DAYLIGHT_DETECTOR,
188                )
189                .is_some()
190        );
191        assert!(
192            behavior
193                .get_block_entity_ticker(&world, state, &vanilla_block_entity_types::CHEST)
194                .is_none()
195        );
196    }
197
198    #[test]
199    fn java_round_matches_vanilla_half_toward_positive_infinity() {
200        assert_eq!(java_round(0.5), 1);
201        assert_eq!(java_round(-0.5), 0);
202    }
203
204    #[test]
205    fn signal_strength_matches_vanilla_sun_angle_adjustment() {
206        assert_eq!(
207            DaylightDetectorBlock::calculate_signal_strength(15, 0.0, false),
208            15
209        );
210        assert_eq!(
211            DaylightDetectorBlock::calculate_signal_strength(15, 77.625_66, false),
212            7
213        );
214        assert_eq!(
215            DaylightDetectorBlock::calculate_signal_strength(15, 180.0, false),
216            0
217        );
218        assert_eq!(
219            DaylightDetectorBlock::calculate_signal_strength(4, 180.0, true),
220            11
221        );
222    }
223
224    #[test]
225    fn vanilla_trig_table_controls_overworld_rounding_boundary() {
226        init_vanilla_registry();
227        let world = fresh_test_world("daylight_detector_trig_boundary");
228        assert_eq!(
229            world
230                .level_data
231                .write()
232                .world_clocks_mut()
233                .set_total_ticks(&vanilla_world_clocks::OVERWORLD, 680),
234            Some(())
235        );
236        let sun_angle_degrees = world.sun_angle_degrees();
237
238        let mut adjusted_angle = sun_angle_degrees * DEGREES_TO_RADIANS;
239        let offset = if adjusted_angle < PI { 0.0 } else { TAU };
240        adjusted_angle += (offset - adjusted_angle) * 0.2;
241        assert_eq!(java_round(11.0 * adjusted_angle.cos()), 7);
242        assert_eq!(
243            DaylightDetectorBlock::calculate_signal_strength(11, sun_angle_degrees, false),
244            6
245        );
246    }
247}