Skip to main content

steel_core/behavior/items/
sign_item.rs

1//! Sign item behavior implementation.
2//!
3//! Places sign blocks and opens the sign editor after placement.
4//! Handles both standing signs (on ground) and wall signs (on walls).
5//!
6//! **Vanilla reference:** `SignItem` extends `StandingAndWallBlockItem` and only
7//! overrides `updateCustomBlockEntityTag` to open the sign editor after placement.
8
9use std::sync::Arc;
10use steel_macros::item_behavior;
11use steel_registry::REGISTRY;
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_tags::BlockTag;
17use steel_registry::vanilla_game_events;
18use steel_utils::types::UpdateFlags;
19use steel_utils::{BlockPos, BlockStateId};
20
21use super::standing_and_wall_block_item::StandingAndWallBlockItem;
22use crate::behavior::context::{InteractionResult, UseOnContext};
23use crate::behavior::{BLOCK_BEHAVIORS, ItemBehavior};
24use crate::entity::Entity;
25use crate::world::game_event::GameEventContext;
26use crate::world::{LevelReader as _, World};
27
28/// Behavior for sign items that place sign blocks and open the editor.
29///
30/// In vanilla, `SignItem` extends `StandingAndWallBlockItem` and only overrides
31/// `updateCustomBlockEntityTag` to open the sign editor after placement.
32///
33/// The `_standing_block`, `_wall_block`, and `_attachment_direction` fields are read by the
34/// build script via `#[json_arg]` to generate constructor calls from `classes.json`.
35/// The actual values are forwarded into `inner` — the fields themselves are not used at runtime.
36#[item_behavior]
37pub struct SignItem {
38    #[json_arg(vanilla_blocks, json = "block")]
39    _standing_block: BlockRef,
40    #[json_arg(vanilla_blocks, json = "wall_block")]
41    _wall_block: BlockRef,
42    #[json_arg(
43        r#enum = "Direction",
44        module = "steel_registry::blocks::properties",
45        json = "attachment_direction"
46    )]
47    _attachment_direction: Direction,
48    /// Placement logic delegate (vanilla: `SignItem extends StandingAndWallBlockItem`).
49    inner: StandingAndWallBlockItem,
50}
51
52impl SignItem {
53    /// Creates a new sign item behavior for the given sign blocks.
54    #[must_use]
55    pub const fn new(
56        standing_block: BlockRef,
57        wall_block: BlockRef,
58        attachment_direction: Direction,
59    ) -> Self {
60        Self {
61            _standing_block: standing_block,
62            _wall_block: wall_block,
63            _attachment_direction: attachment_direction,
64            inner: StandingAndWallBlockItem::new(standing_block, wall_block, attachment_direction),
65        }
66    }
67}
68
69impl ItemBehavior for SignItem {
70    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
71        let mut place_context = context.build_place_context();
72        if !place_context.can_place() {
73            return InteractionResult::Fail;
74        }
75        let place_pos = place_context.place_pos();
76
77        let Some(new_state) = self.inner.get_placement_state(&place_context) else {
78            return InteractionResult::Fail;
79        };
80
81        if !context
82            .world
83            .set_block(place_pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE)
84        {
85            return InteractionResult::Fail;
86        }
87        let placed_state = context.world.get_block_state(place_pos);
88
89        let block = self.inner.get_block_for_state(new_state);
90        let sound_type = &block.config.sound_type;
91        context.world.play_block_sound(
92            sound_type.place_sound,
93            place_pos,
94            sound_type.volume,
95            sound_type.pitch,
96            Some(context.player.id()),
97        );
98        context.world.game_event(
99            &vanilla_game_events::BLOCK_PLACE,
100            place_pos,
101            &GameEventContext::new(Some(context.player), Some(placed_state)),
102        );
103
104        place_context.with_item_mut(|item| item.shrink(1));
105
106        // Sign-specific: Open the sign editor for the player (front text by default)
107        context.player.open_sign_editor(place_pos, true);
108
109        InteractionResult::Success
110    }
111}
112
113/// Behavior for hanging sign items that place hanging sign blocks.
114///
115/// Hanging signs can be placed as ceiling hanging signs or wall hanging signs.
116#[item_behavior]
117pub struct HangingSignItem {
118    /// The ceiling hanging sign block.
119    #[json_arg(vanilla_blocks, json = "block")]
120    pub ceiling_block: BlockRef,
121    /// The wall hanging sign block.
122    #[json_arg(vanilla_blocks, json = "wall_block")]
123    pub wall_block: BlockRef,
124}
125
126impl HangingSignItem {
127    /// Creates a new hanging sign item behavior.
128    #[must_use]
129    pub const fn new(ceiling_block: BlockRef, wall_block: BlockRef) -> Self {
130        Self {
131            ceiling_block,
132            wall_block,
133        }
134    }
135}
136
137/// Checks if a wall hanging sign can attach to a neighboring block.
138///
139/// This matches vanilla's `WallHangingSignBlock.canAttachTo`.
140fn can_attach_to(
141    world: &Arc<World>,
142    sign_facing: Direction,
143    attach_pos: BlockPos,
144    attach_face: Direction,
145) -> bool {
146    let attach_state = world.get_block_state(attach_pos);
147    let attach_block = REGISTRY.blocks.by_state_id(attach_state);
148
149    if let Some(block) = attach_block
150        && block.has_tag(&BlockTag::WALL_HANGING_SIGNS)
151    {
152        // Wall hanging signs can chain if they're on the same axis
153        if let Some(neighbor_facing) =
154            attach_state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING)
155        {
156            return neighbor_facing.axis() == sign_facing.axis();
157        }
158    }
159
160    // Otherwise, check for sturdy face with FULL support
161    world.is_face_sturdy_for(attach_state, attach_pos, attach_face, SupportType::Full)
162}
163
164/// Checks if a wall hanging sign can be placed at the given position.
165///
166/// This matches vanilla's `WallHangingSignBlock.canPlace` which is called
167/// from `HangingSignItem.canPlace` in addition to `canSurvive`.
168fn can_wall_hanging_sign_place(world: &Arc<World>, state: BlockStateId, pos: BlockPos) -> bool {
169    let Some(facing) = state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING) else {
170        return false;
171    };
172
173    let clockwise = facing.rotate_y_clockwise();
174    let counter_clockwise = facing.rotate_y_counter_clockwise();
175
176    let can_attach_clockwise = {
177        let attach_pos = clockwise.relative(pos);
178        can_attach_to(world, facing, attach_pos, counter_clockwise)
179    };
180
181    let can_attach_counter = {
182        let attach_pos = counter_clockwise.relative(pos);
183        can_attach_to(world, facing, attach_pos, clockwise)
184    };
185
186    can_attach_clockwise || can_attach_counter
187}
188
189/// Checks if a wall hanging sign block state can be placed.
190///
191/// This matches vanilla's `HangingSignItem.canPlace` override which adds
192/// an additional check for `WallHangingSignBlock.canPlace`.
193fn can_place_hanging_sign(world: &Arc<World>, state: BlockStateId, pos: BlockPos) -> bool {
194    let block = REGISTRY.blocks.by_state_id(state);
195
196    // If it's a wall hanging sign, we need the additional canPlace check
197    if let Some(block) = block
198        && block.has_tag(&BlockTag::WALL_HANGING_SIGNS)
199        && !can_wall_hanging_sign_place(world, state, pos)
200    {
201        return false;
202    }
203
204    // All hanging signs need canSurvive check (handled by get_state_for_placement)
205    true
206}
207
208impl ItemBehavior for HangingSignItem {
209    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
210        let mut place_context = context.build_place_context();
211        if !place_context.can_place() {
212            return InteractionResult::Fail;
213        }
214        let place_pos = place_context.place_pos();
215
216        let block_behaviors = &*BLOCK_BEHAVIORS;
217
218        // Try ceiling hanging sign first if clicked from below, otherwise try wall
219        let blocks_to_try = if context.hit_result.direction == Direction::Down {
220            [self.ceiling_block, self.wall_block]
221        } else {
222            [self.wall_block, self.ceiling_block]
223        };
224
225        let mut new_state = None;
226        let mut placed_block = None;
227        for block in blocks_to_try {
228            let behavior = block_behaviors.get_behavior(block);
229            let Some(state) = behavior.get_state_for_placement(&place_context) else {
230                continue;
231            };
232
233            // Vanilla's HangingSignItem.canPlace has additional check for wall hanging signs
234            if !can_place_hanging_sign(context.world, state, place_pos) {
235                continue;
236            }
237
238            let collision_shape = state.get_collision_shape_at(place_pos);
239            if context.world.is_unobstructed(collision_shape, place_pos) {
240                new_state = Some(state);
241                placed_block = Some(block);
242                break;
243            }
244        }
245
246        let Some(state) = new_state else {
247            return InteractionResult::Fail;
248        };
249
250        if !context
251            .world
252            .set_block(place_pos, state, UpdateFlags::UPDATE_ALL_IMMEDIATE)
253        {
254            return InteractionResult::Fail;
255        }
256        let placed_state = context.world.get_block_state(place_pos);
257
258        if let Some(block) = placed_block {
259            let sound_type = &block.config.sound_type;
260            context.world.play_block_sound(
261                sound_type.place_sound,
262                place_pos,
263                sound_type.volume,
264                sound_type.pitch,
265                Some(context.player.id()),
266            );
267        }
268        context.world.game_event(
269            &vanilla_game_events::BLOCK_PLACE,
270            place_pos,
271            &GameEventContext::new(Some(context.player), Some(placed_state)),
272        );
273
274        place_context.with_item_mut(|item| item.shrink(1));
275
276        // Sign-specific: Open the sign editor for the player (front text by default)
277        context.player.open_sign_editor(place_pos, true);
278
279        InteractionResult::Success
280    }
281}