1use std::sync::{Arc, Weak};
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{
9 BlockStateProperties, BoolProperty, ComparatorMode, Direction, EnumProperty,
10};
11use steel_registry::{REGISTRY, sound_events, vanilla_blocks};
12use steel_utils::types::UpdateFlags;
13use steel_utils::{BlockPos, BlockStateId, Downcast as _, WorldAabb};
14
15use super::base::DiodeBlock;
16use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
17use crate::behavior::{
18 BLOCK_BEHAVIORS, BlockBehavior, BlockEntityCreation, BlockHitResult, BlockPlaceContext,
19 InteractionResult, InventoryAccess, PlacementSource,
20};
21use crate::block_entity::entities::ComparatorBlockEntity;
22use crate::entity::{Entity, ItemFrame};
23use crate::player::Player;
24use crate::world::tick_scheduler::TickPriority;
25use crate::world::{
26 LevelReader, ScheduledTickAccess, SignalQueryContext, World, is_redstone_conductor,
27};
28
29const DELAY: i32 = 2;
30
31#[block_behavior]
33pub struct ComparatorBlock {
34 diode: DiodeBlock,
35}
36
37const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
38const MODE_COMPARATOR: &EnumProperty<ComparatorMode> = &BlockStateProperties::MODE_COMPARATOR;
39const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
40
41impl ComparatorBlock {
42 #[must_use]
44 pub const fn new(block: BlockRef) -> Self {
45 Self {
46 diode: DiodeBlock::new(block),
47 }
48 }
49
50 fn output_signal(level: &dyn LevelReader, pos: BlockPos) -> i32 {
51 let Some(block_entity) = level.get_block_entity(pos) else {
52 return MIN_REDSTONE_SIGNAL;
53 };
54 block_entity
55 .downcast_ref::<ComparatorBlockEntity>()
56 .map_or(MIN_REDSTONE_SIGNAL, ComparatorBlockEntity::output_signal)
57 }
58
59 fn set_output_signal(world: &Arc<World>, pos: BlockPos, output_signal: i32) -> i32 {
60 let Some(block_entity) = world.get_block_entity(pos) else {
61 return MIN_REDSTONE_SIGNAL;
62 };
63 let Some(comparator) = block_entity.downcast_ref::<ComparatorBlockEntity>() else {
64 return MIN_REDSTONE_SIGNAL;
65 };
66 let old_output = comparator.output_signal();
67 comparator.set_output_signal(output_signal);
68 old_output
69 }
70
71 fn item_frame_signal(world: &World, direction: Direction, pos: BlockPos) -> Option<i32> {
72 let bounds = WorldAabb::new(
73 f64::from(pos.x()),
74 f64::from(pos.y()),
75 f64::from(pos.z()),
76 f64::from(pos.x() + 1),
77 f64::from(pos.y() + 1),
78 f64::from(pos.z() + 1),
79 );
80 let frames = world.get_entities_in_aabb_matching(&bounds, |entity| {
81 entity
82 .as_item_frame()
83 .is_some_and(|frame| frame.direction() == direction)
84 });
85 if frames.len() != 1 {
86 return None;
87 }
88 frames[0]
89 .as_ref()
90 .as_item_frame()
91 .map(ItemFrame::analog_output)
92 }
93
94 fn get_input_signal(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> i32 {
95 let mut result = DiodeBlock::get_input_signal(world.as_ref(), pos, state);
96 let direction = state.get_value(HORIZONTAL_FACING);
97 let mut target_pos = pos.relative(direction);
98 let mut target_state = world.get_block_state(target_pos);
99 let mut target_behavior = BLOCK_BEHAVIORS.get_behavior(target_state.get_block());
100 if target_behavior.has_analog_output_signal(target_state) {
101 return target_behavior.get_analog_output_signal(
102 target_state,
103 world.as_ref(),
104 target_pos,
105 direction.opposite(),
106 );
107 }
108
109 if result >= MAX_REDSTONE_SIGNAL
110 || !is_redstone_conductor(world.as_ref(), target_state, target_pos)
111 {
112 return result;
113 }
114
115 target_pos = target_pos.relative(direction);
116 target_state = world.get_block_state(target_pos);
117 target_behavior = BLOCK_BEHAVIORS.get_behavior(target_state.get_block());
118 let frame_signal = Self::item_frame_signal(world.as_ref(), direction, target_pos);
119 let block_signal = target_behavior
120 .has_analog_output_signal(target_state)
121 .then(|| {
122 target_behavior.get_analog_output_signal(
123 target_state,
124 world.as_ref(),
125 target_pos,
126 direction.opposite(),
127 )
128 });
129 if let Some(analog_signal) = match (frame_signal, block_signal) {
130 (Some(frame), Some(block)) => Some(frame.max(block)),
131 (Some(frame), None) => Some(frame),
132 (None, Some(block)) => Some(block),
133 (None, None) => None,
134 } {
135 result = analog_signal;
136 }
137 result
138 }
139
140 const fn calculate_output_signal(input: i32, alternate: i32, mode: ComparatorMode) -> i32 {
141 if input == MIN_REDSTONE_SIGNAL || alternate > input {
142 return MIN_REDSTONE_SIGNAL;
143 }
144 match mode {
145 ComparatorMode::Compare => input,
146 ComparatorMode::Subtract => input - alternate,
147 }
148 }
149
150 fn calculate_output(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> i32 {
151 let input = Self::get_input_signal(world, pos, state);
152 let alternate = DiodeBlock::get_alternate_signal(world.as_ref(), pos, state, false);
153 Self::calculate_output_signal(input, alternate, state.get_value(MODE_COMPARATOR))
154 }
155
156 const fn should_turn_on_from_signals(input: i32, alternate: i32, mode: ComparatorMode) -> bool {
157 input != MIN_REDSTONE_SIGNAL
158 && (input > alternate
159 || (input == alternate && matches!(mode, ComparatorMode::Compare)))
160 }
161
162 fn should_turn_on(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> bool {
163 let input = Self::get_input_signal(world, pos, state);
164 let alternate = DiodeBlock::get_alternate_signal(world.as_ref(), pos, state, false);
165 Self::should_turn_on_from_signals(input, alternate, state.get_value(MODE_COMPARATOR))
166 }
167
168 fn check_tick_on_neighbor(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
169 if world.will_tick_block_this_tick(pos, self.diode.block) {
170 return;
171 }
172 let output = Self::calculate_output(world, pos, state);
173 if output == Self::output_signal(world.as_ref(), pos)
174 && state.get_value(POWERED) == Self::should_turn_on(world, pos, state)
175 {
176 return;
177 }
178 let priority = if DiodeBlock::should_prioritize(world.as_ref(), pos, state) {
179 TickPriority::High
180 } else {
181 TickPriority::Normal
182 };
183 world.schedule_block_tick(pos, self.diode.block, DELAY, priority);
184 }
185
186 fn refresh_output_state(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
187 let output = Self::calculate_output(world, pos, state);
188 let old_output = Self::set_output_signal(world, pos, output);
189 if old_output == output && state.get_value(MODE_COMPARATOR) != ComparatorMode::Compare {
190 return;
191 }
192
193 let should_turn_on = Self::should_turn_on(world, pos, state);
194 let powered = state.get_value(POWERED);
195 if powered != should_turn_on {
196 world.set_block(
197 pos,
198 state.set_value(POWERED, should_turn_on),
199 UpdateFlags::UPDATE_CLIENTS,
200 );
201 }
202 self.diode.update_neighbors_in_front(world, pos, state);
203 }
204}
205
206impl BlockBehavior for ComparatorBlock {
207 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
208 DiodeBlock::can_survive(world, pos)
209 }
210
211 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
212 Some(self.diode.state_for_placement(context))
213 }
214
215 fn update_shape(
216 &self,
217 state: BlockStateId,
218 world: &dyn ScheduledTickAccess,
219 _pos: BlockPos,
220 direction: Direction,
221 neighbor_pos: BlockPos,
222 neighbor_state: BlockStateId,
223 ) -> BlockStateId {
224 if direction == Direction::Down
225 && !DiodeBlock::can_survive_on(world, neighbor_pos, neighbor_state)
226 {
227 REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
228 } else {
229 state
230 }
231 }
232
233 fn use_without_item(
234 &self,
235 state: BlockStateId,
236 world: &Arc<World>,
237 pos: BlockPos,
238 player: &Player,
239 _hit_result: &BlockHitResult,
240 _inv: &mut InventoryAccess,
241 ) -> InteractionResult {
242 if !player.abilities.lock().may_build {
243 return InteractionResult::Pass;
244 }
245
246 let mode = state.get_value(MODE_COMPARATOR);
247 let next_mode = if mode == ComparatorMode::Compare {
248 ComparatorMode::Subtract
249 } else {
250 ComparatorMode::Compare
251 };
252 let pitch = if next_mode == ComparatorMode::Subtract {
253 0.55
254 } else {
255 0.5
256 };
257 let next_state = state.set_value(MODE_COMPARATOR, next_mode);
258 world.play_block_sound(
259 &sound_events::BLOCK_COMPARATOR_CLICK,
260 pos,
261 0.3,
262 pitch,
263 Some(player.id()),
264 );
265 world.set_block(pos, next_state, UpdateFlags::UPDATE_CLIENTS);
266 if world.get_block_state(pos).get_block() == self.diode.block {
267 self.refresh_output_state(world, pos, next_state);
268 }
269 InteractionResult::Success
270 }
271
272 fn handle_neighbor_changed(
273 &self,
274 state: BlockStateId,
275 world: &Arc<World>,
276 pos: BlockPos,
277 _source_block: BlockRef,
278 _moved_by_piston: bool,
279 ) {
280 self.diode.handle_neighbor_changed(state, world, pos, || {
281 self.check_tick_on_neighbor(world, pos, state);
282 });
283 }
284
285 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
286 self.refresh_output_state(world, pos, state);
287 }
288
289 fn set_placed_by(
290 &self,
291 state: BlockStateId,
292 world: &Arc<World>,
293 pos: BlockPos,
294 _source: &PlacementSource<'_>,
295 ) {
296 self.diode
297 .set_placed_by(world, pos, Self::should_turn_on(world, pos, state));
298 }
299
300 fn on_place(
301 &self,
302 state: BlockStateId,
303 world: &Arc<World>,
304 pos: BlockPos,
305 _old_state: BlockStateId,
306 _moved_by_piston: bool,
307 ) {
308 self.diode.on_place(state, world, pos);
309 }
310
311 fn affect_neighbors_after_removal(
312 &self,
313 state: BlockStateId,
314 world: &Arc<World>,
315 pos: BlockPos,
316 moved_by_piston: bool,
317 ) {
318 self.diode
319 .affect_neighbors_after_removal(state, world, pos, moved_by_piston);
320 }
321
322 fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
323 true
324 }
325
326 fn is_diode(&self) -> bool {
327 true
328 }
329
330 fn get_own_signal(
331 &self,
332 state: BlockStateId,
333 world: &dyn LevelReader,
334 pos: BlockPos,
335 _context: SignalQueryContext,
336 ) -> i32 {
337 DiodeBlock::own_signal(state, Self::output_signal(world, pos))
338 }
339
340 fn get_signal(
341 &self,
342 state: BlockStateId,
343 world: &dyn LevelReader,
344 pos: BlockPos,
345 direction: Direction,
346 _context: SignalQueryContext,
347 ) -> i32 {
348 DiodeBlock::signal(state, direction, Self::output_signal(world, pos))
349 }
350
351 fn get_direct_signal(
352 &self,
353 state: BlockStateId,
354 world: &dyn LevelReader,
355 pos: BlockPos,
356 direction: Direction,
357 context: SignalQueryContext,
358 ) -> i32 {
359 self.get_signal(state, world, pos, direction, context)
360 }
361
362 fn trigger_event(
363 &self,
364 _state: BlockStateId,
365 world: &Arc<World>,
366 pos: BlockPos,
367 param_a: i32,
368 param_b: i32,
369 ) -> bool {
370 let Some(block_entity) = world.get_block_entity(pos) else {
371 return false;
372 };
373 block_entity.trigger_event(param_a, param_b)
374 }
375
376 fn new_block_entity(
377 &self,
378 level: Weak<World>,
379 pos: BlockPos,
380 state: BlockStateId,
381 ) -> BlockEntityCreation {
382 BlockEntityCreation::Created(Arc::new(ComparatorBlockEntity::new(level, pos, state)))
383 }
384
385 }
387
388#[cfg(test)]
389mod tests {
390 use glam::DVec3;
391 use steel_registry::entity_type::EntityTypeRef;
392 use steel_registry::init_vanilla_registry;
393 use steel_registry::{vanilla_blocks, vanilla_entities};
394 use steel_utils::ChunkPos;
395
396 use super::*;
397 use crate::entity::{EntityBase, SharedEntity};
398 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
399
400 struct TestItemFrame {
401 base: EntityBase,
402 direction: Direction,
403 analog_output: i32,
404 }
405
406 crate::entity::impl_test_downcast_type!(TestItemFrame);
407
408 impl Entity for TestItemFrame {
409 fn base(&self) -> &EntityBase {
410 &self.base
411 }
412
413 fn entity_type(&self) -> EntityTypeRef {
414 &vanilla_entities::ITEM_FRAME
415 }
416 }
417
418 impl ItemFrame for TestItemFrame {
419 fn direction(&self) -> Direction {
420 self.direction
421 }
422
423 fn analog_output(&self) -> i32 {
424 self.analog_output
425 }
426 }
427
428 #[test]
429 fn output_calculation_matches_compare_and_subtract_modes() {
430 assert_eq!(
431 ComparatorBlock::calculate_output_signal(10, 6, ComparatorMode::Compare),
432 10
433 );
434 assert_eq!(
435 ComparatorBlock::calculate_output_signal(10, 6, ComparatorMode::Subtract),
436 4
437 );
438 assert_eq!(
439 ComparatorBlock::calculate_output_signal(6, 10, ComparatorMode::Subtract),
440 0
441 );
442 assert_eq!(
443 ComparatorBlock::calculate_output_signal(0, 0, ComparatorMode::Compare),
444 0
445 );
446 }
447
448 #[test]
449 fn equality_powers_only_compare_mode() {
450 assert!(ComparatorBlock::should_turn_on_from_signals(
451 7,
452 7,
453 ComparatorMode::Compare
454 ));
455 assert!(!ComparatorBlock::should_turn_on_from_signals(
456 7,
457 7,
458 ComparatorMode::Subtract
459 ));
460 assert!(!ComparatorBlock::should_turn_on_from_signals(
461 0,
462 0,
463 ComparatorMode::Compare
464 ));
465 }
466
467 #[test]
468 fn comparator_creates_typed_output_storage() {
469 init_vanilla_registry();
470 let behavior = ComparatorBlock::new(&vanilla_blocks::COMPARATOR);
471 let entity = behavior
472 .new_block_entity(
473 Weak::new(),
474 BlockPos::new(0, 64, 0),
475 vanilla_blocks::COMPARATOR.default_state(),
476 )
477 .into_created()
478 .expect("comparator should create its block entity");
479 assert!(entity.downcast_ref::<ComparatorBlockEntity>().is_some());
480 }
481
482 #[test]
483 fn item_frame_signal_uses_item_frame_capability() {
484 init_vanilla_registry();
485 let world = fresh_test_world("comparator_item_frame_capability");
486 let pos = BlockPos::new(8, 64, 8);
487 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
488
489 let frame: SharedEntity = Arc::new(TestItemFrame {
490 base: EntityBase::new(
491 9_001,
492 DVec3::new(8.5, 64.25, 8.5),
493 vanilla_entities::ITEM_FRAME.dimensions,
494 Arc::downgrade(&world),
495 ),
496 direction: Direction::North,
497 analog_output: 6,
498 });
499 world
500 .try_add_entity(frame)
501 .expect("test item frame should enter loaded chunk");
502
503 assert_eq!(
504 ComparatorBlock::item_frame_signal(world.as_ref(), Direction::North, pos),
505 Some(6)
506 );
507 }
508}