Skip to main content

steel_core/behavior/items/
scaffolding_block_item.rs

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/// Vanilla scaffolding's directional extension placement.
12#[item_behavior]
13pub struct ScaffoldingBlockItem {
14    #[json_arg(vanilla_blocks, json = "block")]
15    block: BlockRef,
16    base: BlockItem,
17}
18
19impl ScaffoldingBlockItem {
20    /// Creates a scaffolding block item behavior.
21    #[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}