1use std::sync::Arc;
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, EnumProperty, IntProperty, RedstoneSide,
10};
11use steel_registry::{REGISTRY, vanilla_blocks};
12use steel_utils::types::UpdateFlags;
13use steel_utils::{BlockPos, BlockStateId, Direction};
14
15use super::evaluator::DefaultRedstoneWireEvaluator;
16use crate::behavior::blocks::redstone::MIN_REDSTONE_SIGNAL;
17use crate::behavior::{
18 BLOCK_BEHAVIORS, BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult,
19 InventoryAccess,
20};
21use crate::player::Player;
22use crate::world::{
23 LevelReader, ScheduledTickAccess, SignalQueryContext, World, is_redstone_conductor,
24};
25
26#[block_behavior]
32pub struct RedStoneWireBlock {
33 block: BlockRef,
34 cross_state: BlockStateId,
35 evaluator: DefaultRedstoneWireEvaluator,
36}
37
38const EAST_REDSTONE: &EnumProperty<RedstoneSide> = &BlockStateProperties::EAST_REDSTONE;
39const FACING: &EnumProperty<Direction> = &BlockStateProperties::FACING;
40const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
41const NORTH_REDSTONE: &EnumProperty<RedstoneSide> = &BlockStateProperties::NORTH_REDSTONE;
42const POWER: &IntProperty = &BlockStateProperties::POWER;
43const SOUTH_REDSTONE: &EnumProperty<RedstoneSide> = &BlockStateProperties::SOUTH_REDSTONE;
44const WEST_REDSTONE: &EnumProperty<RedstoneSide> = &BlockStateProperties::WEST_REDSTONE;
45
46impl RedStoneWireBlock {
47 #[must_use]
49 pub fn new(block: BlockRef) -> Self {
50 let cross_state = block
51 .default_state()
52 .set_value(NORTH_REDSTONE, RedstoneSide::Side)
53 .set_value(EAST_REDSTONE, RedstoneSide::Side)
54 .set_value(SOUTH_REDSTONE, RedstoneSide::Side)
55 .set_value(WEST_REDSTONE, RedstoneSide::Side);
56 Self {
57 block,
58 cross_state,
59 evaluator: DefaultRedstoneWireEvaluator::new(block),
60 }
61 }
62
63 const fn property_for_direction(
65 direction: Direction,
66 ) -> Option<&'static EnumProperty<RedstoneSide>> {
67 match direction {
68 Direction::North => Some(NORTH_REDSTONE),
69 Direction::East => Some(EAST_REDSTONE),
70 Direction::South => Some(SOUTH_REDSTONE),
71 Direction::West => Some(WEST_REDSTONE),
72 Direction::Down | Direction::Up => None,
73 }
74 }
75
76 fn is_connected(side: RedstoneSide) -> bool {
78 side != RedstoneSide::None
79 }
80
81 fn is_cross(state: BlockStateId) -> bool {
82 Direction::HORIZONTAL.into_iter().all(|direction| {
83 Self::property_for_direction(direction)
84 .is_some_and(|property| Self::is_connected(state.get_value(property)))
85 })
86 }
87
88 fn is_dot(state: BlockStateId) -> bool {
89 Direction::HORIZONTAL.into_iter().all(|direction| {
90 Self::property_for_direction(direction)
91 .is_some_and(|property| !Self::is_connected(state.get_value(property)))
92 })
93 }
94
95 fn get_connection_state(
96 &self,
97 level: &dyn LevelReader,
98 state: BlockStateId,
99 pos: BlockPos,
100 ) -> BlockStateId {
101 let was_dot = Self::is_dot(state);
102 let mut state = self.get_missing_connections(
103 level,
104 self.block
105 .default_state()
106 .set_value(POWER, state.get_value(POWER)),
107 pos,
108 );
109 if was_dot && Self::is_dot(state) {
110 return state;
111 }
112
113 let north = Self::is_connected(state.get_value(NORTH_REDSTONE));
114 let south = Self::is_connected(state.get_value(SOUTH_REDSTONE));
115 let east = Self::is_connected(state.get_value(EAST_REDSTONE));
116 let west = Self::is_connected(state.get_value(WEST_REDSTONE));
117 let north_south_empty = !north && !south;
118 let east_west_empty = !east && !west;
119
120 if !west && north_south_empty {
121 state = state.set_value(WEST_REDSTONE, RedstoneSide::Side);
122 }
123 if !east && north_south_empty {
124 state = state.set_value(EAST_REDSTONE, RedstoneSide::Side);
125 }
126 if !north && east_west_empty {
127 state = state.set_value(NORTH_REDSTONE, RedstoneSide::Side);
128 }
129 if !south && east_west_empty {
130 state = state.set_value(SOUTH_REDSTONE, RedstoneSide::Side);
131 }
132
133 state
134 }
135
136 fn get_missing_connections(
137 &self,
138 level: &dyn LevelReader,
139 mut state: BlockStateId,
140 pos: BlockPos,
141 ) -> BlockStateId {
142 let above_state = level.get_block_state(pos.above());
143 let can_connect_up = !is_redstone_conductor(level, above_state, pos);
146
147 for direction in Direction::HORIZONTAL {
148 let Some(property) = Self::property_for_direction(direction) else {
149 continue;
150 };
151 if !Self::is_connected(state.get_value(property)) {
152 state = state.set_value(
153 property,
154 self.get_connecting_side_with_up(level, pos, direction, can_connect_up),
155 );
156 }
157 }
158
159 state
160 }
161
162 fn get_connecting_side(
163 &self,
164 level: &dyn LevelReader,
165 pos: BlockPos,
166 direction: Direction,
167 ) -> RedstoneSide {
168 let above_pos = pos.above();
169 let can_connect_up = !is_redstone_conductor(level, level.get_block_state(above_pos), pos);
170 self.get_connecting_side_with_up(level, pos, direction, can_connect_up)
171 }
172
173 fn get_connecting_side_with_up(
175 &self,
176 level: &dyn LevelReader,
177 pos: BlockPos,
178 direction: Direction,
179 can_connect_up: bool,
180 ) -> RedstoneSide {
181 let relative_pos = pos.relative(direction);
182 let relative_state = level.get_block_state(relative_pos);
183
184 if can_connect_up {
185 let behavior = BLOCK_BEHAVIORS.get_behavior(relative_state.get_block());
186 let is_placeable_above =
187 behavior.is_trapdoor() || Self::can_survive_on(level, relative_pos, relative_state);
188 if is_placeable_above
189 && self.should_connect_to(level.get_block_state(relative_pos.above()), None)
190 {
191 if level.is_face_sturdy(relative_state, relative_pos, direction.opposite()) {
192 return RedstoneSide::Up;
193 }
194 return RedstoneSide::Side;
195 }
196 }
197
198 if !self.should_connect_to(relative_state, Some(direction))
199 && (is_redstone_conductor(level, relative_state, relative_pos)
200 || !self.should_connect_to(level.get_block_state(relative_pos.below()), None))
201 {
202 RedstoneSide::None
203 } else {
204 RedstoneSide::Side
205 }
206 }
207
208 fn should_connect_to(&self, state: BlockStateId, direction: Option<Direction>) -> bool {
209 if state.get_block() == self.block {
210 return true;
211 }
212 if state.get_block() == &vanilla_blocks::REPEATER {
213 let facing = state.get_value(HORIZONTAL_FACING);
214 return direction == Some(facing) || direction == Some(facing.opposite());
215 }
216 if state.get_block() == &vanilla_blocks::OBSERVER {
217 return direction == Some(state.get_value(FACING));
218 }
219
220 direction.is_some()
221 && BLOCK_BEHAVIORS
222 .get_behavior(state.get_block())
223 .is_signal_source(state, SignalQueryContext::DEFAULT)
224 }
225
226 fn can_survive_on(level: &dyn LevelReader, pos: BlockPos, state: BlockStateId) -> bool {
227 level.is_face_sturdy(state, pos, Direction::Up)
228 || state.get_block() == &vanilla_blocks::HOPPER
229 }
230
231 fn check_corner_change_at(&self, world: &Arc<World>, pos: BlockPos) {
232 if world.get_block_state(pos).get_block() != self.block {
233 return;
234 }
235
236 world.update_neighbors_at(pos, self.block);
237 for direction in Direction::ALL {
238 world.update_neighbors_at(pos.relative(direction), self.block);
239 }
240 }
241
242 fn update_neighbors_of_neighboring_wires(&self, world: &Arc<World>, pos: BlockPos) {
243 for direction in Direction::HORIZONTAL {
244 self.check_corner_change_at(world, pos.relative(direction));
245 }
246
247 for direction in Direction::HORIZONTAL {
248 let target = pos.relative(direction);
249 let target_state = world.get_block_state(target);
250 if is_redstone_conductor(world.as_ref(), target_state, target) {
251 self.check_corner_change_at(world, target.above());
252 } else {
253 self.check_corner_change_at(world, target.below());
254 }
255 }
256 }
257
258 fn updates_on_shape_change(
259 world: &Arc<World>,
260 pos: BlockPos,
261 old_state: BlockStateId,
262 new_state: BlockStateId,
263 ) {
264 for direction in Direction::HORIZONTAL {
265 let Some(property) = Self::property_for_direction(direction) else {
266 continue;
267 };
268 if Self::is_connected(old_state.get_value(property))
269 == Self::is_connected(new_state.get_value(property))
270 {
271 continue;
272 }
273
274 let relative_pos = pos.relative(direction);
275 let relative_state = world.get_block_state(relative_pos);
276 if is_redstone_conductor(world.as_ref(), relative_state, relative_pos) {
277 world.update_neighbors_at_except_from_facing(
278 relative_pos,
279 new_state.get_block(),
280 direction.opposite(),
281 );
282 }
283 }
284 }
285}
286
287impl BlockBehavior for RedStoneWireBlock {
288 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
289 Some(self.get_connection_state(
290 context.world.as_ref(),
291 self.cross_state,
292 context.place_pos(),
293 ))
294 }
295
296 fn update_shape(
297 &self,
298 state: BlockStateId,
299 world: &dyn ScheduledTickAccess,
300 pos: BlockPos,
301 direction: Direction,
302 neighbor_pos: BlockPos,
303 neighbor_state: BlockStateId,
304 ) -> BlockStateId {
305 if direction == Direction::Down {
306 return if Self::can_survive_on(world, neighbor_pos, neighbor_state) {
307 state
308 } else {
309 REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
310 };
311 }
312 if direction == Direction::Up {
313 return self.get_connection_state(world, state, pos);
314 }
315
316 let Some(property) = Self::property_for_direction(direction) else {
317 return state;
318 };
319 let side_connection = self.get_connecting_side(world, pos, direction);
320 if Self::is_connected(side_connection) == Self::is_connected(state.get_value(property))
321 && !Self::is_cross(state)
322 {
323 state.set_value(property, side_connection)
324 } else {
325 self.get_connection_state(
326 world,
327 self.cross_state
328 .set_value(POWER, state.get_value(POWER))
329 .set_value(property, side_connection),
330 pos,
331 )
332 }
333 }
334
335 fn update_indirect_neighbour_shapes(
336 &self,
337 state: BlockStateId,
338 world: &Arc<World>,
339 pos: BlockPos,
340 flags: UpdateFlags,
341 update_limit: i32,
342 ) {
343 for direction in Direction::HORIZONTAL {
344 let Some(property) = Self::property_for_direction(direction) else {
345 continue;
346 };
347 if !Self::is_connected(state.get_value(property)) {
348 continue;
349 }
350
351 let adjacent_pos = pos.relative(direction);
352 if world.get_block_state(adjacent_pos).get_block() == self.block {
353 continue;
354 }
355
356 let below_pos = adjacent_pos.below();
357 if world.get_block_state(below_pos).get_block() == self.block {
358 let neighbor_pos = below_pos.relative(direction.opposite());
359 world.neighbor_shape_changed(
360 direction.opposite(),
361 below_pos,
362 neighbor_pos,
363 world.get_block_state(neighbor_pos),
364 flags,
365 update_limit,
366 );
367 }
368
369 let above_pos = adjacent_pos.above();
370 if world.get_block_state(above_pos).get_block() == self.block {
371 let neighbor_pos = above_pos.relative(direction.opposite());
372 world.neighbor_shape_changed(
373 direction.opposite(),
374 above_pos,
375 neighbor_pos,
376 world.get_block_state(neighbor_pos),
377 flags,
378 update_limit,
379 );
380 }
381 }
382 }
383
384 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
385 let below_pos = pos.below();
386 Self::can_survive_on(world, below_pos, world.get_block_state(below_pos))
387 }
388
389 fn on_place(
390 &self,
391 state: BlockStateId,
392 world: &Arc<World>,
393 pos: BlockPos,
394 old_state: BlockStateId,
395 _moved_by_piston: bool,
396 ) {
397 if old_state.get_block() == self.block {
398 return;
399 }
400
401 self.evaluator.update_power_strength(world, pos, state);
402 for direction in [Direction::Down, Direction::Up] {
403 world.update_neighbors_at(pos.relative(direction), self.block);
404 }
405 self.update_neighbors_of_neighboring_wires(world, pos);
406 }
407
408 fn affect_neighbors_after_removal(
409 &self,
410 state: BlockStateId,
411 world: &Arc<World>,
412 pos: BlockPos,
413 moved_by_piston: bool,
414 ) {
415 if moved_by_piston {
416 return;
417 }
418
419 for direction in Direction::ALL {
420 world.update_neighbors_at(pos.relative(direction), self.block);
421 }
422 self.evaluator.update_power_strength(world, pos, state);
423 self.update_neighbors_of_neighboring_wires(world, pos);
424 }
425
426 fn handle_neighbor_changed(
427 &self,
428 state: BlockStateId,
429 world: &Arc<World>,
430 pos: BlockPos,
431 _source_block: BlockRef,
432 _moved_by_piston: bool,
433 ) {
434 if self.can_survive(state, world.as_ref(), pos) {
437 self.evaluator.update_power_strength(world, pos, state);
438 } else {
439 world.drop_resources(state, pos);
440 world.remove_block(pos, false);
441 }
442 }
443
444 fn use_without_item(
445 &self,
446 state: BlockStateId,
447 world: &Arc<World>,
448 pos: BlockPos,
449 player: &Player,
450 _hit_result: &BlockHitResult,
451 _inv: &mut InventoryAccess,
452 ) -> InteractionResult {
453 if !player.abilities.lock().may_build {
454 return InteractionResult::Pass;
455 }
456 if !Self::is_cross(state) && !Self::is_dot(state) {
457 return InteractionResult::Pass;
458 }
459
460 let new_base_state = if Self::is_cross(state) {
461 self.block.default_state()
462 } else {
463 self.cross_state
464 };
465 let new_state = self.get_connection_state(
466 world.as_ref(),
467 new_base_state.set_value(POWER, state.get_value(POWER)),
468 pos,
469 );
470 if new_state == state {
471 return InteractionResult::Pass;
472 }
473
474 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
475 Self::updates_on_shape_change(world, pos, state, new_state);
476 InteractionResult::Success
477 }
478
479 fn is_signal_source(&self, _state: BlockStateId, context: SignalQueryContext) -> bool {
480 context.wire_signals_enabled()
481 }
482
483 fn get_own_signal(
484 &self,
485 state: BlockStateId,
486 _world: &dyn LevelReader,
487 _pos: BlockPos,
488 _context: SignalQueryContext,
489 ) -> i32 {
490 i32::from(state.get_value(POWER))
492 }
493
494 fn get_signal(
495 &self,
496 state: BlockStateId,
497 world: &dyn LevelReader,
498 pos: BlockPos,
499 direction: Direction,
500 context: SignalQueryContext,
501 ) -> i32 {
502 if !context.wire_signals_enabled() || direction == Direction::Down {
503 return MIN_REDSTONE_SIGNAL;
504 }
505
506 let power = self.get_own_signal(state, world, pos, context);
507 if power == MIN_REDSTONE_SIGNAL {
508 return MIN_REDSTONE_SIGNAL;
509 }
510 if direction == Direction::Up {
511 return power;
512 }
513
514 let Some(property) = Self::property_for_direction(direction.opposite()) else {
515 return MIN_REDSTONE_SIGNAL;
516 };
517 if Self::is_connected(
518 self.get_connection_state(world, state, pos)
519 .get_value(property),
520 ) {
521 power
522 } else {
523 MIN_REDSTONE_SIGNAL
524 }
525 }
526
527 fn get_direct_signal(
528 &self,
529 state: BlockStateId,
530 world: &dyn LevelReader,
531 pos: BlockPos,
532 direction: Direction,
533 context: SignalQueryContext,
534 ) -> i32 {
535 if context.wire_signals_enabled() {
536 self.get_signal(state, world, pos, direction, context)
537 } else {
538 MIN_REDSTONE_SIGNAL
539 }
540 }
541
542 }
545
546#[cfg(test)]
547mod tests {
548 use steel_registry::init_vanilla_registry;
549
550 use super::*;
551 use crate::behavior::init_behaviors;
552 use crate::test_support::TestLevel;
553
554 fn wire() -> RedStoneWireBlock {
555 init_vanilla_registry();
556 init_behaviors();
557 RedStoneWireBlock::new(&vanilla_blocks::REDSTONE_WIRE)
558 }
559
560 #[test]
561 fn isolated_cross_and_dot_preserve_their_vanilla_shapes() {
562 let behavior = wire();
563 let level = TestLevel::default().with_block(
564 BlockPos::new(0, 63, 0),
565 vanilla_blocks::STONE.default_state(),
566 );
567 let pos = BlockPos::new(0, 64, 0);
568
569 let cross = behavior.get_connection_state(&level, behavior.cross_state, pos);
570 let dot = behavior.get_connection_state(
571 &level,
572 vanilla_blocks::REDSTONE_WIRE.default_state(),
573 pos,
574 );
575
576 assert!(RedStoneWireBlock::is_cross(cross));
577 assert!(RedStoneWireBlock::is_dot(dot));
578 }
579
580 #[test]
581 fn wire_climbs_sturdy_neighbor_only_when_above_is_connectable() {
582 let behavior = wire();
583 let pos = BlockPos::new(0, 64, 0);
584 let level = TestLevel::default()
585 .with_block(pos.below(), vanilla_blocks::STONE.default_state())
586 .with_block(pos.east(), vanilla_blocks::STONE.default_state())
587 .with_block(pos.east().above(), behavior.block.default_state());
588
589 assert_eq!(
590 behavior.get_connecting_side(&level, pos, Direction::East),
591 RedstoneSide::Up
592 );
593
594 level.set_test_block(pos.above(), vanilla_blocks::STONE.default_state());
595 assert_eq!(
596 behavior.get_connecting_side(&level, pos, Direction::East),
597 RedstoneSide::None
598 );
599 }
600
601 #[test]
602 fn powered_wire_signal_follows_recomputed_connections() {
603 let behavior = wire();
604 let pos = BlockPos::new(0, 64, 0);
605 let state = behavior
606 .block
607 .default_state()
608 .set_value(POWER, 9)
609 .set_value(NORTH_REDSTONE, RedstoneSide::Side)
610 .set_value(EAST_REDSTONE, RedstoneSide::Side);
611 let level = TestLevel::default()
612 .with_block(pos.below(), vanilla_blocks::STONE.default_state())
613 .with_block(pos.north(), behavior.block.default_state())
614 .with_block(pos.east(), behavior.block.default_state());
615
616 assert_eq!(
617 behavior.get_signal(
618 state,
619 &level,
620 pos,
621 Direction::West,
622 SignalQueryContext::DEFAULT,
623 ),
624 9
625 );
626 assert_eq!(
627 behavior.get_signal(
628 state,
629 &level,
630 pos,
631 Direction::East,
632 SignalQueryContext::DEFAULT,
633 ),
634 0
635 );
636 assert_eq!(
637 behavior.get_signal(
638 state,
639 &level,
640 pos,
641 Direction::Up,
642 SignalQueryContext::without_wire_signals(),
643 ),
644 0
645 );
646 }
647}