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