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::{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#[block_behavior]
38pub struct PistonBaseBlock {
39 block: BlockRef,
40 #[json_arg(value, json = "is_sticky")]
41 sticky: bool,
42}
43
44impl PistonBaseBlock {
45 #[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 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 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 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}