1use std::cmp::Ordering;
6use std::sync::{Arc, Weak};
7
8use steel_macros::block_behavior;
9use steel_math::{
10 DEGREE_90, DEGREE_180, RAD_TO_DEG_F64, convert_to_rotation_segment, wrap_degrees,
11};
12use steel_protocol::packets::game::SoundSource;
13use steel_registry::REGISTRY;
14use steel_registry::block_entity_type::BlockEntityTypeRef;
15use steel_registry::blocks::BlockRef;
16use steel_registry::blocks::block_state_ext::BlockStateExt;
17use steel_registry::blocks::properties::{
18 BlockStateProperties, BoolProperty, Direction, EnumProperty, IntProperty,
19};
20use steel_registry::blocks::shapes::SupportType;
21use steel_registry::{
22 stat::vanilla_stat_types, vanilla_block_entity_types, vanilla_blocks, vanilla_game_events,
23};
24use steel_utils::{
25 BlockPos, BlockStateId, Downcast as _,
26 types::{InteractionHand, SignTextSlot},
27};
28
29use crate::behavior::block::{
30 BlockBehavior, BlockEntityCreation, schedule_water_tick_if_waterlogged,
31};
32use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
33use crate::behavior::{ITEM_BEHAVIORS, InventoryAccess, PlacementSource};
34use crate::block_entity::{BlockEntity, BlockEntityTicker, entities::SignBlockEntity};
35use crate::entity::Entity;
36use crate::player::Player;
37use crate::world::{LevelReader, ScheduledTickAccess, World, game_event::GameEventContext};
38
39fn get_nearest_looking_directions(rotation: f32, clicked_face: Direction) -> Vec<Direction> {
43 let mut directions = Vec::with_capacity(4);
47
48 let all_horizontal = [
50 Direction::North,
51 Direction::East,
52 Direction::South,
53 Direction::West,
54 ];
55
56 let mut scored: Vec<(Direction, f32)> = all_horizontal
58 .iter()
59 .map(|&dir| {
60 let dir_angle = dir.to_yaw();
61 (dir, wrap_degrees(rotation - dir_angle).abs())
62 })
63 .collect();
64
65 scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
66
67 for (dir, _) in scored {
68 directions.push(dir);
69 }
70
71 if clicked_face.is_horizontal() {
73 let opposite = clicked_face.opposite();
74 if let Some(pos) = directions.iter().position(|&d| d == opposite) {
75 directions.remove(pos);
76 directions.insert(0, opposite);
77 }
78 }
79
80 directions
81}
82
83pub fn facing_text_slot(state: BlockStateId, pos: BlockPos, player: &Player) -> SignTextSlot {
88 let sign_y_rot = get_sign_rotation_degrees(state);
90
91 let player_pos = player.position();
93 let dx = player_pos.x - (f64::from(pos.0.x) + 0.5);
94 let dz = player_pos.z - (f64::from(pos.0.z) + 0.5);
95
96 let player_angle = (dz.atan2(dx) * RAD_TO_DEG_F64) as f32 - DEGREE_90;
98
99 if wrap_degrees(player_angle - sign_y_rot).abs() <= DEGREE_90 {
101 SignTextSlot::Front
102 } else {
103 SignTextSlot::Back
104 }
105}
106
107fn get_sign_rotation_degrees(state: BlockStateId) -> f32 {
109 if let Some(rotation) = state.try_get_value(ROTATION_16) {
111 return f32::from(rotation) * 22.5;
112 }
113
114 if let Some(facing) = state.try_get_value(HORIZONTAL_FACING) {
116 return facing.to_yaw();
117 }
118
119 0.0
120}
121
122fn can_support_standing_sign(world: &(impl LevelReader + ?Sized), 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(
137 world: &(impl LevelReader + ?Sized),
138 pos: BlockPos,
139 facing: Direction,
140) -> bool {
141 let behind_pos = facing.opposite().relative(pos);
143 let behind_state = world.get_block_state(behind_pos);
144 behind_state.is_solid()
145}
146
147fn can_ceiling_hanging_sign_survive(world: &(impl LevelReader + ?Sized), pos: BlockPos) -> bool {
149 let above_pos = BlockPos::new(pos.x(), pos.y() + 1, pos.z());
150 let above_state = world.get_block_state(above_pos);
151 world.is_face_sturdy_for(above_state, above_pos, Direction::Down, SupportType::Center)
152}
153
154fn can_attach_to(
160 world: &(impl LevelReader + ?Sized),
161 sign_facing: Direction,
162 attach_pos: BlockPos,
163 attach_face: Direction,
164) -> bool {
165 let attach_state = world.get_block_state(attach_pos);
166 let attach_block = REGISTRY.blocks.by_state_id(attach_state);
167
168 if let Some(block) = attach_block
170 && block.key.path.contains("wall_hanging_sign")
171 {
172 if let Some(neighbor_facing) = attach_state.try_get_value(HORIZONTAL_FACING) {
174 return neighbor_facing.axis() == sign_facing.axis();
175 }
176 }
177
178 world.is_face_sturdy_for(attach_state, attach_pos, attach_face, SupportType::Full)
180}
181
182fn can_wall_hanging_sign_survive(
187 world: &(impl LevelReader + ?Sized),
188 pos: BlockPos,
189 facing: Direction,
190) -> bool {
191 let clockwise = facing.rotate_y_clockwise();
192 let counter_clockwise = facing.rotate_y_counter_clockwise();
193
194 let can_attach_clockwise = {
195 let attach_pos = clockwise.relative(pos);
196 can_attach_to(world, facing, attach_pos, counter_clockwise)
197 };
198
199 let can_attach_counter = {
200 let attach_pos = counter_clockwise.relative(pos);
201 can_attach_to(world, facing, attach_pos, clockwise)
202 };
203
204 can_attach_clockwise || can_attach_counter
205}
206
207fn try_apply_sign_item(
208 state: BlockStateId,
209 world: &Arc<World>,
210 pos: BlockPos,
211 player: &Player,
212 inv: &mut InventoryAccess,
213) -> InteractionResult {
214 let Some(block_entity) = world.get_block_entity(pos) else {
215 return InteractionResult::Pass;
216 };
217 let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>() else {
218 return InteractionResult::Pass;
219 };
220
221 let stack = inv.with_item(|item| item.clone());
223 let behavior = ITEM_BEHAVIORS.get_behavior(stack.item());
224 let Some(applicator) = behavior.as_sign_applicator() else {
225 return InteractionResult::TryEmptyHandInteraction;
226 };
227 if !player.abilities.lock().may_build
228 || sign.is_waxed()
229 || sign.is_other_player_editing(player.gameprofile.id())
230 {
231 return InteractionResult::TryEmptyHandInteraction;
232 }
233
234 let slot = facing_text_slot(state, pos, player);
235 if !applicator.can_apply_to_sign(&sign.get_text(slot), &stack, player)
236 || !applicator.try_apply_to_sign(world, sign, slot, &stack, player)
237 {
238 return InteractionResult::TryEmptyHandInteraction;
239 }
240
241 sign.set_changed();
242 if let Some(nbt) = sign.get_update_tag() {
243 world.broadcast_block_entity_update(pos, sign.get_type(), nbt);
244 }
245 player.award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
246 world.game_event(
247 &vanilla_game_events::BLOCK_CHANGE,
248 pos,
249 &GameEventContext::new(Some(player), Some(state)),
250 );
251 let has_infinite_materials = player.has_infinite_materials();
252 inv.with_item(|item| item.consume_one(has_infinite_materials));
253
254 InteractionResult::Success
255}
256
257fn try_open_sign_editor(
268 state: BlockStateId,
269 world: &Arc<World>,
270 pos: BlockPos,
271 player: &Player,
272) -> InteractionResult {
273 let Some(block_entity) = world.get_block_entity(pos) else {
275 return InteractionResult::Pass;
276 };
277
278 let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>() else {
279 return InteractionResult::Pass;
280 };
281
282 if sign.is_waxed() {
283 world.play_sound(
284 sign.sign_interaction_failed_sound_event(),
285 SoundSource::Blocks,
286 pos,
287 1.0,
288 1.0,
289 None,
290 );
291 return InteractionResult::Success; }
293
294 if sign.is_other_player_editing(player.gameprofile.id()) {
295 return InteractionResult::Pass;
296 }
297
298 if !player.abilities.lock().may_build {
299 return InteractionResult::Pass;
300 }
301
302 let slot = facing_text_slot(state, pos, player);
304 if !sign.get_text(slot).has_editable_text() {
305 return InteractionResult::Pass;
306 }
307
308 open_text_edit(sign, player, pos, slot);
309 InteractionResult::Success
310}
311
312fn open_text_edit(sign: &SignBlockEntity, player: &Player, pos: BlockPos, slot: SignTextSlot) {
314 sign.set_player_who_may_edit(Some(player.gameprofile.id()));
315 player.open_sign_editor(pos, slot);
316}
317
318fn open_sign_editor_on_place(world: &Arc<World>, pos: BlockPos, player: &Player) {
320 let Some(block_entity) = world.get_block_entity(pos) else {
321 return;
322 };
323 let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>() else {
324 return;
325 };
326 if sign.is_waxed() || !sign.get_text(SignTextSlot::Front).has_editable_text() {
327 return;
328 }
329
330 open_text_edit(sign, player, pos, SignTextSlot::Front);
331}
332
333#[block_behavior]
335pub struct StandingSignBlock {
336 block: BlockRef,
337}
338
339const ATTACHED: &BoolProperty = &BlockStateProperties::ATTACHED;
340const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
341const ROTATION_16: &IntProperty = &BlockStateProperties::ROTATION_16;
342
343impl StandingSignBlock {
344 #[must_use]
346 pub const fn new(block: BlockRef) -> Self {
347 Self { block }
348 }
349}
350
351impl BlockBehavior for StandingSignBlock {
352 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
353 true
354 }
355
356 fn update_shape(
357 &self,
358 state: BlockStateId,
359 world: &dyn ScheduledTickAccess,
360 pos: BlockPos,
361 direction: Direction,
362 _neighbor_pos: BlockPos,
363 _neighbor_state: BlockStateId,
364 ) -> BlockStateId {
365 if direction == Direction::Down && !can_support_standing_sign(world, pos) {
367 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
368 }
369 schedule_water_tick_if_waterlogged(state, world, pos);
370 state
371 }
372
373 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
374 if !can_support_standing_sign(context.world, context.place_pos()) {
376 return None;
377 }
378
379 let rotation = convert_to_rotation_segment(context.rotation() + DEGREE_180);
382
383 Some(self.block.default_state().set_value(ROTATION_16, rotation))
384 }
385
386 fn new_block_entity(
387 &self,
388 level: Weak<World>,
389 pos: BlockPos,
390 state: BlockStateId,
391 ) -> BlockEntityCreation {
392 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new(level, pos, state)))
393 }
394
395 fn get_block_entity_ticker(
396 &self,
397 _world: &Arc<World>,
398 _state: BlockStateId,
399 block_entity_type: BlockEntityTypeRef,
400 ) -> Option<BlockEntityTicker> {
401 BlockEntityTicker::for_matching_entity_tick(
402 block_entity_type,
403 &vanilla_block_entity_types::SIGN,
404 )
405 }
406
407 fn use_item_on(
408 &self,
409 state: BlockStateId,
410 world: &Arc<World>,
411 pos: BlockPos,
412 player: &Player,
413 _hand: InteractionHand,
414 _hit_result: &BlockHitResult,
415 inv: &mut InventoryAccess,
416 ) -> InteractionResult {
417 try_apply_sign_item(state, world, pos, player, inv)
418 }
419
420 fn use_without_item(
421 &self,
422 state: BlockStateId,
423 world: &Arc<World>,
424 pos: BlockPos,
425 player: &Player,
426 _hit_result: &BlockHitResult,
427 _inv: &mut InventoryAccess,
428 ) -> InteractionResult {
429 try_open_sign_editor(state, world, pos, player)
430 }
431
432 fn set_placed_by(
433 &self,
434 _state: BlockStateId,
435 world: &Arc<World>,
436 pos: BlockPos,
437 source: &PlacementSource<'_>,
438 ) {
439 if let Some(player) = source.player() {
440 open_sign_editor_on_place(world, pos, player);
441 }
442 }
443}
444
445#[block_behavior]
447pub struct WallSignBlock {
448 block: BlockRef,
449}
450
451impl WallSignBlock {
452 #[must_use]
454 pub const fn new(block: BlockRef) -> Self {
455 Self { block }
456 }
457}
458
459impl BlockBehavior for WallSignBlock {
460 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
461 true
462 }
463
464 fn update_shape(
465 &self,
466 state: BlockStateId,
467 world: &dyn ScheduledTickAccess,
468 pos: BlockPos,
469 direction: Direction,
470 _neighbor_pos: BlockPos,
471 _neighbor_state: BlockStateId,
472 ) -> BlockStateId {
473 if let Some(facing) = state.try_get_value(HORIZONTAL_FACING)
476 && direction.opposite() == facing
477 && !can_wall_sign_survive(world, pos, facing)
478 {
479 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
480 }
481 schedule_water_tick_if_waterlogged(state, world, pos);
482 state
483 }
484
485 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
486 let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
488
489 for direction in directions {
490 let facing = direction.opposite();
492
493 if can_wall_sign_survive(context.world, context.place_pos(), facing) {
495 return Some(
496 self.block
497 .default_state()
498 .set_value(HORIZONTAL_FACING, facing),
499 );
500 }
501 }
502
503 None
505 }
506
507 fn new_block_entity(
508 &self,
509 level: Weak<World>,
510 pos: BlockPos,
511 state: BlockStateId,
512 ) -> BlockEntityCreation {
513 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new(level, pos, state)))
514 }
515
516 fn get_block_entity_ticker(
517 &self,
518 _world: &Arc<World>,
519 _state: BlockStateId,
520 block_entity_type: BlockEntityTypeRef,
521 ) -> Option<BlockEntityTicker> {
522 BlockEntityTicker::for_matching_entity_tick(
523 block_entity_type,
524 &vanilla_block_entity_types::SIGN,
525 )
526 }
527
528 fn use_item_on(
529 &self,
530 state: BlockStateId,
531 world: &Arc<World>,
532 pos: BlockPos,
533 player: &Player,
534 _hand: InteractionHand,
535 _hit_result: &BlockHitResult,
536 inv: &mut InventoryAccess,
537 ) -> InteractionResult {
538 try_apply_sign_item(state, world, pos, player, inv)
539 }
540
541 fn use_without_item(
542 &self,
543 state: BlockStateId,
544 world: &Arc<World>,
545 pos: BlockPos,
546 player: &Player,
547 _hit_result: &BlockHitResult,
548 _inv: &mut InventoryAccess,
549 ) -> InteractionResult {
550 try_open_sign_editor(state, world, pos, player)
551 }
552
553 fn set_placed_by(
554 &self,
555 _state: BlockStateId,
556 world: &Arc<World>,
557 pos: BlockPos,
558 source: &PlacementSource<'_>,
559 ) {
560 if let Some(player) = source.player() {
561 open_sign_editor_on_place(world, pos, player);
562 }
563 }
564}
565
566#[block_behavior]
568pub struct CeilingHangingSignBlock {
569 block: BlockRef,
570}
571
572impl CeilingHangingSignBlock {
573 #[must_use]
575 pub const fn new(block: BlockRef) -> Self {
576 Self { block }
577 }
578}
579
580impl BlockBehavior for CeilingHangingSignBlock {
581 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
582 true
583 }
584
585 fn update_shape(
586 &self,
587 state: BlockStateId,
588 world: &dyn ScheduledTickAccess,
589 pos: BlockPos,
590 direction: Direction,
591 _neighbor_pos: BlockPos,
592 _neighbor_state: BlockStateId,
593 ) -> BlockStateId {
594 if direction == Direction::Up && !can_ceiling_hanging_sign_survive(world, pos) {
596 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
597 }
598 schedule_water_tick_if_waterlogged(state, world, pos);
599 state
600 }
601
602 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
603 if !can_ceiling_hanging_sign_survive(context.world, context.place_pos()) {
605 return None;
606 }
607
608 let above_pos = BlockPos::new(
609 context.place_pos().x(),
610 context.place_pos().y() + 1,
611 context.place_pos().z(),
612 );
613 let above_state = context.world.get_block_state(above_pos);
614
615 let direction = Direction::from_yaw(context.rotation());
617 let is_above_full = context.world.is_face_sturdy_for(
618 above_state,
619 above_pos,
620 Direction::Down,
621 SupportType::Full,
622 );
623
624 let above_block = REGISTRY.blocks.by_state_id(above_state);
626 let is_below_hanging_sign =
627 above_block.is_some_and(|b| b.key.path.contains("hanging_sign"));
628
629 let attached_to_middle = if is_below_hanging_sign {
631 if let Some(above_facing) = above_state.try_get_value(HORIZONTAL_FACING) {
633 above_facing.axis() != direction.axis()
635 } else if let Some(above_rotation) = above_state.try_get_value(ROTATION_16) {
636 let above_direction = rotation_to_direction(above_rotation);
638 above_direction.is_none_or(|d| d.axis() != direction.axis())
639 } else {
640 !is_above_full
641 }
642 } else {
643 !is_above_full
644 };
645
646 let rotation = if attached_to_middle {
648 convert_to_rotation_segment(context.rotation() + DEGREE_180)
650 } else {
651 convert_to_rotation_segment(direction.opposite().to_yaw())
653 };
654
655 Some(
656 self.block
657 .default_state()
658 .set_value(ROTATION_16, rotation)
659 .set_value(ATTACHED, attached_to_middle),
660 )
661 }
662
663 fn new_block_entity(
664 &self,
665 level: Weak<World>,
666 pos: BlockPos,
667 state: BlockStateId,
668 ) -> BlockEntityCreation {
669 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new_hanging(level, pos, state)))
670 }
671
672 fn get_block_entity_ticker(
673 &self,
674 _world: &Arc<World>,
675 _state: BlockStateId,
676 block_entity_type: BlockEntityTypeRef,
677 ) -> Option<BlockEntityTicker> {
678 BlockEntityTicker::for_matching_entity_tick(
679 block_entity_type,
680 &vanilla_block_entity_types::HANGING_SIGN,
681 )
682 }
683
684 fn use_item_on(
685 &self,
686 state: BlockStateId,
687 world: &Arc<World>,
688 pos: BlockPos,
689 player: &Player,
690 _hand: InteractionHand,
691 _hit_result: &BlockHitResult,
692 inv: &mut InventoryAccess,
693 ) -> InteractionResult {
694 try_apply_sign_item(state, world, pos, player, inv)
695 }
696
697 fn use_without_item(
698 &self,
699 state: BlockStateId,
700 world: &Arc<World>,
701 pos: BlockPos,
702 player: &Player,
703 _hit_result: &BlockHitResult,
704 _inv: &mut InventoryAccess,
705 ) -> InteractionResult {
706 try_open_sign_editor(state, world, pos, player)
707 }
708
709 fn set_placed_by(
710 &self,
711 _state: BlockStateId,
712 world: &Arc<World>,
713 pos: BlockPos,
714 source: &PlacementSource<'_>,
715 ) {
716 if let Some(player) = source.player() {
717 open_sign_editor_on_place(world, pos, player);
718 }
719 }
720}
721
722const fn rotation_to_direction(rotation: u8) -> Option<Direction> {
724 match rotation {
725 0 => Some(Direction::South),
726 4 => Some(Direction::West),
727 8 => Some(Direction::North),
728 12 => Some(Direction::East),
729 _ => None,
730 }
731}
732
733#[block_behavior]
735pub struct WallHangingSignBlock {
736 block: BlockRef,
737}
738
739impl WallHangingSignBlock {
740 #[must_use]
742 pub const fn new(block: BlockRef) -> Self {
743 Self { block }
744 }
745}
746
747impl BlockBehavior for WallHangingSignBlock {
748 fn is_possible_to_respawn_in_this(&self, _state: BlockStateId) -> bool {
749 true
750 }
751
752 fn update_shape(
753 &self,
754 state: BlockStateId,
755 world: &dyn ScheduledTickAccess,
756 pos: BlockPos,
757 direction: Direction,
758 _neighbor_pos: BlockPos,
759 _neighbor_state: BlockStateId,
760 ) -> BlockStateId {
761 if let Some(facing) = state.try_get_value(HORIZONTAL_FACING) {
764 if direction.axis() == facing.rotate_y_clockwise().axis()
766 && !can_wall_hanging_sign_survive(world, pos, facing)
767 {
768 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
769 }
770 }
771 schedule_water_tick_if_waterlogged(state, world, pos);
772 state
773 }
774
775 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
776 let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
778
779 for direction in directions {
780 if direction.axis() == context.clicked_face().axis() {
783 continue;
784 }
785
786 let facing = direction.opposite();
787
788 if can_wall_hanging_sign_survive(context.world, context.place_pos(), facing) {
790 return Some(
791 self.block
792 .default_state()
793 .set_value(HORIZONTAL_FACING, facing),
794 );
795 }
796 }
797
798 None
800 }
801
802 fn new_block_entity(
803 &self,
804 level: Weak<World>,
805 pos: BlockPos,
806 state: BlockStateId,
807 ) -> BlockEntityCreation {
808 BlockEntityCreation::Created(Arc::new(SignBlockEntity::new_hanging(level, pos, state)))
809 }
810
811 fn get_block_entity_ticker(
812 &self,
813 _world: &Arc<World>,
814 _state: BlockStateId,
815 block_entity_type: BlockEntityTypeRef,
816 ) -> Option<BlockEntityTicker> {
817 BlockEntityTicker::for_matching_entity_tick(
818 block_entity_type,
819 &vanilla_block_entity_types::HANGING_SIGN,
820 )
821 }
822
823 fn use_item_on(
824 &self,
825 state: BlockStateId,
826 world: &Arc<World>,
827 pos: BlockPos,
828 player: &Player,
829 _hand: InteractionHand,
830 _hit_result: &BlockHitResult,
831 inv: &mut InventoryAccess,
832 ) -> InteractionResult {
833 try_apply_sign_item(state, world, pos, player, inv)
834 }
835
836 fn use_without_item(
837 &self,
838 state: BlockStateId,
839 world: &Arc<World>,
840 pos: BlockPos,
841 player: &Player,
842 _hit_result: &BlockHitResult,
843 _inv: &mut InventoryAccess,
844 ) -> InteractionResult {
845 try_open_sign_editor(state, world, pos, player)
846 }
847
848 fn set_placed_by(
849 &self,
850 _state: BlockStateId,
851 world: &Arc<World>,
852 pos: BlockPos,
853 source: &PlacementSource<'_>,
854 ) {
855 if let Some(player) = source.player() {
856 open_sign_editor_on_place(world, pos, player);
857 }
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use steel_registry::data_components::vanilla_components::DYE;
864 use steel_registry::item_stack::ItemStack;
865 use steel_registry::{DyeColor, init_vanilla_registry, vanilla_items};
866 use steel_utils::ChunkPos;
867 use steel_utils::types::UpdateFlags;
868 use text_components::TextComponent;
869
870 use super::*;
871 use crate::behavior::init_behaviors;
872 use crate::block_entity::entities::SignText;
873 use crate::test_support::{
874 TestLevel, TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk,
875 };
876
877 const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
878
879 #[test]
880 fn standing_sign_only_schedules_water_when_support_survives() {
881 init_vanilla_registry();
882 let pos = BlockPos::new(0, 64, 0);
883 let sign = StandingSignBlock::new(&vanilla_blocks::OAK_SIGN);
884 let state = vanilla_blocks::OAK_SIGN
885 .default_state()
886 .set_value(WATERLOGGED, true);
887 let supported =
888 TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
889
890 assert_eq!(
891 sign.update_shape(
892 state,
893 &supported,
894 pos,
895 Direction::North,
896 pos.north(),
897 vanilla_blocks::AIR.default_state(),
898 ),
899 state
900 );
901 assert!(supported.scheduled_water_tick());
902
903 let unsupported = TestLevel::default();
904 assert!(
905 sign.update_shape(
906 state,
907 &unsupported,
908 pos,
909 Direction::Down,
910 pos.below(),
911 vanilla_blocks::AIR.default_state(),
912 )
913 .is_air()
914 );
915 assert!(!unsupported.scheduled_water_tick());
916 }
917
918 #[test]
919 fn sign_variants_select_their_matching_vanilla_tickers() {
920 init_vanilla_registry();
921 let world_fixture = fresh_test_world("sign_ticker_selection");
922 let world = &world_fixture.world;
923
924 let standing = StandingSignBlock::new(&vanilla_blocks::OAK_SIGN);
925 assert!(
926 standing
927 .get_block_entity_ticker(
928 world,
929 vanilla_blocks::OAK_SIGN.default_state(),
930 &vanilla_block_entity_types::SIGN,
931 )
932 .is_some()
933 );
934
935 let wall = WallSignBlock::new(&vanilla_blocks::OAK_WALL_SIGN);
936 assert!(
937 wall.get_block_entity_ticker(
938 world,
939 vanilla_blocks::OAK_WALL_SIGN.default_state(),
940 &vanilla_block_entity_types::SIGN,
941 )
942 .is_some()
943 );
944
945 let ceiling_hanging = CeilingHangingSignBlock::new(&vanilla_blocks::OAK_HANGING_SIGN);
946 assert!(
947 ceiling_hanging
948 .get_block_entity_ticker(
949 world,
950 vanilla_blocks::OAK_HANGING_SIGN.default_state(),
951 &vanilla_block_entity_types::HANGING_SIGN,
952 )
953 .is_some()
954 );
955
956 let wall_hanging = WallHangingSignBlock::new(&vanilla_blocks::OAK_WALL_HANGING_SIGN);
957 assert!(
958 wall_hanging
959 .get_block_entity_ticker(
960 world,
961 vanilla_blocks::OAK_WALL_HANGING_SIGN.default_state(),
962 &vanilla_block_entity_types::HANGING_SIGN,
963 )
964 .is_some()
965 );
966 assert!(
967 wall_hanging
968 .get_block_entity_ticker(
969 world,
970 vanilla_blocks::OAK_WALL_HANGING_SIGN.default_state(),
971 &vanilla_block_entity_types::SIGN,
972 )
973 .is_none()
974 );
975 }
976
977 #[test]
978 fn try_open_sign_editor_checks_waxed_and_may_build() {
979 init_vanilla_registry();
980 let world_fixture = fresh_test_world("sign_editor_checks");
981 let world = &world_fixture.world;
982 let pos = BlockPos::new(0, 64, 0);
983 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
984 let state = vanilla_blocks::OAK_SIGN.default_state();
985 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_ALL));
986
987 let sign = Arc::new(SignBlockEntity::new(Arc::downgrade(world), pos, state));
988 let block_entity: Arc<dyn BlockEntity> = Arc::<SignBlockEntity>::clone(&sign);
989 assert!(world.set_block_entity(block_entity));
990
991 let player = TestPlayerBuilder::new(Arc::clone(world), "Editor", 1).build();
992
993 sign.wax();
994 assert_eq!(
995 try_open_sign_editor(state, world, pos, &player),
996 InteractionResult::Success
997 );
998
999 let unwaxed = Arc::new(SignBlockEntity::new(Arc::downgrade(world), pos, state));
1000 assert!(world.set_block_entity(unwaxed));
1001 player.abilities.lock().may_build = false;
1002 assert_eq!(
1003 try_open_sign_editor(state, world, pos, &player),
1004 InteractionResult::Pass
1005 );
1006 }
1007
1008 #[test]
1009 fn try_apply_sign_item_dyes_sign_text() {
1010 init_vanilla_registry();
1011 init_behaviors();
1012 let world_fixture = fresh_test_world("sign_dye_application");
1013 let world = &world_fixture.world;
1014 let pos = BlockPos::new(0, 64, 0);
1015 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
1016 let state = vanilla_blocks::OAK_SIGN.default_state();
1017 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_ALL));
1018
1019 let sign = Arc::new(SignBlockEntity::new(Arc::downgrade(world), pos, state));
1020 let mut text = SignText::new();
1021 text.set_message(0, TextComponent::plain("Steel"));
1022 sign.set_text(text, SignTextSlot::Front);
1023 let block_entity: Arc<dyn BlockEntity> = Arc::<SignBlockEntity>::clone(&sign);
1024 assert!(world.set_block_entity(block_entity));
1025
1026 let player = TestPlayerBuilder::new(Arc::clone(world), "Painter", 2).build();
1027 let mut dye_stack = ItemStack::new(&vanilla_items::RED_DYE);
1028 dye_stack.set(DYE, DyeColor::Red);
1029 player
1030 .inventory
1031 .lock()
1032 .set_item_in_hand(InteractionHand::MainHand, dye_stack.clone());
1033
1034 let mut inv =
1035 InventoryAccess::new(Arc::clone(&player.inventory), InteractionHand::MainHand);
1036 assert_eq!(
1037 try_apply_sign_item(state, world, pos, &player, &mut inv),
1038 InteractionResult::Success
1039 );
1040 assert_eq!(sign.get_text(SignTextSlot::Front).color, DyeColor::Red);
1041 assert_eq!(
1042 player
1043 .inventory
1044 .lock()
1045 .get_item_in_hand(InteractionHand::MainHand)
1046 .count(),
1047 0
1048 );
1049
1050 player
1051 .inventory
1052 .lock()
1053 .set_item_in_hand(InteractionHand::MainHand, dye_stack);
1054 let mut inv =
1055 InventoryAccess::new(Arc::clone(&player.inventory), InteractionHand::MainHand);
1056 assert_eq!(
1057 try_apply_sign_item(state, world, pos, &player, &mut inv),
1058 InteractionResult::TryEmptyHandInteraction
1059 );
1060 }
1061}