1use steel_macros::item_behavior;
2use steel_registry::blocks::{BlockRef, block_state_ext::BlockStateExt, properties::Direction};
3
4use super::block_item::SurvivalCheck;
5use crate::behavior::blocks::ScaffoldingBlock;
6use crate::behavior::{
7 BlockItem, BlockPlaceContext, BlockStateBehaviorExt, InteractionResult, ItemBehavior,
8 UseOnContext,
9};
10
11#[item_behavior]
13pub struct ScaffoldingBlockItem {
14 #[json_arg(vanilla_blocks, json = "block")]
15 block: BlockRef,
16 base: BlockItem,
17}
18
19impl ScaffoldingBlockItem {
20 #[must_use]
22 pub const fn new(block: BlockRef) -> Self {
23 Self {
24 block,
25 base: BlockItem::new(block),
26 }
27 }
28
29 fn place(&self, context: BlockPlaceContext<'_>) -> InteractionResult {
30 self.base.place_with_policy(
31 context,
32 |place_context| self.update_placement_context(place_context),
33 SurvivalCheck::Skipped,
34 BlockItem::place_block,
35 self.block.config.sound_type.place_sound,
36 )
37 }
38
39 fn update_placement_context<'a>(
40 &self,
41 context: BlockPlaceContext<'a>,
42 ) -> Option<BlockPlaceContext<'a>> {
43 let pos = context.place_pos();
44 let state = context.world.get_block_state(pos);
45 if state.get_block() != self.block {
46 return (ScaffoldingBlock::get_distance(context.world.as_ref(), pos)
47 < ScaffoldingBlock::STABILITY_MAX_DISTANCE)
48 .then_some(context);
49 }
50
51 let direction = if context.is_secondary_use_active() {
52 if context.is_inside() {
53 context.clicked_face().opposite()
54 } else {
55 context.clicked_face()
56 }
57 } else if context.clicked_face() == Direction::Up {
58 context.horizontal_direction()
59 } else {
60 Direction::Up
61 };
62
63 let mut horizontal_distance = 0;
64 let mut placement_pos = direction.relative(pos);
65 while horizontal_distance < ScaffoldingBlock::STABILITY_MAX_DISTANCE {
66 if !context.world.is_in_world_bounds(placement_pos) {
67 if placement_pos.y() > context.world.get_max_y()
68 && let Some(player) = context.player()
69 {
70 player.send_build_limit_too_high_message(context.world.get_max_y());
71 }
72 break;
73 }
74
75 let state = context.world.get_block_state(placement_pos);
76 if state.get_block() != self.block {
77 if state.can_be_replaced(&context) {
78 return Some(context.at(placement_pos, direction));
79 }
80 break;
81 }
82
83 placement_pos = direction.relative(placement_pos);
84 if direction.is_horizontal() {
85 horizontal_distance += 1;
86 }
87 }
88
89 None
90 }
91}
92
93impl ItemBehavior for ScaffoldingBlockItem {
94 fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
95 self.place(context.build_place_context())
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use std::sync::Arc;
102
103 use glam::DVec3;
104 use steel_registry::blocks::{
105 block_state_ext::BlockStateExt,
106 properties::{BlockStateProperties, BoolProperty, IntProperty},
107 };
108 use steel_registry::item_stack::ItemStack;
109 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_items};
110 use steel_utils::types::{InteractionHand, UpdateFlags};
111 use steel_utils::{BlockPos, ChunkPos};
112
113 use super::*;
114 use crate::behavior::{BlockHitResult, PlacementOrientation, PlacementSource, init_behaviors};
115 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
116 use crate::world::{LevelReader, World};
117
118 const BOTTOM: &BoolProperty = &BlockStateProperties::BOTTOM;
119 const DISTANCE: &IntProperty = &BlockStateProperties::STABILITY_DISTANCE;
120 const EAST_FACING_YAW_DEGREES: f32 = 270.0;
121 const VANILLA_STABILITY_MAX_DISTANCE: u8 = 7;
122 const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
123
124 fn test_world(key: &'static str) -> Arc<World> {
125 init_vanilla_registry();
126 init_behaviors();
127 let world = fresh_test_world(key);
128 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
129 world
130 }
131
132 fn set_block(world: &Arc<World>, pos: BlockPos, state: steel_utils::BlockStateId) {
133 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
134 }
135
136 fn scaffolding_state(distance: u8) -> steel_utils::BlockStateId {
137 vanilla_blocks::SCAFFOLDING
138 .default_state()
139 .set_value(DISTANCE, distance)
140 .set_value(BOTTOM, distance > 0)
141 }
142
143 fn place_context<'a>(
144 world: &'a Arc<World>,
145 stack: &'a mut ItemStack,
146 hit_pos: BlockPos,
147 clicked_face: Direction,
148 inside: bool,
149 secondary_use: bool,
150 yaw: f32,
151 ) -> BlockPlaceContext<'a> {
152 let source = PlacementSource::direct(
153 None,
154 InteractionHand::MainHand,
155 stack,
156 PlacementOrientation::Player {
157 rotation: yaw,
158 pitch: 0.0,
159 },
160 secondary_use,
161 );
162 BlockPlaceContext::new(
163 world,
164 source,
165 &BlockHitResult {
166 location: DVec3::new(
167 f64::from(hit_pos.x()) + 0.5,
168 f64::from(hit_pos.y()) + 0.5,
169 f64::from(hit_pos.z()) + 0.5,
170 ),
171 direction: clicked_face,
172 block_pos: hit_pos,
173 miss: false,
174 inside,
175 world_border_hit: false,
176 },
177 )
178 }
179
180 fn routed_pos(
181 item: &ScaffoldingBlockItem,
182 world: &Arc<World>,
183 hit_pos: BlockPos,
184 clicked_face: Direction,
185 inside: bool,
186 secondary_use: bool,
187 yaw: f32,
188 ) -> Option<BlockPos> {
189 let mut stack = ItemStack::new(&vanilla_items::SCAFFOLDING);
190 let context = place_context(
191 world,
192 &mut stack,
193 hit_pos,
194 clicked_face,
195 inside,
196 secondary_use,
197 yaw,
198 );
199 item.update_placement_context(context)
200 .map(|context| context.place_pos())
201 }
202
203 #[test]
204 fn routing_matches_normal_secondary_and_inside_direction_rules() {
205 let world = test_world("scaffolding_item_directions");
206 let pos = BlockPos::new(8, 64, 8);
207 set_block(&world, pos, scaffolding_state(0));
208 let item = ScaffoldingBlockItem::new(&vanilla_blocks::SCAFFOLDING);
209
210 assert_eq!(
211 routed_pos(
212 &item,
213 &world,
214 pos,
215 Direction::Up,
216 false,
217 false,
218 EAST_FACING_YAW_DEGREES,
219 ),
220 Some(pos.east())
221 );
222 assert_eq!(
223 routed_pos(
224 &item,
225 &world,
226 pos,
227 Direction::North,
228 false,
229 false,
230 EAST_FACING_YAW_DEGREES,
231 ),
232 Some(pos.above())
233 );
234 assert_eq!(
235 routed_pos(
236 &item,
237 &world,
238 pos,
239 Direction::North,
240 false,
241 true,
242 EAST_FACING_YAW_DEGREES,
243 ),
244 Some(pos.north())
245 );
246 assert_eq!(
247 routed_pos(
248 &item,
249 &world,
250 pos,
251 Direction::North,
252 true,
253 true,
254 EAST_FACING_YAW_DEGREES,
255 ),
256 Some(pos.south())
257 );
258 }
259
260 #[test]
261 fn horizontal_routing_allows_seventh_position_but_not_eighth() {
262 let world = test_world("scaffolding_item_horizontal_limit");
263 let start = BlockPos::new(4, 64, 8);
264 let item = ScaffoldingBlockItem::new(&vanilla_blocks::SCAFFOLDING);
265 let max_distance = i32::from(VANILLA_STABILITY_MAX_DISTANCE);
266 for offset in 0..max_distance {
267 set_block(
268 &world,
269 BlockPos::new(start.x() + offset, start.y(), start.z()),
270 scaffolding_state(offset as u8),
271 );
272 }
273
274 let seventh = BlockPos::new(start.x() + max_distance, start.y(), start.z());
275 assert_eq!(
276 routed_pos(
277 &item,
278 &world,
279 start,
280 Direction::Up,
281 false,
282 false,
283 EAST_FACING_YAW_DEGREES,
284 ),
285 Some(seventh)
286 );
287
288 set_block(
289 &world,
290 seventh,
291 scaffolding_state(VANILLA_STABILITY_MAX_DISTANCE),
292 );
293 assert_eq!(
294 routed_pos(
295 &item,
296 &world,
297 start,
298 Direction::Up,
299 false,
300 false,
301 EAST_FACING_YAW_DEGREES,
302 ),
303 None
304 );
305 }
306
307 #[test]
308 fn distance_seven_extension_places_then_waits_for_stability_tick() {
309 let world = test_world("scaffolding_item_unstable_extension");
310 let start = BlockPos::new(4, 64, 8);
311 set_block(&world, start.below(), vanilla_blocks::STONE.default_state());
312 let last_stable_offset = i32::from(VANILLA_STABILITY_MAX_DISTANCE) - 1;
313 for offset in 0..=last_stable_offset {
314 set_block(
315 &world,
316 BlockPos::new(start.x() + offset, start.y(), start.z()),
317 scaffolding_state(offset as u8),
318 );
319 }
320
321 let item = ScaffoldingBlockItem::new(&vanilla_blocks::SCAFFOLDING);
322 let end = BlockPos::new(start.x() + last_stable_offset, start.y(), start.z());
323 let placed = end.east();
324 let mut stack = ItemStack::with_count(&vanilla_items::SCAFFOLDING, 2);
325 let context = place_context(
326 &world,
327 &mut stack,
328 end,
329 Direction::Up,
330 false,
331 false,
332 EAST_FACING_YAW_DEGREES,
333 );
334
335 assert_eq!(item.place(context), InteractionResult::Success);
336 let state = world.get_block_state(placed);
337 assert_eq!(state.get_block(), &vanilla_blocks::SCAFFOLDING);
338 assert_eq!(state.get_value(DISTANCE), VANILLA_STABILITY_MAX_DISTANCE);
339 assert!(state.get_value(BOTTOM));
340 assert_eq!(stack.count(), 1);
341 }
342
343 #[test]
344 fn unsupported_direct_target_is_rejected_without_consuming_item() {
345 let world = test_world("scaffolding_item_unsupported_direct");
346 let pos = BlockPos::new(8, 64, 8);
347 let item = ScaffoldingBlockItem::new(&vanilla_blocks::SCAFFOLDING);
348 let mut stack = ItemStack::with_count(&vanilla_items::SCAFFOLDING, 2);
349 let context = place_context(&world, &mut stack, pos, Direction::Up, false, false, 0.0);
350
351 assert_eq!(item.place(context), InteractionResult::Fail);
352 assert!(world.get_block_state(pos).is_air());
353 assert_eq!(stack.count(), 2);
354 }
355
356 #[test]
357 fn supported_source_water_placement_is_waterlogged() {
358 let world = test_world("scaffolding_item_waterlogged");
359 let pos = BlockPos::new(8, 64, 8);
360 set_block(&world, pos.below(), vanilla_blocks::STONE.default_state());
361 set_block(&world, pos, vanilla_blocks::WATER.default_state());
362 let item = ScaffoldingBlockItem::new(&vanilla_blocks::SCAFFOLDING);
363 let mut stack = ItemStack::new(&vanilla_items::SCAFFOLDING);
364 let context = place_context(&world, &mut stack, pos, Direction::Up, false, false, 0.0);
365
366 assert_eq!(item.place(context), InteractionResult::Success);
367 let state = world.get_block_state(pos);
368 assert_eq!(state.get_block(), &vanilla_blocks::SCAFFOLDING);
369 assert_eq!(state.get_value(DISTANCE), 0);
370 assert!(!state.get_value(BOTTOM));
371 assert!(state.get_value(WATERLOGGED));
372 assert!(stack.is_empty());
373 }
374}