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 has_infinite_materials = context.player.has_infinite_materials();
72        let mut place_context = context.build_place_context();
73        if !place_context.can_place() {
74            return InteractionResult::Fail;
75        }
76        let place_pos = place_context.place_pos();
77
78        let Some(new_state) = self.inner.get_placement_state(&place_context) else {
79            return InteractionResult::Fail;
80        };
81
82        if !context
83            .world
84            .set_block(place_pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE)
85        {
86            return InteractionResult::Fail;
87        }
88        let placed_state = context.world.get_block_state(place_pos);
89
90        let block = self.inner.get_block_for_state(new_state);
91        let sound_type = &block.config.sound_type;
92        context.world.play_block_sound(
93            sound_type.place_sound,
94            place_pos,
95            sound_type.volume,
96            sound_type.pitch,
97            Some(context.player.id()),
98        );
99        context.world.game_event(
100            &vanilla_game_events::BLOCK_PLACE,
101            place_pos,
102            &GameEventContext::new(Some(context.player), Some(placed_state)),
103        );
104
105        place_context.with_item_mut(|item| item.consume_one(has_infinite_materials));
106
107        // Sign-specific: Open the sign editor for the player (front text by default)
108        context.player.open_sign_editor(place_pos, true);
109
110        InteractionResult::Success
111    }
112}
113
114/// Behavior for hanging sign items that place hanging sign blocks.
115///
116/// Hanging signs can be placed as ceiling hanging signs or wall hanging signs.
117#[item_behavior]
118pub struct HangingSignItem {
119    /// The ceiling hanging sign block.
120    #[json_arg(vanilla_blocks, json = "block")]
121    pub ceiling_block: BlockRef,
122    /// The wall hanging sign block.
123    #[json_arg(vanilla_blocks, json = "wall_block")]
124    pub wall_block: BlockRef,
125}
126
127impl HangingSignItem {
128    /// Creates a new hanging sign item behavior.
129    #[must_use]
130    pub const fn new(ceiling_block: BlockRef, wall_block: BlockRef) -> Self {
131        Self {
132            ceiling_block,
133            wall_block,
134        }
135    }
136}
137
138/// Checks if a wall hanging sign can attach to a neighboring block.
139///
140/// This matches vanilla's `WallHangingSignBlock.canAttachTo`.
141fn can_attach_to(
142    world: &Arc<World>,
143    sign_facing: Direction,
144    attach_pos: BlockPos,
145    attach_face: Direction,
146) -> bool {
147    let attach_state = world.get_block_state(attach_pos);
148    let attach_block = REGISTRY.blocks.by_state_id(attach_state);
149
150    if let Some(block) = attach_block
151        && block.has_tag(&BlockTag::WALL_HANGING_SIGNS)
152    {
153        // Wall hanging signs can chain if they're on the same axis
154        if let Some(neighbor_facing) =
155            attach_state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING)
156        {
157            return neighbor_facing.axis() == sign_facing.axis();
158        }
159    }
160
161    // Otherwise, check for sturdy face with FULL support
162    world.is_face_sturdy_for(attach_state, attach_pos, attach_face, SupportType::Full)
163}
164
165/// Checks if a wall hanging sign can be placed at the given position.
166///
167/// This matches vanilla's `WallHangingSignBlock.canPlace` which is called
168/// from `HangingSignItem.canPlace` in addition to `canSurvive`.
169fn can_wall_hanging_sign_place(world: &Arc<World>, state: BlockStateId, pos: BlockPos) -> bool {
170    let Some(facing) = state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING) else {
171        return false;
172    };
173
174    let clockwise = facing.rotate_y_clockwise();
175    let counter_clockwise = facing.rotate_y_counter_clockwise();
176
177    let can_attach_clockwise = {
178        let attach_pos = clockwise.relative(pos);
179        can_attach_to(world, facing, attach_pos, counter_clockwise)
180    };
181
182    let can_attach_counter = {
183        let attach_pos = counter_clockwise.relative(pos);
184        can_attach_to(world, facing, attach_pos, clockwise)
185    };
186
187    can_attach_clockwise || can_attach_counter
188}
189
190/// Checks if a wall hanging sign block state can be placed.
191///
192/// This matches vanilla's `HangingSignItem.canPlace` override which adds
193/// an additional check for `WallHangingSignBlock.canPlace`.
194fn can_place_hanging_sign(world: &Arc<World>, state: BlockStateId, pos: BlockPos) -> bool {
195    let block = REGISTRY.blocks.by_state_id(state);
196
197    // If it's a wall hanging sign, we need the additional canPlace check
198    if let Some(block) = block
199        && block.has_tag(&BlockTag::WALL_HANGING_SIGNS)
200        && !can_wall_hanging_sign_place(world, state, pos)
201    {
202        return false;
203    }
204
205    // All hanging signs need canSurvive check (handled by get_state_for_placement)
206    true
207}
208
209impl ItemBehavior for HangingSignItem {
210    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
211        let has_infinite_materials = context.player.has_infinite_materials();
212        let mut place_context = context.build_place_context();
213        if !place_context.can_place() {
214            return InteractionResult::Fail;
215        }
216        let place_pos = place_context.place_pos();
217
218        let block_behaviors = &*BLOCK_BEHAVIORS;
219
220        // Try ceiling hanging sign first if clicked from below, otherwise try wall
221        let blocks_to_try = if context.hit_result.direction == Direction::Down {
222            [self.ceiling_block, self.wall_block]
223        } else {
224            [self.wall_block, self.ceiling_block]
225        };
226
227        let mut new_state = None;
228        let mut placed_block = None;
229        for block in blocks_to_try {
230            let behavior = block_behaviors.get_behavior(block);
231            let Some(state) = behavior.get_state_for_placement(&place_context) else {
232                continue;
233            };
234
235            // Vanilla's HangingSignItem.canPlace has additional check for wall hanging signs
236            if !can_place_hanging_sign(context.world, state, place_pos) {
237                continue;
238            }
239
240            let collision_shape = state.get_collision_shape_at(place_pos);
241            if context.world.is_unobstructed(collision_shape, place_pos) {
242                new_state = Some(state);
243                placed_block = Some(block);
244                break;
245            }
246        }
247
248        let Some(state) = new_state else {
249            return InteractionResult::Fail;
250        };
251
252        if !context
253            .world
254            .set_block(place_pos, state, UpdateFlags::UPDATE_ALL_IMMEDIATE)
255        {
256            return InteractionResult::Fail;
257        }
258        let placed_state = context.world.get_block_state(place_pos);
259
260        if let Some(block) = placed_block {
261            let sound_type = &block.config.sound_type;
262            context.world.play_block_sound(
263                sound_type.place_sound,
264                place_pos,
265                sound_type.volume,
266                sound_type.pitch,
267                Some(context.player.id()),
268            );
269        }
270        context.world.game_event(
271            &vanilla_game_events::BLOCK_PLACE,
272            place_pos,
273            &GameEventContext::new(Some(context.player), Some(placed_state)),
274        );
275
276        place_context.with_item_mut(|item| item.consume_one(has_infinite_materials));
277
278        // Sign-specific: Open the sign editor for the player (front text by default)
279        context.player.open_sign_editor(place_pos, true);
280
281        InteractionResult::Success
282    }
283}