1use 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#[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 #[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 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 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 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}