1use std::cmp::Ordering;
6use std::f64::consts::PI;
7use std::sync::{Arc, Weak};
8
9use steel_macros::block_behavior;
10use steel_registry::REGISTRY;
11use steel_registry::block_entity_type::BlockEntityTypeRef;
12use steel_registry::blocks::BlockRef;
13use steel_registry::blocks::block_state_ext::BlockStateExt;
14use steel_registry::blocks::properties::{BlockStateProperties, Direction};
15use steel_registry::blocks::shapes::SupportType;
16use steel_registry::{vanilla_block_entity_types, vanilla_blocks};
17use steel_utils::{BlockPos, BlockStateId, Downcast as _};
18
19use crate::behavior::InventoryAccess;
20use crate::behavior::block::{
21 BlockBehavior, BlockEntityCreation, schedule_water_tick_if_waterlogged,
22};
23use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
24use crate::block_entity::{BlockEntityTicker, entities::SignBlockEntity};
25use crate::entity::Entity;
26use crate::player::Player;
27use crate::world::{LevelReader, ScheduledTickAccess, World};
28
29fn convert_to_rotation_segment(degrees: f32) -> u8 {
34 let normalized = degrees.rem_euclid(360.0);
36 (((normalized / 22.5) + 0.5) as u8) & 15
39}
40
41fn get_nearest_looking_directions(rotation: f32, clicked_face: Direction) -> Vec<Direction> {
45 let mut directions = Vec::with_capacity(4);
49
50 let all_horizontal = [
52 Direction::North,
53 Direction::East,
54 Direction::South,
55 Direction::West,
56 ];
57
58 let mut scored: Vec<(Direction, f32)> = all_horizontal
60 .iter()
61 .map(|&dir| {
62 let dir_angle = dir.to_yaw();
63 let diff = (rotation - dir_angle + 180.0).rem_euclid(360.0) - 180.0;
64 (dir, diff.abs())
65 })
66 .collect();
67
68 scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
69
70 for (dir, _) in scored {
71 directions.push(dir);
72 }
73
74 if clicked_face.is_horizontal() {
76 let opposite = clicked_face.opposite();
77 if let Some(pos) = directions.iter().position(|&d| d == opposite) {
78 directions.remove(pos);
79 directions.insert(0, opposite);
80 }
81 }
82
83 directions
84}
85
86fn is_facing_front_text(state: BlockStateId, pos: BlockPos, player: &Player) -> bool {
91 let sign_y_rot = get_sign_rotation_degrees(state);
93
94 let player_pos = player.position();
96 let dx = player_pos.x - (f64::from(pos.0.x) + 0.5);
97 let dz = player_pos.z - (f64::from(pos.0.z) + 0.5);
98
99 let player_angle = (dz.atan2(dx) * 180.0 / PI) as f32 - 90.0;
101
102 let diff = (sign_y_rot - player_angle + 180.0).rem_euclid(360.0) - 180.0;
104 diff.abs() <= 90.0
105}
106
107fn get_sign_rotation_degrees(state: BlockStateId) -> f32 {
109 if let Some(rotation) = state.try_get_value(&BlockStateProperties::ROTATION_16) {
111 return f32::from(rotation) * 22.5;
112 }
113
114 if let Some(facing) = state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING) {
116 return facing.to_yaw();
117 }
118
119 0.0
120}
121
122fn can_support_standing_sign(world: &dyn LevelReader, pos: BlockPos) -> bool {
127 let below_pos = BlockPos::new(pos.x(), pos.y() - 1, pos.z());
128 let below_state = world.get_block_state(below_pos);
129 below_state.is_solid()
130}
131
132fn can_wall_sign_survive(world: &dyn LevelReader, pos: BlockPos, facing: Direction) -> bool {
137 let behind_pos = facing.opposite().relative(pos);
139 let behind_state = world.get_block_state(behind_pos);
140 behind_state.is_solid()
141}
142
143fn can_ceiling_hanging_sign_survive(world: &dyn LevelReader, pos: BlockPos) -> bool {
145 let above_pos = BlockPos::new(pos.x(), pos.y() + 1, pos.z());
146 let above_state = world.get_block_state(above_pos);
147 world.is_face_sturdy_for(above_state, above_pos, Direction::Down, SupportType::Center)
148}
149
150fn can_attach_to(
156 world: &dyn LevelReader,
157 sign_facing: Direction,
158 attach_pos: BlockPos,
159 attach_face: Direction,
160) -> bool {
161 let attach_state = world.get_block_state(attach_pos);
162 let attach_block = REGISTRY.blocks.by_state_id(attach_state);
163
164 if let Some(block) = attach_block
166 && block.key.path.contains("wall_hanging_sign")
167 {
168 if let Some(neighbor_facing) =
170 attach_state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING)
171 {
172 return neighbor_facing.axis() == sign_facing.axis();
173 }
174 }
175
176 world.is_face_sturdy_for(attach_state, attach_pos, attach_face, SupportType::Full)
178}
179
180fn can_wall_hanging_sign_survive(
185 world: &dyn LevelReader,
186 pos: BlockPos,
187 facing: Direction,
188) -> bool {
189 let clockwise = facing.rotate_y_clockwise();
190 let counter_clockwise = facing.rotate_y_counter_clockwise();
191
192 let can_attach_clockwise = {
193 let attach_pos = clockwise.relative(pos);
194 can_attach_to(world, facing, attach_pos, counter_clockwise)
195 };
196
197 let can_attach_counter = {
198 let attach_pos = counter_clockwise.relative(pos);
199 can_attach_to(world, facing, attach_pos, clockwise)
200 };
201
202 can_attach_clockwise || can_attach_counter
203}
204
205fn try_open_sign_editor(
237 state: BlockStateId,
238 world: &Arc<World>,
239 pos: BlockPos,
240 player: &Player,
241) -> InteractionResult {
242 let Some(block_entity) = world.get_block_entity(pos) else {
244 return InteractionResult::Pass;
245 };
246
247 let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>() else {
248 return InteractionResult::Pass;
249 };
250
251 if sign.is_waxed() {
253 return InteractionResult::Success; }
256
257 if sign.is_other_player_editing(player.gameprofile.id) {
259 return InteractionResult::Pass;
260 }
261
262 let is_front_text = is_facing_front_text(state, pos, player);
270
271 sign.set_player_who_may_edit(Some(player.gameprofile.id));
273
274 player.open_sign_editor(pos, is_front_text);
276 InteractionResult::Success
277}
278
279#[block_behavior]
281pub struct StandingSignBlock {
282 block: BlockRef,
283}
284
285impl StandingSignBlock {
286 #[must_use]
288 pub const fn new(block: BlockRef) -> Self {
289 Self { block }
290 }
291}
292
293impl BlockBehavior for StandingSignBlock {
294 fn update_shape(
295 &self,
296 state: BlockStateId,
297 world: &dyn ScheduledTickAccess,
298 pos: BlockPos,
299 direction: Direction,
300 _neighbor_pos: BlockPos,
301 _neighbor_state: BlockStateId,
302 ) -> BlockStateId {
303 if direction == Direction::Down && !can_support_standing_sign(world, pos) {
305 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
306 }
307 schedule_water_tick_if_waterlogged(state, world, pos);
308 state
309 }
310
311 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
312 if !can_support_standing_sign(context.world, context.place_pos()) {
314 return None;
315 }
316
317 let rotation = convert_to_rotation_segment(context.rotation() + 180.0);
320
321 Some(
322 self.block
323 .default_state()
324 .set_value(&BlockStateProperties::ROTATION_16, rotation),
325 )
326 }
327
328 fn new_block_entity(
329 &self,
330 level: Weak<World>,
331 pos: BlockPos,
332 state: BlockStateId,
333 ) -> BlockEntityCreation {
334 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new(level, pos, state)))
335 }
336
337 fn get_block_entity_ticker(
338 &self,
339 _world: &Arc<World>,
340 _state: BlockStateId,
341 block_entity_type: BlockEntityTypeRef,
342 ) -> Option<BlockEntityTicker> {
343 BlockEntityTicker::for_matching_entity_tick(
344 block_entity_type,
345 &vanilla_block_entity_types::SIGN,
346 )
347 }
348
349 fn use_without_item(
350 &self,
351 state: BlockStateId,
352 world: &Arc<World>,
353 pos: BlockPos,
354 player: &Player,
355 _hit_result: &BlockHitResult,
356 _inv: &mut InventoryAccess,
357 ) -> InteractionResult {
358 try_open_sign_editor(state, world, pos, player)
359 }
360}
361
362#[block_behavior]
364pub struct WallSignBlock {
365 block: BlockRef,
366}
367
368impl WallSignBlock {
369 #[must_use]
371 pub const fn new(block: BlockRef) -> Self {
372 Self { block }
373 }
374}
375
376impl BlockBehavior for WallSignBlock {
377 fn update_shape(
378 &self,
379 state: BlockStateId,
380 world: &dyn ScheduledTickAccess,
381 pos: BlockPos,
382 direction: Direction,
383 _neighbor_pos: BlockPos,
384 _neighbor_state: BlockStateId,
385 ) -> BlockStateId {
386 if let Some(facing) = state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING)
389 && direction.opposite() == facing
390 && !can_wall_sign_survive(world, pos, facing)
391 {
392 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
393 }
394 schedule_water_tick_if_waterlogged(state, world, pos);
395 state
396 }
397
398 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
399 let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
401
402 for direction in directions {
403 let facing = direction.opposite();
405
406 if can_wall_sign_survive(context.world, context.place_pos(), facing) {
408 return Some(
409 self.block
410 .default_state()
411 .set_value(&BlockStateProperties::HORIZONTAL_FACING, facing),
412 );
413 }
414 }
415
416 None
418 }
419
420 fn new_block_entity(
421 &self,
422 level: Weak<World>,
423 pos: BlockPos,
424 state: BlockStateId,
425 ) -> BlockEntityCreation {
426 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new(level, pos, state)))
427 }
428
429 fn get_block_entity_ticker(
430 &self,
431 _world: &Arc<World>,
432 _state: BlockStateId,
433 block_entity_type: BlockEntityTypeRef,
434 ) -> Option<BlockEntityTicker> {
435 BlockEntityTicker::for_matching_entity_tick(
436 block_entity_type,
437 &vanilla_block_entity_types::SIGN,
438 )
439 }
440
441 fn use_without_item(
442 &self,
443 state: BlockStateId,
444 world: &Arc<World>,
445 pos: BlockPos,
446 player: &Player,
447 _hit_result: &BlockHitResult,
448 _inv: &mut InventoryAccess,
449 ) -> InteractionResult {
450 try_open_sign_editor(state, world, pos, player)
451 }
452}
453
454#[block_behavior]
456pub struct CeilingHangingSignBlock {
457 block: BlockRef,
458}
459
460impl CeilingHangingSignBlock {
461 #[must_use]
463 pub const fn new(block: BlockRef) -> Self {
464 Self { block }
465 }
466}
467
468impl BlockBehavior for CeilingHangingSignBlock {
469 fn update_shape(
470 &self,
471 state: BlockStateId,
472 world: &dyn ScheduledTickAccess,
473 pos: BlockPos,
474 direction: Direction,
475 _neighbor_pos: BlockPos,
476 _neighbor_state: BlockStateId,
477 ) -> BlockStateId {
478 if direction == Direction::Up && !can_ceiling_hanging_sign_survive(world, pos) {
480 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
481 }
482 schedule_water_tick_if_waterlogged(state, world, pos);
483 state
484 }
485
486 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
487 if !can_ceiling_hanging_sign_survive(context.world, context.place_pos()) {
489 return None;
490 }
491
492 let above_pos = BlockPos::new(
493 context.place_pos().x(),
494 context.place_pos().y() + 1,
495 context.place_pos().z(),
496 );
497 let above_state = context.world.get_block_state(above_pos);
498
499 let direction = Direction::from_yaw(context.rotation());
501 let is_above_full = context.world.is_face_sturdy_for(
502 above_state,
503 above_pos,
504 Direction::Down,
505 SupportType::Full,
506 );
507
508 let above_block = REGISTRY.blocks.by_state_id(above_state);
510 let is_below_hanging_sign =
511 above_block.is_some_and(|b| b.key.path.contains("hanging_sign"));
512
513 let attached_to_middle = if is_below_hanging_sign {
515 if let Some(above_facing) =
517 above_state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING)
518 {
519 above_facing.axis() != direction.axis()
521 } else if let Some(above_rotation) =
522 above_state.try_get_value(&BlockStateProperties::ROTATION_16)
523 {
524 let above_direction = rotation_to_direction(above_rotation);
526 above_direction.is_none_or(|d| d.axis() != direction.axis())
527 } else {
528 !is_above_full
529 }
530 } else {
531 !is_above_full
532 };
533
534 let rotation = if attached_to_middle {
536 convert_to_rotation_segment(context.rotation() + 180.0)
538 } else {
539 convert_to_rotation_segment(direction.opposite().to_yaw())
541 };
542
543 Some(
544 self.block
545 .default_state()
546 .set_value(&BlockStateProperties::ROTATION_16, rotation)
547 .set_value(&BlockStateProperties::ATTACHED, attached_to_middle),
548 )
549 }
550
551 fn new_block_entity(
552 &self,
553 level: Weak<World>,
554 pos: BlockPos,
555 state: BlockStateId,
556 ) -> BlockEntityCreation {
557 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new_hanging(level, pos, state)))
558 }
559
560 fn get_block_entity_ticker(
561 &self,
562 _world: &Arc<World>,
563 _state: BlockStateId,
564 block_entity_type: BlockEntityTypeRef,
565 ) -> Option<BlockEntityTicker> {
566 BlockEntityTicker::for_matching_entity_tick(
567 block_entity_type,
568 &vanilla_block_entity_types::HANGING_SIGN,
569 )
570 }
571
572 fn use_without_item(
573 &self,
574 state: BlockStateId,
575 world: &Arc<World>,
576 pos: BlockPos,
577 player: &Player,
578 _hit_result: &BlockHitResult,
579 _inv: &mut InventoryAccess,
580 ) -> InteractionResult {
581 try_open_sign_editor(state, world, pos, player)
582 }
583}
584
585const fn rotation_to_direction(rotation: u8) -> Option<Direction> {
587 match rotation {
588 0 => Some(Direction::South),
589 4 => Some(Direction::West),
590 8 => Some(Direction::North),
591 12 => Some(Direction::East),
592 _ => None,
593 }
594}
595
596#[block_behavior]
598pub struct WallHangingSignBlock {
599 block: BlockRef,
600}
601
602impl WallHangingSignBlock {
603 #[must_use]
605 pub const fn new(block: BlockRef) -> Self {
606 Self { block }
607 }
608}
609
610impl BlockBehavior for WallHangingSignBlock {
611 fn update_shape(
612 &self,
613 state: BlockStateId,
614 world: &dyn ScheduledTickAccess,
615 pos: BlockPos,
616 direction: Direction,
617 _neighbor_pos: BlockPos,
618 _neighbor_state: BlockStateId,
619 ) -> BlockStateId {
620 if let Some(facing) = state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING) {
623 if direction.axis() == facing.rotate_y_clockwise().axis()
625 && !can_wall_hanging_sign_survive(world, pos, facing)
626 {
627 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
628 }
629 }
630 schedule_water_tick_if_waterlogged(state, world, pos);
631 state
632 }
633
634 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
635 let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
637
638 for direction in directions {
639 if direction.axis() == context.clicked_face().axis() {
642 continue;
643 }
644
645 let facing = direction.opposite();
646
647 if can_wall_hanging_sign_survive(context.world, context.place_pos(), facing) {
649 return Some(
650 self.block
651 .default_state()
652 .set_value(&BlockStateProperties::HORIZONTAL_FACING, facing),
653 );
654 }
655 }
656
657 None
659 }
660
661 fn new_block_entity(
662 &self,
663 level: Weak<World>,
664 pos: BlockPos,
665 state: BlockStateId,
666 ) -> BlockEntityCreation {
667 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new_hanging(level, pos, state)))
668 }
669
670 fn get_block_entity_ticker(
671 &self,
672 _world: &Arc<World>,
673 _state: BlockStateId,
674 block_entity_type: BlockEntityTypeRef,
675 ) -> Option<BlockEntityTicker> {
676 BlockEntityTicker::for_matching_entity_tick(
677 block_entity_type,
678 &vanilla_block_entity_types::HANGING_SIGN,
679 )
680 }
681
682 fn use_without_item(
683 &self,
684 state: BlockStateId,
685 world: &Arc<World>,
686 pos: BlockPos,
687 player: &Player,
688 _hit_result: &BlockHitResult,
689 _inv: &mut InventoryAccess,
690 ) -> InteractionResult {
691 try_open_sign_editor(state, world, pos, player)
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use steel_registry::init_vanilla_registry;
698
699 use super::*;
700 use crate::test_support::{TestLevel, fresh_test_world};
701
702 #[test]
703 fn standing_sign_only_schedules_water_when_support_survives() {
704 init_vanilla_registry();
705 let pos = BlockPos::new(0, 64, 0);
706 let sign = StandingSignBlock::new(&vanilla_blocks::OAK_SIGN);
707 let state = vanilla_blocks::OAK_SIGN
708 .default_state()
709 .set_value(&BlockStateProperties::WATERLOGGED, true);
710 let supported =
711 TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
712
713 assert_eq!(
714 sign.update_shape(
715 state,
716 &supported,
717 pos,
718 Direction::North,
719 pos.north(),
720 vanilla_blocks::AIR.default_state(),
721 ),
722 state
723 );
724 assert!(supported.scheduled_water_tick());
725
726 let unsupported = TestLevel::default();
727 assert!(
728 sign.update_shape(
729 state,
730 &unsupported,
731 pos,
732 Direction::Down,
733 pos.below(),
734 vanilla_blocks::AIR.default_state(),
735 )
736 .is_air()
737 );
738 assert!(!unsupported.scheduled_water_tick());
739 }
740
741 #[test]
742 fn sign_variants_select_their_matching_vanilla_tickers() {
743 init_vanilla_registry();
744 let world = fresh_test_world("sign_ticker_selection");
745
746 let standing = StandingSignBlock::new(&vanilla_blocks::OAK_SIGN);
747 assert!(
748 standing
749 .get_block_entity_ticker(
750 &world,
751 vanilla_blocks::OAK_SIGN.default_state(),
752 &vanilla_block_entity_types::SIGN,
753 )
754 .is_some()
755 );
756
757 let wall = WallSignBlock::new(&vanilla_blocks::OAK_WALL_SIGN);
758 assert!(
759 wall.get_block_entity_ticker(
760 &world,
761 vanilla_blocks::OAK_WALL_SIGN.default_state(),
762 &vanilla_block_entity_types::SIGN,
763 )
764 .is_some()
765 );
766
767 let ceiling_hanging = CeilingHangingSignBlock::new(&vanilla_blocks::OAK_HANGING_SIGN);
768 assert!(
769 ceiling_hanging
770 .get_block_entity_ticker(
771 &world,
772 vanilla_blocks::OAK_HANGING_SIGN.default_state(),
773 &vanilla_block_entity_types::HANGING_SIGN,
774 )
775 .is_some()
776 );
777
778 let wall_hanging = WallHangingSignBlock::new(&vanilla_blocks::OAK_WALL_HANGING_SIGN);
779 assert!(
780 wall_hanging
781 .get_block_entity_ticker(
782 &world,
783 vanilla_blocks::OAK_WALL_HANGING_SIGN.default_state(),
784 &vanilla_block_entity_types::HANGING_SIGN,
785 )
786 .is_some()
787 );
788 assert!(
789 wall_hanging
790 .get_block_entity_ticker(
791 &world,
792 vanilla_blocks::OAK_WALL_HANGING_SIGN.default_state(),
793 &vanilla_block_entity_types::SIGN,
794 )
795 .is_none()
796 );
797 }
798}