Skip to main content

steel_core/behavior/items/
block_item.rs

1//! Block item behavior implementation.
2
3use steel_macros::item_behavior;
4use steel_registry::{
5    blocks::{BlockRef, block_state_ext::BlockStateExt, shapes::OffsetVoxelShape},
6    sound_event::SoundEventRef,
7    vanilla_blocks, vanilla_game_events,
8};
9use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
10
11use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext, ItemBehavior};
12use crate::entity::Entity;
13use crate::fluid::{FluidStateExt as _, get_fluid_state};
14use crate::world::game_event::GameEventContext;
15use crate::{
16    behavior::context::{BlockPlaceContext, InteractionResult, UseOnContext},
17    player::Player,
18};
19
20pub(super) enum SurvivalCheck {
21    Required,
22    Skipped,
23}
24
25/// Behavior for items that place blocks.
26#[item_behavior]
27pub struct BlockItem {
28    /// The block this item places.
29    #[json_arg(vanilla_blocks, json = "block")]
30    pub block: BlockRef,
31}
32
33impl BlockItem {
34    const PLACE_BLOCK_FLAGS: UpdateFlags = UpdateFlags::UPDATE_ALL_IMMEDIATE;
35
36    /// Creates a new block item behavior for the given block.
37    #[must_use]
38    pub const fn new(block: BlockRef) -> Self {
39        Self { block }
40    }
41
42    pub(super) fn place_with(
43        &self,
44        context: BlockPlaceContext<'_>,
45        place_block: impl FnOnce(&BlockPlaceContext<'_>, BlockStateId) -> bool,
46    ) -> InteractionResult {
47        self.place_with_policy(
48            context,
49            Some,
50            SurvivalCheck::Required,
51            place_block,
52            self.block.config.sound_type.place_sound,
53            |place_context| self.get_state_for_placement(place_context),
54        )
55    }
56
57    pub(super) fn place_with_sound_and_block(
58        &self,
59        context: BlockPlaceContext<'_>,
60        place_block: impl FnOnce(&BlockPlaceContext<'_>, BlockStateId) -> bool,
61        place_sound: SoundEventRef,
62    ) -> InteractionResult {
63        self.place_with_policy(
64            context,
65            Some,
66            SurvivalCheck::Required,
67            place_block,
68            place_sound,
69            |place_context| self.get_state_for_placement(place_context),
70        )
71    }
72
73    #[expect(
74        clippy::manual_midpoint,
75        reason = "Matches vanilla BlockItem::place's sound volume formula"
76    )]
77    pub(super) fn place_with_policy<'a>(
78        &self,
79        context: BlockPlaceContext<'a>,
80        update_context: impl FnOnce(BlockPlaceContext<'a>) -> Option<BlockPlaceContext<'a>>,
81        survival_check: SurvivalCheck,
82        place_block: impl FnOnce(&BlockPlaceContext<'a>, BlockStateId) -> bool,
83        place_sound: SoundEventRef,
84        get_state_for_placement: impl FnOnce(&BlockPlaceContext<'a>) -> Option<BlockStateId>,
85    ) -> InteractionResult {
86        if !context.can_place() {
87            return InteractionResult::Fail;
88        }
89        let Some(mut context) = update_context(context) else {
90            return InteractionResult::Fail;
91        };
92        let place_pos = context.place_pos();
93
94        let Some(new_state) = get_state_for_placement(&context) else {
95            return InteractionResult::Fail;
96        };
97
98        let behavior = BLOCK_BEHAVIORS.get_behavior(new_state.get_block());
99        if matches!(survival_check, SurvivalCheck::Required)
100            && !behavior.can_survive(new_state, context.world.as_ref(), place_pos)
101        {
102            return InteractionResult::Fail;
103        }
104
105        let collision_context = context.player().map_or_else(
106            BlockCollisionContext::placement_without_entity,
107            |player| {
108                BlockCollisionContext::with_position(player.position().y, player.is_descending())
109            },
110        );
111        let collision_shape = OffsetVoxelShape::new(
112            behavior.get_collision_shape(
113                new_state,
114                context.world.as_ref(),
115                place_pos,
116                collision_context,
117            ),
118            behavior.get_collision_shape_offset(
119                new_state,
120                context.world.as_ref(),
121                place_pos,
122                collision_context,
123            ),
124        );
125        if !context.world.is_unobstructed(collision_shape, place_pos) {
126            return InteractionResult::Fail;
127        }
128
129        if !place_block(&context, new_state) {
130            return InteractionResult::Fail;
131        }
132
133        let placed_state = context.world.get_block_state(place_pos);
134        if placed_state.get_block() == new_state.get_block() {
135            Self::update_block_entity_components(&context, place_pos);
136            let placed_behavior = BLOCK_BEHAVIORS.get_behavior(placed_state.get_block());
137            placed_behavior.set_placed_by(placed_state, context.world, place_pos, context.source());
138        }
139
140        // Play place sound (exclude the placing player, they hear it client-side)
141        context.world.play_block_sound(
142            place_sound,
143            place_pos,
144            (self.block.config.sound_type.volume + 1.0) / 2.0,
145            self.block.config.sound_type.pitch * 0.8,
146            context.player().map(Entity::id),
147        );
148        context.world.game_event(
149            &vanilla_game_events::BLOCK_PLACE,
150            place_pos,
151            &GameEventContext::new(
152                context.player().map(|player| player as &dyn Entity),
153                Some(placed_state),
154            ),
155        );
156
157        let has_infinite_materials = context.player().is_some_and(Player::has_infinite_materials);
158        context.with_item_mut(|item| item.consume_one(has_infinite_materials));
159
160        InteractionResult::Success
161    }
162
163    /// Places this block using an already constructed placement context.
164    pub fn place(&self, context: BlockPlaceContext<'_>) -> InteractionResult {
165        self.place_with(context, Self::place_block)
166    }
167
168    fn update_block_entity_components(context: &BlockPlaceContext<'_>, pos: BlockPos) {
169        let Some(block_entity) = context.world.get_block_entity(pos) else {
170            return;
171        };
172        context
173            .source()
174            .with_item(|stack| block_entity.apply_components_from_item_stack(stack));
175        block_entity.set_changed();
176    }
177
178    pub(super) fn place_block(context: &BlockPlaceContext<'_>, state: BlockStateId) -> bool {
179        context
180            .world
181            .set_block(context.place_pos(), state, Self::PLACE_BLOCK_FLAGS)
182    }
183
184    pub(super) fn get_state_for_placement(
185        &self,
186        context: &BlockPlaceContext<'_>,
187    ) -> Option<BlockStateId> {
188        let behavior = BLOCK_BEHAVIORS.get_behavior(self.block);
189        behavior.get_state_for_placement(context)
190    }
191}
192
193impl ItemBehavior for BlockItem {
194    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
195        self.place(context.build_place_context())
196    }
197
198    fn can_fit_inside_container_items(&self) -> bool {
199        BLOCK_BEHAVIORS
200            .get_behavior(self.block)
201            .fits_inside_container_items()
202    }
203}
204
205/// Behavior for double-high block items (doors, tall flowers, etc.).
206///
207/// Vanilla's `DoubleHighBlockItem` extends `BlockItem` and overrides `placeBlock`
208/// to place the upper half block above the lower half.
209///
210/// The `_block` field is read by the build script via `#[json_arg]` to generate constructor
211/// calls from `classes.json`. The actual value is forwarded into `base`.
212#[item_behavior]
213pub struct DoubleHighBlockItem {
214    #[json_arg(vanilla_blocks, json = "block")]
215    _block: BlockRef,
216    base: BlockItem,
217}
218
219impl DoubleHighBlockItem {
220    const PREPARE_UPPER_FLAGS: UpdateFlags =
221        UpdateFlags::UPDATE_ALL_IMMEDIATE.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
222
223    /// Creates a new double-high block item behavior for the given block.
224    #[must_use]
225    pub const fn new(block: BlockRef) -> Self {
226        Self {
227            _block: block,
228            base: BlockItem::new(block),
229        }
230    }
231
232    fn place_block(context: &BlockPlaceContext<'_>, state: BlockStateId) -> bool {
233        let above = context.place_pos().above();
234        let above_state = if get_fluid_state(context.world, above).is_water() {
235            vanilla_blocks::WATER.default_state()
236        } else {
237            vanilla_blocks::AIR.default_state()
238        };
239        let _ = context
240            .world
241            .set_block(above, above_state, Self::PREPARE_UPPER_FLAGS);
242
243        BlockItem::place_block(context, state)
244    }
245}
246
247impl ItemBehavior for DoubleHighBlockItem {
248    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
249        self.base
250            .place_with(context.build_place_context(), Self::place_block)
251    }
252}