steel_core/behavior/blocks/redstone/
daylight_detector_block.rs1use 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
25const DEGREES_TO_RADIANS: f32 = 0.017_453_292;
27
28#[block_behavior]
30pub struct DaylightDetectorBlock {
31 block: BlockRef,
32}
33
34impl DaylightDetectorBlock {
35 #[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}