steel_core/behavior/items/
block_item.rs1use 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#[item_behavior]
27pub struct BlockItem {
28 #[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 #[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 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 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#[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 #[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}