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::{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#[block_behavior]
28pub struct DaylightDetectorBlock {
29 block: BlockRef,
30}
31
32const INVERTED: &BoolProperty = &BlockStateProperties::INVERTED;
33const POWER: &IntProperty = &BlockStateProperties::POWER;
34
35impl DaylightDetectorBlock {
36 #[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(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_fixture = fresh_test_world("daylight_detector_block_entity");
166 let world = &world_fixture.world;
167 let state = vanilla_blocks::DAYLIGHT_DETECTOR.default_state();
168 let behavior = DaylightDetectorBlock::new(&vanilla_blocks::DAYLIGHT_DETECTOR);
169 let entity = behavior
170 .new_block_entity(Arc::downgrade(world), BlockPos::ZERO, state)
171 .into_created()
172 .expect("daylight detector should create block entity");
173 assert_eq!(
174 entity.get_type(),
175 &vanilla_block_entity_types::DAYLIGHT_DETECTOR
176 );
177 assert_eq!(entity.get_block_state(), state);
178 assert!(
179 behavior
180 .get_block_entity_ticker(
181 world,
182 state,
183 &vanilla_block_entity_types::DAYLIGHT_DETECTOR,
184 )
185 .is_some()
186 );
187 assert!(
188 behavior
189 .get_block_entity_ticker(world, state, &vanilla_block_entity_types::CHEST)
190 .is_none()
191 );
192 }
193
194 #[test]
195 fn java_round_matches_vanilla_half_toward_positive_infinity() {
196 assert_eq!(java_round(0.5), 1);
197 assert_eq!(java_round(-0.5), 0);
198 }
199
200 #[test]
201 fn signal_strength_matches_vanilla_sun_angle_adjustment() {
202 assert_eq!(
203 DaylightDetectorBlock::calculate_signal_strength(15, 0.0, false),
204 15
205 );
206 assert_eq!(
207 DaylightDetectorBlock::calculate_signal_strength(15, 77.625_66, false),
208 7
209 );
210 assert_eq!(
211 DaylightDetectorBlock::calculate_signal_strength(15, 180.0, false),
212 0
213 );
214 assert_eq!(
215 DaylightDetectorBlock::calculate_signal_strength(4, 180.0, true),
216 11
217 );
218 }
219
220 #[test]
221 fn vanilla_trig_table_controls_overworld_rounding_boundary() {
222 init_vanilla_registry();
223 let world_fixture = fresh_test_world("daylight_detector_trig_boundary");
224 let world = &world_fixture.world;
225 assert_eq!(
226 world
227 .level_data
228 .write()
229 .world_clocks_mut()
230 .set_total_ticks(&vanilla_world_clocks::OVERWORLD, 680),
231 Some(())
232 );
233 let sun_angle_degrees = world.sun_angle_degrees();
234
235 let mut adjusted_angle = sun_angle_degrees * DEG_TO_RAD;
236 let offset = if adjusted_angle < PI { 0.0 } else { TAU };
237 adjusted_angle += (offset - adjusted_angle) * 0.2;
238 assert_eq!(java_round(11.0 * adjusted_angle.cos()), 7);
239 assert_eq!(
240 DaylightDetectorBlock::calculate_signal_strength(11, sun_angle_degrees, false),
241 6
242 );
243 }
244}