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},
6    vanilla_blocks, vanilla_game_events,
7};
8use steel_utils::{BlockStateId, types::UpdateFlags};
9
10use crate::behavior::context::{BlockPlaceContext, InteractionResult, UseOnContext};
11use crate::behavior::{BLOCK_BEHAVIORS, ItemBehavior};
12use crate::entity::Entity;
13use crate::fluid::{FluidStateExt as _, get_fluid_state};
14use crate::world::game_event::GameEventContext;
15
16/// Behavior for items that place blocks.
17#[item_behavior]
18pub struct BlockItem {
19    /// The block this item places.
20    #[json_arg(vanilla_blocks, json = "block")]
21    pub block: BlockRef,
22}
23
24impl BlockItem {
25    const PLACE_BLOCK_FLAGS: UpdateFlags = UpdateFlags::UPDATE_ALL_IMMEDIATE;
26
27    /// Creates a new block item behavior for the given block.
28    #[must_use]
29    pub const fn new(block: BlockRef) -> Self {
30        Self { block }
31    }
32
33    fn place_with(
34        &self,
35        mut context: BlockPlaceContext<'_>,
36        place_block: impl FnOnce(&BlockPlaceContext<'_>, BlockStateId) -> bool,
37    ) -> InteractionResult {
38        if !context.can_place() {
39            return InteractionResult::Fail;
40        }
41        let place_pos = context.place_pos();
42
43        let behavior = BLOCK_BEHAVIORS.get_behavior(self.block);
44        let Some(new_state) = behavior.get_state_for_placement(&context) else {
45            return InteractionResult::Fail;
46        };
47
48        if !behavior.can_survive(new_state, context.world, place_pos) {
49            return InteractionResult::Fail;
50        }
51
52        let collision_shape = new_state.get_collision_shape_at(place_pos);
53        if !context.world.is_unobstructed(collision_shape, place_pos) {
54            return InteractionResult::Fail;
55        }
56
57        if !place_block(&context, new_state) {
58            return InteractionResult::Fail;
59        }
60
61        let placed_state = context.world.get_block_state(place_pos);
62        if placed_state.get_block() == self.block {
63            let placed_behavior = BLOCK_BEHAVIORS.get_behavior(placed_state.get_block());
64            placed_behavior.set_placed_by(placed_state, context.world, place_pos, context.source());
65        }
66
67        // Play place sound (exclude the placing player, they hear it client-side)
68        let sound_type = &self.block.config.sound_type;
69        context.world.play_block_sound(
70            sound_type.place_sound,
71            place_pos,
72            sound_type.volume,
73            sound_type.pitch,
74            context.player().map(Entity::id),
75        );
76        context.world.game_event(
77            &vanilla_game_events::BLOCK_PLACE,
78            place_pos,
79            &GameEventContext::new(
80                context.player().map(|player| player as &dyn Entity),
81                Some(placed_state),
82            ),
83        );
84
85        context.with_item_mut(|item| item.shrink(1));
86
87        InteractionResult::Success
88    }
89
90    /// Places this block using an already constructed placement context.
91    pub fn place(&self, context: BlockPlaceContext<'_>) -> InteractionResult {
92        self.place_with(context, Self::place_block)
93    }
94
95    fn place_block(context: &BlockPlaceContext<'_>, state: BlockStateId) -> bool {
96        context
97            .world
98            .set_block(context.place_pos(), state, Self::PLACE_BLOCK_FLAGS)
99    }
100}
101
102impl ItemBehavior for BlockItem {
103    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
104        self.place(context.build_place_context())
105    }
106}
107
108/// Behavior for double-high block items (doors, tall flowers, etc.).
109///
110/// Vanilla's `DoubleHighBlockItem` extends `BlockItem` and overrides `placeBlock`
111/// to place the upper half block above the lower half.
112///
113/// The `_block` field is read by the build script via `#[json_arg]` to generate constructor
114/// calls from `classes.json`. The actual value is forwarded into `base`.
115#[item_behavior]
116pub struct DoubleHighBlockItem {
117    #[json_arg(vanilla_blocks, json = "block")]
118    _block: BlockRef,
119    base: BlockItem,
120}
121
122impl DoubleHighBlockItem {
123    const PREPARE_UPPER_FLAGS: UpdateFlags =
124        UpdateFlags::UPDATE_ALL_IMMEDIATE.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
125
126    /// Creates a new double-high block item behavior for the given block.
127    #[must_use]
128    pub const fn new(block: BlockRef) -> Self {
129        Self {
130            _block: block,
131            base: BlockItem::new(block),
132        }
133    }
134
135    fn place_block(context: &BlockPlaceContext<'_>, state: BlockStateId) -> bool {
136        let above = context.place_pos().above();
137        let above_state = if get_fluid_state(context.world, above).is_water() {
138            vanilla_blocks::WATER.default_state()
139        } else {
140            vanilla_blocks::AIR.default_state()
141        };
142        let _ = context
143            .world
144            .set_block(above, above_state, Self::PREPARE_UPPER_FLAGS);
145
146        BlockItem::place_block(context, state)
147    }
148}
149
150impl ItemBehavior for DoubleHighBlockItem {
151    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
152        self.base
153            .place_with(context.build_place_context(), Self::place_block)
154    }
155}