diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java index d1364826a..4d19de0b3 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java @@ -48,6 +48,7 @@ import org.geysermc.geyser.adapters.spigot.SpigotAdapters; import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor; import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager; import org.geysermc.platform.spigot.command.SpigotCommandSender; +import org.geysermc.platform.spigot.world.GeyserPistonListener; import org.geysermc.platform.spigot.world.GeyserSpigot1_11CraftingListener; import org.geysermc.platform.spigot.world.GeyserSpigotBlockPlaceListener; import org.geysermc.platform.spigot.world.manager.*; @@ -227,6 +228,8 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(connector, this.geyserWorldManager); Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this); + Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(connector, this.geyserWorldManager), this); + if (isPre1_12) { // Register events needed to send all recipes to the client Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigot1_11CraftingListener(connector), this); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserPistonListener.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserPistonListener.java new file mode 100644 index 000000000..73d0300ff --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserPistonListener.java @@ -0,0 +1,132 @@ +/* + * 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.platform.spigot.world; + +import com.github.steveice10.mc.protocol.data.game.world.block.value.PistonValueType; +import com.nukkitx.math.vector.Vector3i; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPistonEvent; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.cache.PistonCache; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; +import org.geysermc.connector.network.translators.world.block.entity.PistonBlockEntity; +import org.geysermc.connector.utils.Direction; +import org.geysermc.platform.spigot.world.manager.GeyserSpigotWorldManager; + +import java.util.List; + +public class GeyserPistonListener implements Listener { + private final GeyserConnector connector; + private final GeyserSpigotWorldManager worldManager; + + public GeyserPistonListener(GeyserConnector connector, GeyserSpigotWorldManager worldManager) { + this.connector = connector; + this.worldManager = worldManager; + } + + // The handlers' parent class cannot be registered + @EventHandler(priority = EventPriority.MONITOR) + public void onPistonExtend(BlockPistonExtendEvent event) { + onPistonAction(event); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPistonRetract(BlockPistonRetractEvent event) { + onPistonAction(event); + } + + private void onPistonAction(BlockPistonEvent event) { + if (event.isCancelled()) { + return; + } + + World world = event.getBlock().getWorld(); + boolean isExtend = event instanceof BlockPistonExtendEvent; + + Location location = event.getBlock().getLocation(); + Vector3i position = getVector(location); + PistonValueType type = isExtend ? PistonValueType.PUSHING : PistonValueType.PULLING; + boolean sticky = event.isSticky(); + + Object2IntMap attachedBlocks = new Object2IntOpenHashMap<>(); + boolean blocksFilled = false; + + for (GeyserSession session : connector.getPlayers()) { + Player player = Bukkit.getPlayer(session.getPlayerEntity().getUuid()); + if (player == null || !player.getWorld().equals(world)) { + continue; + } + + int dX = Math.abs(location.getBlockX() - player.getLocation().getBlockX()) >> 4; + int dZ = Math.abs(location.getBlockZ() - player.getLocation().getBlockZ()) >> 4; + if ((dX * dX + dZ * dZ) > session.getRenderDistance() * session.getRenderDistance()) { + // Ignore pistons outside the player's render distance + continue; + } + + // Trying to grab the blocks from the world like other platforms would result in the moving piston block + // being returned instead. + if (!blocksFilled) { + // Blocks currently require a player for 1.12, so let's just leech off one player to get all blocks + // and call it a day for the rest of the sessions (mostly to save on execution time) + List blocks = isExtend ? ((BlockPistonExtendEvent) event).getBlocks() : ((BlockPistonRetractEvent) event).getBlocks(); + for (Block block : blocks) { + Location attachedLocation = block.getLocation(); + attachedBlocks.put(getVector(attachedLocation), worldManager.getBlockNetworkId(player, block, + attachedLocation.getBlockX(), attachedLocation.getBlockY(), attachedLocation.getBlockZ())); + } + blocksFilled = true; + } + + int pistonBlockId = worldManager.getBlockNetworkId(player, event.getBlock(), location.getBlockX(), location.getBlockY(), location.getBlockZ()); + // event.getDirection() is unreliable + Direction orientation = BlockStateValues.getPistonOrientation(pistonBlockId); + + session.executeInEventLoop(() -> { + PistonCache pistonCache = session.getPistonCache(); + PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> + new PistonBlockEntity(session, position, orientation, sticky, !isExtend)); + blockEntity.setAction(type, attachedBlocks); + }); + } + } + + private Vector3i getVector(Location location) { + return Vector3i.from(location.getX(), location.getY(), location.getZ()); + } +} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java index e32b52f24..6d856edff 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java @@ -66,7 +66,6 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager { } @Override - @SuppressWarnings("deprecation") public int getBlockAt(GeyserSession session, int x, int y, int z) { Player player = Bukkit.getPlayer(session.getPlayerEntity().getUsername()); if (player == null) { @@ -76,12 +75,19 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager { // Prevent nasty async errors if a player is loading in return BlockStateValues.JAVA_AIR_ID; } + + Block block = player.getWorld().getBlockAt(x, y, z); + return getBlockNetworkId(player, block, x, y, z); + } + + @Override + @SuppressWarnings("deprecation") + public int getBlockNetworkId(Player player, Block block, int x, int y, int z) { // Get block entity storage BlockStorage storage = Via.getManager().getConnectionManager().getConnectedClient(player.getUniqueId()).get(BlockStorage.class); - Block block = player.getWorld().getBlockAt(x, y, z); // Black magic that gets the old block state ID - int blockId = (block.getType().getId() << 4) | (block.getData() & 0xF); - return getLegacyBlock(storage, blockId, x, y, z); + int oldBlockId = (block.getType().getId() << 4) | (block.getData() & 0xF); + return getLegacyBlock(storage, oldBlockId, x, y, z); } /** diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java index be6342172..516cf13dc 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java @@ -77,7 +77,11 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager { return BlockStateValues.JAVA_AIR_ID; } - return BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(world.getBlockAt(x, y, z).getBlockData().getAsString(), BlockStateValues.JAVA_AIR_ID); + return getBlockNetworkId(bukkitPlayer, world.getBlockAt(x, y, z), x, y, z); + } + + public int getBlockNetworkId(Player player, Block block, int x, int y, int z) { + return BlockRegistries.JAVA_IDENTIFIERS.getOrDefault(block.getBlockData().getAsString(), BlockStateValues.JAVA_AIR_ID); } @Override diff --git a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java index e798ad468..91c3d401a 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java @@ -103,7 +103,7 @@ public class FishingHookEntity extends ThrowableEntity { // TODO Push bounding box out of collision to improve movement collided = true; } - blockCollision.setPosition(null); + blockCollision.reset(); } int waterLevel = BlockStateValues.getWaterLevel(blockID); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 9a9b06393..707fc9717 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -147,6 +147,7 @@ public class GeyserSession implements CommandSender { private EntityEffectCache effectCache; private final FormCache formCache; private final LodestoneCache lodestoneCache; + private final PistonCache pistonCache; private final PreferencesCache preferencesCache; private final TagCache tagCache; private WorldCache worldCache; @@ -446,6 +447,7 @@ public class GeyserSession implements CommandSender { this.effectCache = new EntityEffectCache(); this.formCache = new FormCache(this); this.lodestoneCache = new LodestoneCache(); + this.pistonCache = new PistonCache(this); this.preferencesCache = new PreferencesCache(this); this.tagCache = new TagCache(); this.worldCache = new WorldCache(this); @@ -912,6 +914,7 @@ public class GeyserSession implements CommandSender { */ protected void tick() { try { + pistonCache.tick(); // Check to see if the player's position needs updating - a position update should be sent once every 3 seconds if (spawned && (System.currentTimeMillis() - lastMovementTimestamp) > 3000) { // Recalculate in case something else changed position diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java new file mode 100644 index 000000000..aa929759c --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/PistonCache.java @@ -0,0 +1,187 @@ +/* + * 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.session.cache; + +import com.nukkitx.math.vector.Vector3d; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import org.geysermc.connector.entity.player.SessionPlayerEntity; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.collision.BoundingBox; +import org.geysermc.connector.network.translators.world.block.entity.PistonBlockEntity; +import org.geysermc.connector.utils.Axis; + +import java.util.Map; + +@Getter +public class PistonCache { + @Getter(AccessLevel.PRIVATE) + private final GeyserSession session; + + /** + * Maps the position of a piston to its block entity + */ + private final Map pistons = new Object2ObjectOpenHashMap<>(); + + /** + * Maps the position of a moving block to the piston moving it + * Positions in this map represent the starting position of the block + */ + private final Map movingBlocksMap = new Object2ObjectOpenHashMap<>(); + + private Vector3d playerDisplacement = Vector3d.ZERO; + + @Setter + private Vector3f playerMotion = Vector3f.ZERO; + + /** + * Stores whether a player has/will collide with any moving blocks. + */ + @Setter + private boolean playerCollided = false; + + /** + * Stores whether a player has/will collide with any slime blocks. + * This is used to prevent movement from being corrected when players + * are about to hit a slime block. + */ + @Setter + private boolean playerSlimeCollision = false; + + /** + * Stores whether a player is standing on a honey block. + * This is used to ignore movement from Bedrock to prevent them from + * falling off. + */ + @Setter + private boolean playerAttachedToHoney = false; + + public PistonCache(GeyserSession session) { + this.session = session; + } + + public void tick() { + resetPlayerMovement(); + if (!pistons.isEmpty()) { + pistons.values().forEach(PistonBlockEntity::updateMovement); + sendPlayerMovement(); + sendPlayerMotion(); + // Update blocks after movement, so that players don't get stuck inside blocks + pistons.values().forEach(PistonBlockEntity::updateBlocks); + + pistons.entrySet().removeIf((entry) -> entry.getValue().canBeRemoved()); + + if (pistons.isEmpty() && !movingBlocksMap.isEmpty()) { + session.getConnector().getLogger().error("The moving block map has de-synced!"); + for (Map.Entry entry : movingBlocksMap.entrySet()) { + session.getConnector().getLogger().error("Moving Block at " + entry.getKey() + " was previously owned by the piston at " + entry.getValue().getPosition()); + } + } + } + } + + private void resetPlayerMovement() { + playerDisplacement = Vector3d.ZERO; + playerMotion = Vector3f.ZERO; + playerCollided = false; + playerSlimeCollision = false; + playerAttachedToHoney = false; + } + + private void sendPlayerMovement() { + if (!playerDisplacement.equals(Vector3d.ZERO) && playerMotion.equals(Vector3f.ZERO)) { + SessionPlayerEntity playerEntity = session.getPlayerEntity(); + boolean isOnGround = playerDisplacement.getY() > 0 || playerEntity.isOnGround(); + Vector3d position = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter(); + playerEntity.moveAbsolute(session, position.toFloat(), playerEntity.getRotation(), isOnGround, true); + } + } + + private void sendPlayerMotion() { + if (!playerMotion.equals(Vector3f.ZERO)) { + SessionPlayerEntity playerEntity = session.getPlayerEntity(); + playerEntity.setMotion(playerMotion); + + SetEntityMotionPacket setEntityMotionPacket = new SetEntityMotionPacket(); + setEntityMotionPacket.setRuntimeEntityId(playerEntity.getGeyserId()); + setEntityMotionPacket.setMotion(playerMotion); + session.sendUpstreamPacket(setEntityMotionPacket); + } + } + + /** + * Add to the player's displacement and move the player's bounding box + * The total displacement is capped to a range of -0.51 to 0.51 per tick + * + * @param displacement The displacement to apply to the player's bounding box + */ + public void displacePlayer(Vector3d displacement) { + Vector3d totalDisplacement = playerDisplacement.add(displacement); + // Clamp to range -0.51 to 0.51 + totalDisplacement = totalDisplacement.max(-0.51d, -0.51d, -0.51d).min(0.51d, 0.51d, 0.51d); + + Vector3d delta = totalDisplacement.sub(playerDisplacement); + // Check if the piston is pushing a player into collision + delta = session.getCollisionManager().correctPlayerMovement(delta, true); + + session.getCollisionManager().getPlayerBoundingBox().translate(delta.getX(), delta.getY(), delta.getZ()); + + playerDisplacement = totalDisplacement; + } + + /** + * @param blockPos The block position to test + * @param boundingBox The bounding box that moves + * @param axis The axis to apply the offset + * @param offset The current maximum distance the bounding box can travel + * @return The new maximum distance the bounding box can travel without colliding with the tested moving block + */ + public double computeCollisionOffset(Vector3i blockPos, BoundingBox boundingBox, Axis axis, double offset) { + PistonBlockEntity piston = movingBlocksMap.get(blockPos); + if (piston != null) { + return piston.computeCollisionOffset(blockPos, boundingBox, axis, offset); + } + return offset; + } + + public boolean checkCollision(Vector3i blockPos, BoundingBox boundingBox) { + PistonBlockEntity piston = movingBlocksMap.get(blockPos); + if (piston != null) { + return piston.checkCollision(blockPos, boundingBox); + } + return false; + } + + public void clear() { + pistons.clear(); + movingBlocksMap.clear(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java index 6882e6e0c..8645d7a87 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -92,9 +92,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator 0) { + double min = axis.choose(getMin().add(boxOffset)); + double max = axis.choose(otherBoundingBox.getMax()); + if ((min - max) >= -2.0 * CollisionManager.COLLISION_TOLERANCE) { + offset = Math.min(min - max, offset); + } + } else if (offset < 0) { + double min = axis.choose(otherBoundingBox.getMin()); + double max = axis.choose(getMax().add(boxOffset)); + if ((min - max) >= -2.0 * CollisionManager.COLLISION_TOLERANCE) { + offset = Math.max(max - min, offset); + } + } + return offset; + } + + /** + * Get the distance required to move this bounding box to one of otherBoundingBox's sides + * + * @param otherBoundingBox The stationary bounding box + * @param side The side of otherBoundingBox to snap this bounding box to + * @return The distance to move in the direction of {@code side} + */ + public double getIntersectionSize(BoundingBox otherBoundingBox, Direction side) { + switch (side) { + case DOWN: + return getMax().getY() - otherBoundingBox.getMin().getY(); + case UP: + return otherBoundingBox.getMax().getY() - getMin().getY(); + case NORTH: + return getMax().getZ() - otherBoundingBox.getMin().getZ(); + case SOUTH: + return otherBoundingBox.getMax().getZ() - getMin().getZ(); + case WEST: + return getMax().getX() - otherBoundingBox.getMin().getX(); + case EAST: + return otherBoundingBox.getMax().getX() - getMin().getX(); + } + return 0; + } + + @SneakyThrows(CloneNotSupportedException.class) + @Override + public BoundingBox clone() { + BoundingBox clone = (BoundingBox) super.clone(); + clone.middleX = middleX; + clone.middleY = middleY; + clone.middleZ = middleZ; + + clone.sizeX = sizeX; + clone.sizeY = sizeY; + clone.sizeZ = sizeZ; + return clone; + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java index 52754f4a4..8ec42d4a9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java @@ -31,17 +31,19 @@ import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityFlags; -import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; -import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; +import com.nukkitx.protocol.bedrock.packet.*; import com.nukkitx.protocol.bedrock.v448.Bedrock_v448; import lombok.Getter; import lombok.Setter; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.cache.PistonCache; import org.geysermc.connector.network.translators.collision.translators.BlockCollision; +import org.geysermc.connector.network.translators.collision.translators.ScaffoldingCollision; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import org.geysermc.connector.utils.BlockUtils; +import org.geysermc.connector.utils.Axis; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -79,6 +81,14 @@ public class CollisionManager { */ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#####", new DecimalFormatSymbols(Locale.ENGLISH)); + private static final double PLAYER_STEP_UP = 0.6; + + /** + * The maximum squared distance between a Bedrock players' movement and our predicted movement before + * the player is teleported to the correct position + */ + private static final double INCORRECT_MOVEMENT_THRESHOLD = 0.08; + public CollisionManager(GeyserSession session) { this.session = session; this.playerBoundingBox = new BoundingBox(0, 0, 0, 0.6, 1.8, 0.6); @@ -119,13 +129,18 @@ public class CollisionManager { /** * Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between - * the two versions. + * the two versions. Will also send corrected movement packets back to Bedrock if they collide with pistons. * * @param bedrockPosition the current Bedrock position of the client * @param onGround whether the Bedrock player is on the ground * @return the position to send to the Java server, or null to cancel sending the packet */ public Vector3d adjustBedrockPosition(Vector3f bedrockPosition, boolean onGround) { + PistonCache pistonCache = session.getPistonCache(); + // Bedrock clients tend to fall off of honey blocks, so we need to teleport them to the new position + if (pistonCache.isPlayerAttachedToHoney()) { + return null; + } // We need to parse the float as a string since casting a float to a double causes us to // lose precision and thus, causes players to get stuck when walking near walls double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset(); @@ -133,18 +148,33 @@ public class CollisionManager { Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY, Double.parseDouble(Float.toString(bedrockPosition.getZ()))); - updatePlayerBoundingBox(position); - + Vector3d startingPos = playerBoundingBox.getBottomCenter(); + Vector3d movement = position.sub(startingPos); + Vector3d adjustedMovement = correctPlayerMovement(movement, false); + playerBoundingBox.translate(adjustedMovement.getX(), adjustedMovement.getY(), adjustedMovement.getZ()); + playerBoundingBox.translate(pistonCache.getPlayerMotion().getX(), pistonCache.getPlayerMotion().getY(), pistonCache.getPlayerMotion().getZ()); // Correct player position if (!correctPlayerPosition()) { // Cancel the movement if it needs to be cancelled recalculatePosition(); return null; } + // The server can't complain about our movement if we never send it + // TODO get rid of this and handle teleports smoothly + if (pistonCache.isPlayerCollided()) { + return null; + } - position = Vector3d.from(playerBoundingBox.getMiddleX(), - playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2), - playerBoundingBox.getMiddleZ()); + position = playerBoundingBox.getBottomCenter(); + + boolean newOnGround = adjustedMovement.getY() != movement.getY() && movement.getY() < 0 || onGround; + // Send corrected position to Bedrock if they differ by too much to prevent de-syncs + if (onGround != newOnGround || movement.distanceSquared(adjustedMovement) > INCORRECT_MOVEMENT_THRESHOLD) { + PlayerEntity playerEntity = session.getPlayerEntity(); + if (pistonCache.getPlayerMotion().equals(Vector3f.ZERO) && !pistonCache.isPlayerSlimeCollision()) { + playerEntity.moveAbsolute(session, position.toFloat(), playerEntity.getRotation(), newOnGround, true); + } + } if (!onGround) { // Trim the position to prevent rounding errors that make Java think we are clipping into a block @@ -178,17 +208,20 @@ public class CollisionManager { box.getMiddleY() - (box.getSizeY() / 2), box.getMiddleZ()); + // Expand volume by 1 in each direction to include moving blocks + double pistonExpand = session.getPistonCache().getPistons().isEmpty() ? 0 : 1; + // Loop through all blocks that could collide - int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE)); - int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE); + int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE + pistonExpand)); + int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE + pistonExpand); // Y extends 0.5 blocks down because of fence hitboxes - int minCollisionY = (int) Math.floor(position.getY() - 0.5); + int minCollisionY = (int) Math.floor(position.getY() - 0.5 - COLLISION_TOLERANCE - pistonExpand / 2.0); - int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY()); + int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY() + pistonExpand); - int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE)); - int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE); + int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE + pistonExpand)); + int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE + pistonExpand); for (int y = minCollisionY; y < maxCollisionY + 1; y++) { for (int x = minCollisionX; x < maxCollisionX + 1; x++) { @@ -223,7 +256,7 @@ public class CollisionManager { BlockCollision blockCollision = BlockUtils.getCollisionAt(session, blockPos); if (blockCollision != null) { blockCollision.beforeCorrectPosition(playerBoundingBox); - blockCollision.setPosition(null); + blockCollision.reset(); } } @@ -234,7 +267,7 @@ public class CollisionManager { if (!blockCollision.correctPosition(session, playerBoundingBox)) { return false; } - blockCollision.setPosition(null); + blockCollision.reset(); } } @@ -243,6 +276,106 @@ public class CollisionManager { return true; } + public Vector3d correctPlayerMovement(Vector3d movement, boolean checkWorld) { + if (!checkWorld && session.getPistonCache().getPistons().isEmpty()) { // There is nothing to check + return movement; + } + return correctMovement(movement, playerBoundingBox, session.getPlayerEntity().isOnGround(), PLAYER_STEP_UP, checkWorld); + } + + public Vector3d correctMovement(Vector3d movement, BoundingBox boundingBox, boolean onGround, double stepUp, boolean checkWorld) { + Vector3d adjustedMovement = movement; + if (!movement.equals(Vector3d.ZERO)) { + adjustedMovement = correctMovementForCollisions(movement, boundingBox, checkWorld); + } + + boolean verticalCollision = adjustedMovement.getY() != movement.getY(); + boolean horizontalCollision = adjustedMovement.getX() != movement.getX() || adjustedMovement.getZ() != movement.getZ(); + boolean falling = movement.getY() < 0; + onGround = onGround || (verticalCollision && falling); + if (onGround && horizontalCollision) { + Vector3d horizontalMovement = Vector3d.from(movement.getX(), 0, movement.getZ()); + Vector3d stepUpMovement = correctMovementForCollisions(horizontalMovement.up(stepUp), boundingBox, checkWorld); + + BoundingBox stretchedBoundingBox = boundingBox.clone(); + stretchedBoundingBox.extend(horizontalMovement); + double maxStepUp = correctMovementForCollisions(Vector3d.from(0, stepUp, 0), stretchedBoundingBox, checkWorld).getY(); + if (maxStepUp < stepUp) { // The player collided with a block above them + boundingBox.translate(0, maxStepUp, 0); + Vector3d adjustedStepUpMovement = correctMovementForCollisions(horizontalMovement, boundingBox, checkWorld); + boundingBox.translate(0, -maxStepUp, 0); + + if (squaredHorizontalLength(adjustedStepUpMovement) > squaredHorizontalLength(stepUpMovement)) { + stepUpMovement = adjustedStepUpMovement.up(maxStepUp); + } + } + + if (squaredHorizontalLength(stepUpMovement) > squaredHorizontalLength(adjustedMovement)) { + boundingBox.translate(stepUpMovement.getX(), stepUpMovement.getY(), stepUpMovement.getZ()); + // Apply the player's remaining vertical movement + double verticalMovement = correctMovementForCollisions(Vector3d.from(0, movement.getY() - stepUpMovement.getY(), 0), boundingBox, checkWorld).getY(); + boundingBox.translate(-stepUpMovement.getX(), -stepUpMovement.getY(), -stepUpMovement.getZ()); + + stepUpMovement = stepUpMovement.up(verticalMovement); + adjustedMovement = stepUpMovement; + } + } + return adjustedMovement; + } + + private double squaredHorizontalLength(Vector3d vector) { + return vector.getX() * vector.getX() + vector.getZ() * vector.getZ(); + } + + private Vector3d correctMovementForCollisions(Vector3d movement, BoundingBox boundingBox, boolean checkWorld) { + double movementX = movement.getX(); + double movementY = movement.getY(); + double movementZ = movement.getZ(); + + BoundingBox movementBoundingBox = boundingBox.clone(); + movementBoundingBox.extend(movement); + + List collidableBlocks = getCollidableBlocks(movementBoundingBox); + + if (Math.abs(movementY) > CollisionManager.COLLISION_TOLERANCE) { + movementY = computeCollisionOffset(boundingBox, Axis.Y, movementY, collidableBlocks, checkWorld); + boundingBox.translate(0, movementY, 0); + } + boolean checkZFirst = Math.abs(movementZ) > Math.abs(movementX); + if (checkZFirst && Math.abs(movementZ) > CollisionManager.COLLISION_TOLERANCE) { + movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, collidableBlocks, checkWorld); + boundingBox.translate(0, 0, movementZ); + } + if (Math.abs(movementX) > CollisionManager.COLLISION_TOLERANCE) { + movementX = computeCollisionOffset(boundingBox, Axis.X, movementX, collidableBlocks, checkWorld); + boundingBox.translate(movementX, 0, 0); + } + if (!checkZFirst && Math.abs(movementZ) > CollisionManager.COLLISION_TOLERANCE) { + movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, collidableBlocks, checkWorld); + boundingBox.translate(0, 0, movementZ); + } + + boundingBox.translate(-movementX, -movementY, -movementZ); + return Vector3d.from(movementX, movementY, movementZ); + } + + private double computeCollisionOffset(BoundingBox boundingBox, Axis axis, double offset, List collidableBlocks, boolean checkWorld) { + for (Vector3i blockPos : collidableBlocks) { + if (checkWorld) { + BlockCollision blockCollision = BlockUtils.getCollisionAt(session, blockPos); + if (blockCollision != null && !(blockCollision instanceof ScaffoldingCollision)) { + offset = blockCollision.computeCollisionOffset(boundingBox, axis, offset); + blockCollision.reset(); + } + } + offset = session.getPistonCache().computeCollisionOffset(blockPos, boundingBox, axis, offset); + if (Math.abs(offset) < COLLISION_TOLERANCE) { + return 0; + } + } + return offset; + } + /** * @return true if the block located at the player's floor position plus 1 would intersect with the player, * were they not sneaking @@ -260,7 +393,8 @@ public class CollisionManager { playerBoundingBox.setSizeY(EntityType.PLAYER.getHeight()); playerBoundingBox.setMiddleY(standingY); boolean result = collision.checkIntersection(playerBoundingBox); - collision.setPosition(null); + result |= session.getPistonCache().checkCollision(position, playerBoundingBox); + collision.reset(); playerBoundingBox.setSizeY(originalHeight); playerBoundingBox.setMiddleY(originalY); return result; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/collision/translators/BlockCollision.java b/connector/src/main/java/org/geysermc/connector/network/translators/collision/translators/BlockCollision.java index 8d1b1b925..0b23973e8 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/collision/translators/BlockCollision.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/collision/translators/BlockCollision.java @@ -30,8 +30,9 @@ import com.nukkitx.math.vector.Vector3i; import lombok.EqualsAndHashCode; import lombok.Getter; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.collision.CollisionManager; import org.geysermc.connector.network.translators.collision.BoundingBox; +import org.geysermc.connector.network.translators.collision.CollisionManager; +import org.geysermc.connector.utils.Axis; @EqualsAndHashCode public class BlockCollision { @@ -42,6 +43,13 @@ public class BlockCollision { @EqualsAndHashCode.Exclude protected final ThreadLocal position; + /** + * Store a Vector3d to allow the collision to be offset by a fractional amount + * This is used only in {@link #checkIntersection(BoundingBox)} and {@link #computeCollisionOffset(BoundingBox, Axis, double)} + */ + @EqualsAndHashCode.Exclude + protected final ThreadLocal positionOffset; + /** * This is used for the step up logic. * Usually, the player can only step up a block if they are on the same Y level as its bottom face or higher @@ -61,12 +69,22 @@ public class BlockCollision { protected BlockCollision(BoundingBox[] boxes) { this.boundingBoxes = boxes; this.position = new ThreadLocal<>(); + this.positionOffset = new ThreadLocal<>(); } public void setPosition(Vector3i newPosition) { this.position.set(newPosition); } + public void setPositionOffset(Vector3d newOffset) { + this.positionOffset.set(newOffset); + } + + public void reset() { + this.position.set(null); + this.positionOffset.set(null); + } + /** * Overridden in classes like SnowCollision and GrassPathCollision when correction code needs to be run before the * main correction @@ -156,17 +174,33 @@ public class BlockCollision { return true; } - public boolean checkIntersection(BoundingBox playerCollision) { + private Vector3d getFullPos() { Vector3i blockPos = this.position.get(); - int x = blockPos.getX(); - int y = blockPos.getY(); - int z = blockPos.getZ(); + Vector3d blockOffset = this.positionOffset.get(); + if (blockOffset != null && blockOffset != Vector3d.ZERO) { + return blockOffset.add(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } + return blockPos.toDouble(); + } + public boolean checkIntersection(BoundingBox playerCollision) { + Vector3d blockPos = getFullPos(); for (BoundingBox b : boundingBoxes) { - if (b.checkIntersection(x, y, z, playerCollision)) { + if (b.checkIntersection(blockPos, playerCollision)) { return true; } } return false; } + + public double computeCollisionOffset(BoundingBox boundingBox, Axis axis, double offset) { + Vector3d blockPos = getFullPos(); + for (BoundingBox b : boundingBoxes) { + offset = b.getMaxOffset(blockPos, boundingBox, axis, offset); + if (Math.abs(offset) < CollisionManager.COLLISION_TOLERANCE) { + return 0; + } + } + return offset; + } } \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockValueTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockValueTranslator.java index 892a41070..2f46ed6e6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockValueTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockValueTranslator.java @@ -33,13 +33,16 @@ import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.BlockEventPacket; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +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.PacketTranslator; import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import org.geysermc.connector.network.translators.world.block.entity.NoteblockBlockEntityTranslator; - -import java.util.concurrent.TimeUnit; +import org.geysermc.connector.network.translators.world.block.entity.PistonBlockEntity; +import org.geysermc.connector.utils.Direction; @Translator(packet = ServerBlockValuePacket.class) public class JavaBlockValueTranslator extends PacketTranslator { @@ -60,15 +63,41 @@ public class JavaBlockValueTranslator extends PacketTranslator new PistonBlockEntity(session, pos, direction, true, true)); + if (blockEntity.getAction() != action) { + blockEntity.setAction(action, Object2IntMaps.emptyMap()); + } + } } else { - retractPiston(session, position, 1.0f, 1.0f); + PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> { + int blockId = session.getConnector().getWorldManager().getBlockAt(session, position); + boolean sticky = BlockStateValues.isStickyPiston(blockId); + boolean extended = action != PistonValueType.PUSHING; + return new PistonBlockEntity(session, pos, direction, sticky, extended); + }); + blockEntity.setAction(action); } } else if (packet.getValue() instanceof MobSpawnerValue) { blockEventPacket.setEventType(1); @@ -111,65 +140,4 @@ public class JavaBlockValueTranslator extends PacketTranslator - extendPiston(session, position, (progress >= 1.0f) ? 1.0f : progress + 0.5f, progress), - 20, TimeUnit.MILLISECONDS); - } - } - - /** - * Emulate a piston retracting. - * @param session GeyserSession - * @param position Block position - * @param progress Current progress of piston - * @param lastProgress Last progress of piston - */ - private void retractPiston(GeyserSession session, Vector3i position, float progress, float lastProgress) { - BlockEntityDataPacket blockEntityDataPacket = new BlockEntityDataPacket(); - blockEntityDataPacket.setBlockPosition(position); - byte state = (byte) ((progress == 0.0f && lastProgress == 0.0f) ? 0 : 3); - blockEntityDataPacket.setData(buildPistonTag(position, progress, lastProgress, state)); - session.sendUpstreamPacket(blockEntityDataPacket); - if (lastProgress != 0.0f) { - session.getConnector().getGeneralThreadPool().schedule(() -> - retractPiston(session, position, (progress <= 0.0f) ? 0.0f : progress - 0.5f, progress), - 20, TimeUnit.MILLISECONDS); - } - } - - /** - * Build a piston tag - * @param position Piston position - * @param progress Current progress of piston - * @param lastProgress Last progress of piston - * @param state - * @return Bedrock CompoundTag of piston - */ - private NbtMap buildPistonTag(Vector3i position, float progress, float lastProgress, byte state) { - NbtMapBuilder builder = NbtMap.builder() - .putInt("x", position.getX()) - .putInt("y", position.getY()) - .putInt("z", position.getZ()) - .putFloat("Progress", progress) - .putFloat("LastProgress", lastProgress) - .putString("id", "PistonArm") - .putByte("NewState", state) - .putByte("State", state); - return builder.build(); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java index d66f8592d..2c297613f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java @@ -27,7 +27,12 @@ package org.geysermc.connector.network.translators.world.block; import com.fasterxml.jackson.databind.JsonNode; import it.unimi.dsi.fastutil.ints.*; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import org.geysermc.connector.registry.BlockRegistries; +import org.geysermc.connector.registry.type.BlockMapping; +import org.geysermc.connector.utils.Direction; +import org.geysermc.connector.utils.PistonBehavior; import java.util.Map; import java.util.function.BiFunction; @@ -43,8 +48,12 @@ public class BlockStateValues { private static final Int2ObjectMap FLOWER_POT_VALUES = new Int2ObjectOpenHashMap<>(); private static final Int2BooleanMap LECTERN_BOOK_STATES = new Int2BooleanOpenHashMap(); private static final Int2IntMap NOTEBLOCK_PITCHES = new Int2IntOpenHashMap(); - private static final Int2BooleanMap IS_STICKY_PISTON = new Int2BooleanOpenHashMap(); private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap(); + private static final Int2BooleanMap IS_STICKY_PISTON = new Int2BooleanOpenHashMap(); + private static final Object2IntMap PISTON_HEADS = new Object2IntOpenHashMap<>(); + private static final Int2ObjectMap PISTON_ORIENTATION = new Int2ObjectOpenHashMap<>(); + private static final IntSet ALL_PISTON_HEADS = new IntOpenHashSet(); + private static final IntSet MOVING_PISTONS = new IntOpenHashSet(); private static final Int2ByteMap SKULL_VARIANTS = new Int2ByteOpenHashMap(); private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap(); private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap(); @@ -57,6 +66,8 @@ public class BlockStateValues { public static int JAVA_COBWEB_ID; public static int JAVA_FURNACE_ID; public static int JAVA_FURNACE_LIT_ID; + public static int JAVA_HONEY_BLOCK_ID; + public static int JAVA_SLIME_BLOCK_ID; public static int JAVA_SPAWNER_ID; public static int JAVA_WATER_ID; @@ -115,10 +126,20 @@ public class BlockStateValues { return; } - if (javaId.contains("piston")) { - // True if extended, false if not - PISTON_VALUES.put(javaBlockState, javaId.contains("extended=true")); + if (javaId.contains("piston[")) { // minecraft:moving_piston, minecraft:sticky_piston, minecraft:piston + if (javaId.startsWith("minecraft:moving_piston")) { + MOVING_PISTONS.add(javaBlockState); + } else { + PISTON_VALUES.put(javaBlockState, javaId.contains("extended=true")); + } IS_STICKY_PISTON.put(javaBlockState, javaId.contains("sticky")); + PISTON_ORIENTATION.put(javaBlockState, getBlockDirection(javaId)); + return; + } else if (javaId.startsWith("minecraft:piston_head")) { + ALL_PISTON_HEADS.add(javaBlockState); + if (javaId.contains("short=false")) { + PISTON_HEADS.put(getBlockDirection(javaId), javaBlockState); + } return; } @@ -249,6 +270,81 @@ public class BlockStateValues { return IS_STICKY_PISTON.get(blockState); } + public static boolean isPistonHead(int state) { + return ALL_PISTON_HEADS.contains(state); + } + + /** + * Get the Java Block State for a piston head for a specific direction + * This is used in PistonBlockEntity to get the BlockCollision for the piston head. + * + * @param direction Direction the piston head points in + * @return Block state for the piston head + */ + public static int getPistonHead(Direction direction) { + return PISTON_HEADS.getOrDefault(direction, BlockStateValues.JAVA_AIR_ID); + } + + /** + * Check if a block is a minecraft:moving_piston + * This is used in ChunkUtils to prevent them from being placed as it causes + * pistons to flicker and it is not needed + * + * @param state Block state of the block + * @return True if the block is a moving_piston + */ + public static boolean isMovingPiston(int state) { + return MOVING_PISTONS.contains(state); + } + + /** + * This is used in GeyserPistonEvents.java and accepts minecraft:piston, + * minecraft:sticky_piston, and minecraft:moving_piston. + * + * @param state The block state of the piston base + * @return The direction in which the piston faces + */ + public static Direction getPistonOrientation(int state) { + return PISTON_ORIENTATION.get(state); + } + + /** + * Checks if a block sticks to other blocks + * (Slime and honey blocks) + * + * @param state The block state + * @return True if the block sticks to adjacent blocks + */ + public static boolean isBlockSticky(int state) { + return state == JAVA_SLIME_BLOCK_ID || state == JAVA_HONEY_BLOCK_ID; + } + + /** + * Check if two blocks are attached to each other. + * + * @param stateA The block state of block a + * @param stateB The block state of block b + * @return True if the blocks are attached to each other + */ + public static boolean isBlockAttached(int stateA, int stateB) { + boolean aSticky = isBlockSticky(stateA); + boolean bSticky = isBlockSticky(stateB); + if (aSticky && bSticky) { + // Only matching sticky blocks are attached together + // Honey + Honey & Slime + Slime + return stateA == stateB; + } + return aSticky || bSticky; + } + + /** + * @param state The block state of the block + * @return true if a piston can break the block + */ + public static boolean canPistonDestroyBlock(int state) { + return BlockRegistries.JAVA_BLOCKS.getOrDefault(state, BlockMapping.AIR).getPistonBehavior() == PistonBehavior.DESTROY; + } + /** * Skull variations are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock. * This gives a byte variant ID that Bedrock can use. @@ -323,4 +419,21 @@ public class BlockStateValues { } return 0.6f; } + + private static Direction getBlockDirection(String javaId) { + if (javaId.contains("down")) { + return Direction.DOWN; + } else if (javaId.contains("up")) { + return Direction.UP; + } else if (javaId.contains("south")) { + return Direction.SOUTH; + } else if (javaId.contains("west")) { + return Direction.WEST; + } else if (javaId.contains("north")) { + return Direction.NORTH; + } else if (javaId.contains("east")) { + return Direction.EAST; + } + throw new IllegalStateException(); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/PistonBlockEntity.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/PistonBlockEntity.java new file mode 100644 index 000000000..99d9da30d --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/PistonBlockEntity.java @@ -0,0 +1,832 @@ +/* + * 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; + break; + case PULLING: + case CANCELLED_MID_PUSH: + progress = 1; + break; + } + 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; + break; + case PULLING: + case CANCELLED_MID_PUSH: + progress = 1; + break; + } + 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; + } + switch (block.getPistonBehavior()) { + case BLOCK: + case DESTROY: + return false; + case PUSH_ONLY: // Glazed terracotta can only be pushed + return isPushing; + } + // Pistons can't move block entities + return !block.isBlockEntity(); + } + + /** + * 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; + } + break; + case CANCELLED_MID_PUSH: + case PULLING: + lastProgress = progress; + progress -= 0.5f; + if (progress <= 0.0f) { + progress = 0.0f; + } + break; + } + } + + /** + * @return True if the piston has finished its movement, otherwise false + */ + public boolean isDone() { + switch (action) { + case PUSHING: + return progress == 1.0f && lastProgress == 1.0f; + case PULLING: + case CANCELLED_MID_PUSH: + return progress == 0.0f && lastProgress == 0.0f; + } + return true; + } + + 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(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/PistonBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/PistonBlockEntityTranslator.java index f6211703b..e06a60121 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/PistonBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/PistonBlockEntityTranslator.java @@ -27,7 +27,6 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.nbt.NbtMap; -import com.nukkitx.nbt.NbtMapBuilder; import org.geysermc.connector.network.translators.world.block.BlockStateValues; /** @@ -52,19 +51,8 @@ public class PistonBlockEntityTranslator { * @return Bedrock tag of piston. */ public static NbtMap getTag(int blockState, Vector3i position) { - NbtMapBuilder tagBuilder = NbtMap.builder() - .putInt("x", position.getX()) - .putInt("y", position.getY()) - .putInt("z", position.getZ()) - .putByte("isMovable", (byte) 1) - .putString("id", "PistonArm"); - boolean extended = BlockStateValues.getPistonValues().get(blockState); - // 1f if extended, otherwise 0f - tagBuilder.putFloat("Progress", (extended) ? 1.0f : 0.0f); - // 1 if sticky, 0 if not - tagBuilder.putByte("Sticky", (byte) ((BlockStateValues.isStickyPiston(blockState)) ? 1 : 0)); - - return tagBuilder.build(); + boolean sticky = BlockStateValues.isStickyPiston(blockState); + return PistonBlockEntity.buildStaticPistonTag(position, extended, sticky); } } diff --git a/connector/src/main/java/org/geysermc/connector/registry/populator/BlockRegistryPopulator.java b/connector/src/main/java/org/geysermc/connector/registry/populator/BlockRegistryPopulator.java index 6de39efab..6393a9c7a 100644 --- a/connector/src/main/java/org/geysermc/connector/registry/populator/BlockRegistryPopulator.java +++ b/connector/src/main/java/org/geysermc/connector/registry/populator/BlockRegistryPopulator.java @@ -44,6 +44,7 @@ import org.geysermc.connector.registry.type.BlockMapping; import org.geysermc.connector.registry.type.BlockMappings; import org.geysermc.connector.utils.BlockUtils; import org.geysermc.connector.utils.FileUtils; +import org.geysermc.connector.utils.PistonBehavior; import java.io.DataInputStream; import java.io.InputStream; @@ -133,6 +134,7 @@ public class BlockRegistryPopulator { int commandBlockRuntimeId = -1; int javaRuntimeId = -1; int waterRuntimeId = -1; + int movingBlockRuntimeId = -1; Iterator> blocksIterator = BLOCKS_JSON.fields(); BiFunction stateMapper = STATE_MAPPER.getOrDefault(palette.getKey(), (i, s) -> null); @@ -166,6 +168,8 @@ public class BlockRegistryPopulator { case "minecraft:command_block[conditional=false,facing=north]": commandBlockRuntimeId = bedrockRuntimeId; break; + case "minecraft:moving_piston[facing=north,type=normal]": + movingBlockRuntimeId = bedrockRuntimeId; } if (javaId.contains("jigsaw")) { @@ -209,6 +213,11 @@ public class BlockRegistryPopulator { } builder.bedrockAirId(airRuntimeId); + if (movingBlockRuntimeId == -1) { + throw new AssertionError("Unable to find moving block in palette"); + } + builder.bedrockMovingBlockId(movingBlockRuntimeId); + // Loop around again to find all item frame runtime IDs for (Object2IntMap.Entry entry : blockStateOrderedMap.object2IntEntrySet()) { String name = entry.getKey().getString("name"); @@ -248,6 +257,8 @@ public class BlockRegistryPopulator { int cobwebBlockId = -1; int furnaceRuntimeId = -1; int furnaceLitRuntimeId = -1; + int honeyBlockRuntimeId = -1; + int slimeBlockRuntimeId = -1; int spawnerRuntimeId = -1; int uniqueJavaId = -1; int waterRuntimeId = -1; @@ -281,6 +292,24 @@ public class BlockRegistryPopulator { builder.pickItem(pickItemNode.textValue().intern()); } + if (javaId.equals("minecraft:obsidian") || javaId.equals("minecraft:crying_obsidian") || javaId.startsWith("minecraft:respawn_anchor")) { + builder.pistonBehavior(PistonBehavior.BLOCK); + } else { + JsonNode pistonBehaviorNode = entry.getValue().get("piston_behavior"); + if (pistonBehaviorNode != null) { + builder.pistonBehavior(PistonBehavior.getByName(pistonBehaviorNode.textValue())); + } else { + builder.pistonBehavior(PistonBehavior.NORMAL); + } + } + + JsonNode hasBlockEntityNode = entry.getValue().get("has_block_entity"); + if (hasBlockEntityNode != null) { + builder.isBlockEntity(hasBlockEntityNode.booleanValue()); + } else { + builder.isBlockEntity(false); + } + BlockStateValues.storeBlockStateValues(entry.getKey(), javaRuntimeId, entry.getValue()); String cleanJavaIdentifier = BlockUtils.getCleanIdentifier(entry.getKey()); @@ -320,6 +349,10 @@ public class BlockRegistryPopulator { } else if ("minecraft:water[level=0]".equals(javaId)) { waterRuntimeId = javaRuntimeId; + } else if (javaId.equals("minecraft:honey_block")) { + honeyBlockRuntimeId = javaRuntimeId; + } else if (javaId.equals("minecraft:slime_block")) { + slimeBlockRuntimeId = javaRuntimeId; } } if (bellBlockId == -1) { @@ -342,6 +375,16 @@ public class BlockRegistryPopulator { } BlockStateValues.JAVA_FURNACE_LIT_ID = furnaceLitRuntimeId; + if (honeyBlockRuntimeId == -1) { + throw new AssertionError("Unable to find honey block in palette"); + } + BlockStateValues.JAVA_HONEY_BLOCK_ID = honeyBlockRuntimeId; + + if (slimeBlockRuntimeId == -1) { + throw new AssertionError("Unable to find slime block in palette"); + } + BlockStateValues.JAVA_SLIME_BLOCK_ID = slimeBlockRuntimeId; + if (spawnerRuntimeId == -1) { throw new AssertionError("Unable to find spawner in palette"); } diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/BlockMapping.java b/connector/src/main/java/org/geysermc/connector/registry/type/BlockMapping.java index 1602372b9..f9a074e91 100644 --- a/connector/src/main/java/org/geysermc/connector/registry/type/BlockMapping.java +++ b/connector/src/main/java/org/geysermc/connector/registry/type/BlockMapping.java @@ -28,7 +28,9 @@ package org.geysermc.connector.registry.type; import lombok.Builder; import lombok.Value; import org.geysermc.connector.utils.BlockUtils; +import org.geysermc.connector.utils.PistonBehavior; +import javax.annotation.Nonnull; import javax.annotation.Nullable; @Builder @@ -51,6 +53,10 @@ public class BlockMapping { int collisionIndex; @Nullable String pickItem; + @Nonnull + PistonBehavior pistonBehavior; + boolean isBlockEntity; + /** * @return the identifier without the additional block states */ diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/BlockMappings.java b/connector/src/main/java/org/geysermc/connector/registry/type/BlockMappings.java index 7cdc91341..0c92d6a4c 100644 --- a/connector/src/main/java/org/geysermc/connector/registry/type/BlockMappings.java +++ b/connector/src/main/java/org/geysermc/connector/registry/type/BlockMappings.java @@ -40,6 +40,7 @@ import java.util.Map; public class BlockMappings { int bedrockAirId; int bedrockWaterId; + int bedrockMovingBlockId; int blockStateVersion; diff --git a/connector/src/main/java/org/geysermc/connector/utils/Axis.java b/connector/src/main/java/org/geysermc/connector/utils/Axis.java new file mode 100644 index 000000000..4294744be --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/Axis.java @@ -0,0 +1,50 @@ +/* + * 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.utils; + +import com.nukkitx.math.vector.Vector3d; + +public enum Axis { + X, Y, Z; + + public static final Axis[] VALUES = values(); + + /** + * @param vector The vector + * @return The component of the vector in this axis + */ + public double choose(Vector3d vector) { + switch (this) { + case X: + return vector.getX(); + case Y: + return vector.getY(); + case Z: + return vector.getZ(); + } + return -1; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java index b557f62e5..db2271e5c 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java @@ -27,6 +27,7 @@ package org.geysermc.connector.utils; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3i; import org.geysermc.connector.inventory.GeyserItemStack; import org.geysermc.connector.inventory.PlayerInventory; @@ -247,6 +248,16 @@ public class BlockUtils { BlockCollision collision = Registries.COLLISIONS.get(blockId); if (collision != null) { collision.setPosition(blockPos); + collision.setPositionOffset(null); + } + return collision; + } + + public static BlockCollision getCollision(int blockId, Vector3i blockPos, Vector3d blockOffset) { + BlockCollision collision = Registries.COLLISIONS.get(blockId); + if (collision != null) { + collision.setPosition(blockPos); + collision.setPositionOffset(blockOffset); } return collision; } diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java index 392eb965a..993555a10 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -367,25 +367,29 @@ public class ChunkUtils { skull.despawnEntity(session, position); } - int blockId = session.getBlockMappings().getBedrockBlockId(blockState); + // Prevent moving_piston from being placed + // It's used for extending piston heads, but it isn't needed on Bedrock and causes pistons to flicker + if (!BlockStateValues.isMovingPiston(blockState)) { + int blockId = session.getBlockMappings().getBedrockBlockId(blockState); - UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); - updateBlockPacket.setDataLayer(0); - updateBlockPacket.setBlockPosition(position); - updateBlockPacket.setRuntimeId(blockId); - updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); - updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK); - session.sendUpstreamPacket(updateBlockPacket); + UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); + updateBlockPacket.setDataLayer(0); + updateBlockPacket.setBlockPosition(position); + updateBlockPacket.setRuntimeId(blockId); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); + updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK); + session.sendUpstreamPacket(updateBlockPacket); - UpdateBlockPacket waterPacket = new UpdateBlockPacket(); - waterPacket.setDataLayer(1); - waterPacket.setBlockPosition(position); - if (BlockRegistries.WATERLOGGED.get().contains(blockState)) { - waterPacket.setRuntimeId(session.getBlockMappings().getBedrockWaterId()); - } else { - waterPacket.setRuntimeId(session.getBlockMappings().getBedrockAirId()); + UpdateBlockPacket waterPacket = new UpdateBlockPacket(); + waterPacket.setDataLayer(1); + waterPacket.setBlockPosition(position); + if (BlockRegistries.WATERLOGGED.get().contains(blockState)) { + waterPacket.setRuntimeId(session.getBlockMappings().getBedrockWaterId()); + } else { + waterPacket.setRuntimeId(session.getBlockMappings().getBedrockAirId()); + } + session.sendUpstreamPacket(waterPacket); } - session.sendUpstreamPacket(waterPacket); BlockStateValues.getLecternBookStates().compute(blockState, (key, newLecternHasBook) -> { // Determine if this block is a lectern diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index 71522423c..0e4de64e4 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -65,6 +65,7 @@ public class DimensionUtils { session.getItemFrameCache().clear(); session.getLecternCache().clear(); session.getLodestoneCache().clear(); + session.getPistonCache().clear(); session.getSkullCache().clear(); Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0); diff --git a/connector/src/main/java/org/geysermc/connector/utils/Direction.java b/connector/src/main/java/org/geysermc/connector/utils/Direction.java new file mode 100644 index 000000000..7cde4a0ac --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/Direction.java @@ -0,0 +1,80 @@ +/* + * 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.utils; + +import com.github.steveice10.mc.protocol.data.game.world.block.value.PistonValue; +import com.nukkitx.math.vector.Vector3i; +import lombok.Getter; + +import javax.annotation.Nonnull; + +public enum Direction { + DOWN(1, Vector3i.from(0, -1, 0), Axis.Y, PistonValue.DOWN), + UP(0, Vector3i.UNIT_Y, Axis.Y, PistonValue.UP), + NORTH(3, Vector3i.from(0, 0, -1), Axis.Z, PistonValue.NORTH), + SOUTH(2, Vector3i.UNIT_Z, Axis.Z, PistonValue.SOUTH), + WEST(5, Vector3i.from(-1, 0, 0), Axis.X, PistonValue.WEST), + EAST(4, Vector3i.UNIT_X, Axis.X, PistonValue.EAST); + + public static final Direction[] VALUES = values(); + + private final int reversedId; + @Getter + private final Vector3i unitVector; + @Getter + private final Axis axis; + @Getter + private final PistonValue pistonValue; + + Direction(int reversedId, Vector3i unitVector, Axis axis, PistonValue pistonValue) { + this.reversedId = reversedId; + this.unitVector = unitVector; + this.axis = axis; + this.pistonValue = pistonValue; + } + + public Direction reversed() { + return VALUES[reversedId]; + } + + public boolean isVertical() { + return axis == Axis.Y; + } + + public boolean isHorizontal() { + return axis == Axis.X || axis == Axis.Z; + } + + @Nonnull + public static Direction fromPistonValue(PistonValue pistonValue) { + for (Direction direction : VALUES) { + if (direction.pistonValue == pistonValue) { + return direction; + } + } + throw new IllegalStateException(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/PistonBehavior.java b/connector/src/main/java/org/geysermc/connector/utils/PistonBehavior.java new file mode 100644 index 000000000..67579e2ad --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/PistonBehavior.java @@ -0,0 +1,47 @@ +/* + * 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.utils; + +import java.util.Locale; + +public enum PistonBehavior { + NORMAL, + BLOCK, + DESTROY, + PUSH_ONLY; + + public static final PistonBehavior[] VALUES = values(); + + public static PistonBehavior getByName(String name) { + String upperCase = name.toUpperCase(Locale.ROOT); + for (PistonBehavior type : VALUES) { + if (type.name().equals(upperCase)) { + return type; + } + } + return NORMAL; + } +}