All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.nukkit.block.BlockPistonBase Maven / Gradle / Ivy

There is a newer version: 1.20.40-r1
Show newest version
package cn.nukkit.block;

import cn.nukkit.Player;
import cn.nukkit.api.PowerNukkitDifference;
import cn.nukkit.api.PowerNukkitOnly;
import cn.nukkit.api.Since;
import cn.nukkit.blockentity.BlockEntity;
import cn.nukkit.blockentity.BlockEntityMovingBlock;
import cn.nukkit.blockentity.BlockEntityPistonArm;
import cn.nukkit.blockproperty.BlockProperties;
import cn.nukkit.blockproperty.CommonBlockProperties;
import cn.nukkit.blockstate.BlockState;
import cn.nukkit.blockstate.BlockStateRegistry;
import cn.nukkit.event.block.BlockPistonEvent;
import cn.nukkit.inventory.InventoryHolder;
import cn.nukkit.item.Item;
import cn.nukkit.level.Level;
import cn.nukkit.level.Position;
import cn.nukkit.level.Sound;
import cn.nukkit.level.vibration.VibrationEvent;
import cn.nukkit.level.vibration.VibrationType;
import cn.nukkit.math.BlockFace;
import cn.nukkit.math.BlockVector3;
import cn.nukkit.math.Vector3;
import cn.nukkit.nbt.tag.CompoundTag;
import cn.nukkit.utils.Faceable;
import cn.nukkit.utils.RedstoneComponent;
import com.google.common.collect.Lists;
import lombok.extern.log4j.Log4j2;

import org.jetbrains.annotations.NotNull;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author CreeperFace
 */
@PowerNukkitDifference(info = "Implements RedstoneComponent.", since = "1.4.0.0-PN")
@PowerNukkitDifference(since = "1.4.0.0-PN", info = "Implements BlockEntityHolder only in PowerNukkit")
@Log4j2
public abstract class BlockPistonBase extends BlockTransparentMeta implements Faceable, RedstoneComponent, BlockEntityHolder {

    @PowerNukkitOnly
    @Since("1.5.0.0-PN")
    public static final BlockProperties PROPERTIES = CommonBlockProperties.FACING_DIRECTION_BLOCK_PROPERTIES;

    public boolean sticky = false;

    public BlockPistonBase(int meta) {
        super(meta);
    }

    /**
     * @return 指定方块是否能向指定方向推动
     */
    public static boolean canPush(Block block, BlockFace face, boolean destroyBlocks, boolean extending) {
        var min = block.level.getMinHeight();
        var max = block.level.getMaxHeight() - 1;
        if (
                block.getY() >= min && (face != BlockFace.DOWN || block.getY() != min) &&
                        block.getY() <= max && (face != BlockFace.UP || block.getY() != max)
        ) {
            if (extending && !block.canBePushed() || !extending && !block.canBePulled())
                return false;
            if (block.breaksWhenMoved())
                return destroyBlocks || block.sticksToPiston();
            var blockEntity = block.getLevelBlockEntity();
            return blockEntity == null || blockEntity.isMovable();
        }

        return false;
    }

    @Since("1.4.0.0-PN")
    @PowerNukkitOnly
    @NotNull
    @Override
    public BlockProperties getProperties() {
        return PROPERTIES;
    }

    @Since("1.4.0.0-PN")
    @PowerNukkitOnly
    @NotNull
    @Override
    public Class getBlockEntityClass() {
        return BlockEntityPistonArm.class;
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @NotNull
    @Override
    public String getBlockEntityType() {
        return BlockEntity.PISTON_ARM;
    }

    @Override
    public double getResistance() {
        return 2.5;
    }

    @Override
    public double getHardness() {
        return 0.5;
    }

    @PowerNukkitOnly
    @Override
    public int getWaterloggingLevel() {
        return 1;
    }

    @Override
    @PowerNukkitDifference(info = "Using new method for checking if powered", since = "1.4.0.0-PN")
    public boolean place(@NotNull Item item, @NotNull Block block, @NotNull Block target, @NotNull BlockFace face, double fx, double fy, double fz, @Nullable Player player) {
        if (player != null) {
            if (Math.abs(player.getFloorX() - this.x) <= 1 && Math.abs(player.getFloorZ() - this.z) <= 1) {
                double y = player.y + player.getEyeHeight();

                if (y - this.y > 2) {
                    this.setPropertyValue(CommonBlockProperties.FACING_DIRECTION, BlockFace.UP);
                } else if (this.y - y > 0) {
                    this.setPropertyValue(CommonBlockProperties.FACING_DIRECTION, BlockFace.DOWN);
                } else {
                    this.setPropertyValue(CommonBlockProperties.FACING_DIRECTION, player.getHorizontalFacing());
                }
            } else {
                this.setPropertyValue(CommonBlockProperties.FACING_DIRECTION, player.getHorizontalFacing());
            }
        }
        this.level.setBlock(block, this, true, true);
        var nbt = BlockEntity.getDefaultCompound(this, BlockEntity.PISTON_ARM)
                .putInt("facing", this.getBlockFace().getIndex())
                .putBoolean("Sticky", this.sticky)
                .putBoolean("powered", isGettingPower());
        var piston = (BlockEntityPistonArm) BlockEntity.createBlockEntity(BlockEntity.PISTON_ARM, this.level.getChunk(getChunkX(), getChunkZ()), nbt);
        piston.powered = isGettingPower();
        this.checkState(piston.powered);
        return true;
    }

    @Override
    public boolean onBreak(Item item) {
        this.level.setBlock(this, Block.get(BlockID.AIR), true, true);
        var block = this.getSide(getBlockFace());
        if (block instanceof BlockPistonHead b && b.getBlockFace() == this.getBlockFace())
            block.onBreak(item);
        return true;
    }

    public boolean isExtended() {
        var face = getBlockFace();
        var block = getSide(face);
        return block instanceof BlockPistonHead b && b.getBlockFace() == face;
    }

    @Override
    @PowerNukkitDifference(info = "Using new method for checking if powered + update all around redstone torches, " +
            "even if the piston can't move.", since = "1.4.0.0-PN")
    public int onUpdate(int type) {
        if (type == Level.BLOCK_UPDATE_REDSTONE || type == Level.BLOCK_UPDATE_MOVED) {
            if (!this.level.getServer().isRedstoneEnabled())
                return 0;
            level.scheduleUpdate(this, 1);
            return type;
        }
        if (type == Level.BLOCK_UPDATE_NORMAL || type == Level.BLOCK_UPDATE_SCHEDULED) {
            if (!this.level.getServer().isRedstoneEnabled())
                return 0;
            // We can't use getOrCreateBlockEntity(), because the update method is called on block place,
            // before the "real" BlockEntity is set. That means, if we'd use the other method here,
            // it would create two BlockEntities.
            var arm = this.getBlockEntity();
            if (arm == null) return 0;
            boolean powered = this.isGettingPower();
            this.updateAroundRedstoneTorches(powered);
            if (arm.state % 2 == 0 && arm.powered != powered && checkState(powered)) {
                arm.powered = powered;
                if (arm.chunk != null)
                    arm.chunk.setChanged();
                if (powered && !isExtended())
                    //推出未成功,下一个计划刻再次自检
                    //TODO: 这里可以记录阻挡的方块并在阻挡因素移除后同步更新到活塞,而不是使用计划刻
                    level.scheduleUpdate(this, 1);
                return type;
            }
            //上一次推出未成功
            if (type == Level.BLOCK_UPDATE_SCHEDULED && powered && !isExtended() && !checkState(true))
                //依然不成功,下一个计划刻继续自检
                level.scheduleUpdate(this, 1);
            return type;
        }
        return 0;
    }

    @PowerNukkitOnly
    @Since("1.4.0.0-PN")
    @Override
    public boolean isGettingPower() {
        var face = getBlockFace();
        for (var side : BlockFace.values()) {
            if (side == face)
                continue;
            var b = this.getSide(side);
            if (b.getId() == Block.REDSTONE_WIRE && b.getDamage() > 0)
                return true;
            if (this.level.isSidePowered(b, side))
                return true;
        }
        return false;
    }

    protected void updateAroundRedstoneTorches(boolean powered) {
        for (BlockFace side : BlockFace.values()) {
            if ((getSide(side) instanceof BlockRedstoneTorch && powered)
                    || (getSide(side) instanceof BlockRedstoneTorchUnlit && !powered)) {
                BlockTorch torch = (BlockTorch) getSide(side);

                BlockTorch.TorchAttachment torchAttachment = torch.getTorchAttachment();
                Block support = torch.getSide(torchAttachment.getAttachedFace());

                if (support.getLocation().equals(this.getLocation())) {
                    torch.onUpdate(Level.BLOCK_UPDATE_REDSTONE);
                }
            }
        }
    }

    protected boolean checkState(Boolean isPowered) {
        if (!this.level.getServer().isRedstoneEnabled())
            return false;
        if (isPowered == null)
            isPowered = this.isGettingPower();
        var face = getBlockFace();
        var block = getSide(face);
        boolean isExtended;
        if (block instanceof BlockPistonHead b) {
            if (b.getBlockFace() != face)
                return false;
            isExtended = true;
        } else isExtended = false;
        if (isPowered && !isExtended) {
            if (!this.doMove(true))
                return false;
            this.getLevel().addSound(this, Sound.TILE_PISTON_OUT);
            this.getLevel().getVibrationManager().callVibrationEvent(new VibrationEvent(this, this.add(0.5, 0.5, 0.5), VibrationType.PISTON_EXTEND));
            return true;
        } else if (!isPowered && isExtended) {
            if (!this.doMove(false))
                return false;
            this.getLevel().addSound(this, Sound.TILE_PISTON_IN);
            this.getLevel().getVibrationManager().callVibrationEvent(new VibrationEvent(this, this.add(0.5, 0.5, 0.5), VibrationType.PISTON_CONTRACT));
            return true;
        }
        return false;
    }

    protected boolean doMove(boolean extending) {
        var pistonFace = getBlockFace();
        var calculator = new BlocksCalculator(extending);
        boolean canMove = calculator.canMove();
        if (!canMove && extending)
            return false;
        List toMoveBlockVec = new ArrayList<>();
        var event = new BlockPistonEvent(this, pistonFace, calculator.getBlocksToMove(), calculator.getBlocksToDestroy(), extending);
        this.level.getServer().getPluginManager().callEvent(event);
        if (event.isCancelled())
            return false;
        var oldPosList = new ArrayList();
        var blockEntityHolderList = new ArrayList>();
        var nbtList = new ArrayList();
        if (canMove && (this.sticky || extending)) {
            var destroyBlocks = calculator.getBlocksToDestroy();
            //破坏需要破坏的方块
            for (int i = destroyBlocks.size() - 1; i >= 0; --i) {
                var block = destroyBlocks.get(i);
                //清除位置上所含的水等
                level.setBlock(block, 1, Block.get(BlockID.AIR), true, false);
                this.level.useBreakOn(block);
            }
            var blocksToMove = calculator.getBlocksToMove();
            toMoveBlockVec = blocksToMove.stream().map(Vector3::asBlockVector3).collect(Collectors.toList());
            var moveDirection = extending ? pistonFace : pistonFace.getOpposite();
            for (Block blockToMove : blocksToMove) {
                var oldPos = new Vector3(blockToMove.x, blockToMove.y, blockToMove.z);
                var newPos = blockToMove.getSidePos(moveDirection);
                //清除位置上所含的水等
                level.setBlock(newPos, 1, Block.get(AIR), true, false);
                //TODO: 使用Block-State Tag而不是id-meta
                //2023/2/8: NBTIO.getBlockHelper()有性能问题,先不换用
                CompoundTag movingBlockTag = /*NBTIO.putBlockHelper(blockToMove);*/new CompoundTag()
                        .putInt("id", blockToMove.getId()) //only for nukkit purpose
                        .putInt("meta", blockToMove.getDamage()) //only for nukkit purpose
                        .putString("name", BlockStateRegistry.getPersistenceName(blockToMove.getId()))
                        .putShort("val", blockToMove.getDamage());
                CompoundTag nbt = BlockEntity.getDefaultCompound(newPos, BlockEntity.MOVING_BLOCK)
                        .putBoolean("expanding", extending)
                        .putInt("pistonPosX", this.getFloorX())
                        .putInt("pistonPosY", this.getFloorY())
                        .putInt("pistonPosZ", this.getFloorZ())
                        .putCompound("movingBlock", movingBlockTag);
                var blockEntity = this.level.getBlockEntity(oldPos);
                //移动方块实体
                if (blockEntity != null && !(blockEntity instanceof BlockEntityMovingBlock)) {
                    blockEntity.saveNBT();
                    nbt.putCompound("movingEntity", new CompoundTag(blockEntity.namedTag.getTags()));
                    if (blockEntity instanceof InventoryHolder)
                        ((InventoryHolder) blockEntity).getInventory().clearAll();
                    blockEntity.close();
                }
                oldPosList.add(oldPos);
                blockEntityHolderList.add((BlockEntityHolder) BlockState.of(BlockID.MOVING_BLOCK).getBlock(Position.fromObject(newPos, this.level)));
                nbtList.add(nbt);
            }
        }
        this.getBlockEntity().preMove(extending, toMoveBlockVec);
        //生成moving_block
        if (!oldPosList.isEmpty()) {
            for (int i = 0; i < oldPosList.size(); i++) {
                var oldPos = oldPosList.get(i);
                var blockEntityHolder = blockEntityHolderList.get(i);
                var nbt = nbtList.get(i);
                BlockEntityHolder.setBlockAndCreateEntity(blockEntityHolder, true, true, nbt);
                if (this.level.getBlock(oldPos).getId() != BlockID.MOVING_BLOCK)
                    this.level.setBlock(oldPos, Block.get(BlockID.AIR));
            }
        }
        //创建活塞臂方块
        if (extending) {
            var pistonArmPos = this.getSide(pistonFace);
            //清除位置上所含的水等
            level.setBlock(pistonArmPos, 1, Block.get(AIR), true, false);
            this.level.setBlock(pistonArmPos, createHead(this.getDamage()));
        }
        //开始移动
        this.getBlockEntity().move();
        return true;
    }

    @PowerNukkitOnly
    protected BlockPistonHead createHead(int damage) {
        return (BlockPistonHead) Block.get(getPistonHeadBlockId(), damage);
    }

    @PowerNukkitOnly
    public abstract int getPistonHeadBlockId();

    @Override
    public BlockFace getBlockFace() {
        var face = BlockFace.fromIndex(this.getDamage());
        return face.getHorizontalIndex() >= 0 ? face.getOpposite() : face;
    }

    @Override
    public boolean isSolid() {
        return false;
    }

    public class BlocksCalculator {

        private static int MOVE_BLOCK_LIMIT = 12;

        public static int getMoveBlockLimit() {
            return MOVE_BLOCK_LIMIT;
        }

        public static void setMoveBlockLimit(int moveBlockLimit) {
            if (moveBlockLimit < 0)
                throw new IllegalArgumentException("The move block limit must be greater than or equal to 0");
            MOVE_BLOCK_LIMIT = moveBlockLimit;
        }

        private final Vector3 pistonPos;
        private final Block blockToMove;
        private final BlockFace moveDirection;
        private final boolean extending;
        private final List toMove = new ArrayList<>() {
            @Override
            public int indexOf(Object o) {
                if (o == null) {
                    for (int i = 0; i < size(); i++)
                        if (get(i) == null)
                            return i;
                } else {
                    for (int i = 0; i < size(); i++) {
                        if (o.equals(get(i)))
                            return i;
                    }
                }
                return -1;
            }

            @Override //以防万一
            public boolean contains(Object o) {
                return indexOf(o) >= 0;
            }
        };
        private final List toDestroy = new ArrayList<>();
        private Vector3 armPos;

        public BlocksCalculator(boolean extending) {
            this.pistonPos = getLocation();
            this.extending = extending;

            BlockFace face = getBlockFace();
            if (!extending) {
                this.armPos = pistonPos.getSideVec(face);
            }

            if (extending) {
                this.moveDirection = face;
                this.blockToMove = getSide(face);
            } else {
                this.moveDirection = face.getOpposite();
                if (sticky) {
                    this.blockToMove = getSide(face, 2);
                } else {
                    this.blockToMove = null;
                }
            }
        }

        public boolean canMove() {
            if (!sticky && !extending)
                return true;
            this.toMove.clear();
            this.toDestroy.clear();
            var block = this.blockToMove;
            if (!canPush(block, this.moveDirection, true, extending))
                return false;
            if (block.breaksWhenMoved()) {
                if (extending || block.sticksToPiston())
                    this.toDestroy.add(this.blockToMove);
                return true;
            }
            if (!this.addBlockLine(this.blockToMove, this.blockToMove.getSide(this.moveDirection.getOpposite()), true))
                return false;
            for (int i = 0; i < this.toMove.size(); ++i) {
                var b = this.toMove.get(i);
                if (b.canSticksBlock() && !this.addBranchingBlocks(b)) {
                    return false;
                }
            }
            return true;
        }

        protected boolean addBlockLine(Block origin, Block from, boolean mainBlockLine) {
            var block = origin.clone();
            if (block.getId() == AIR)
                return true;
            if (!mainBlockLine && block.canSticksBlock() && from.canSticksBlock() && block.getId() != from.getId())
                return true;
            if (!canPush(origin, this.moveDirection, false, extending))
                return true;
            if (origin.equals(this.pistonPos))
                return true;
            if (this.toMove.contains(origin))
                return true;
            if (this.toMove.size() >= MOVE_BLOCK_LIMIT)
                return false;
            this.toMove.add(block);
            var count = 1;
            var beStuck = new ArrayList();
            while (block.canSticksBlock()) {
                var oldBlock = block.clone();
                block = origin.getSide(this.moveDirection.getOpposite(), count);
                if ((!extending || !mainBlockLine) && block.canSticksBlock() && oldBlock.canSticksBlock() && block.getId() != oldBlock.getId())
                    break;
                if (block.getId() == AIR || !canPush(block, this.moveDirection, false, extending) || block.equals(this.pistonPos))
                    break;
                if (block.breaksWhenMoved() && block.sticksToPiston()) {
                    this.toDestroy.add(block);
                    break;
                }
                if (count + this.toMove.size() > MOVE_BLOCK_LIMIT)
                    return false;
                count++;
                beStuck.add(block);
            }
            int beStuckCount = beStuck.size();
            if (beStuckCount > 0)
                this.toMove.addAll(Lists.reverse(beStuck));
            int step = 1;
            while (true) {
                var nextBlock = origin.getSide(this.moveDirection, step);
                int index = this.toMove.indexOf(nextBlock);
                if (index > -1) {
                    this.reorderListAtCollision(beStuckCount, index);
                    for (int i = 0; i <= index + beStuckCount; ++i) {
                        var b = this.toMove.get(i);
                        if ((b.canSticksBlock()) && !this.addBranchingBlocks(b))
                            return false;
                    }
                    return true;
                }
                if (nextBlock.getId() == AIR || nextBlock.equals(armPos))
                    return true;
                if (!canPush(nextBlock, this.moveDirection, true, extending) || nextBlock.equals(this.pistonPos))
                    return false;
                if (nextBlock.breaksWhenMoved()) {
                    this.toDestroy.add(nextBlock);
                    return true;
                }
                if (this.toMove.size() >= MOVE_BLOCK_LIMIT)
                    return false;
                this.toMove.add(nextBlock);
                ++beStuckCount;
                ++step;
            }
        }

        private void reorderListAtCollision(int count, int index) {
            var list = new ArrayList<>(this.toMove.subList(0, index));
            var list1 = new ArrayList<>(this.toMove.subList(this.toMove.size() - count, this.toMove.size()));
            var list2 = new ArrayList<>(this.toMove.subList(index, this.toMove.size() - count));
            this.toMove.clear();
            this.toMove.addAll(list);
            this.toMove.addAll(list1);
            this.toMove.addAll(list2);
        }

        protected boolean addBranchingBlocks(Block block) {
            for (BlockFace face : BlockFace.values()) {
                if (face.getAxis() != this.moveDirection.getAxis() && !this.addBlockLine(block.getSide(face), block, false))
                    return false;
            }
            return true;
        }

        public List getBlocksToMove() {
            return this.toMove;
        }

        public List getBlocksToDestroy() {
            return this.toDestroy;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy