/* * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @author GeyserMC * @link https://github.com/GeyserMC/Geyser */ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.mc.protocol.data.game.world.block.value.PistonValueType; import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; import org.geysermc.common.PlatformType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.cache.PistonCache; import org.geysermc.connector.network.translators.collision.BoundingBox; import org.geysermc.connector.network.translators.collision.CollisionManager; import org.geysermc.connector.network.translators.collision.translators.BlockCollision; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import org.geysermc.connector.registry.BlockRegistries; import org.geysermc.connector.registry.Registries; import org.geysermc.connector.registry.type.BlockMapping; import org.geysermc.connector.utils.*; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.Set; public class PistonBlockEntity { private final GeyserSession session; @Getter private final Vector3i position; private final Direction orientation; private final boolean sticky; @Getter private PistonValueType action; /** * A map of attached block positions to Java ids. */ private final Object2IntMap attachedBlocks = new Object2IntOpenHashMap<>(); /** * A flattened array of the positions of attached blocks, stored in XYZ order. */ private int[] flattenedAttachedBlocks = new int[0]; private boolean placedFinalBlocks = true; /** * The position of the piston head */ private float progress; private float lastProgress; private long timeSinceCompletion = 0; private static final BoundingBox SOLID_BOUNDING_BOX = new BoundingBox(0.5, 0.5, 0.5, 1, 1, 1); private static final BoundingBox HONEY_BOUNDING_BOX; /** * The number of ticks to wait after a piston finishes its movement before * it can be removed */ private static final int REMOVAL_DELAY = 5; static { // Create a ~1 x ~0.5 x ~1 bounding box above the honey block BlockCollision blockCollision = Registries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID); if (blockCollision == null) { throw new RuntimeException("Failed to find honey block collision"); } BoundingBox blockBoundingBox = blockCollision.getBoundingBoxes()[0]; double honeyHeight = blockBoundingBox.getMax().getY(); double boundingBoxHeight = 1.5 - honeyHeight; HONEY_BOUNDING_BOX = new BoundingBox(0.5, honeyHeight + boundingBoxHeight / 2, 0.5, blockBoundingBox.getSizeX(), boundingBoxHeight, blockBoundingBox.getSizeZ()); } public PistonBlockEntity(GeyserSession session, Vector3i position, Direction orientation, boolean sticky, boolean extended) { this.session = session; this.position = position; this.orientation = orientation; this.sticky = sticky; if (extended) { // Fully extended this.action = PistonValueType.PUSHING; this.progress = 1.0f; } else { // Fully retracted this.action = PistonValueType.PULLING; this.progress = 0.0f; } this.lastProgress = this.progress; } /** * Set whether the piston is pulling or pushing blocks * * @param action PULLING or PUSHING or CANCELED_MID_PUSH */ public void setAction(PistonValueType action) { if (this.action == action) { return; } placeFinalBlocks(); removeMovingBlocks(); this.action = action; if (action == PistonValueType.PUSHING || (action == PistonValueType.PULLING && sticky)) { // Blocks only move when pushing or pulling with sticky pistons findAffectedBlocks(); removeBlocks(); createMovingBlocks(); } else { removePistonHead(); } placedFinalBlocks = false; // Set progress and lastProgress to allow 0 tick pistons to animate switch (action) { case PUSHING -> progress = 0; case PULLING, CANCELLED_MID_PUSH -> progress = 1; } lastProgress = progress; BlockEntityUtils.updateBlockEntity(session, buildPistonTag(), position); } public void setAction(PistonValueType action, Object2IntMap attachedBlocks) { // Don't check if this.action == action, since on some Paper versions BlockPistonRetractEvent is called multiple times // with the first 1-2 events being empty. placeFinalBlocks(); removeMovingBlocks(); this.action = action; if (action == PistonValueType.PUSHING || (action == PistonValueType.PULLING && sticky)) { // Blocks only move when pushing or pulling with sticky pistons if (attachedBlocks.size() <= 12) { this.attachedBlocks.putAll(attachedBlocks); flattenPositions(); } removeBlocks(); createMovingBlocks(); } else { removePistonHead(); } placedFinalBlocks = false; // Set progress and lastProgress to allow 0 tick pistons to animate switch (action) { case PUSHING -> progress = 0; case PULLING, CANCELLED_MID_PUSH -> progress = 1; } lastProgress = progress; BlockEntityUtils.updateBlockEntity(session, buildPistonTag(), position); } /** * Update the position of the piston head, moving blocks, and players. */ public void updateMovement() { if (isDone()) { timeSinceCompletion++; return; } else { timeSinceCompletion = 0; } updateProgress(); pushPlayer(); BlockEntityUtils.updateBlockEntity(session, buildPistonTag(), position); } /** * Place attached blocks in their final position when done pushing or pulling */ public void updateBlocks() { if (isDone()) { // Update blocks only once if (timeSinceCompletion == 0) { placeFinalBlocks(); } // Give a few ticks for player collisions to be fully resolved if (timeSinceCompletion >= REMOVAL_DELAY) { removeMovingBlocks(); } } } private void removePistonHead() { Vector3i blockInFront = position.add(orientation.getUnitVector()); int blockId = session.getConnector().getWorldManager().getBlockAt(session, blockInFront); if (BlockStateValues.isPistonHead(blockId)) { ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockInFront); } else if (session.getConnector().getPlatformType() == PlatformType.SPIGOT && blockId == BlockStateValues.JAVA_AIR_ID) { // Spigot removes the piston head from the cache, but we need to send the block update ourselves ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockInFront); } } /** * Find the blocks that will be pushed or pulled by the piston */ private void findAffectedBlocks() { Set blocksChecked = new ObjectOpenHashSet<>(); Queue blocksToCheck = new LinkedList<>(); Vector3i directionOffset = orientation.getUnitVector(); Vector3i movement = getMovement(); blocksChecked.add(position); // Don't check the piston itself if (action == PistonValueType.PULLING) { blocksChecked.add(getPistonHeadPos()); // Don't check the piston head blocksToCheck.add(position.add(directionOffset.mul(2))); } else if (action == PistonValueType.PUSHING) { removePistonHead(); // Remove lingering piston heads blocksToCheck.add(position.add(directionOffset)); } boolean moveBlocks = true; while (!blocksToCheck.isEmpty() && attachedBlocks.size() <= 12) { Vector3i blockPos = blocksToCheck.remove(); // Skip blocks we've already checked if (!blocksChecked.add(blockPos)) { continue; } int blockId = session.getConnector().getWorldManager().getBlockAt(session, blockPos); if (blockId == BlockStateValues.JAVA_AIR_ID) { continue; } if (canMoveBlock(blockId, action == PistonValueType.PUSHING)) { attachedBlocks.put(blockPos, blockId); if (BlockStateValues.isBlockSticky(blockId)) { // For honey blocks and slime blocks check the blocks adjacent to it for (Direction direction : Direction.VALUES) { Vector3i offset = direction.getUnitVector(); // Only check blocks that aren't being pushed by the current block if (offset.equals(movement)) { continue; } Vector3i adjacentPos = blockPos.add(offset); // Ignore the piston block itself if (adjacentPos.equals(position)) { continue; } // Ignore the piston head if (action == PistonValueType.PULLING && position.add(directionOffset).equals(adjacentPos)) { continue; } int adjacentBlockId = session.getConnector().getWorldManager().getBlockAt(session, adjacentPos); if (adjacentBlockId != BlockStateValues.JAVA_AIR_ID && BlockStateValues.isBlockAttached(blockId, adjacentBlockId) && canMoveBlock(adjacentBlockId, false)) { // If it is another slime/honey block we need to check its adjacent blocks if (BlockStateValues.isBlockSticky(adjacentBlockId)) { blocksToCheck.add(adjacentPos); } else { attachedBlocks.put(adjacentPos, adjacentBlockId); blocksChecked.add(adjacentPos); blocksToCheck.add(adjacentPos.add(movement)); } } } } // Check next block in line blocksToCheck.add(blockPos.add(movement)); } else if (!BlockStateValues.canPistonDestroyBlock(blockId)) { // Block can't be moved or destroyed, so it blocks all block movement moveBlocks = false; break; } } if (!moveBlocks || attachedBlocks.size() > 12) { attachedBlocks.clear(); } else { flattenPositions(); } } private boolean canMoveBlock(int javaId, boolean isPushing) { if (javaId == BlockStateValues.JAVA_AIR_ID) { return true; } // Pistons can only be moved if they aren't extended if (PistonBlockEntityTranslator.isBlock(javaId)) { return !BlockStateValues.getPistonValues().get(javaId); } BlockMapping block = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaId, BlockMapping.AIR); // Bedrock, End portal frames, etc. can't be moved if (block.getHardness() == -1.0d) { return false; } return switch (block.getPistonBehavior()) { case BLOCK, DESTROY -> false; case PUSH_ONLY -> isPushing; // Glazed terracotta can only be pushed default -> !block.isBlockEntity(); // Pistons can't move block entities }; } /** * Get the unit vector for the direction of movement * * @return The movement of the blocks */ private Vector3i getMovement() { if (action == PistonValueType.PULLING) { return orientation.reversed().getUnitVector(); } return orientation.getUnitVector(); // PUSHING and CANCELLED_MID_PUSH } /** * Replace all attached blocks with air */ private void removeBlocks() { for (Vector3i blockPos : attachedBlocks.keySet()) { ChunkUtils.updateBlock(session, BlockStateValues.JAVA_AIR_ID, blockPos); } if (action != PistonValueType.PUSHING) { removePistonHead(); } } /** * Push the player * If the player is pushed, the displacement is added to playerDisplacement in PistonCache * If the player contacts a slime block, playerMotion in PistonCache is updated */ public void pushPlayer() { Vector3i direction = orientation.getUnitVector(); double blockMovement = lastProgress; if (action == PistonValueType.PULLING || action == PistonValueType.CANCELLED_MID_PUSH) { blockMovement = 1f - lastProgress; } BoundingBox playerBoundingBox = session.getCollisionManager().getPlayerBoundingBox(); // Shrink the collision in the other axes slightly, to avoid false positives when pressed up against the side of blocks Vector3d shrink = Vector3i.ONE.sub(direction.abs()).toDouble().mul(CollisionManager.COLLISION_TOLERANCE * 2); playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() - shrink.getX()); playerBoundingBox.setSizeY(playerBoundingBox.getSizeY() - shrink.getY()); playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() - shrink.getZ()); // Resolve collision with the piston head int pistonHeadId = BlockStateValues.getPistonHead(orientation); pushPlayerBlock(pistonHeadId, getPistonHeadPos().toDouble(), blockMovement, playerBoundingBox); // Resolve collision with any attached moving blocks, but skip slime blocks // This prevents players from being launched by slime blocks covered by other blocks for (Object2IntMap.Entry entry : attachedBlocks.object2IntEntrySet()) { int blockId = entry.getIntValue(); if (blockId != BlockStateValues.JAVA_SLIME_BLOCK_ID) { Vector3d blockPos = entry.getKey().toDouble(); pushPlayerBlock(blockId, blockPos, blockMovement, playerBoundingBox); } } // Resolve collision with slime blocks for (Object2IntMap.Entry entry : attachedBlocks.object2IntEntrySet()) { int blockId = entry.getIntValue(); if (blockId == BlockStateValues.JAVA_SLIME_BLOCK_ID) { Vector3d blockPos = entry.getKey().toDouble(); pushPlayerBlock(blockId, blockPos, blockMovement, playerBoundingBox); } } // Undo shrink playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() + shrink.getX()); playerBoundingBox.setSizeY(playerBoundingBox.getSizeY() + shrink.getY()); playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() + shrink.getZ()); } /** * Checks if a player is attached to the top of a honey block * * @param blockPos The position of the honey block * @param playerBoundingBox The player's bounding box * @return True if the player attached, otherwise false */ private boolean isPlayerAttached(Vector3d blockPos, BoundingBox playerBoundingBox) { if (orientation.isVertical()) { return false; } return session.getPlayerEntity().isOnGround() && HONEY_BOUNDING_BOX.checkIntersection(blockPos, playerBoundingBox); } /** * Launches a player if the player is on the pushing side of the slime block * * @param blockPos The position of the slime block * @param playerPos The player's position */ private void applySlimeBlockMotion(Vector3d blockPos, Vector3d playerPos) { Direction movementDirection = orientation; // Invert direction when pulling if (action == PistonValueType.PULLING) { movementDirection = movementDirection.reversed(); } Vector3f movement = getMovement().toFloat(); Vector3f motion = session.getPistonCache().getPlayerMotion(); double motionX = motion.getX(); double motionY = motion.getY(); double motionZ = motion.getZ(); blockPos = blockPos.add(0.5, 0.5, 0.5); // Move to the center of the slime block switch (movementDirection) { case DOWN: if (playerPos.getY() < blockPos.getY()) { motionY = movement.getY(); } break; case UP: if (playerPos.getY() > blockPos.getY()) { motionY = movement.getY(); } break; case NORTH: if (playerPos.getZ() < blockPos.getZ()) { motionZ = movement.getZ(); } break; case SOUTH: if (playerPos.getZ() > blockPos.getZ()) { motionZ = movement.getZ(); } break; case WEST: if (playerPos.getX() < blockPos.getX()) { motionX = movement.getX(); } break; case EAST: if (playerPos.getX() > blockPos.getX()) { motionX = movement.getX(); } break; } session.getPistonCache().setPlayerMotion(Vector3f.from(motionX, motionY, motionZ)); } private double getBlockIntersection(BlockCollision blockCollision, Vector3d blockPos, Vector3d extend, BoundingBox boundingBox, Direction direction) { Direction oppositeDirection = direction.reversed(); double maxIntersection = 0; for (BoundingBox b : blockCollision.getBoundingBoxes()) { b = b.clone(); b.extend(extend); b.translate(blockPos.getX(), blockPos.getY(), blockPos.getZ()); if (b.checkIntersection(Vector3d.ZERO, boundingBox)) { double intersection = boundingBox.getIntersectionSize(b, direction); double oppositeIntersection = boundingBox.getIntersectionSize(b, oppositeDirection); if (intersection < oppositeIntersection) { maxIntersection = Math.max(intersection, maxIntersection); } } } return maxIntersection; } private void pushPlayerBlock(int javaId, Vector3d startingPos, double blockMovement, BoundingBox playerBoundingBox) { PistonCache pistonCache = session.getPistonCache(); Vector3d movement = getMovement().toDouble(); // Check if the player collides with the movingBlock block entity Vector3d finalBlockPos = startingPos.add(movement); if (SOLID_BOUNDING_BOX.checkIntersection(finalBlockPos, playerBoundingBox)) { pistonCache.setPlayerCollided(true); if (javaId == BlockStateValues.JAVA_SLIME_BLOCK_ID) { pistonCache.setPlayerSlimeCollision(true); applySlimeBlockMotion(finalBlockPos, Vector3d.from(playerBoundingBox.getMiddleX(), playerBoundingBox.getMiddleY(), playerBoundingBox.getMiddleZ())); } } Vector3d blockPos = startingPos.add(movement.mul(blockMovement)); if (javaId == BlockStateValues.JAVA_HONEY_BLOCK_ID && isPlayerAttached(blockPos, playerBoundingBox)) { pistonCache.setPlayerCollided(true); pistonCache.setPlayerAttachedToHoney(true); double delta = Math.abs(progress - lastProgress); pistonCache.displacePlayer(movement.mul(delta)); } else { // Move the player out of collision BlockCollision blockCollision = Registries.COLLISIONS.get(javaId); if (blockCollision != null) { Vector3d extend = movement.mul(Math.min(1 - blockMovement, 0.5)); Direction movementDirection = orientation; if (action == PistonValueType.PULLING) { movementDirection = orientation.reversed(); } double intersection = getBlockIntersection(blockCollision, blockPos, extend, playerBoundingBox, movementDirection); if (intersection > 0) { pistonCache.setPlayerCollided(true); pistonCache.displacePlayer(movement.mul(intersection + 0.01d)); if (javaId == BlockStateValues.JAVA_SLIME_BLOCK_ID) { pistonCache.setPlayerSlimeCollision(true); applySlimeBlockMotion(blockPos, Vector3d.from(playerBoundingBox.getMiddleX(), playerBoundingBox.getMiddleY(), playerBoundingBox.getMiddleZ())); } } } } } private BlockCollision getCollision(Vector3i blockPos) { int blockId = getAttachedBlockId(blockPos); if (blockId != BlockStateValues.JAVA_AIR_ID) { double movementProgress = progress; if (action == PistonValueType.PULLING || action == PistonValueType.CANCELLED_MID_PUSH) { movementProgress = 1f - progress; } Vector3d offset = getMovement().toDouble().mul(movementProgress); return BlockUtils.getCollision(blockId, blockPos, offset); } return null; } /** * Compute the maximum movement of a bounding box that won't collide with the moving block attached to this piston * * @param blockPos The position of the moving block * @param boundingBox The bounding box of the moving entity * @param axis The axis of movement * @param movement The movement in the axis * @return The adjusted movement */ public double computeCollisionOffset(Vector3i blockPos, BoundingBox boundingBox, Axis axis, double movement) { BlockCollision blockCollision = getCollision(blockPos); if (blockCollision != null) { double adjustedMovement = blockCollision.computeCollisionOffset(boundingBox, axis, movement); blockCollision.reset(); if (getAttachedBlockId(blockPos) == BlockStateValues.JAVA_SLIME_BLOCK_ID && adjustedMovement != movement) { session.getPistonCache().setPlayerSlimeCollision(true); } return adjustedMovement; } return movement; } public boolean checkCollision(Vector3i blockPos, BoundingBox boundingBox) { BlockCollision blockCollision = getCollision(blockPos); if (blockCollision != null) { boolean result = blockCollision.checkIntersection(boundingBox); blockCollision.reset(); return result; } return false; } private int getAttachedBlockId(Vector3i blockPos) { if (blockPos.equals(getPistonHeadPos())) { return BlockStateValues.getPistonHead(orientation); } else { return attachedBlocks.getOrDefault(blockPos, BlockStateValues.JAVA_AIR_ID); } } /** * Create moving block entities for each attached block */ private void createMovingBlocks() { // Map the final position of each block to this block entity Map movingBlockMap = session.getPistonCache().getMovingBlocksMap(); attachedBlocks.forEach((blockPos, javaId) -> movingBlockMap.put(blockPos, this)); movingBlockMap.put(getPistonHeadPos(), this); Vector3i movement = getMovement(); BoundingBox playerBoundingBox = session.getCollisionManager().getPlayerBoundingBox().clone(); if (orientation == Direction.UP) { // Extend the bounding box down, to catch collisions when the player is falling down playerBoundingBox.extend(0, -256, 0); playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() + 0.5); playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() + 0.5); } attachedBlocks.forEach((blockPos, javaId) -> { Vector3i newPos = blockPos.add(movement); if (SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), playerBoundingBox) || SOLID_BOUNDING_BOX.checkIntersection(newPos.toDouble(), playerBoundingBox)) { session.getPistonCache().setPlayerCollided(true); if (javaId == BlockStateValues.JAVA_SLIME_BLOCK_ID) { session.getPistonCache().setPlayerSlimeCollision(true); } // Don't place moving blocks that collide with the player // because of https://bugs.mojang.com/browse/MCPE-96035 return; } // Place a moving block at the new location of the block UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK); updateBlockPacket.setBlockPosition(newPos); updateBlockPacket.setRuntimeId(session.getBlockMappings().getBedrockMovingBlockId()); updateBlockPacket.setDataLayer(0); session.sendUpstreamPacket(updateBlockPacket); // Update moving block with correct details BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(newPos, javaId, position), newPos); }); } /** * Place blocks that don't collide with the player into their final position * otherwise the player will fall off the block. * The Java server will handle updating the blocks that do collide later. */ private void placeFinalBlocks() { // Prevent blocks from being placed multiple times since it is called in // setAction and updateBlocks if (placedFinalBlocks) { return; } placedFinalBlocks = true; Vector3i movement = getMovement(); attachedBlocks.forEach((blockPos, javaId) -> { blockPos = blockPos.add(movement); // Send a final block entity packet to detach blocks BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(blockPos, javaId, Direction.DOWN.getUnitVector()), blockPos); // Don't place blocks that collide with the player if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) { ChunkUtils.updateBlock(session, javaId, blockPos); } }); if (action == PistonValueType.PUSHING) { Vector3i pistonHeadPos = getPistonHeadPos().add(movement); if (!SOLID_BOUNDING_BOX.checkIntersection(pistonHeadPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) { ChunkUtils.updateBlock(session, BlockStateValues.getPistonHead(orientation), pistonHeadPos); } } } /** * Remove moving blocks from the piston cache */ private void removeMovingBlocks() { Map movingBlockMap = session.getPistonCache().getMovingBlocksMap(); attachedBlocks.forEach((blockPos, javaId) -> movingBlockMap.remove(blockPos)); attachedBlocks.clear(); movingBlockMap.remove(getPistonHeadPos()); flattenedAttachedBlocks = new int[0]; } /** * Flatten the positions of attached blocks into a 1D array */ private void flattenPositions() { flattenedAttachedBlocks = new int[3 * attachedBlocks.size()]; int i = 0; for (Vector3i position : attachedBlocks.keySet()) { flattenedAttachedBlocks[3 * i] = position.getX(); flattenedAttachedBlocks[3 * i + 1] = position.getY(); flattenedAttachedBlocks[3 * i + 2] = position.getZ(); i++; } } /** * Get the Bedrock state of the piston * * @return 0 - Fully retracted, 1 - Extending, 2 - Fully extended, 3 - Retracting */ private byte getState() { switch (action) { case PUSHING: return (byte) (isDone() ? 2 : 1); case PULLING: return (byte) (isDone() ? 0 : 3); default: if (progress == 1.0f) { return 2; } return (byte) (isDone() ? 0 : 2); } } /** * @return The starting position of the piston head */ private Vector3i getPistonHeadPos() { if (action == PistonValueType.PUSHING) { return position; } return position.add(orientation.getUnitVector()); } /** * Update the progress or position of the piston head */ private void updateProgress() { switch (action) { case PUSHING -> { lastProgress = progress; progress += 0.5f; if (progress >= 1.0f) { progress = 1.0f; } } case CANCELLED_MID_PUSH, PULLING -> { lastProgress = progress; progress -= 0.5f; if (progress <= 0.0f) { progress = 0.0f; } } } } /** * @return True if the piston has finished its movement, otherwise false */ public boolean isDone() { return switch (action) { case PUSHING -> progress == 1.0f && lastProgress == 1.0f; case PULLING, CANCELLED_MID_PUSH -> progress == 0.0f && lastProgress == 0.0f; }; } public boolean canBeRemoved() { return isDone() && timeSinceCompletion > REMOVAL_DELAY; } /** * Create a piston data tag with the data in this block entity * * @return A piston data tag */ private NbtMap buildPistonTag() { NbtMapBuilder builder = NbtMap.builder() .putString("id", "PistonArm") .putIntArray("AttachedBlocks", flattenedAttachedBlocks) .putFloat("Progress", progress) .putFloat("LastProgress", lastProgress) .putByte("NewState", getState()) .putByte("State", getState()) .putByte("Sticky", (byte) (sticky ? 1 : 0)) .putByte("isMovable", (byte) 0) .putInt("x", position.getX()) .putInt("y", position.getY()) .putInt("z", position.getZ()); return builder.build(); } /** * Create a piston data tag that has fully extended/retracted * * @param position The position for the base of the piston * @param extended Whether the piston is extended or retracted * @param sticky Whether the piston is a sticky piston or a regular piston * @return A piston data tag for a fully extended/retracted piston */ public static NbtMap buildStaticPistonTag(Vector3i position, boolean extended, boolean sticky) { NbtMapBuilder builder = NbtMap.builder() .putString("id", "PistonArm") .putFloat("Progress", extended ? 1.0f : 0.0f) .putFloat("LastProgress", extended ? 1.0f : 0.0f) .putByte("NewState", (byte) (extended ? 2 : 0)) .putByte("State", (byte) (extended ? 2 : 0)) .putByte("Sticky", (byte) (sticky ? 1 : 0)) .putByte("isMovable", (byte) 0) .putInt("x", position.getX()) .putInt("y", position.getY()) .putInt("z", position.getZ()); return builder.build(); } /** * Create a moving block tag of a block that will be moved by a piston * * @param position The ending position of the block (The location of the movingBlock block entity) * @param javaId The Java Id of the block that is moving * @param pistonPosition The position for the base of the piston that's moving the block * @return A moving block data tag */ private NbtMap buildMovingBlockTag(Vector3i position, int javaId, Vector3i pistonPosition) { // Get Bedrock block state data NbtMap movingBlock = session.getBlockMappings().getBedrockBlockStates().get(session.getBlockMappings().getBedrockBlockId(javaId)); NbtMapBuilder builder = NbtMap.builder() .putString("id", "MovingBlock") .putCompound("movingBlock", movingBlock) .putByte("isMovable", (byte) 1) .putInt("pistonPosX", pistonPosition.getX()) .putInt("pistonPosY", pistonPosition.getY()) .putInt("pistonPosZ", pistonPosition.getZ()) .putInt("x", position.getX()) .putInt("y", position.getY()) .putInt("z", position.getZ()); if (PistonBlockEntityTranslator.isBlock(javaId)) { builder.putCompound("movingEntity", PistonBlockEntityTranslator.getTag(javaId, position)); } return builder.build(); } }