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::Immoveable => return false,
211                PushReaction::Popped => return allow_destroyable,
212                PushReaction::Push => return direction == connection_direction,
213                PushReaction::PushPull | PushReaction::IgnoreEntity => {}
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::PushPull && !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, TestWorld, 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    ) -> (TestWorld, Arc<ChunkHolder>, BlockPos, BlockPos) {
627        init_vanilla_registry();
628        init_behaviors();
629        let world_fixture = fresh_test_world(key);
630        let world = &world_fixture.world;
631        let piston_pos = BlockPos::new(8, 64, 8);
632        let power_pos = piston_pos.west();
633        let holder = insert_ready_full_chunk(world, ChunkPos::from_block_pos(piston_pos));
634        let piston_state = piston
635            .default_state()
636            .set_value(FACING, Direction::East)
637            .set_value(EXTENDED, false);
638        assert!(world.set_block(
639            piston_pos.east(),
640            vanilla_blocks::STONE.default_state(),
641            UpdateFlags::UPDATE_NONE,
642        ));
643        assert!(world.set_block(piston_pos, piston_state, UpdateFlags::UPDATE_NONE));
644        assert!(world.set_block(
645            power_pos,
646            vanilla_blocks::REDSTONE_BLOCK.default_state(),
647            UpdateFlags::UPDATE_ALL,
648        ));
649        world.run_block_events();
650        (world_fixture, holder, piston_pos, power_pos)
651    }
652
653    #[test]
654    fn pushability_honors_bounds_reactions_and_block_entities() {
655        init_vanilla_registry();
656        init_behaviors();
657        let level = TestLevel::default();
658        let pos = BlockPos::new(0, 64, 0);
659
660        assert!(PistonBaseBlock::is_pushable(
661            vanilla_blocks::STONE.default_state(),
662            &level,
663            pos,
664            Direction::East,
665            false,
666            Direction::East,
667        ));
668        assert!(!PistonBaseBlock::is_pushable(
669            vanilla_blocks::OBSIDIAN.default_state(),
670            &level,
671            pos,
672            Direction::East,
673            false,
674            Direction::East,
675        ));
676        assert!(!PistonBaseBlock::is_pushable(
677            vanilla_blocks::CHEST.default_state(),
678            &level,
679            pos,
680            Direction::East,
681            false,
682            Direction::East,
683        ));
684    }
685
686    #[test]
687    fn placement_uses_player_look_direction_not_clicked_face() {
688        init_vanilla_registry();
689        init_behaviors();
690        let world_fixture = fresh_test_world("piston_look_placement");
691        let world = &world_fixture.world;
692        let support_pos = BlockPos::new(8, 64, 8);
693        insert_ready_full_chunk(world, ChunkPos::from_block_pos(support_pos));
694        assert!(world.set_block(
695            support_pos,
696            vanilla_blocks::STONE.default_state(),
697            UpdateFlags::UPDATE_NONE,
698        ));
699
700        let mut stack = ItemStack::new(&vanilla_items::PISTON);
701        let source = PlacementSource::direct(
702            None,
703            InteractionHand::MainHand,
704            &mut stack,
705            PlacementOrientation::Player {
706                rotation: 0.0,
707                pitch: 80.0,
708            },
709            false,
710        );
711        let context = BlockPlaceContext::new(
712            world,
713            source,
714            &BlockHitResult {
715                location: DVec3::new(9.0, 64.5, 8.5),
716                direction: Direction::East,
717                block_pos: support_pos,
718                miss: false,
719                inside: false,
720                world_border_hit: false,
721            },
722        );
723
724        let state = PistonBaseBlock::new(&vanilla_blocks::PISTON, false)
725            .get_state_for_placement(&context)
726            .expect("piston placement should produce a state");
727        assert_eq!(context.clicked_face(), Direction::East);
728        assert_eq!(state.get_value(FACING), Direction::Up,);
729    }
730
731    #[test]
732    fn moving_piston_delegates_loot_to_carried_state() {
733        let (world_fixture, _holder, piston_pos, _power_pos) =
734            powered_piston_world("moving_piston_loot", &vanilla_blocks::PISTON);
735        let world = &world_fixture.world;
736        let moving_pos = piston_pos.relative_n(Direction::East, 2);
737        let moving_state = world.get_block_state(moving_pos);
738        let tool = ItemStack::new(&vanilla_items::IRON_PICKAXE);
739        let drops = BlockLootContext::new(world, moving_pos)
740            .with_tool(&tool)
741            .get_drops(moving_state);
742
743        assert_eq!(drops.len(), 1);
744        assert_eq!(drops[0].item(), &*vanilla_items::COBBLESTONE);
745    }
746
747    #[test]
748    fn normal_piston_extends_settles_and_retracts_without_pulling() {
749        let (world_fixture, _holder, piston_pos, power_pos) =
750            powered_piston_world("normal_piston_cycle", &vanilla_blocks::PISTON);
751        let world = &world_fixture.world;
752        assert!(world.get_block_state(piston_pos).get_value(EXTENDED));
753        assert_eq!(
754            world.get_block_state(piston_pos.east()).get_block(),
755            &vanilla_blocks::MOVING_PISTON
756        );
757        assert_eq!(
758            world
759                .get_block_state(piston_pos.relative_n(Direction::East, 2))
760                .get_block(),
761            &vanilla_blocks::MOVING_PISTON
762        );
763
764        tick_block_entities(world, 3);
765        assert_eq!(
766            world.get_block_state(piston_pos.east()).get_block(),
767            &vanilla_blocks::PISTON_HEAD
768        );
769        assert_eq!(
770            world
771                .get_block_state(piston_pos.relative_n(Direction::East, 2))
772                .get_block(),
773            &vanilla_blocks::STONE
774        );
775
776        assert!(world.remove_block(power_pos, false));
777        world.run_block_events();
778        assert_eq!(
779            world.get_block_state(piston_pos).get_block(),
780            &vanilla_blocks::MOVING_PISTON
781        );
782        tick_block_entities(world, 3);
783        let base = world.get_block_state(piston_pos);
784        assert_eq!(base.get_block(), &vanilla_blocks::PISTON);
785        assert!(!base.get_value(EXTENDED));
786        assert!(world.get_block_state(piston_pos.east()).is_air());
787        assert_eq!(
788            world
789                .get_block_state(piston_pos.relative_n(Direction::East, 2))
790                .get_block(),
791            &vanilla_blocks::STONE
792        );
793    }
794
795    #[test]
796    fn retracting_piston_keeps_rear_face_attachments_supported() {
797        init_vanilla_registry();
798        init_behaviors();
799        let world_fixture = fresh_test_world("piston_rear_face_support");
800        let world = &world_fixture.world;
801        let piston_pos = BlockPos::new(8, 64, 8);
802        let button_pos = piston_pos.west();
803        let power_pos = piston_pos.north();
804        let _holder = insert_ready_full_chunk(world, ChunkPos::from_block_pos(piston_pos));
805        let piston_state = vanilla_blocks::PISTON
806            .default_state()
807            .set_value(FACING, Direction::East)
808            .set_value(EXTENDED, false);
809        let button_state = vanilla_blocks::OAK_BUTTON
810            .default_state()
811            .set_value(ATTACH_FACE, AttachFace::Wall)
812            .set_value(HORIZONTAL_FACING, Direction::West);
813
814        assert!(world.set_block(piston_pos, piston_state, UpdateFlags::UPDATE_NONE));
815        assert!(world.set_block(button_pos, button_state, UpdateFlags::UPDATE_NONE));
816        assert!(world.set_block(
817            power_pos,
818            vanilla_blocks::REDSTONE_BLOCK.default_state(),
819            UpdateFlags::UPDATE_ALL,
820        ));
821        world.run_block_events();
822        assert_eq!(
823            world.get_block_state(button_pos).get_block(),
824            &vanilla_blocks::OAK_BUTTON
825        );
826
827        tick_block_entities(world, 3);
828        assert!(world.remove_block(power_pos, false));
829        world.run_block_events();
830
831        let moving = world.get_block_state(piston_pos);
832        assert_eq!(moving.get_block(), &vanilla_blocks::MOVING_PISTON);
833        assert!(world.is_face_sturdy(moving, piston_pos, Direction::West));
834        assert!(!world.is_face_sturdy(moving, piston_pos, Direction::East));
835        assert!(!world.is_face_sturdy(moving, piston_pos, Direction::Up));
836        assert_eq!(
837            world.get_block_state(button_pos).get_block(),
838            &vanilla_blocks::OAK_BUTTON
839        );
840
841        tick_block_entities(world, 3);
842        assert_eq!(
843            world.get_block_state(button_pos).get_block(),
844            &vanilla_blocks::OAK_BUTTON
845        );
846    }
847
848    #[test]
849    fn sticky_piston_pulls_settled_normal_block() {
850        let (world_fixture, _holder, piston_pos, power_pos) =
851            powered_piston_world("sticky_piston_cycle", &vanilla_blocks::STICKY_PISTON);
852        let world = &world_fixture.world;
853        tick_block_entities(world, 3);
854
855        assert!(world.remove_block(power_pos, false));
856        world.run_block_events();
857        assert_eq!(
858            world.get_block_state(piston_pos.east()).get_block(),
859            &vanilla_blocks::MOVING_PISTON
860        );
861        tick_block_entities(world, 3);
862
863        let base = world.get_block_state(piston_pos);
864        assert_eq!(base.get_block(), &vanilla_blocks::STICKY_PISTON);
865        assert!(!base.get_value(EXTENDED));
866        assert_eq!(
867            world.get_block_state(piston_pos.east()).get_block(),
868            &vanilla_blocks::STONE
869        );
870        assert!(
871            world
872                .get_block_state(piston_pos.relative_n(Direction::East, 2))
873                .is_air()
874        );
875    }
876}