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::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
29/// Converts a rotation in degrees to a 16-segment rotation value (0-15).
30///
31/// This is equivalent to vanilla's `RotationSegment.convertToSegment(float)`.
32/// Each segment is 22.5 degrees, and rotation is measured clockwise from south.
33fn convert_to_rotation_segment(degrees: f32) -> u8 {
34    // Normalize to 0-360
35    let normalized = degrees.rem_euclid(360.0);
36    // Convert to segment (each segment is 22.5 degrees)
37    // Round to nearest segment
38    (((normalized / 22.5) + 0.5) as u8) & 15
39}
40
41/// Gets the nearest looking directions from the player's rotation.
42///
43/// Returns horizontal directions in order of how closely they match the player's look direction.
44fn get_nearest_looking_directions(rotation: f32, clicked_face: Direction) -> Vec<Direction> {
45    // Build list of directions in order of preference
46    // Start with the opposite of the clicked face (most natural for wall signs)
47    // Then add directions based on player facing
48    let mut directions = Vec::with_capacity(4);
49
50    // Add horizontal directions in order of how closely they match player's look
51    let all_horizontal = [
52        Direction::North,
53        Direction::East,
54        Direction::South,
55        Direction::West,
56    ];
57
58    // Calculate angle for each direction and sort by distance to player's rotation
59    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, prefer placing on the opposite side
75    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
86/// Calculates whether the player is facing the front of a sign.
87///
88/// Uses the sign's rotation (from block state) and the player's position
89/// relative to the sign to determine which side they're looking at.
90fn is_facing_front_text(state: BlockStateId, pos: BlockPos, player: &Player) -> bool {
91    // Get the sign's Y rotation in degrees from the block state
92    let sign_y_rot = get_sign_rotation_degrees(state);
93
94    // Calculate player's angle relative to the sign center
95    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    // Calculate angle from sign to player (in degrees, -90 to account for Minecraft's coordinate system)
100    let player_angle = (dz.atan2(dx) * 180.0 / PI) as f32 - 90.0;
101
102    // Front text if the angle difference is <= 90 degrees
103    let diff = (sign_y_rot - player_angle + 180.0).rem_euclid(360.0) - 180.0;
104    diff.abs() <= 90.0
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(&BlockStateProperties::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(&BlockStateProperties::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: &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
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(world: &dyn LevelReader, pos: BlockPos, facing: Direction) -> bool {
137    // Wall sign needs a solid block behind it
138    let behind_pos = facing.opposite().relative(pos);
139    let behind_state = world.get_block_state(behind_pos);
140    behind_state.is_solid()
141}
142
143/// Checks if a ceiling hanging sign can survive at the given position.
144fn 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
150/// Checks if a wall hanging sign can attach to a neighboring block.
151///
152/// Vanilla's `WallHangingSignBlock.canAttachTo` checks:
153/// 1. If the neighbor is a wall hanging sign on the same axis, allow attachment
154/// 2. Otherwise, check if the face is sturdy with FULL support type
155fn 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    // Check if it's another wall hanging sign (vanilla uses BlockTags.WALL_HANGING_SIGNS)
165    if let Some(block) = attach_block
166        && block.key.path.contains("wall_hanging_sign")
167    {
168        // Wall hanging signs can chain if they're on the same axis
169        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    // Otherwise, check for sturdy face with FULL support
177    world.is_face_sturdy_for(attach_state, attach_pos, attach_face, SupportType::Full)
178}
179
180/// Checks if a wall hanging sign can survive at the given position.
181///
182/// Wall hanging signs need support on at least one side perpendicular to facing.
183/// This matches vanilla's `WallHangingSignBlock.canPlace`.
184fn 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
205// TODO: Implement sign applicators (use_with_item):
206// - Dye items: Change sign text color (front or back based on player facing)
207//   - Check if sign is not waxed
208//   - Get the SignText for the side player is facing
209//   - If color differs from dye color, update it and consume the dye
210//   - Play DYE_USE sound
211// - Glow Ink Sac: Make sign text glow
212//   - Check if sign is not waxed
213//   - If text is not already glowing, set has_glowing_text = true
214//   - Consume the ink sac
215//   - Play GLOW_INK_SAC_USE sound
216// - Ink Sac: Remove glow from sign text
217//   - Check if sign is not waxed
218//   - If text is glowing, set has_glowing_text = false
219//   - Consume the ink sac
220//   - Play INK_SAC_USE sound
221// - Honeycomb: Wax the sign (prevents future edits)
222//   - If sign is not already waxed, set is_waxed = true
223//   - Consume the honeycomb
224//   - Play HONEYCOMB_WAX_ON sound
225//   - Spawn WAX_ON particles
226
227/// Attempts to open the sign editor for a player.
228///
229/// Checks all conditions required by vanilla:
230/// 1. Block entity exists and is a sign
231/// 2. Sign is not waxed
232/// 3. No other player is currently editing
233/// 4. Player has build permission (`may_build`)
234///
235/// Returns `Success` if the editor was opened, `Pass` otherwise.
236fn try_open_sign_editor(
237    state: BlockStateId,
238    world: &Arc<World>,
239    pos: BlockPos,
240    player: &Player,
241) -> InteractionResult {
242    // Get the block entity
243    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    // Check 1: Is the sign waxed?
252    if sign.is_waxed() {
253        // TODO: Play waxed sign interaction fail sound
254        return InteractionResult::Success; // Vanilla returns SUCCESS even when waxed
255    }
256
257    // Check 2: Is another player editing?
258    if sign.is_other_player_editing(player.gameprofile.id) {
259        return InteractionResult::Pass;
260    }
261
262    // Check 3: Player must have build permission
263    // TODO: Implement may_build check properly
264    // if !player.may_build() {
265    //     return InteractionResult::Pass;
266    // }
267
268    // Determine which side the player is facing
269    let is_front_text = is_facing_front_text(state, pos, player);
270
271    // Set the editing player lock
272    sign.set_player_who_may_edit(Some(player.gameprofile.id));
273
274    // Open the editor
275    player.open_sign_editor(pos, is_front_text);
276    InteractionResult::Success
277}
278
279/// Behavior for standing sign blocks (placed on ground).
280#[block_behavior]
281pub struct StandingSignBlock {
282    block: BlockRef,
283}
284
285impl StandingSignBlock {
286    /// Creates a new standing sign block behavior.
287    #[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        // Standing signs break when the block below is removed
304        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        // Check if we can place on the block below
313        if !can_support_standing_sign(context.world, context.place_pos()) {
314            return None;
315        }
316
317        // Calculate rotation from player's yaw
318        // Vanilla: RotationSegment.convertToSegment(context.getRotation() + 180.0F)
319        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/// Behavior for wall sign blocks (attached to walls).
363#[block_behavior]
364pub struct WallSignBlock {
365    block: BlockRef,
366}
367
368impl WallSignBlock {
369    /// Creates a new wall sign block behavior.
370    #[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        // Wall signs break when the block they're attached to is removed
387        // The sign is attached to the block opposite of its facing direction
388        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        // Try each horizontal direction based on player's look direction
400        let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
401
402        for direction in directions {
403            // The sign faces the opposite direction of where it's attached
404            let facing = direction.opposite();
405
406            // Check if sign can survive with this facing
407            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        // No valid placement found
417        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/// Behavior for ceiling hanging sign blocks.
455#[block_behavior]
456pub struct CeilingHangingSignBlock {
457    block: BlockRef,
458}
459
460impl CeilingHangingSignBlock {
461    /// Creates a new ceiling hanging sign block behavior.
462    #[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        // Ceiling hanging signs break when the block above is removed
479        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        // Check if we can hang from the block above
488        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        // Determine if we should attach to the middle or not based on block above
500        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        // Check if block above is also a hanging sign
509        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        // Determine if attached to middle based on vanilla logic
514        let attached_to_middle = if is_below_hanging_sign {
515            // When below another hanging sign, check if we can chain
516            if let Some(above_facing) =
517                above_state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING)
518            {
519                // Wall hanging sign above - check axis alignment
520                above_facing.axis() != direction.axis()
521            } else if let Some(above_rotation) =
522                above_state.try_get_value(&BlockStateProperties::ROTATION_16)
523            {
524                // Ceiling hanging sign above - check if we can align
525                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        // Calculate rotation
535        let rotation = if attached_to_middle {
536            // Attached to middle - use player rotation
537            convert_to_rotation_segment(context.rotation() + 180.0)
538        } else {
539            // Attached to chains - align with direction
540            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
585/// Converts a rotation segment (0-15) to a cardinal direction, if applicable.
586const 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/// Behavior for wall hanging sign blocks.
597#[block_behavior]
598pub struct WallHangingSignBlock {
599    block: BlockRef,
600}
601
602impl WallHangingSignBlock {
603    /// Creates a new wall hanging sign block behavior.
604    #[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        // Wall hanging signs break when blocks on the perpendicular axis are removed
621        // and they can no longer survive
622        if let Some(facing) = state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING) {
623            // Check if the change is on the perpendicular axis (clockwise/counterclockwise)
624            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        // Try each horizontal direction based on player's look direction
636        let directions = get_nearest_looking_directions(context.rotation(), context.clicked_face());
637
638        for direction in directions {
639            // Wall hanging signs face perpendicular to the wall they're attached to
640            // Skip if the clicked face is on the same axis
641            if direction.axis() == context.clicked_face().axis() {
642                continue;
643            }
644
645            let facing = direction.opposite();
646
647            // Check if sign can survive with this facing
648            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        // No valid placement found
658        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}