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::{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#[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 |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 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 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#[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 #[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}