Skip to main content

steel_core/behavior/blocks/decoration/
sign_block.rs

1//! Sign block behavior implementation.
2//!
3//! Handles sign placement and block entity creation for all sign types.
4
5use 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
39/// Gets the nearest looking directions from the player's rotation.
40///
41/// Returns horizontal directions in order of how closely they match the player's look direction.
42fn get_nearest_looking_directions(rotation: f32, clicked_face: Direction) -> Vec<Direction> {
43    // Build list of directions in order of preference
44    // Start with the opposite of the clicked face (most natural for wall signs)
45    // Then add directions based on player facing
46    let mut directions = Vec::with_capacity(4);
47
48    // Add horizontal directions in order of how closely they match player's look
49    let all_horizontal = [
50        Direction::North,
51        Direction::East,
52        Direction::South,
53        Direction::West,
54    ];
55
56    // Calculate angle for each direction and sort by distance to player's rotation
57    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, prefer placing on the opposite side
72    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
83/// Calculates which side of a sign the player is facing.
84///
85/// Uses the sign's rotation (from block state) and the player's position
86/// relative to the sign to determine which side they're looking at.
87pub fn facing_text_slot(state: BlockStateId, pos: BlockPos, player: &Player) -> SignTextSlot {
88    // Get the sign's Y rotation in degrees from the block state
89    let sign_y_rot = get_sign_rotation_degrees(state);
90
91    // Calculate player's angle relative to the sign center
92    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    // Calculate angle from sign to player (in degrees, -90 to account for Minecraft's coordinate system)
97    let player_angle = (dz.atan2(dx) * RAD_TO_DEG_F64) as f32 - DEGREE_90;
98
99    // Front text if the angle difference is <= 90 degrees
100    if wrap_degrees(player_angle - sign_y_rot).abs() <= DEGREE_90 {
101        SignTextSlot::Front
102    } else {
103        SignTextSlot::Back
104    }
105}
106
107/// Gets the Y rotation of a sign in degrees from its block state.
108fn get_sign_rotation_degrees(state: BlockStateId) -> f32 {
109    // Standing signs use "rotation" property (0-15, each step is 22.5 degrees)
110    if let Some(rotation) = state.try_get_value(ROTATION_16) {
111        return f32::from(rotation) * 22.5;
112    }
113
114    // Wall signs use "facing" property
115    if let Some(facing) = state.try_get_value(HORIZONTAL_FACING) {
116        return facing.to_yaw();
117    }
118
119    0.0
120}
121
122/// Checks if a block state can support a standing sign.
123///
124/// Vanilla uses `isSolid()` which checks if the collision shape is a full cube.
125/// This means signs cannot be placed on other signs, fences, walls, etc.
126fn 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
132/// Checks if a wall sign can survive at the given position with the given facing.
133///
134/// Vanilla uses `isSolid()` which allows wall signs to be placed on other signs
135/// (since signs have `forceSolidOn`).
136fn can_wall_sign_survive(
137    world: &(impl LevelReader + ?Sized),
138    pos: BlockPos,
139    facing: Direction,
140) -> bool {
141    // Wall sign needs a solid block behind it
142    let behind_pos = facing.opposite().relative(pos);
143    let behind_state = world.get_block_state(behind_pos);
144    behind_state.is_solid()
145}
146
147/// Checks if a ceiling hanging sign can survive at the given position.
148fn 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
154/// Checks if a wall hanging sign can attach to a neighboring block.
155///
156/// Vanilla's `WallHangingSignBlock.canAttachTo` checks:
157/// 1. If the neighbor is a wall hanging sign on the same axis, allow attachment
158/// 2. Otherwise, check if the face is sturdy with FULL support type
159fn 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    // Check if it's another wall hanging sign (vanilla uses BlockTags.WALL_HANGING_SIGNS)
169    if let Some(block) = attach_block
170        && block.key.path.contains("wall_hanging_sign")
171    {
172        // Wall hanging signs can chain if they're on the same axis
173        if let Some(neighbor_facing) = attach_state.try_get_value(HORIZONTAL_FACING) {
174            return neighbor_facing.axis() == sign_facing.axis();
175        }
176    }
177
178    // Otherwise, check for sturdy face with FULL support
179    world.is_face_sturdy_for(attach_state, attach_pos, attach_face, SupportType::Full)
180}
181
182/// Checks if a wall hanging sign can survive at the given position.
183///
184/// Wall hanging signs need support on at least one side perpendicular to facing.
185/// This matches vanilla's `WallHangingSignBlock.canPlace`.
186fn 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    // Keep the inventory unlocked while an applicator changes the world.
222    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
257/// Attempts to open the sign editor for a player.
258///
259/// Checks all conditions required by vanilla:
260/// 1. Block entity exists and is a sign
261/// 2. Sign is not waxed
262/// 3. No other player is currently editing
263/// 4. Player has build permission (`may_build`)
264/// 5. Facing side contains only editable text
265///
266/// Returns `Success` if the editor was opened, `Pass` otherwise.
267fn try_open_sign_editor(
268    state: BlockStateId,
269    world: &Arc<World>,
270    pos: BlockPos,
271    player: &Player,
272) -> InteractionResult {
273    // Get the block entity
274    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; // Vanilla returns SUCCESS even when waxed
292    }
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    // Determine which side the player is facing
303    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
312/// Opens the sign editor for `player`, taking the edit lock.
313fn 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
318/// Opens the front text editor for the player who just placed the sign.
319fn 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/// Behavior for standing sign blocks (placed on ground).
334#[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    /// Creates a new standing sign block behavior.
345    #[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        // Standing signs break when the block below is removed
366        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        // Check if we can place on the block below
375        if !can_support_standing_sign(context.world, context.place_pos()) {
376            return None;
377        }
378
379        // Calculate rotation from player's yaw
380        // Vanilla: RotationSegment.convertToSegment(context.getRotation() + 180.0F)
381        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/// Behavior for wall sign blocks (attached to walls).
446#[block_behavior]
447pub struct WallSignBlock {
448    block: BlockRef,
449}
450
451impl WallSignBlock {
452    /// Creates a new wall sign block behavior.
453    #[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        // Wall signs break when the block they're attached to is removed
474        // The sign is attached to the block opposite of its facing direction
475        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        // Try each horizontal direction based on player's look direction
487        let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
488
489        for direction in directions {
490            // The sign faces the opposite direction of where it's attached
491            let facing = direction.opposite();
492
493            // Check if sign can survive with this facing
494            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        // No valid placement found
504        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/// Behavior for ceiling hanging sign blocks.
567#[block_behavior]
568pub struct CeilingHangingSignBlock {
569    block: BlockRef,
570}
571
572impl CeilingHangingSignBlock {
573    /// Creates a new ceiling hanging sign block behavior.
574    #[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        // Ceiling hanging signs break when the block above is removed
595        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        // Check if we can hang from the block above
604        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        // Determine if we should attach to the middle or not based on block above
616        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        // Check if block above is also a hanging sign
625        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        // Determine if attached to middle based on vanilla logic
630        let attached_to_middle = if is_below_hanging_sign {
631            // When below another hanging sign, check if we can chain
632            if let Some(above_facing) = above_state.try_get_value(HORIZONTAL_FACING) {
633                // Wall hanging sign above - check axis alignment
634                above_facing.axis() != direction.axis()
635            } else if let Some(above_rotation) = above_state.try_get_value(ROTATION_16) {
636                // Ceiling hanging sign above - check if we can align
637                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        // Calculate rotation
647        let rotation = if attached_to_middle {
648            // Attached to middle - use player rotation
649            convert_to_rotation_segment(context.rotation() + DEGREE_180)
650        } else {
651            // Attached to chains - align with direction
652            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
722/// Converts a rotation segment (0-15) to a cardinal direction, if applicable.
723const 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/// Behavior for wall hanging sign blocks.
734#[block_behavior]
735pub struct WallHangingSignBlock {
736    block: BlockRef,
737}
738
739impl WallHangingSignBlock {
740    /// Creates a new wall hanging sign block behavior.
741    #[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        // Wall hanging signs break when blocks on the perpendicular axis are removed
762        // and they can no longer survive
763        if let Some(facing) = state.try_get_value(HORIZONTAL_FACING) {
764            // Check if the change is on the perpendicular axis (clockwise/counterclockwise)
765            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        // Try each horizontal direction based on player's look direction
777        let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
778
779        for direction in directions {
780            // Wall hanging signs face perpendicular to the wall they're attached to
781            // Skip if the clicked face is on the same axis
782            if direction.axis() == context.clicked_face().axis() {
783                continue;
784            }
785
786            let facing = direction.opposite();
787
788            // Check if sign can survive with this facing
789            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        // No valid placement found
799        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}