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            |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}