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