1use std::cmp::Ordering;
6use std::sync::{Arc, Weak};
7
8use steel_macros::block_behavior;
9use steel_math::{DEGREE_90, DEGREE_180, DEGREE_360, RAD_TO_DEG_F64, convert_to_rotation_segment};
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::{
15 BlockStateProperties, BoolProperty, Direction, EnumProperty, IntProperty,
16};
17use steel_registry::blocks::shapes::SupportType;
18use steel_registry::{vanilla_block_entity_types, vanilla_blocks};
19use steel_utils::{BlockPos, BlockStateId, Downcast as _};
20
21use crate::behavior::InventoryAccess;
22use crate::behavior::block::{
23 BlockBehavior, BlockEntityCreation, schedule_water_tick_if_waterlogged,
24};
25use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
26use crate::block_entity::{BlockEntityTicker, entities::SignBlockEntity};
27use crate::entity::Entity;
28use crate::player::Player;
29use crate::world::{LevelReader, ScheduledTickAccess, World};
30
31fn get_nearest_looking_directions(rotation: f32, clicked_face: Direction) -> Vec<Direction> {
35 let mut directions = Vec::with_capacity(4);
39
40 let all_horizontal = [
42 Direction::North,
43 Direction::East,
44 Direction::South,
45 Direction::West,
46 ];
47
48 let mut scored: Vec<(Direction, f32)> = all_horizontal
50 .iter()
51 .map(|&dir| {
52 let dir_angle = dir.to_yaw();
53 let diff = (rotation - dir_angle + DEGREE_180).rem_euclid(DEGREE_360) - DEGREE_180;
54 (dir, diff.abs())
55 })
56 .collect();
57
58 scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
59
60 for (dir, _) in scored {
61 directions.push(dir);
62 }
63
64 if clicked_face.is_horizontal() {
66 let opposite = clicked_face.opposite();
67 if let Some(pos) = directions.iter().position(|&d| d == opposite) {
68 directions.remove(pos);
69 directions.insert(0, opposite);
70 }
71 }
72
73 directions
74}
75
76pub fn is_facing_front_text(state: BlockStateId, pos: BlockPos, player: &Player) -> bool {
81 let sign_y_rot = get_sign_rotation_degrees(state);
83
84 let player_pos = player.position();
86 let dx = player_pos.x - (f64::from(pos.0.x) + 0.5);
87 let dz = player_pos.z - (f64::from(pos.0.z) + 0.5);
88
89 let player_angle = (dz.atan2(dx) * RAD_TO_DEG_F64) as f32 - DEGREE_90;
91
92 let diff = (sign_y_rot - player_angle + DEGREE_180).rem_euclid(DEGREE_360) - DEGREE_180;
94 diff.abs() <= DEGREE_90
95}
96
97fn get_sign_rotation_degrees(state: BlockStateId) -> f32 {
99 if let Some(rotation) = state.try_get_value(ROTATION_16) {
101 return f32::from(rotation) * 22.5;
102 }
103
104 if let Some(facing) = state.try_get_value(HORIZONTAL_FACING) {
106 return facing.to_yaw();
107 }
108
109 0.0
110}
111
112fn can_support_standing_sign(world: &dyn LevelReader, pos: BlockPos) -> bool {
117 let below_pos = BlockPos::new(pos.x(), pos.y() - 1, pos.z());
118 let below_state = world.get_block_state(below_pos);
119 below_state.is_solid()
120}
121
122fn can_wall_sign_survive(world: &dyn LevelReader, pos: BlockPos, facing: Direction) -> bool {
127 let behind_pos = facing.opposite().relative(pos);
129 let behind_state = world.get_block_state(behind_pos);
130 behind_state.is_solid()
131}
132
133fn can_ceiling_hanging_sign_survive(world: &dyn LevelReader, pos: BlockPos) -> bool {
135 let above_pos = BlockPos::new(pos.x(), pos.y() + 1, pos.z());
136 let above_state = world.get_block_state(above_pos);
137 world.is_face_sturdy_for(above_state, above_pos, Direction::Down, SupportType::Center)
138}
139
140fn can_attach_to(
146 world: &dyn LevelReader,
147 sign_facing: Direction,
148 attach_pos: BlockPos,
149 attach_face: Direction,
150) -> bool {
151 let attach_state = world.get_block_state(attach_pos);
152 let attach_block = REGISTRY.blocks.by_state_id(attach_state);
153
154 if let Some(block) = attach_block
156 && block.key.path.contains("wall_hanging_sign")
157 {
158 if let Some(neighbor_facing) = attach_state.try_get_value(HORIZONTAL_FACING) {
160 return neighbor_facing.axis() == sign_facing.axis();
161 }
162 }
163
164 world.is_face_sturdy_for(attach_state, attach_pos, attach_face, SupportType::Full)
166}
167
168fn can_wall_hanging_sign_survive(
173 world: &dyn LevelReader,
174 pos: BlockPos,
175 facing: Direction,
176) -> bool {
177 let clockwise = facing.rotate_y_clockwise();
178 let counter_clockwise = facing.rotate_y_counter_clockwise();
179
180 let can_attach_clockwise = {
181 let attach_pos = clockwise.relative(pos);
182 can_attach_to(world, facing, attach_pos, counter_clockwise)
183 };
184
185 let can_attach_counter = {
186 let attach_pos = counter_clockwise.relative(pos);
187 can_attach_to(world, facing, attach_pos, clockwise)
188 };
189
190 can_attach_clockwise || can_attach_counter
191}
192
193fn try_open_sign_editor(
225 state: BlockStateId,
226 world: &Arc<World>,
227 pos: BlockPos,
228 player: &Player,
229) -> InteractionResult {
230 let Some(block_entity) = world.get_block_entity(pos) else {
232 return InteractionResult::Pass;
233 };
234
235 let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>() else {
236 return InteractionResult::Pass;
237 };
238
239 if sign.is_waxed() {
241 return InteractionResult::Success; }
244
245 if sign.is_other_player_editing(player.gameprofile.id) {
247 return InteractionResult::Pass;
248 }
249
250 let is_front_text = is_facing_front_text(state, pos, player);
258
259 sign.set_player_who_may_edit(Some(player.gameprofile.id));
261
262 player.open_sign_editor(pos, is_front_text);
264 InteractionResult::Success
265}
266
267#[block_behavior]
269pub struct StandingSignBlock {
270 block: BlockRef,
271}
272
273const ATTACHED: &BoolProperty = &BlockStateProperties::ATTACHED;
274const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
275const ROTATION_16: &IntProperty = &BlockStateProperties::ROTATION_16;
276
277impl StandingSignBlock {
278 #[must_use]
280 pub const fn new(block: BlockRef) -> Self {
281 Self { block }
282 }
283}
284
285impl BlockBehavior for StandingSignBlock {
286 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
287 true
288 }
289
290 fn update_shape(
291 &self,
292 state: BlockStateId,
293 world: &dyn ScheduledTickAccess,
294 pos: BlockPos,
295 direction: Direction,
296 _neighbor_pos: BlockPos,
297 _neighbor_state: BlockStateId,
298 ) -> BlockStateId {
299 if direction == Direction::Down && !can_support_standing_sign(world, pos) {
301 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
302 }
303 schedule_water_tick_if_waterlogged(state, world, pos);
304 state
305 }
306
307 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
308 if !can_support_standing_sign(context.world, context.place_pos()) {
310 return None;
311 }
312
313 let rotation = convert_to_rotation_segment(context.rotation() + DEGREE_180);
316
317 Some(self.block.default_state().set_value(ROTATION_16, rotation))
318 }
319
320 fn new_block_entity(
321 &self,
322 level: Weak<World>,
323 pos: BlockPos,
324 state: BlockStateId,
325 ) -> BlockEntityCreation {
326 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new(level, pos, state)))
327 }
328
329 fn get_block_entity_ticker(
330 &self,
331 _world: &Arc<World>,
332 _state: BlockStateId,
333 block_entity_type: BlockEntityTypeRef,
334 ) -> Option<BlockEntityTicker> {
335 BlockEntityTicker::for_matching_entity_tick(
336 block_entity_type,
337 &vanilla_block_entity_types::SIGN,
338 )
339 }
340
341 fn use_without_item(
342 &self,
343 state: BlockStateId,
344 world: &Arc<World>,
345 pos: BlockPos,
346 player: &Player,
347 _hit_result: &BlockHitResult,
348 _inv: &mut InventoryAccess,
349 ) -> InteractionResult {
350 try_open_sign_editor(state, world, pos, player)
351 }
352}
353
354#[block_behavior]
356pub struct WallSignBlock {
357 block: BlockRef,
358}
359
360impl WallSignBlock {
361 #[must_use]
363 pub const fn new(block: BlockRef) -> Self {
364 Self { block }
365 }
366}
367
368impl BlockBehavior for WallSignBlock {
369 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
370 true
371 }
372
373 fn update_shape(
374 &self,
375 state: BlockStateId,
376 world: &dyn ScheduledTickAccess,
377 pos: BlockPos,
378 direction: Direction,
379 _neighbor_pos: BlockPos,
380 _neighbor_state: BlockStateId,
381 ) -> BlockStateId {
382 if let Some(facing) = state.try_get_value(HORIZONTAL_FACING)
385 && direction.opposite() == facing
386 && !can_wall_sign_survive(world, pos, facing)
387 {
388 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
389 }
390 schedule_water_tick_if_waterlogged(state, world, pos);
391 state
392 }
393
394 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
395 let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
397
398 for direction in directions {
399 let facing = direction.opposite();
401
402 if can_wall_sign_survive(context.world, context.place_pos(), facing) {
404 return Some(
405 self.block
406 .default_state()
407 .set_value(HORIZONTAL_FACING, facing),
408 );
409 }
410 }
411
412 None
414 }
415
416 fn new_block_entity(
417 &self,
418 level: Weak<World>,
419 pos: BlockPos,
420 state: BlockStateId,
421 ) -> BlockEntityCreation {
422 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new(level, pos, state)))
423 }
424
425 fn get_block_entity_ticker(
426 &self,
427 _world: &Arc<World>,
428 _state: BlockStateId,
429 block_entity_type: BlockEntityTypeRef,
430 ) -> Option<BlockEntityTicker> {
431 BlockEntityTicker::for_matching_entity_tick(
432 block_entity_type,
433 &vanilla_block_entity_types::SIGN,
434 )
435 }
436
437 fn use_without_item(
438 &self,
439 state: BlockStateId,
440 world: &Arc<World>,
441 pos: BlockPos,
442 player: &Player,
443 _hit_result: &BlockHitResult,
444 _inv: &mut InventoryAccess,
445 ) -> InteractionResult {
446 try_open_sign_editor(state, world, pos, player)
447 }
448}
449
450#[block_behavior]
452pub struct CeilingHangingSignBlock {
453 block: BlockRef,
454}
455
456impl CeilingHangingSignBlock {
457 #[must_use]
459 pub const fn new(block: BlockRef) -> Self {
460 Self { block }
461 }
462}
463
464impl BlockBehavior for CeilingHangingSignBlock {
465 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
466 true
467 }
468
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) = above_state.try_get_value(HORIZONTAL_FACING) {
517 above_facing.axis() != direction.axis()
519 } else if let Some(above_rotation) = above_state.try_get_value(ROTATION_16) {
520 let above_direction = rotation_to_direction(above_rotation);
522 above_direction.is_none_or(|d| d.axis() != direction.axis())
523 } else {
524 !is_above_full
525 }
526 } else {
527 !is_above_full
528 };
529
530 let rotation = if attached_to_middle {
532 convert_to_rotation_segment(context.rotation() + DEGREE_180)
534 } else {
535 convert_to_rotation_segment(direction.opposite().to_yaw())
537 };
538
539 Some(
540 self.block
541 .default_state()
542 .set_value(ROTATION_16, rotation)
543 .set_value(ATTACHED, attached_to_middle),
544 )
545 }
546
547 fn new_block_entity(
548 &self,
549 level: Weak<World>,
550 pos: BlockPos,
551 state: BlockStateId,
552 ) -> BlockEntityCreation {
553 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new_hanging(level, pos, state)))
554 }
555
556 fn get_block_entity_ticker(
557 &self,
558 _world: &Arc<World>,
559 _state: BlockStateId,
560 block_entity_type: BlockEntityTypeRef,
561 ) -> Option<BlockEntityTicker> {
562 BlockEntityTicker::for_matching_entity_tick(
563 block_entity_type,
564 &vanilla_block_entity_types::HANGING_SIGN,
565 )
566 }
567
568 fn use_without_item(
569 &self,
570 state: BlockStateId,
571 world: &Arc<World>,
572 pos: BlockPos,
573 player: &Player,
574 _hit_result: &BlockHitResult,
575 _inv: &mut InventoryAccess,
576 ) -> InteractionResult {
577 try_open_sign_editor(state, world, pos, player)
578 }
579}
580
581const fn rotation_to_direction(rotation: u8) -> Option<Direction> {
583 match rotation {
584 0 => Some(Direction::South),
585 4 => Some(Direction::West),
586 8 => Some(Direction::North),
587 12 => Some(Direction::East),
588 _ => None,
589 }
590}
591
592#[block_behavior]
594pub struct WallHangingSignBlock {
595 block: BlockRef,
596}
597
598impl WallHangingSignBlock {
599 #[must_use]
601 pub const fn new(block: BlockRef) -> Self {
602 Self { block }
603 }
604}
605
606impl BlockBehavior for WallHangingSignBlock {
607 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
608 true
609 }
610
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(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(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 const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
703
704 #[test]
705 fn standing_sign_only_schedules_water_when_support_survives() {
706 init_vanilla_registry();
707 let pos = BlockPos::new(0, 64, 0);
708 let sign = StandingSignBlock::new(&vanilla_blocks::OAK_SIGN);
709 let state = vanilla_blocks::OAK_SIGN
710 .default_state()
711 .set_value(WATERLOGGED, true);
712 let supported =
713 TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
714
715 assert_eq!(
716 sign.update_shape(
717 state,
718 &supported,
719 pos,
720 Direction::North,
721 pos.north(),
722 vanilla_blocks::AIR.default_state(),
723 ),
724 state
725 );
726 assert!(supported.scheduled_water_tick());
727
728 let unsupported = TestLevel::default();
729 assert!(
730 sign.update_shape(
731 state,
732 &unsupported,
733 pos,
734 Direction::Down,
735 pos.below(),
736 vanilla_blocks::AIR.default_state(),
737 )
738 .is_air()
739 );
740 assert!(!unsupported.scheduled_water_tick());
741 }
742
743 #[test]
744 fn sign_variants_select_their_matching_vanilla_tickers() {
745 init_vanilla_registry();
746 let world = fresh_test_world("sign_ticker_selection");
747
748 let standing = StandingSignBlock::new(&vanilla_blocks::OAK_SIGN);
749 assert!(
750 standing
751 .get_block_entity_ticker(
752 &world,
753 vanilla_blocks::OAK_SIGN.default_state(),
754 &vanilla_block_entity_types::SIGN,
755 )
756 .is_some()
757 );
758
759 let wall = WallSignBlock::new(&vanilla_blocks::OAK_WALL_SIGN);
760 assert!(
761 wall.get_block_entity_ticker(
762 &world,
763 vanilla_blocks::OAK_WALL_SIGN.default_state(),
764 &vanilla_block_entity_types::SIGN,
765 )
766 .is_some()
767 );
768
769 let ceiling_hanging = CeilingHangingSignBlock::new(&vanilla_blocks::OAK_HANGING_SIGN);
770 assert!(
771 ceiling_hanging
772 .get_block_entity_ticker(
773 &world,
774 vanilla_blocks::OAK_HANGING_SIGN.default_state(),
775 &vanilla_block_entity_types::HANGING_SIGN,
776 )
777 .is_some()
778 );
779
780 let wall_hanging = WallHangingSignBlock::new(&vanilla_blocks::OAK_WALL_HANGING_SIGN);
781 assert!(
782 wall_hanging
783 .get_block_entity_ticker(
784 &world,
785 vanilla_blocks::OAK_WALL_HANGING_SIGN.default_state(),
786 &vanilla_block_entity_types::HANGING_SIGN,
787 )
788 .is_some()
789 );
790 assert!(
791 wall_hanging
792 .get_block_entity_ticker(
793 &world,
794 vanilla_blocks::OAK_WALL_HANGING_SIGN.default_state(),
795 &vanilla_block_entity_types::SIGN,
796 )
797 .is_none()
798 );
799 }
800}