Smooth Pistons (#1542)

With proper piston collision for players as well.
This commit is contained in:
David Choo 2021-09-09 21:20:25 -04:00 committed by GitHub
parent 1199d50338
commit 8461cf76b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1911 additions and 142 deletions

View File

@ -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);

View File

@ -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<Vector3i> 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<Block> 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());
}
}

View File

@ -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);
}
/**

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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<Vector3i, PistonBlockEntity> 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<Vector3i, PistonBlockEntity> 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<Vector3i, PistonBlockEntity> 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();
}
}

View File

@ -92,9 +92,9 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
session.sendDownstreamPacket(playerRotationPacket);
} else {
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround());
if (position != null) { // A null return value cancels the packet
if (isValidMove(session, packet.getMode(), entity.getPosition(), packet.getPosition())) {
if (isValidMove(session, packet.getMode(), entity.getPosition(), packet.getPosition())) {
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround());
if (position != null) { // A null return value cancels the packet
Packet movePacket;
if (rotationChanged) {
// Send rotation updates as well
@ -134,11 +134,11 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
session.sendUpstreamPacket(movePlayerPacket);
}
}
} else {
// Not a valid move
session.getConnector().getLogger().debug("Recalculating position...");
session.getCollisionManager().recalculatePosition();
}
} else {
// Not a valid move
session.getConnector().getLogger().debug("Recalculating position...");
session.getCollisionManager().recalculatePosition();
}
}

View File

@ -25,11 +25,14 @@
package org.geysermc.connector.network.translators.collision;
import com.nukkitx.math.vector.Vector3d;
import lombok.*;
import org.geysermc.connector.utils.Axis;
import org.geysermc.connector.utils.Direction;
@Data
@AllArgsConstructor
public class BoundingBox {
public class BoundingBox implements Cloneable {
private double middleX;
private double middleY;
private double middleZ;
@ -44,9 +47,128 @@ public class BoundingBox {
middleZ += z;
}
public boolean checkIntersection(int offsetX, int offsetY, int offsetZ, BoundingBox otherBox) {
public void extend(double x, double y, double z) {
middleX += x / 2;
middleY += y / 2;
middleZ += z / 2;
sizeX += Math.abs(x);
sizeY += Math.abs(y);
sizeZ += Math.abs(z);
}
public void extend(Vector3d extend) {
extend(extend.getX(), extend.getY(), extend.getZ());
}
public boolean checkIntersection(double offsetX, double offsetY, double offsetZ, BoundingBox otherBox) {
return (Math.abs((middleX + offsetX) - otherBox.getMiddleX()) * 2 < (sizeX + otherBox.getSizeX())) &&
(Math.abs((middleY + offsetY) - otherBox.getMiddleY()) * 2 < (sizeY + otherBox.getSizeY())) &&
(Math.abs((middleZ + offsetZ) - otherBox.getMiddleZ()) * 2 < (sizeZ + otherBox.getSizeZ()));
}
public boolean checkIntersection(Vector3d offset, BoundingBox otherBox) {
return checkIntersection(offset.getX(), offset.getY(), offset.getZ(), otherBox);
}
public Vector3d getMin() {
double x = middleX - sizeX / 2;
double y = middleY - sizeY / 2;
double z = middleZ - sizeZ / 2;
return Vector3d.from(x, y, z);
}
public Vector3d getMax() {
double x = middleX + sizeX / 2;
double y = middleY + sizeY / 2;
double z = middleZ + sizeZ / 2;
return Vector3d.from(x, y, z);
}
public Vector3d getBottomCenter() {
return Vector3d.from(middleX, middleY - sizeY / 2, middleZ);
}
private boolean checkOverlapInAxis(Vector3d offset, BoundingBox otherBox, Axis axis) {
switch (axis) {
case X:
return Math.abs((middleX + offset.getX()) - otherBox.getMiddleX()) * 2 < (sizeX + otherBox.getSizeX());
case Y:
return Math.abs((middleY + offset.getY()) - otherBox.getMiddleY()) * 2 < (sizeY + otherBox.getSizeY());
case Z:
return Math.abs((middleZ + offset.getZ()) - otherBox.getMiddleZ()) * 2 < (sizeZ + otherBox.getSizeZ());
}
return false;
}
/**
* Find the maximum offset of another bounding box in an axis that will not collide with this bounding box
*
* @param boxOffset The offset of this bounding box
* @param otherBoundingBox The bounding box that is moving
* @param axis The axis of movement
* @param offset The current max offset
* @return The new max offset
*/
public double getMaxOffset(Vector3d boxOffset, BoundingBox otherBoundingBox, Axis axis, double offset) {
// Make sure that the bounding box overlaps in the other axes
for (Axis a : Axis.VALUES) {
if (a != axis && !checkOverlapInAxis(boxOffset, otherBoundingBox, a)) {
return offset;
}
}
if (offset > 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;
}
}

View File

@ -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<Vector3i> 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<Vector3i> 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;

View File

@ -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<Vector3i> 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<Vector3d> 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;
}
}

View File

@ -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<ServerBlockValuePacket> {
@ -60,15 +63,41 @@ public class JavaBlockValueTranslator extends PacketTranslator<ServerBlockValueP
} else if (packet.getValue() instanceof NoteBlockValue) {
NoteblockBlockEntityTranslator.translate(session, packet.getPosition());
} else if (packet.getValue() instanceof PistonValue) {
PistonValueType type = (PistonValueType) packet.getType();
// Unlike everything else, pistons need a block entity packet to convey motion
// TODO: Doesn't register on chunk load; needs to be interacted with first
PistonValueType action = (PistonValueType) packet.getType();
Direction direction = Direction.fromPistonValue((PistonValue) packet.getValue());
Vector3i position = Vector3i.from(packet.getPosition().getX(), packet.getPosition().getY(), packet.getPosition().getZ());
if (type == PistonValueType.PUSHING) {
extendPiston(session, position, 0.0f, 0.0f);
PistonCache pistonCache = session.getPistonCache();
if (session.getConnector().getPlatformType() == PlatformType.SPIGOT) {
// Mostly handled in the GeyserPistonEvents class
// Retracting sticky pistons is an exception, since the event is not called on Spigot from 1.13.2 - 1.17.1
// See https://github.com/PaperMC/Paper/blob/6fa1983e9ce177a4a412d5b950fd978620174777/patches/server/0304-Fire-BlockPistonRetractEvent-for-all-empty-pistons.patch
if (action == PistonValueType.PULLING || action == PistonValueType.CANCELLED_MID_PUSH) {
int pistonBlock = session.getConnector().getWorldManager().getBlockAt(session, position);
if (!BlockStateValues.isStickyPiston(pistonBlock)) {
return;
}
if (action != PistonValueType.CANCELLED_MID_PUSH) {
Vector3i blockInFrontPos = position.add(direction.getUnitVector());
int blockInFront = session.getConnector().getWorldManager().getBlockAt(session, blockInFrontPos);
if (blockInFront != BlockStateValues.JAVA_AIR_ID) {
// Piston pulled something
return;
}
}
PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> 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<ServerBlockValueP
session.sendUpstreamPacket(blockEntityPacket);
}
}
/**
* Emulating a piston extending
* @param session GeyserSession
* @param position Block position
* @param progress How far the piston is
* @param lastProgress How far the piston last was
*/
private void extendPiston(GeyserSession session, Vector3i position, float progress, float lastProgress) {
BlockEntityDataPacket blockEntityDataPacket = new BlockEntityDataPacket();
blockEntityDataPacket.setBlockPosition(position);
byte state = (byte) ((progress == 1.0f && lastProgress == 1.0f) ? 2 : 1);
blockEntityDataPacket.setData(buildPistonTag(position, progress, lastProgress, state));
session.sendUpstreamPacket(blockEntityDataPacket);
if (lastProgress != 1.0f) {
session.getConnector().getGeneralThreadPool().schedule(() ->
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();
}
}

View File

@ -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<String> 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<Direction> PISTON_HEADS = new Object2IntOpenHashMap<>();
private static final Int2ObjectMap<Direction> 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();
}
}

View File

@ -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<Vector3i> 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<Vector3i> 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<Vector3i> blocksChecked = new ObjectOpenHashSet<>();
Queue<Vector3i> 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<Vector3i> 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<Vector3i> 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<Vector3i, PistonBlockEntity> 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<Vector3i, PistonBlockEntity> 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();
}
}

View File

@ -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);
}
}

View File

@ -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<Map.Entry<String, JsonNode>> blocksIterator = BLOCKS_JSON.fields();
BiFunction<String, NbtMapBuilder, String> 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<NbtMap> 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");
}

View File

@ -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
*/

View File

@ -40,6 +40,7 @@ import java.util.Map;
public class BlockMappings {
int bedrockAirId;
int bedrockWaterId;
int bedrockMovingBlockId;
int blockStateVersion;

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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;
}
}