use std::{collections::HashMap, sync::Arc}; use pumpkin_data::{ Block, BlockDirection, BlockState, FacingExt, block_properties::{ BlockProperties, MovingPistonLikeProperties, PistonHeadLikeProperties, PistonType, }, block_state::PistonBehavior, }; use pumpkin_util::math::position::BlockPos; use pumpkin_world::{ BlockStateId, block::entities::{has_block_block_entity, piston::PistonBlockEntity}, world::BlockFlags, }; use crate::{ block::{ BlockBehaviour, BlockFuture, BlockMetadata, OnNeighborUpdateArgs, OnPlaceArgs, OnSyncedBlockEventArgs, PlacedArgs, blocks::redstone::is_emitting_redstone_power, }, world::World, }; use super::PistonHandler; pub(crate) type PistonProps = pumpkin_data::block_properties::StickyPistonLikeProperties; pub struct PistonBlock; impl BlockMetadata for PistonBlock { fn ids() -> Box<[u16]> { [Block::PISTON.id, Block::STICKY_PISTON.id].into() } } impl PistonBlock { #[must_use] pub fn is_movable( block: &Block, state: &BlockState, dir: BlockDirection, can_break: bool, piston_dir: BlockDirection, ) -> bool { // TODO: more checks if state.is_air() { return true; } // Vanilla hardcoded them aswell if block == &Block::OBSIDIAN || block == &Block::CRYING_OBSIDIAN || block == &Block::RESPAWN_ANCHOR || block == &Block::REINFORCED_DEEPSLATE { return false; } if block == &Block::PISTON || block == &Block::STICKY_PISTON { let props = PistonProps::from_state_id(state.id, block); if props.extended { return false; } } else { #[expect(clippy::float_cmp)] if state.hardness == -1.0 { return false; } match state.piston_behavior { pumpkin_data::block_state::PistonBehavior::Destroy => return can_break, pumpkin_data::block_state::PistonBehavior::Block => return false, pumpkin_data::block_state::PistonBehavior::PushOnly => return dir == piston_dir, _ => {} } } !has_block_block_entity(block) } } impl BlockBehaviour for PistonBlock { fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { let mut props = PistonProps::default(args.block); props.extended = false; props.facing = args.player.living_entity.entity.get_facing().opposite(); props.to_state_id(args.block) }) } fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { if args.old_state_id == args.state_id { return; } try_move(args.world, args.block, args.position).await; }) } fn on_neighbor_update<'a>(&'a self, args: OnNeighborUpdateArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { try_move(args.world, args.block, args.position).await; }) } #[expect(clippy::too_many_lines)] fn on_synced_block_event<'a>( &'a self, args: OnSyncedBlockEventArgs<'a>, ) -> BlockFuture<'a, bool> { Box::pin(async move { let (block, world, pos, r#type, data) = ( args.block, args.world, args.position, args.r#type, args.data, ); let state = world.get_block_state(pos).await; let mut props = PistonProps::from_state_id(state.id, block); let dir = props.facing.to_block_direction(); // I don't think this is optimal ? let sticky = block == &Block::STICKY_PISTON; let should_extend = should_extend(world, pos, dir).await; if should_extend && (r#type == 1 || r#type == 2) { props.extended = true; world .set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_LISTENERS) .await; return false; } // This may prevents when something happens in the one tick before this function got called if !should_extend && r#type == 0 { return false; } // Extend Piston if r#type == 0 { if !move_piston(world, dir, pos, true, sticky).await { return false; } props.extended = true; world .set_block_state( pos, props.to_state_id(block), BlockFlags::NOTIFY_ALL | BlockFlags::MOVED, ) .await; return true; } // Reduce Piston let extended_pos = pos.offset(dir.to_offset()); if let Some(block_entity) = world.get_block_entity(&extended_pos).await { let piston = block_entity .as_any() .downcast_ref::() .unwrap(); piston.finish(world.clone()).await; } let mut props = MovingPistonLikeProperties::default(&Block::MOVING_PISTON); props.facing = dir.to_facing(); props.r#type = if sticky { PistonType::Sticky } else { PistonType::Normal }; world .set_block_state( pos, props.to_state_id(&Block::MOVING_PISTON), BlockFlags::FORCE_STATE, ) .await; let mut props = PistonProps::default(block); props.facing = BlockDirection::by_index((data & 7) as usize) .unwrap() .to_facing(); world .add_block_entity(Arc::new(PistonBlockEntity { position: *pos, facing: dir, pushed_block_state: BlockState::from_id(props.to_state_id(block)), current_progress: 0.0.into(), last_progress: 0.0.into(), extending: false, source: true, })) .await; world.update_neighbors(pos, None).await; if sticky { let pos = pos.offset_dir(dir.to_offset(), 2); let (block, state) = world.get_block_and_state(&pos).await; let mut bl2 = false; if block == &Block::MOVING_PISTON && let Some(entity) = world.get_block_entity(&pos).await { let piston = entity.as_any().downcast_ref::().unwrap(); if piston.facing == dir && piston.extending { piston.finish(world.clone()).await; bl2 = true; } } if !bl2 { if r#type == 1 && !state.is_air() && Self::is_movable(block, state, dir, false, dir) && (state.piston_behavior == PistonBehavior::Normal || block == &Block::PISTON || block == &Block::STICKY_PISTON) { move_piston(world, dir, &pos, false, sticky).await; } else { // remove world .set_block_state( &extended_pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL, ) .await; } } } else { // remove world .set_block_state( &extended_pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL, ) .await; } true }) } } async fn should_extend(world: &World, block_pos: &BlockPos, piston_dir: BlockDirection) -> bool { for dir in BlockDirection::all() { let neighbor_pos = block_pos.offset(dir.to_offset()); let (block, state) = world.get_block_and_state(&neighbor_pos).await; // Pistons can't be powered from the same direction as they are facing if dir == piston_dir || !is_emitting_redstone_power(block, state, world, &neighbor_pos, dir).await { continue; } return true; } let neighbor_pos = block_pos.offset(BlockDirection::Down.to_offset()); let (block, state) = world.get_block_and_state(&neighbor_pos).await; if is_emitting_redstone_power(block, state, world, block_pos, BlockDirection::Down).await { return true; } for dir in BlockDirection::all() { let neighbor_pos = block_pos.up().offset(dir.to_offset()); let (block, state) = world.get_block_and_state(&neighbor_pos).await; if dir == BlockDirection::Down || !is_emitting_redstone_power(block, state, world, &neighbor_pos, dir).await { continue; } return true; } false } async fn try_move(world: &Arc, block: &Block, block_pos: &BlockPos) { let state = world.get_block_state(block_pos).await; let props = PistonProps::from_state_id(state.id, block); let dir = props.facing.to_block_direction(); let should_extent = should_extend(world, block_pos, dir).await; if should_extent && !props.extended { if PistonHandler::new(world, *block_pos, dir, true) .calculate_push() .await { world .add_synced_block_event(*block_pos, 0, dir.to_index()) .await; } } else if !should_extent && props.extended { let new_pos = block_pos.offset_dir(dir.to_offset(), 2); let (new_block, new_state) = world.get_block_and_state_id(&new_pos).await; let mut r#type = 1; if new_block == &Block::MOVING_PISTON { let new_props = MovingPistonLikeProperties::from_state_id(new_state, new_block); if new_props.facing == props.facing && let Some(entity) = world.get_block_entity(&new_pos).await { let piston = entity.as_any().downcast_ref::().unwrap(); if piston.extending && piston.current_progress.load() < 0.5 // TODO: more stuff... { // Piston reduced too quickly, if its a stick piston no blocks will be dragged r#type = 2; } } } world .add_synced_block_event(*block_pos, r#type, dir.to_index()) .await; } } #[expect(clippy::too_many_lines)] async fn move_piston( world: &Arc, dir: BlockDirection, block_pos: &BlockPos, extend: bool, sticky: bool, ) -> bool { let extended_pos = block_pos.offset(dir.to_offset()); if !extend && world.get_block(&extended_pos).await == &Block::PISTON_HEAD { world .set_block_state( &extended_pos, Block::AIR.default_state.id, BlockFlags::FORCE_STATE, ) .await; } let mut handler = PistonHandler::new(world, *block_pos, dir, extend); if !handler.calculate_push().await { return false; } let mut moved_blocks_map: HashMap = HashMap::new(); let moved_blocks: Vec = handler.moved_blocks; let mut moved_block_states: Vec<&BlockState> = Vec::new(); for &block_pos in &moved_blocks { let block_state = world.get_block_state(&block_pos).await; moved_block_states.push(block_state); moved_blocks_map.insert(block_pos, block_state); } let broken_blocks: Vec = handler.broken_blocks; let mut affected_block_states: Vec<&BlockState> = Vec::with_capacity(moved_blocks.len() + broken_blocks.len()); let move_direction = if extend { dir } else { dir.opposite() }; for &broken_block_pos in broken_blocks.iter().rev() { let block_state = world.get_block_state(&broken_block_pos).await; world .break_block( &broken_block_pos, None, BlockFlags::NOTIFY_LISTENERS | BlockFlags::FORCE_STATE, ) .await; affected_block_states.push(block_state); } for (index, &moved_block_pos) in moved_blocks.iter().rev().enumerate() { let block_state = world.get_block_state(&moved_block_pos).await; let target_pos = moved_block_pos.offset(move_direction.to_offset()); moved_blocks_map.remove(&target_pos); let mut props = MovingPistonLikeProperties::default(&Block::MOVING_PISTON); props.facing = dir.to_facing(); let state = props.to_state_id(&Block::MOVING_PISTON); world .set_block_state(&target_pos, state, BlockFlags::MOVED) .await; if let Some(moved_state) = moved_block_states.get(index) { world .add_block_entity(Arc::new(PistonBlockEntity { position: extended_pos, facing: dir.to_facing().to_block_direction(), pushed_block_state: moved_state, current_progress: 0.0.into(), last_progress: 0.0.into(), extending: extend, source: false, })) .await; } affected_block_states.push(block_state); } if extend { let pistion_type = if sticky { PistonType::Sticky } else { PistonType::Normal }; let mut props = MovingPistonLikeProperties::default(&Block::MOVING_PISTON); props.facing = dir.to_facing(); props.r#type = pistion_type; moved_blocks_map.remove(&extended_pos); world .set_block_state( &extended_pos, props.to_state_id(&Block::MOVING_PISTON), BlockFlags::MOVED, ) .await; let mut props = PistonHeadLikeProperties::default(&Block::PISTON_HEAD); props.facing = dir.to_facing(); props.r#type = pistion_type; world .add_block_entity(Arc::new(PistonBlockEntity { position: extended_pos, facing: dir.to_facing().to_block_direction(), pushed_block_state: BlockState::from_id(props.to_state_id(&Block::PISTON_HEAD)), current_progress: 0.0.into(), last_progress: 0.0.into(), extending: true, source: true, })) .await; } let air_state = Block::AIR.default_state.id; for &pos in moved_blocks_map.keys() { world .set_block_state( &pos, air_state, BlockFlags::NOTIFY_LISTENERS | BlockFlags::FORCE_STATE | BlockFlags::MOVED, ) .await; } for (pos, state) in &moved_blocks_map { world .block_registry .prepare( world, pos, Block::from_state_id(state.id), state.id, BlockFlags::NOTIFY_LISTENERS, ) .await; world.update_neighbors(pos, None).await; world .block_registry .prepare( world, pos, &Block::AIR, air_state, BlockFlags::NOTIFY_LISTENERS, ) .await; } for (i, &broken_block_pos) in broken_blocks.iter().rev().enumerate() { if let Some(block_state) = affected_block_states.get(i) { world .block_registry .on_state_replaced( world, Block::from_state_id(block_state.id), &broken_block_pos, block_state.id, // ? false, ) .await; world .block_registry .prepare( world, &broken_block_pos, Block::from_state_id(block_state.id), block_state.id, BlockFlags::NOTIFY_LISTENERS, ) .await; world.update_neighbors(&broken_block_pos, None).await; } } for &moved_block_pos in moved_blocks.iter().rev() { world.update_neighbors(&moved_block_pos, None).await; } if extend { world.update_neighbors(&extended_pos, None).await; } true }