1use std::cell::Cell;
4use std::sync::{Arc, Weak};
5
6use glam::DVec3;
7use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
8use simdnbt::owned::NbtCompound;
9use steel_registry::blocks::behavior::PushReaction;
10use steel_registry::blocks::block_state_ext::BlockStateExt as _;
11use steel_registry::blocks::properties::{BlockStateProperties, Direction, PistonType};
12use steel_registry::{vanilla_block_entity_types, vanilla_blocks};
13use steel_utils::axis::Axis;
14use steel_utils::nbt::NbtNumeric as _;
15use steel_utils::nbt::NbtValueInput as _;
16use steel_utils::types::UpdateFlags;
17use steel_utils::{
18 BlockLocalAabb, BlockPos, BlockStateId, DowncastType, DowncastTypeKey, WorldAabb,
19 locks::SyncMutex,
20};
21
22use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionBoxes, BlockCollisionContext};
23use crate::block_entity::block_state_nbt;
24use crate::block_entity::{BlockEntity, BlockEntityBase, BlockEntityLifecycleExt as _};
25use crate::entity::{Entity, SharedEntity};
26use crate::physics::MoverType;
27use crate::world::{LevelReader, World};
28
29const PUSH_OFFSET: f64 = 0.01;
30
31thread_local! {
32 static NOCLIP: Cell<Option<Direction>> = const { Cell::new(None) };
33}
34
35struct NoClipGuard;
36
37impl NoClipGuard {
38 fn set(direction: Direction) -> Self {
39 NOCLIP.set(Some(direction));
40 Self
41 }
42}
43
44impl Drop for NoClipGuard {
45 fn drop(&mut self) {
46 NOCLIP.set(None);
47 }
48}
49
50pub struct PistonMovingBlockEntity {
53 base: BlockEntityBase,
54 moving: SyncMutex<PistonMovingState>,
55}
56
57#[derive(Clone, Copy)]
58struct PistonMovingState {
59 moved_state: BlockStateId,
60 direction: Direction,
61 extending: bool,
62 source_piston: bool,
63 progress: f32,
64 progress_o: f32,
65 last_ticked: i64,
66}
67
68unsafe impl DowncastType for PistonMovingBlockEntity {
70 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/piston_moving");
71}
72
73impl PistonMovingBlockEntity {
74 #[must_use]
76 pub fn new(world: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
77 Self::new_moving(
78 world,
79 pos,
80 state,
81 vanilla_blocks::AIR.default_state(),
82 Direction::Down,
83 false,
84 false,
85 )
86 }
87
88 #[must_use]
90 pub fn new_moving(
91 world: Weak<World>,
92 pos: BlockPos,
93 state: BlockStateId,
94 moved_state: BlockStateId,
95 direction: Direction,
96 extending: bool,
97 source_piston: bool,
98 ) -> Self {
99 Self {
100 base: BlockEntityBase::new(&vanilla_block_entity_types::PISTON, world, pos, state),
101 moving: SyncMutex::new(PistonMovingState {
102 moved_state,
103 direction,
104 extending,
105 source_piston,
106 progress: 0.0,
107 progress_o: 0.0,
108 last_ticked: 0,
109 }),
110 }
111 }
112
113 #[must_use]
115 pub fn is_extending(&self) -> bool {
116 self.moving.lock().extending
117 }
118
119 #[must_use]
121 pub fn direction(&self) -> Direction {
122 self.moving.lock().direction
123 }
124
125 #[must_use]
127 pub fn is_source_piston(&self) -> bool {
128 self.moving.lock().source_piston
129 }
130
131 #[must_use]
133 pub fn moved_state(&self) -> BlockStateId {
134 self.moving.lock().moved_state
135 }
136
137 #[must_use]
139 pub fn last_ticked(&self) -> i64 {
140 self.moving.lock().last_ticked
141 }
142
143 #[must_use]
145 pub fn progress(&self, partial_tick: f32) -> f32 {
146 let partial_tick = partial_tick.min(1.0);
147 let moving = self.moving.lock();
148 (moving.progress - moving.progress_o).mul_add(partial_tick, moving.progress_o)
149 }
150
151 #[must_use]
153 pub fn movement_direction(&self) -> Direction {
154 self.moving.lock().movement_direction()
155 }
156
157 #[must_use]
159 pub fn push_direction(&self) -> Direction {
160 self.moving.lock().movement_direction()
161 }
162
163 #[must_use]
165 pub fn collision_boxes(&self, world: &dyn LevelReader, pos: BlockPos) -> BlockCollisionBoxes {
166 let moving = *self.moving.lock();
167 moving.collision_boxes(world, pos)
168 }
169
170 pub fn final_tick(&self, world: &Arc<World>) -> bool {
172 let moving = {
173 let mut moving = self.moving.lock();
174 if moving.progress_o >= 1.0 {
175 return false;
176 }
177 moving.progress = 1.0;
178 moving.progress_o = 1.0;
179 *moving
180 };
181 let pos = self.get_block_pos();
182 let owned_position = world.remove_block_entity_if_same(self);
183 self.set_removed();
184 if owned_position {
185 moving.finish_movement_early(world, pos);
186 }
187 true
188 }
189}
190
191impl PistonMovingState {
192 fn can_move_collided_entity(entity: &dyn Entity, cause_bounce: bool) -> bool {
195 !entity.is_spectator()
196 && entity.piston_push_reaction() != PushReaction::IgnoreEntity
197 && (!cause_bounce || entity.as_player().is_none())
198 }
199
200 const fn movement_direction(self) -> Direction {
201 if self.extending {
202 self.direction
203 } else {
204 self.direction.opposite()
205 }
206 }
207
208 fn extended_progress(self, progress: f32) -> f32 {
209 if self.extending {
210 progress - 1.0
211 } else {
212 1.0 - progress
213 }
214 }
215
216 fn collision_related_state(self) -> BlockStateId {
217 let behavior = BLOCK_BEHAVIORS.get_behavior(self.moved_state.get_block());
218 if !self.extending && self.source_piston && behavior.is_piston_base() {
219 vanilla_blocks::PISTON_HEAD
220 .default_state()
221 .set_value(&BlockStateProperties::SHORT, self.progress > 0.25)
222 .set_value(
223 &BlockStateProperties::PISTON_TYPE,
224 if self.moved_state.get_block() == &vanilla_blocks::STICKY_PISTON {
225 PistonType::Sticky
226 } else {
227 PistonType::Normal
228 },
229 )
230 .set_value(
231 &BlockStateProperties::FACING,
232 self.moved_state.get_value(&BlockStateProperties::FACING),
233 )
234 } else {
235 self.moved_state
236 }
237 }
238
239 fn state_collision_boxes(
240 state: BlockStateId,
241 world: &dyn LevelReader,
242 pos: BlockPos,
243 ) -> BlockCollisionBoxes {
244 BLOCK_BEHAVIORS
245 .get_behavior(state.get_block())
246 .get_collision_boxes(state, world, pos, BlockCollisionContext::empty())
247 }
248
249 fn boxes_bounds(boxes: &BlockCollisionBoxes) -> Option<BlockLocalAabb> {
250 let mut boxes = boxes.iter().filter(|aabb| !aabb.is_empty());
251 let mut bounds = *boxes.next()?;
252 for aabb in boxes {
253 bounds = BlockLocalAabb::encapsulating(&bounds, aabb);
254 }
255 Some(bounds)
256 }
257
258 #[must_use]
260 pub fn collision_boxes(&self, world: &dyn LevelReader, pos: BlockPos) -> BlockCollisionBoxes {
261 let mut result = BlockCollisionBoxes::new();
262 let moved_behavior = BLOCK_BEHAVIORS.get_behavior(self.moved_state.get_block());
263 if !self.extending && self.source_piston && moved_behavior.is_piston_base() {
264 let extended = self
265 .moved_state
266 .set_value(&BlockStateProperties::EXTENDED, true);
267 result.extend(Self::state_collision_boxes(extended, world, pos));
268 }
269
270 let no_clip_direction = NOCLIP.get();
271 if self.progress < 1.0 && no_clip_direction == Some(self.movement_direction()) {
272 return result;
273 }
274
275 let moving_state = if self.source_piston {
276 vanilla_blocks::PISTON_HEAD
277 .default_state()
278 .set_value(&BlockStateProperties::FACING, self.direction)
279 .set_value(
280 &BlockStateProperties::SHORT,
281 self.extending != ((1.0 - self.progress) < 0.25),
282 )
283 } else {
284 self.moved_state
285 };
286 let amount = f64::from(self.extended_progress(self.progress));
287 let (x, y, z) = self.direction.offset();
288 let offset = DVec3::new(
289 f64::from(x) * amount,
290 f64::from(y) * amount,
291 f64::from(z) * amount,
292 );
293 result.extend(
294 Self::state_collision_boxes(moving_state, world, pos)
295 .into_iter()
296 .map(|aabb| aabb.translate(offset)),
297 );
298 result
299 }
300
301 fn move_by_position_and_progress(&self, pos: BlockPos, aabb: BlockLocalAabb) -> WorldAabb {
302 let amount = f64::from(self.extended_progress(self.progress));
303 let (x, y, z) = self.direction.offset();
304 aabb.at_block(pos).translate(DVec3::new(
305 f64::from(x) * amount,
306 f64::from(y) * amount,
307 f64::from(z) * amount,
308 ))
309 }
310
311 fn movement_area(aabb: WorldAabb, direction: Direction, amount: f64) -> WorldAabb {
312 let signed_amount = if matches!(
313 direction,
314 Direction::West | Direction::Down | Direction::North
315 ) {
316 -amount
317 } else {
318 amount
319 };
320 let min = signed_amount.min(0.0);
321 let max = signed_amount.max(0.0);
322 match direction {
323 Direction::West => WorldAabb::new(
324 aabb.min_x() + min,
325 aabb.min_y(),
326 aabb.min_z(),
327 aabb.min_x() + max,
328 aabb.max_y(),
329 aabb.max_z(),
330 ),
331 Direction::East => WorldAabb::new(
332 aabb.max_x() + min,
333 aabb.min_y(),
334 aabb.min_z(),
335 aabb.max_x() + max,
336 aabb.max_y(),
337 aabb.max_z(),
338 ),
339 Direction::Down => WorldAabb::new(
340 aabb.min_x(),
341 aabb.min_y() + min,
342 aabb.min_z(),
343 aabb.max_x(),
344 aabb.min_y() + max,
345 aabb.max_z(),
346 ),
347 Direction::Up => WorldAabb::new(
348 aabb.min_x(),
349 aabb.max_y() + min,
350 aabb.min_z(),
351 aabb.max_x(),
352 aabb.max_y() + max,
353 aabb.max_z(),
354 ),
355 Direction::North => WorldAabb::new(
356 aabb.min_x(),
357 aabb.min_y(),
358 aabb.min_z() + min,
359 aabb.max_x(),
360 aabb.max_y(),
361 aabb.min_z() + max,
362 ),
363 Direction::South => WorldAabb::new(
364 aabb.min_x(),
365 aabb.min_y(),
366 aabb.max_z() + min,
367 aabb.max_x(),
368 aabb.max_y(),
369 aabb.max_z() + max,
370 ),
371 }
372 }
373
374 fn overlap_movement(outside: WorldAabb, movement: Direction, entity: WorldAabb) -> f64 {
375 match movement {
376 Direction::East => outside.max_x() - entity.min_x(),
377 Direction::West => entity.max_x() - outside.min_x(),
378 Direction::Up => outside.max_y() - entity.min_y(),
379 Direction::Down => entity.max_y() - outside.min_y(),
380 Direction::South => outside.max_z() - entity.min_z(),
381 Direction::North => entity.max_z() - outside.min_z(),
382 }
383 }
384
385 fn move_entity_by_piston(
386 piston_direction: Direction,
387 entity: &SharedEntity,
388 delta: f64,
389 movement: Direction,
390 ) {
391 let _no_clip = NoClipGuard::set(piston_direction);
392 let (x, y, z) = movement.offset();
393 let previous_position = entity.position();
394 Arc::clone(entity).move_entity(
395 MoverType::Piston,
396 DVec3::new(
397 delta * f64::from(x),
398 delta * f64::from(y),
399 delta * f64::from(z),
400 ),
401 );
402 entity.apply_effects_from_blocks_between(previous_position, entity.position());
403 entity.remove_latest_movement_recording();
404 }
405
406 fn fix_entity_within_piston_base(
407 pos: BlockPos,
408 entity: &SharedEntity,
409 direction: Direction,
410 delta_progress: f64,
411 ) {
412 let entity_aabb = entity.bounding_box();
413 let box_at_pos = BlockLocalAabb::FULL_BLOCK.at_block(pos);
414 if !entity_aabb.intersects(box_at_pos) {
415 return;
416 }
417
418 let opposite = direction.opposite();
419 let delta = Self::overlap_movement(box_at_pos, opposite, entity_aabb) + PUSH_OFFSET;
420 let intersection = WorldAabb::new(
421 entity_aabb.min_x().max(box_at_pos.min_x()),
422 entity_aabb.min_y().max(box_at_pos.min_y()),
423 entity_aabb.min_z().max(box_at_pos.min_z()),
424 entity_aabb.max_x().min(box_at_pos.max_x()),
425 entity_aabb.max_y().min(box_at_pos.max_y()),
426 entity_aabb.max_z().min(box_at_pos.max_z()),
427 );
428 let intersected_delta =
429 Self::overlap_movement(box_at_pos, opposite, intersection) + PUSH_OFFSET;
430 if (delta - intersected_delta).abs() < PUSH_OFFSET {
431 let delta = delta.min(delta_progress) + PUSH_OFFSET;
432 Self::move_entity_by_piston(direction, entity, delta, opposite);
433 }
434 }
435
436 fn move_collided_entities(self, world: &Arc<World>, pos: BlockPos, new_progress: f32) {
437 let movement = self.movement_direction();
438 let delta_progress = f64::from(new_progress - self.progress);
439 let shape =
440 Self::state_collision_boxes(self.collision_related_state(), world.as_ref(), pos);
441 let Some(bounds) = Self::boxes_bounds(&shape) else {
442 return;
443 };
444 let aabb = self.move_by_position_and_progress(pos, bounds);
445 let query =
446 WorldAabb::encapsulating(&Self::movement_area(aabb, movement, delta_progress), &aabb);
447 let entities = world.get_entities_in_aabb(&query);
448 let cause_bounce = self.moved_state.get_block() == &vanilla_blocks::SLIME_BLOCK;
449
450 for entity in entities {
451 if !Self::can_move_collided_entity(entity.as_ref(), cause_bounce) {
452 continue;
453 }
454 if cause_bounce {
455 let mut velocity = entity.velocity();
456 let (x, y, z) = movement.offset();
457 match movement.axis() {
458 Axis::X => velocity.x = f64::from(x),
459 Axis::Y => velocity.y = f64::from(y),
460 Axis::Z => velocity.z = f64::from(z),
461 }
462 entity.set_velocity(velocity);
463 }
464
465 let mut delta: f64 = 0.0;
466 let entity_aabb = entity.bounding_box();
467 for shape_aabb in &shape {
468 let moving_aabb = Self::movement_area(
469 self.move_by_position_and_progress(pos, *shape_aabb),
470 movement,
471 delta_progress,
472 );
473 if moving_aabb.intersects(entity_aabb) {
474 delta = delta.max(Self::overlap_movement(moving_aabb, movement, entity_aabb));
475 if delta >= delta_progress {
476 break;
477 }
478 }
479 }
480
481 if delta <= 0.0 {
482 continue;
483 }
484 let delta = delta.min(delta_progress) + PUSH_OFFSET;
485 Self::move_entity_by_piston(movement, &entity, delta, movement);
486 if !self.extending && self.source_piston {
487 Self::fix_entity_within_piston_base(pos, &entity, movement, delta_progress);
488 }
489 }
490 }
491
492 fn move_stuck_entities(self, world: &Arc<World>, pos: BlockPos, new_progress: f32) {
493 if self.moved_state.get_block() != &vanilla_blocks::HONEY_BLOCK {
494 return;
495 }
496 let movement = self.movement_direction();
497 if !movement.is_horizontal() {
498 return;
499 }
500
501 let collision = Self::state_collision_boxes(self.moved_state, world.as_ref(), pos);
502 let sticky_top = collision
503 .iter()
504 .map(BlockLocalAabb::max_y)
505 .fold(f64::NEG_INFINITY, f64::max);
506 let local = BlockLocalAabb::new(0.0, sticky_top, 0.0, 1.0, 1.500_001, 1.0);
507 let aabb = self.move_by_position_and_progress(pos, local);
508 let entities = world.get_entities_in_aabb_matching(&aabb, |entity| {
509 let position = entity.position();
510 entity.piston_push_reaction() == PushReaction::PushPull
511 && entity.on_ground()
512 && (entity.is_supported_by(pos)
513 || (position.x >= aabb.min_x()
514 && position.x <= aabb.max_x()
515 && position.z >= aabb.min_z()
516 && position.z <= aabb.max_z()))
517 });
518 let delta_progress = f64::from(new_progress - self.progress);
519 for entity in entities {
520 Self::move_entity_by_piston(movement, &entity, delta_progress, movement);
521 }
522 }
523
524 fn finish_tick(self, world: &Arc<World>, pos: BlockPos) {
525 if world.get_block_state(pos).get_block() != &vanilla_blocks::MOVING_PISTON {
526 return;
527 }
528
529 let mut new_state = world.update_from_neighbor_shapes(self.moved_state, pos);
530 if new_state.get_block() == &vanilla_blocks::AIR {
531 world.set_block(
532 pos,
533 self.moved_state,
534 UpdateFlags::UPDATE_INVISIBLE
535 | UpdateFlags::UPDATE_KNOWN_SHAPE
536 | UpdateFlags::UPDATE_MOVE_BY_PISTON
537 | UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS,
538 );
539 world.update_or_destroy(
540 self.moved_state,
541 new_state,
542 pos,
543 UpdateFlags::UPDATE_ALL,
544 World::UPDATE_LIMIT,
545 );
546 return;
547 }
548
549 if new_state.try_get_value(&BlockStateProperties::WATERLOGGED) == Some(true) {
550 new_state = new_state.set_value(&BlockStateProperties::WATERLOGGED, false);
551 }
552 world.set_block(
553 pos,
554 new_state,
555 UpdateFlags::UPDATE_ALL | UpdateFlags::UPDATE_MOVE_BY_PISTON,
556 );
557 world.neighbor_changed(pos, new_state.get_block());
558 }
559
560 fn finish_movement_early(self, world: &Arc<World>, pos: BlockPos) {
561 if world.get_block_state(pos).get_block() == &vanilla_blocks::MOVING_PISTON {
562 let new_state = if self.source_piston {
563 vanilla_blocks::AIR.default_state()
564 } else {
565 world.update_from_neighbor_shapes(self.moved_state, pos)
566 };
567 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
568 world.neighbor_changed(pos, new_state.get_block());
569 }
570 }
571
572 const fn direction_from_legacy_id(id: i8) -> Direction {
573 let remainder = (id as i32) % 6;
574 let normalized = if remainder < 0 { -remainder } else { remainder };
575 match normalized {
576 1 => Direction::Up,
577 2 => Direction::North,
578 3 => Direction::South,
579 4 => Direction::West,
580 5 => Direction::East,
581 _ => Direction::Down,
582 }
583 }
584
585 const fn direction_legacy_id(direction: Direction) -> i8 {
586 match direction {
587 Direction::Down => 0,
588 Direction::Up => 1,
589 Direction::North => 2,
590 Direction::South => 3,
591 Direction::West => 4,
592 Direction::East => 5,
593 }
594 }
595}
596
597impl BlockEntity for PistonMovingBlockEntity {
598 fn base(&self) -> &BlockEntityBase {
599 &self.base
600 }
601
602 fn pre_remove_side_effects(&self, _pos: BlockPos, _state: BlockStateId) {
603 if let Some(world) = self.get_level() {
604 self.final_tick(&world);
605 }
606 }
607
608 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
609 let view = NbtCompoundView::from(nbt);
610 let mut moving = self.moving.lock();
611 moving.moved_state = view
612 .compound("blockState")
613 .and_then(block_state_nbt::load)
614 .unwrap_or_else(|| vanilla_blocks::AIR.default_state());
615 moving.direction = PistonMovingState::direction_from_legacy_id(
616 view.get("facing")
617 .and_then(|tag| tag.codec_i8())
618 .unwrap_or(0),
619 );
620 moving.progress = view.float_value("progress").unwrap_or(0.0);
621 moving.progress_o = moving.progress;
622 moving.extending = view.byte_value("extending").is_some_and(|value| value != 0);
623 moving.source_piston = view.byte_value("source").is_some_and(|value| value != 0);
624 }
625
626 fn save_additional(&self, nbt: &mut NbtCompound) {
627 let moving = self.moving.lock();
628 nbt.insert("blockState", block_state_nbt::save(moving.moved_state));
629 nbt.insert(
630 "facing",
631 PistonMovingState::direction_legacy_id(moving.direction),
632 );
633 nbt.insert("progress", moving.progress_o);
634 nbt.insert("extending", i8::from(moving.extending));
635 nbt.insert("source", i8::from(moving.source_piston));
636 }
637
638 fn get_update_tag(&self) -> Option<NbtCompound> {
639 Some(self.save_custom_only())
640 }
641
642 fn tick(&self, world: &Arc<World>) {
643 let game_time = world.game_time();
644 let (moving, new_progress) = {
645 let mut moving = self.moving.lock();
646 moving.last_ticked = game_time;
647 moving.progress_o = moving.progress;
648 let new_progress = (moving.progress_o < 1.0).then(|| moving.progress + 0.5);
649 (*moving, new_progress)
650 };
651 let pos = self.get_block_pos();
652 let Some(new_progress) = new_progress else {
653 let owned_position = world.remove_block_entity_if_same(self);
654 self.set_removed();
655 if owned_position {
656 moving.finish_tick(world, pos);
657 }
658 return;
659 };
660
661 moving.move_collided_entities(world, pos, new_progress);
662 moving.move_stuck_entities(world, pos, new_progress);
663 self.moving.lock().progress = new_progress.min(1.0);
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use std::io::Cursor;
670
671 use super::*;
672 use crate::behavior::init_behaviors;
673 use crate::block_entity::SharedBlockEntity;
674 use crate::entity::SharedEntity;
675 use crate::player::Player;
676 use crate::test_support::{
677 TestEntity, TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk,
678 };
679 use glam::DVec3;
680 use simdnbt::borrow::read_compound as read_borrowed_compound;
681 use simdnbt::owned::NbtTag;
682 use steel_registry::{init_vanilla_registry, vanilla_entities};
683 use steel_utils::{ChunkPos, types::GameType};
684
685 fn test_player(world: Arc<World>) -> Arc<Player> {
686 TestPlayerBuilder::new(world, "PistonTestPlayer", 1).build()
687 }
688
689 #[test]
690 fn moving_state_and_progress_round_trip_with_vanilla_keys() {
691 init_vanilla_registry();
692 let state = vanilla_blocks::MOVING_PISTON
693 .default_state()
694 .set_value(&BlockStateProperties::FACING, Direction::West)
695 .set_value(&BlockStateProperties::PISTON_TYPE, PistonType::Sticky);
696 let moved = vanilla_blocks::PISTON_HEAD
697 .default_state()
698 .set_value(&BlockStateProperties::FACING, Direction::West)
699 .set_value(&BlockStateProperties::PISTON_TYPE, PistonType::Sticky)
700 .set_value(&BlockStateProperties::SHORT, true);
701 let source = PistonMovingBlockEntity::new_moving(
702 Weak::new(),
703 BlockPos::new(8, 64, -3),
704 state,
705 moved,
706 Direction::West,
707 true,
708 true,
709 );
710 source.moving.lock().progress_o = 0.5;
711
712 let mut nbt = NbtCompound::new();
713 source.save_additional(&mut nbt);
714 assert!(matches!(nbt.get("facing"), Some(NbtTag::Byte(4))));
715 let mut bytes = Vec::new();
716 nbt.write(&mut bytes);
717 let borrowed = read_borrowed_compound(&mut Cursor::new(bytes.as_slice()))
718 .expect("test NBT should reborrow");
719 let view = NbtCompoundView::from(&borrowed);
720 assert_eq!(view.byte("facing"), Some(4));
721 assert_eq!(view.int("facing"), None);
722
723 let loaded = PistonMovingBlockEntity::new(Weak::new(), source.get_block_pos(), state);
724 loaded.load_additional(&borrowed);
725 assert_eq!(loaded.moved_state(), moved);
726 assert_eq!(loaded.direction(), Direction::West);
727 assert!(loaded.is_extending());
728 assert!(loaded.is_source_piston());
729 assert!((loaded.progress(0.0) - 0.5).abs() < f32::EPSILON);
730 }
731
732 #[test]
733 fn collided_entity_filter_matches_vanilla_player_and_spectator_rules() {
734 init_vanilla_registry();
735 init_behaviors();
736 let world_fixture = fresh_test_world("piston_entity_filter");
737 let world = &world_fixture.world;
738 let player = test_player(Arc::clone(world));
739
740 assert!(PistonMovingState::can_move_collided_entity(
741 player.as_ref(),
742 false
743 ));
744 assert!(!PistonMovingState::can_move_collided_entity(
745 player.as_ref(),
746 true
747 ));
748
749 player.restore_game_modes(GameType::Spectator, None);
750 assert!(!PistonMovingState::can_move_collided_entity(
751 player.as_ref(),
752 false
753 ));
754
755 let entity = TestEntity::new(
756 8_000,
757 DVec3::ZERO,
758 Arc::downgrade(world),
759 &vanilla_entities::MINECART,
760 );
761 assert!(PistonMovingState::can_move_collided_entity(&entity, true));
762 }
763
764 #[test]
765 fn movement_area_matches_vanilla_directional_sweep() {
766 let aabb = WorldAabb::new(1.0, 2.0, 3.0, 2.0, 3.0, 4.0);
767 assert_eq!(
768 PistonMovingState::movement_area(aabb, Direction::East, 0.5),
769 WorldAabb::new(2.0, 2.0, 3.0, 2.5, 3.0, 4.0)
770 );
771 assert_eq!(
772 PistonMovingState::movement_area(aabb, Direction::North, 0.5),
773 WorldAabb::new(1.0, 2.0, 2.5, 2.0, 3.0, 3.0)
774 );
775 }
776
777 #[test]
778 fn piston_entity_move_can_reenter_moving_block_collision() {
779 init_vanilla_registry();
780 init_behaviors();
781 let world_fixture = fresh_test_world("piston_collision_reentry");
782 let world = &world_fixture.world;
783 let pos = BlockPos::new(8, 64, 8);
784 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
785 let state = vanilla_blocks::MOVING_PISTON
786 .default_state()
787 .set_value(&BlockStateProperties::FACING, Direction::East);
788 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
789
790 let piston = Arc::new(PistonMovingBlockEntity::new_moving(
791 Arc::downgrade(world),
792 pos,
793 state,
794 vanilla_blocks::STONE.default_state(),
795 Direction::East,
796 true,
797 false,
798 ));
799 let block_entity: SharedBlockEntity = Arc::<PistonMovingBlockEntity>::clone(&piston);
800 assert!(world.set_block_entity(block_entity));
801
802 let start = DVec3::new(f64::from(pos.x()) + 0.1, f64::from(pos.y()), 8.5);
803 let entity: SharedEntity = TestEntity::shared(
804 8_001,
805 start,
806 Arc::downgrade(world),
807 &vanilla_entities::MINECART,
808 );
809 world
810 .try_add_entity(Arc::clone(&entity))
811 .expect("test entity should enter the loaded chunk");
812
813 piston.tick(world);
814
815 assert!((piston.progress(1.0) - 0.5).abs() < f32::EPSILON);
816 assert!(entity.position().x > start.x);
817 }
818
819 #[test]
820 fn final_tick_marks_a_detached_moving_entity_removed() {
821 init_vanilla_registry();
822 init_behaviors();
823 let world_fixture = fresh_test_world("detached_piston_final_tick");
824 let world = &world_fixture.world;
825 let pos = BlockPos::new(8, 64, 8);
826 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
827 let state = vanilla_blocks::MOVING_PISTON
828 .default_state()
829 .set_value(&BlockStateProperties::FACING, Direction::East);
830 let piston = PistonMovingBlockEntity::new_moving(
831 Arc::downgrade(world),
832 pos,
833 state,
834 vanilla_blocks::STONE.default_state(),
835 Direction::East,
836 true,
837 false,
838 );
839
840 assert!(piston.final_tick(world));
841 assert!(piston.is_removed());
842 }
843
844 #[test]
845 fn stale_final_tick_cannot_remove_or_finish_a_replacement() {
846 init_vanilla_registry();
847 init_behaviors();
848 let world_fixture = fresh_test_world("stale_piston_final_tick");
849 let world = &world_fixture.world;
850 let pos = BlockPos::new(8, 64, 8);
851 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
852 let state = vanilla_blocks::MOVING_PISTON
853 .default_state()
854 .set_value(&BlockStateProperties::FACING, Direction::East);
855 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
856 let stale_piston = Arc::new(PistonMovingBlockEntity::new_moving(
857 Arc::downgrade(world),
858 pos,
859 state,
860 vanilla_blocks::STONE.default_state(),
861 Direction::East,
862 true,
863 false,
864 ));
865 let stale_entity: SharedBlockEntity = Arc::<PistonMovingBlockEntity>::clone(&stale_piston);
866 assert!(world.set_block_entity(stale_entity));
867 let replacement = Arc::new(PistonMovingBlockEntity::new_moving(
868 Arc::downgrade(world),
869 pos,
870 state,
871 vanilla_blocks::GOLD_BLOCK.default_state(),
872 Direction::East,
873 true,
874 false,
875 ));
876 let replacement_entity: SharedBlockEntity =
877 Arc::<PistonMovingBlockEntity>::clone(&replacement);
878 assert!(world.set_block_entity(Arc::clone(&replacement_entity)));
879
880 assert!(stale_piston.final_tick(world));
881
882 let Some(current) = world.get_block_entity(pos) else {
883 panic!("the replacement should remain stored");
884 };
885 assert!(Arc::ptr_eq(¤t, &replacement_entity));
886 assert_eq!(world.get_block_state(pos), state);
887 assert!(!replacement.is_removed());
888 }
889}