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