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::{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        )
54    }
55
56    pub(super) fn place_with_sound_and_block(
57        &self,
58        context: BlockPlaceContext<'_>,
59        place_block: impl FnOnce(&BlockPlaceContext<'_>, BlockStateId) -> bool,
60        place_sound: SoundEventRef,
61    ) -> InteractionResult {
62        self.place_with_policy(
63            context,
64            Some,
65            SurvivalCheck::Required,
66            place_block,
67            place_sound,
68        )
69    }
70
71    #[expect(
72        clippy::manual_midpoint,
73        reason = "Matches vanilla BlockItem::place's sound volume formula"
74    )]
75    pub(super) fn place_with_policy<'a>(
76        &self,
77        context: BlockPlaceContext<'a>,
78        update_context: impl FnOnce(BlockPlaceContext<'a>) -> Option<BlockPlaceContext<'a>>,
79        survival_check: SurvivalCheck,
80        place_block: impl FnOnce(&BlockPlaceContext<'a>, BlockStateId) -> bool,
81        place_sound: SoundEventRef,
82    ) -> InteractionResult {
83        if !context.can_place() {
84            return InteractionResult::Fail;
85        }
86        let Some(mut context) = update_context(context) else {
87            return InteractionResult::Fail;
88        };
89        let place_pos = context.place_pos();
90
91        let behavior = BLOCK_BEHAVIORS.get_behavior(self.block);
92        let Some(new_state) = behavior.get_state_for_placement(&context) else {
93            return InteractionResult::Fail;
94        };
95
96        if matches!(survival_check, SurvivalCheck::Required)
97            && !behavior.can_survive(new_state, context.world.as_ref(), place_pos)
98        {
99            return InteractionResult::Fail;
100        }
101
102        let collision_context = context.player().map_or_else(
103            BlockCollisionContext::placement_without_entity,
104            |player| {
105                BlockCollisionContext::with_position(player.position().y, player.is_descending())
106            },
107        );
108        let collision_shape = OffsetVoxelShape::new(
109            behavior.get_collision_shape(
110                new_state,
111                context.world.as_ref(),
112                place_pos,
113                collision_context,
114            ),
115            behavior.get_collision_shape_offset(
116                new_state,
117                context.world.as_ref(),
118                place_pos,
119                collision_context,
120            ),
121        );
122        if !context.world.is_unobstructed(collision_shape, place_pos) {
123            return InteractionResult::Fail;
124        }
125
126        if !place_block(&context, new_state) {
127            return InteractionResult::Fail;
128        }
129
130        let placed_state = context.world.get_block_state(place_pos);
131        if placed_state.get_block() == self.block {
132            if let Some(block_entity) = context.world.get_block_entity(place_pos) {
133                context.with_item(|item| block_entity.apply_components_from_item(item));
134                block_entity.set_changed();
135            }
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    pub(super) fn place_block(context: &BlockPlaceContext<'_>, state: BlockStateId) -> bool {
169        context
170            .world
171            .set_block(context.place_pos(), state, Self::PLACE_BLOCK_FLAGS)
172    }
173}
174
175impl ItemBehavior for BlockItem {
176    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
177        self.place(context.build_place_context())
178    }
179
180    fn can_fit_inside_container_items(&self) -> bool {
181        BLOCK_BEHAVIORS
182            .get_behavior(self.block)
183            .fits_inside_container_items()
184    }
185}
186
187/// Behavior for double-high block items (doors, tall flowers, etc.).
188///
189/// Vanilla's `DoubleHighBlockItem` extends `BlockItem` and overrides `placeBlock`
190/// to place the upper half block above the lower half.
191///
192/// The `_block` field is read by the build script via `#[json_arg]` to generate constructor
193/// calls from `classes.json`. The actual value is forwarded into `base`.
194#[item_behavior]
195pub struct DoubleHighBlockItem {
196    #[json_arg(vanilla_blocks, json = "block")]
197    _block: BlockRef,
198    base: BlockItem,
199}
200
201impl DoubleHighBlockItem {
202    const PREPARE_UPPER_FLAGS: UpdateFlags =
203        UpdateFlags::UPDATE_ALL_IMMEDIATE.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
204
205    /// Creates a new double-high block item behavior for the given block.
206    #[must_use]
207    pub const fn new(block: BlockRef) -> Self {
208        Self {
209            _block: block,
210            base: BlockItem::new(block),
211        }
212    }
213
214    fn place_block(context: &BlockPlaceContext<'_>, state: BlockStateId) -> bool {
215        let above = context.place_pos().above();
216        let above_state = if get_fluid_state(context.world, above).is_water() {
217            vanilla_blocks::WATER.default_state()
218        } else {
219            vanilla_blocks::AIR.default_state()
220        };
221        let _ = context
222            .world
223            .set_block(above, above_state, Self::PREPARE_UPPER_FLAGS);
224
225        BlockItem::place_block(context, state)
226    }
227}
228
229impl ItemBehavior for DoubleHighBlockItem {
230    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
231        self.base
232            .place_with(context.build_place_context(), Self::place_block)
233    }
234}