Skip to main content

steel_core/behavior/blocks/redstone/piston/
base.rs

1//! Vanilla piston and sticky-piston behavior.
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_protocol::packets::game::SoundSource;
7use steel_registry::blocks::BlockRef;
8use steel_registry::blocks::behavior::PushReaction;
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::blocks::properties::{
11    BlockStateProperties, BoolProperty, Direction, EnumProperty, PistonType,
12};
13use steel_registry::{sound_events, vanilla_blocks, vanilla_game_events};
14use steel_utils::types::UpdateFlags;
15use steel_utils::{BlockPos, BlockStateId, Downcast as _};
16
17use super::structure_resolver::{PistonLevel, PistonStructureResolver};
18use crate::behavior::blocks::redstone::java_hash;
19use crate::behavior::{BLOCK_BEHAVIORS, BlockBehavior, BlockPlaceContext, PlacementSource};
20use crate::block_entity::SharedBlockEntity;
21use crate::block_entity::entities::PistonMovingBlockEntity;
22use crate::entity::ai::path::PathComputationType;
23use crate::world::game_event::GameEventContext;
24use crate::world::{LevelReader, SignalGetter as _, World};
25
26const UPDATE_RETRACT_BASE: UpdateFlags = UpdateFlags::UPDATE_INVISIBLE
27    .union(UpdateFlags::UPDATE_KNOWN_SHAPE)
28    .union(UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS);
29const UPDATE_MOVING_BLOCK: UpdateFlags = UpdateFlags::UPDATE_INVISIBLE
30    .union(UpdateFlags::UPDATE_MOVE_BY_PISTON)
31    .union(UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS);
32const UPDATE_DESTROYED_BLOCK: UpdateFlags =
33    UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
34const UPDATE_CLEARED_MOVED_BLOCK: UpdateFlags = UpdateFlags::UPDATE_CLIENTS
35    .union(UpdateFlags::UPDATE_KNOWN_SHAPE)
36    .union(UpdateFlags::UPDATE_MOVE_BY_PISTON);
37
38/// Vanilla `PistonBaseBlock` shared by normal and sticky pistons.
39#[block_behavior]
40pub struct PistonBaseBlock {
41    block: BlockRef,
42    #[json_arg(value, json = "is_sticky")]
43    sticky: bool,
44}
45
46const EXTENDED: &BoolProperty = &BlockStateProperties::EXTENDED;
47const FACING: &EnumProperty<Direction> = &BlockStateProperties::FACING;
48const PISTON_TYPE: &EnumProperty<PistonType> = &BlockStateProperties::PISTON_TYPE;
49
50impl PistonBaseBlock {
51    /// Creates normal or sticky piston behavior from extracted constructor data.
52    #[must_use]
53    pub const fn new(block: BlockRef, is_sticky: bool) -> Self {
54        Self {
55            block,
56            sticky: is_sticky,
57        }
58    }
59
60    const fn direction_from_legacy_id(id: i32) -> Direction {
61        match id & 7 {
62            1 => Direction::Up,
63            2 => Direction::North,
64            3 => Direction::South,
65            4 => Direction::West,
66            5 => Direction::East,
67            _ => Direction::Down,
68        }
69    }
70
71    const fn direction_legacy_id(direction: Direction) -> i32 {
72        match direction {
73            Direction::Down => 0,
74            Direction::Up => 1,
75            Direction::North => 2,
76            Direction::South => 3,
77            Direction::West => 4,
78            Direction::East => 5,
79        }
80    }
81
82    fn neighbor_signal(world: &World, pos: BlockPos, push_direction: Direction) -> bool {
83        for direction in Direction::ALL {
84            if direction != push_direction && world.has_signal(pos.relative(direction), direction) {
85                return true;
86            }
87        }
88        if world.has_signal(pos, Direction::Down) {
89            return true;
90        }
91
92        let above = pos.above();
93        for direction in Direction::ALL {
94            if direction != Direction::Down
95                && world.has_signal(above.relative(direction), direction)
96            {
97                return true;
98            }
99        }
100        false
101    }
102
103    fn check_if_extend(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
104        let direction = state.get_value(FACING);
105        let powered = Self::neighbor_signal(world, pos, direction);
106        if powered && !state.get_value(EXTENDED) {
107            let mut resolver = PistonStructureResolver::new(world.as_ref(), pos, direction, true);
108            if resolver.resolve() {
109                world.block_event(pos, self.block, 0, Self::direction_legacy_id(direction));
110            }
111            return;
112        }
113        if powered || !state.get_value(EXTENDED) {
114            return;
115        }
116
117        let pushed_pos = pos.relative_n(direction, 2);
118        let pushed_state = world.get_block_state(pushed_pos);
119        let event = if pushed_state.get_block() == &vanilla_blocks::MOVING_PISTON
120            && pushed_state.get_value(FACING) == direction
121            && world
122                .get_block_entity(pushed_pos)
123                .is_some_and(|block_entity| {
124                    block_entity
125                        .downcast_ref::<PistonMovingBlockEntity>()
126                        .is_some_and(|piston| {
127                            piston.is_extending()
128                                && (piston.progress(0.0) < 0.5
129                                    || world.game_time() == piston.last_ticked()
130                                    || world.is_handling_tick())
131                        })
132                }) {
133            2
134        } else {
135            1
136        };
137        world.block_event(pos, self.block, event, Self::direction_legacy_id(direction));
138    }
139
140    fn finish_moving_block_entity(world: &Arc<World>, pos: BlockPos) -> bool {
141        let Some(block_entity) = world.get_block_entity(pos) else {
142            return false;
143        };
144        let Some(piston) = block_entity.downcast_ref::<PistonMovingBlockEntity>() else {
145            return false;
146        };
147        piston.final_tick(world);
148        true
149    }
150
151    fn moving_block_entity(
152        world: &Arc<World>,
153        pos: BlockPos,
154        state: BlockStateId,
155        moved_state: BlockStateId,
156        direction: Direction,
157        extending: bool,
158        source: bool,
159    ) -> SharedBlockEntity {
160        Arc::new(PistonMovingBlockEntity::new_moving(
161            Arc::downgrade(world),
162            pos,
163            state,
164            moved_state,
165            direction,
166            extending,
167            source,
168        ))
169    }
170
171    #[expect(
172        clippy::float_cmp,
173        reason = "vanilla uses -1.0 as the exact unbreakable destroy-time sentinel"
174    )]
175    pub(super) fn is_pushable(
176        state: BlockStateId,
177        world: &dyn PistonLevel,
178        pos: BlockPos,
179        direction: Direction,
180        allow_destroyable: bool,
181        connection_direction: Direction,
182    ) -> bool {
183        if world.is_outside_build_height(pos.y()) || !world.is_within_world_border(pos) {
184            return false;
185        }
186        if state.is_air() {
187            return true;
188        }
189
190        let block = state.get_block();
191        if block == &vanilla_blocks::OBSIDIAN
192            || block == &vanilla_blocks::CRYING_OBSIDIAN
193            || block == &vanilla_blocks::RESPAWN_ANCHOR
194            || block == &vanilla_blocks::REINFORCED_DEEPSLATE
195        {
196            return false;
197        }
198        if (direction == Direction::Down && pos.y() == world.min_y())
199            || (direction == Direction::Up && pos.y() == world.max_y_exclusive() - 1)
200        {
201            return false;
202        }
203
204        let behavior = BLOCK_BEHAVIORS.get_behavior(block);
205        if !behavior.is_piston_base() {
206            if block.config.destroy_time == -1.0 {
207                return false;
208            }
209            match block.config.push_reaction {
210                PushReaction::Block => return false,
211                PushReaction::Destroy => return allow_destroyable,
212                PushReaction::PushOnly => return direction == connection_direction,
213                PushReaction::Normal | PushReaction::Ignore => {}
214            }
215        } else if state.get_value(EXTENDED) {
216            return false;
217        }
218
219        !state.has_block_entity()
220    }
221
222    #[expect(
223        clippy::too_many_lines,
224        reason = "keeping vanilla's ordered piston mutation sequence together makes parity auditable"
225    )]
226    fn move_blocks(
227        &self,
228        world: &Arc<World>,
229        piston_pos: BlockPos,
230        direction: Direction,
231        extending: bool,
232    ) -> bool {
233        let arm_pos = piston_pos.relative(direction);
234        if !extending && world.get_block_state(arm_pos).get_block() == &vanilla_blocks::PISTON_HEAD
235        {
236            world.set_block(
237                arm_pos,
238                vanilla_blocks::AIR.default_state(),
239                UPDATE_RETRACT_BASE,
240            );
241        }
242
243        let mut resolver =
244            PistonStructureResolver::new(world.as_ref(), piston_pos, direction, extending);
245        if !resolver.resolve() {
246            return false;
247        }
248
249        let to_push = resolver.to_push().to_vec();
250        let to_destroy = resolver.to_destroy().to_vec();
251        let push_direction = resolver.push_direction();
252        let mut delete_after_move = Vec::with_capacity(to_push.len());
253        let mut pushed_states = Vec::with_capacity(to_push.len());
254        for &pos in &to_push {
255            let state = world.get_block_state(pos);
256            pushed_states.push(state);
257            delete_after_move.push((pos, state));
258        }
259
260        let mut to_update = Vec::with_capacity(to_push.len() + to_destroy.len());
261        for &pos in to_destroy.iter().rev() {
262            let state = world.get_block_state(pos);
263            // TODO: Pass the block entity to loot evaluation once block-entity components and
264            // post-refactor container item slices are available, as Vanilla does here.
265            world.drop_resources(state, pos);
266            world.set_block(
267                pos,
268                vanilla_blocks::AIR.default_state(),
269                UPDATE_DESTROYED_BLOCK,
270            );
271            world.game_event(
272                &vanilla_game_events::BLOCK_DESTROY,
273                pos,
274                &GameEventContext::new(None, Some(state)),
275            );
276            to_update.push(state);
277        }
278
279        for (index, &pos) in to_push.iter().enumerate().rev() {
280            let state = world.get_block_state(pos);
281            let destination = pos.relative(push_direction);
282            delete_after_move.retain(|(delete_pos, _)| *delete_pos != destination);
283            let moving_state = vanilla_blocks::MOVING_PISTON
284                .default_state()
285                .set_value(FACING, direction);
286            world.set_block(destination, moving_state, UPDATE_MOVING_BLOCK);
287            world.set_block_entity(Self::moving_block_entity(
288                world,
289                destination,
290                moving_state,
291                pushed_states[index],
292                direction,
293                extending,
294                false,
295            ));
296            to_update.push(state);
297        }
298
299        if extending {
300            let head_state = vanilla_blocks::PISTON_HEAD
301                .default_state()
302                .set_value(FACING, direction)
303                .set_value(
304                    PISTON_TYPE,
305                    if self.sticky {
306                        PistonType::Sticky
307                    } else {
308                        PistonType::Normal
309                    },
310                );
311            let moving_state = vanilla_blocks::MOVING_PISTON
312                .default_state()
313                .set_value(FACING, direction)
314                .set_value(
315                    PISTON_TYPE,
316                    if self.sticky {
317                        PistonType::Sticky
318                    } else {
319                        PistonType::Normal
320                    },
321                );
322            delete_after_move.retain(|(delete_pos, _)| *delete_pos != arm_pos);
323            world.set_block(arm_pos, moving_state, UPDATE_MOVING_BLOCK);
324            world.set_block_entity(Self::moving_block_entity(
325                world,
326                arm_pos,
327                moving_state,
328                head_state,
329                direction,
330                true,
331                true,
332            ));
333        }
334
335        // Java's HashMap table stays at 16 buckets for the at-most-twelve entries.
336        delete_after_move.sort_by_key(|(pos, _)| java_hash::bucket(*pos));
337        let air = vanilla_blocks::AIR.default_state();
338        for &(pos, _) in &delete_after_move {
339            world.set_block(pos, air, UPDATE_CLEARED_MOVED_BLOCK);
340        }
341        for &(pos, old_state) in &delete_after_move {
342            BLOCK_BEHAVIORS
343                .get_behavior(old_state.get_block())
344                .update_indirect_neighbour_shapes(
345                    old_state,
346                    world,
347                    pos,
348                    UpdateFlags::UPDATE_CLIENTS,
349                    World::UPDATE_LIMIT,
350                );
351            world.update_neighbour_shapes(
352                air,
353                pos,
354                UpdateFlags::UPDATE_CLIENTS,
355                World::UPDATE_LIMIT,
356            );
357            BLOCK_BEHAVIORS
358                .get_behavior(air.get_block())
359                .update_indirect_neighbour_shapes(
360                    air,
361                    world,
362                    pos,
363                    UpdateFlags::UPDATE_CLIENTS,
364                    World::UPDATE_LIMIT,
365                );
366        }
367
368        // The project intentionally omits experimental redstone orientations.
369        let mut update_index = 0;
370        for &pos in to_destroy.iter().rev() {
371            let state = to_update[update_index];
372            update_index += 1;
373            BLOCK_BEHAVIORS
374                .get_behavior(state.get_block())
375                .affect_neighbors_after_removal(state, world, pos, false);
376            BLOCK_BEHAVIORS
377                .get_behavior(state.get_block())
378                .update_indirect_neighbour_shapes(
379                    state,
380                    world,
381                    pos,
382                    UpdateFlags::UPDATE_CLIENTS,
383                    World::UPDATE_LIMIT,
384                );
385            world.update_neighbors_at(pos, state.get_block());
386        }
387        for &pos in to_push.iter().rev() {
388            let state = to_update[update_index];
389            update_index += 1;
390            world.update_neighbors_at(pos, state.get_block());
391        }
392        if extending {
393            world.update_neighbors_at(arm_pos, &vanilla_blocks::PISTON_HEAD);
394        }
395        true
396    }
397
398    fn trigger_retraction(
399        &self,
400        world: &Arc<World>,
401        pos: BlockPos,
402        direction: Direction,
403        event: i32,
404        event_direction: i32,
405    ) {
406        Self::finish_moving_block_entity(world, pos.relative(direction));
407
408        let piston_type = if self.sticky {
409            PistonType::Sticky
410        } else {
411            PistonType::Normal
412        };
413        let moving_state = vanilla_blocks::MOVING_PISTON
414            .default_state()
415            .set_value(FACING, direction)
416            .set_value(PISTON_TYPE, piston_type);
417        world.set_block(pos, moving_state, UPDATE_RETRACT_BASE);
418        let moved_state = self
419            .block
420            .default_state()
421            .set_value(FACING, Self::direction_from_legacy_id(event_direction));
422        world.set_block_entity(Self::moving_block_entity(
423            world,
424            pos,
425            moving_state,
426            moved_state,
427            direction,
428            false,
429            true,
430        ));
431        world.update_neighbors_at(pos, moving_state.get_block());
432        world.update_neighbour_shapes(
433            moving_state,
434            pos,
435            UpdateFlags::UPDATE_CLIENTS,
436            World::UPDATE_LIMIT,
437        );
438
439        let arm_pos = pos.relative(direction);
440        if self.sticky {
441            let two_pos = pos.relative_n(direction, 2);
442            let two_state = world.get_block_state(two_pos);
443            let piston_piece = if two_state.get_block() == &vanilla_blocks::MOVING_PISTON {
444                let matches = world.get_block_entity(two_pos).is_some_and(|block_entity| {
445                    block_entity
446                        .downcast_ref::<PistonMovingBlockEntity>()
447                        .is_some_and(|piston| {
448                            piston.direction() == direction && piston.is_extending()
449                        })
450                });
451                matches && Self::finish_moving_block_entity(world, two_pos)
452            } else {
453                false
454            };
455
456            if !piston_piece {
457                let reaction = two_state.get_block().config.push_reaction;
458                let piston = BLOCK_BEHAVIORS
459                    .get_behavior(two_state.get_block())
460                    .is_piston_base();
461                if event != 1
462                    || two_state.is_air()
463                    || !Self::is_pushable(
464                        two_state,
465                        world.as_ref(),
466                        two_pos,
467                        direction.opposite(),
468                        false,
469                        direction,
470                    )
471                    || (reaction != PushReaction::Normal && !piston)
472                {
473                    world.remove_block(arm_pos, false);
474                } else {
475                    self.move_blocks(world, pos, direction, false);
476                }
477            }
478        } else {
479            world.remove_block(arm_pos, false);
480        }
481
482        world.play_sound(
483            &sound_events::BLOCK_PISTON_CONTRACT,
484            SoundSource::Blocks,
485            pos,
486            0.5,
487            rand::random_range(0.6..0.75),
488            None,
489        );
490        world.game_event(
491            &vanilla_game_events::BLOCK_DEACTIVATE,
492            pos,
493            &GameEventContext::new(None, Some(moving_state)),
494        );
495    }
496}
497
498impl BlockBehavior for PistonBaseBlock {
499    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
500        Some(
501            self.block
502                .default_state()
503                .set_value(FACING, context.get_nearest_looking_direction().opposite())
504                .set_value(EXTENDED, false),
505        )
506    }
507
508    fn set_placed_by(
509        &self,
510        state: BlockStateId,
511        world: &Arc<World>,
512        pos: BlockPos,
513        _source: &PlacementSource<'_>,
514    ) {
515        self.check_if_extend(world, pos, state);
516    }
517
518    fn on_place(
519        &self,
520        state: BlockStateId,
521        world: &Arc<World>,
522        pos: BlockPos,
523        old_state: BlockStateId,
524        _moved_by_piston: bool,
525    ) {
526        if old_state.get_block() != state.get_block() && world.get_block_entity(pos).is_none() {
527            self.check_if_extend(world, pos, state);
528        }
529    }
530
531    fn handle_neighbor_changed(
532        &self,
533        state: BlockStateId,
534        world: &Arc<World>,
535        pos: BlockPos,
536        _source_block: BlockRef,
537        _moved_by_piston: bool,
538    ) {
539        self.check_if_extend(world, pos, state);
540    }
541
542    fn trigger_event(
543        &self,
544        state: BlockStateId,
545        world: &Arc<World>,
546        pos: BlockPos,
547        event: i32,
548        event_direction: i32,
549    ) -> bool {
550        let direction = state.get_value(FACING);
551        let extended_state = state.set_value(EXTENDED, true);
552        let powered = Self::neighbor_signal(world, pos, direction);
553        if powered && matches!(event, 1 | 2) {
554            world.set_block(pos, extended_state, UpdateFlags::UPDATE_CLIENTS);
555            return false;
556        }
557        if !powered && event == 0 {
558            return false;
559        }
560
561        if event == 0 {
562            if !self.move_blocks(world, pos, direction, true) {
563                return false;
564            }
565            world.set_block(
566                pos,
567                extended_state,
568                UpdateFlags::UPDATE_ALL | UpdateFlags::UPDATE_MOVE_BY_PISTON,
569            );
570            world.play_sound(
571                &sound_events::BLOCK_PISTON_EXTEND,
572                SoundSource::Blocks,
573                pos,
574                0.5,
575                rand::random_range(0.6..0.85),
576                None,
577            );
578            world.game_event(
579                &vanilla_game_events::BLOCK_ACTIVATE,
580                pos,
581                &GameEventContext::new(None, Some(extended_state)),
582            );
583        } else if matches!(event, 1 | 2) {
584            self.trigger_retraction(world, pos, direction, event, event_direction);
585        }
586        true
587    }
588
589    fn is_piston_base(&self) -> bool {
590        true
591    }
592
593    fn is_pathfindable(&self, _state: BlockStateId, _type: PathComputationType) -> bool {
594        false
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use std::sync::Arc;
601
602    use glam::DVec3;
603    use steel_registry::blocks::properties::AttachFace;
604    use steel_registry::init_vanilla_registry;
605    use steel_registry::item_stack::ItemStack;
606    use steel_registry::vanilla_items;
607    use steel_utils::{ChunkPos, types::InteractionHand};
608
609    use super::*;
610    use crate::behavior::{BlockHitResult, BlockLootContext, PlacementOrientation, init_behaviors};
611    use crate::chunk::chunk_holder::ChunkHolder;
612    use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
613
614    const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
615    const ATTACH_FACE: &EnumProperty<AttachFace> = &BlockStateProperties::ATTACH_FACE;
616
617    fn tick_block_entities(world: &Arc<World>, ticks: usize) {
618        for _ in 0..ticks {
619            world.block_entity_tickers().tick(world, true);
620        }
621    }
622
623    fn powered_piston_world(
624        key: &'static str,
625        piston: BlockRef,
626    ) -> (Arc<World>, Arc<ChunkHolder>, BlockPos, BlockPos) {
627        init_vanilla_registry();
628        init_behaviors();
629        let world = fresh_test_world(key);
630        let piston_pos = BlockPos::new(8, 64, 8);
631        let power_pos = piston_pos.west();
632        let holder = insert_ready_full_chunk(&world, ChunkPos::from_block_pos(piston_pos));
633        let piston_state = piston
634            .default_state()
635            .set_value(FACING, Direction::East)
636            .set_value(EXTENDED, false);
637        assert!(world.set_block(
638            piston_pos.east(),
639            vanilla_blocks::STONE.default_state(),
640            UpdateFlags::UPDATE_NONE,
641        ));
642        assert!(world.set_block(piston_pos, piston_state, UpdateFlags::UPDATE_NONE));
643        assert!(world.set_block(
644            power_pos,
645            vanilla_blocks::REDSTONE_BLOCK.default_state(),
646            UpdateFlags::UPDATE_ALL,
647        ));
648        world.run_block_events();
649        (world, holder, piston_pos, power_pos)
650    }
651
652    #[test]
653    fn pushability_honors_bounds_reactions_and_block_entities() {
654        init_vanilla_registry();
655        init_behaviors();
656        let level = TestLevel::default();
657        let pos = BlockPos::new(0, 64, 0);
658
659        assert!(PistonBaseBlock::is_pushable(
660            vanilla_blocks::STONE.default_state(),
661            &level,
662            pos,
663            Direction::East,
664            false,
665            Direction::East,
666        ));
667        assert!(!PistonBaseBlock::is_pushable(
668            vanilla_blocks::OBSIDIAN.default_state(),
669            &level,
670            pos,
671            Direction::East,
672            false,
673            Direction::East,
674        ));
675        assert!(!PistonBaseBlock::is_pushable(
676            vanilla_blocks::CHEST.default_state(),
677            &level,
678            pos,
679            Direction::East,
680            false,
681            Direction::East,
682        ));
683    }
684
685    #[test]
686    fn placement_uses_player_look_direction_not_clicked_face() {
687        init_vanilla_registry();
688        init_behaviors();
689        let world = fresh_test_world("piston_look_placement");
690        let support_pos = BlockPos::new(8, 64, 8);
691        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(support_pos));
692        assert!(world.set_block(
693            support_pos,
694            vanilla_blocks::STONE.default_state(),
695            UpdateFlags::UPDATE_NONE,
696        ));
697
698        let mut stack = ItemStack::new(&vanilla_items::PISTON);
699        let source = PlacementSource::direct(
700            None,
701            InteractionHand::MainHand,
702            &mut stack,
703            PlacementOrientation::Player {
704                rotation: 0.0,
705                pitch: 80.0,
706            },
707            false,
708        );
709        let context = BlockPlaceContext::new(
710            &world,
711            source,
712            &BlockHitResult {
713                location: DVec3::new(9.0, 64.5, 8.5),
714                direction: Direction::East,
715                block_pos: support_pos,
716                miss: false,
717                inside: false,
718                world_border_hit: false,
719            },
720        );
721
722        let state = PistonBaseBlock::new(&vanilla_blocks::PISTON, false)
723            .get_state_for_placement(&context)
724            .expect("piston placement should produce a state");
725        assert_eq!(context.clicked_face(), Direction::East);
726        assert_eq!(state.get_value(FACING), Direction::Up,);
727    }
728
729    #[test]
730    fn moving_piston_delegates_loot_to_carried_state() {
731        let (world, _holder, piston_pos, _power_pos) =
732            powered_piston_world("moving_piston_loot", &vanilla_blocks::PISTON);
733        let moving_pos = piston_pos.relative_n(Direction::East, 2);
734        let moving_state = world.get_block_state(moving_pos);
735        let tool = ItemStack::new(&vanilla_items::IRON_PICKAXE);
736        let drops = BlockLootContext::new(&world, moving_pos)
737            .with_tool(&tool)
738            .get_drops(moving_state);
739
740        assert_eq!(drops.len(), 1);
741        assert_eq!(drops[0].item(), &*vanilla_items::COBBLESTONE);
742    }
743
744    #[test]
745    fn normal_piston_extends_settles_and_retracts_without_pulling() {
746        let (world, _holder, piston_pos, power_pos) =
747            powered_piston_world("normal_piston_cycle", &vanilla_blocks::PISTON);
748        assert!(world.get_block_state(piston_pos).get_value(EXTENDED));
749        assert_eq!(
750            world.get_block_state(piston_pos.east()).get_block(),
751            &vanilla_blocks::MOVING_PISTON
752        );
753        assert_eq!(
754            world
755                .get_block_state(piston_pos.relative_n(Direction::East, 2))
756                .get_block(),
757            &vanilla_blocks::MOVING_PISTON
758        );
759
760        tick_block_entities(&world, 3);
761        assert_eq!(
762            world.get_block_state(piston_pos.east()).get_block(),
763            &vanilla_blocks::PISTON_HEAD
764        );
765        assert_eq!(
766            world
767                .get_block_state(piston_pos.relative_n(Direction::East, 2))
768                .get_block(),
769            &vanilla_blocks::STONE
770        );
771
772        assert!(world.remove_block(power_pos, false));
773        world.run_block_events();
774        assert_eq!(
775            world.get_block_state(piston_pos).get_block(),
776            &vanilla_blocks::MOVING_PISTON
777        );
778        tick_block_entities(&world, 3);
779        let base = world.get_block_state(piston_pos);
780        assert_eq!(base.get_block(), &vanilla_blocks::PISTON);
781        assert!(!base.get_value(EXTENDED));
782        assert!(world.get_block_state(piston_pos.east()).is_air());
783        assert_eq!(
784            world
785                .get_block_state(piston_pos.relative_n(Direction::East, 2))
786                .get_block(),
787            &vanilla_blocks::STONE
788        );
789    }
790
791    #[test]
792    fn retracting_piston_keeps_rear_face_attachments_supported() {
793        init_vanilla_registry();
794        init_behaviors();
795        let world = fresh_test_world("piston_rear_face_support");
796        let piston_pos = BlockPos::new(8, 64, 8);
797        let button_pos = piston_pos.west();
798        let power_pos = piston_pos.north();
799        let _holder = insert_ready_full_chunk(&world, ChunkPos::from_block_pos(piston_pos));
800        let piston_state = vanilla_blocks::PISTON
801            .default_state()
802            .set_value(FACING, Direction::East)
803            .set_value(EXTENDED, false);
804        let button_state = vanilla_blocks::OAK_BUTTON
805            .default_state()
806            .set_value(ATTACH_FACE, AttachFace::Wall)
807            .set_value(HORIZONTAL_FACING, Direction::West);
808
809        assert!(world.set_block(piston_pos, piston_state, UpdateFlags::UPDATE_NONE));
810        assert!(world.set_block(button_pos, button_state, UpdateFlags::UPDATE_NONE));
811        assert!(world.set_block(
812            power_pos,
813            vanilla_blocks::REDSTONE_BLOCK.default_state(),
814            UpdateFlags::UPDATE_ALL,
815        ));
816        world.run_block_events();
817        assert_eq!(
818            world.get_block_state(button_pos).get_block(),
819            &vanilla_blocks::OAK_BUTTON
820        );
821
822        tick_block_entities(&world, 3);
823        assert!(world.remove_block(power_pos, false));
824        world.run_block_events();
825
826        let moving = world.get_block_state(piston_pos);
827        assert_eq!(moving.get_block(), &vanilla_blocks::MOVING_PISTON);
828        assert!(world.is_face_sturdy(moving, piston_pos, Direction::West));
829        assert!(!world.is_face_sturdy(moving, piston_pos, Direction::East));
830        assert!(!world.is_face_sturdy(moving, piston_pos, Direction::Up));
831        assert_eq!(
832            world.get_block_state(button_pos).get_block(),
833            &vanilla_blocks::OAK_BUTTON
834        );
835
836        tick_block_entities(&world, 3);
837        assert_eq!(
838            world.get_block_state(button_pos).get_block(),
839            &vanilla_blocks::OAK_BUTTON
840        );
841    }
842
843    #[test]
844    fn sticky_piston_pulls_settled_normal_block() {
845        let (world, _holder, piston_pos, power_pos) =
846            powered_piston_world("sticky_piston_cycle", &vanilla_blocks::STICKY_PISTON);
847        tick_block_entities(&world, 3);
848
849        assert!(world.remove_block(power_pos, false));
850        world.run_block_events();
851        assert_eq!(
852            world.get_block_state(piston_pos.east()).get_block(),
853            &vanilla_blocks::MOVING_PISTON
854        );
855        tick_block_entities(&world, 3);
856
857        let base = world.get_block_state(piston_pos);
858        assert_eq!(base.get_block(), &vanilla_blocks::STICKY_PISTON);
859        assert!(!base.get_value(EXTENDED));
860        assert_eq!(
861            world.get_block_state(piston_pos.east()).get_block(),
862            &vanilla_blocks::STONE
863        );
864        assert!(
865            world
866                .get_block_state(piston_pos.relative_n(Direction::East, 2))
867                .is_air()
868        );
869    }
870}