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