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