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