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(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}