diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java index d3efd7b20..2b6107729 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java @@ -48,8 +48,8 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PistonCache; import org.geysermc.geyser.translator.collision.BlockCollision; import org.geysermc.geyser.translator.collision.OtherCollision; -import org.geysermc.geyser.translator.collision.ScaffoldingCollision; import org.geysermc.geyser.translator.collision.SolidCollision; +import org.geysermc.geyser.translator.collision.fixes.ScaffoldingCollision; import org.geysermc.geyser.util.BlockUtils; import java.text.DecimalFormat; diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java index bd98ae0a5..fee86b130 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/CollisionRegistryLoader.java @@ -172,4 +172,4 @@ public class CollisionRegistryLoader extends MultiResourceRegistryLoader= boxMinY) { - // Max steppable distance in Minecraft as far as we know is 0.5625 blocks (for beds) - if (boxMaxY - playerMinY <= 0.5625) { - playerCollision.translate(0, boxMaxY - playerMinY, 0); - // Update player Y for next collision box - playerMinY = playerCollision.getMiddleY() - (playerCollision.getSizeY() / 2); - } - } + // Due to floating points errors, or because of block collision difference, player could be slightly clipping into the block. + // So we check if the player is intersecting the block, if they do then push them out. This fixes NoCheatPlus's Passable check and other anticheat checks. + // This check doesn't allow players right up against the block, so they must be pushed slightly away. However, we should only do it if the + // push distance is smaller than "pushAwayTolerance", we don't want to push player out when they're actually inside a block. + for (BoundingBox boundingBox : this.boundingBoxes) { + // Make player collision slightly bigger to pick up on blocks that could cause problems with NoCheatPlus's Passable check. + playerCollision.expand(collisionExpansion, 0, collisionExpansion); - // Make player collision slightly bigger to pick up on blocks that could cause problems with Passable - playerCollision.setSizeX(playerCollision.getSizeX() + CollisionManager.COLLISION_TOLERANCE * 2); - playerCollision.setSizeZ(playerCollision.getSizeZ() + CollisionManager.COLLISION_TOLERANCE * 2); - - // If the player still intersects the block, then push them out - // This fixes NoCheatPlus's Passable check - // This check doesn't allow players right up against the block, so they must be pushed slightly away - if (b.checkIntersection(x, y, z, playerCollision)) { - Vector3d relativePlayerPosition = Vector3d.from(playerCollision.getMiddleX() - x, - playerCollision.getMiddleY() - y, - playerCollision.getMiddleZ() - z); - - // The ULP should give an upper bound on the floating point error - double xULP = Math.ulp((float) Math.max(Math.abs(playerCollision.getMiddleX()) + playerCollision.getSizeX() / 2.0, Math.abs(x) + 1)); - double zULP = Math.ulp((float) Math.max(Math.abs(playerCollision.getMiddleZ()) + playerCollision.getSizeZ() / 2.0, Math.abs(z) + 1)); - - double xPushAwayTolerance = Math.max(pushAwayTolerance, xULP); - double zPushAwayTolerance = Math.max(pushAwayTolerance, zULP); - - double northFaceZPos = b.getMiddleZ() - (b.getSizeZ() / 2); - double translateDistance = northFaceZPos - relativePlayerPosition.getZ() - (playerCollision.getSizeZ() / 2); - if (Math.abs(translateDistance) < zPushAwayTolerance) { - playerCollision.translate(0, 0, translateDistance); - } - - double southFaceZPos = b.getMiddleZ() + (b.getSizeZ() / 2); - translateDistance = southFaceZPos - relativePlayerPosition.getZ() + (playerCollision.getSizeZ() / 2); - if (Math.abs(translateDistance) < zPushAwayTolerance) { - playerCollision.translate(0, 0, translateDistance); - } - - double eastFaceXPos = b.getMiddleX() + (b.getSizeX() / 2); - translateDistance = eastFaceXPos - relativePlayerPosition.getX() + (playerCollision.getSizeX() / 2); - if (Math.abs(translateDistance) < xPushAwayTolerance) { - playerCollision.translate(translateDistance, 0, 0); - } - - double westFaceXPos = b.getMiddleX() - (b.getSizeX() / 2); - translateDistance = westFaceXPos - relativePlayerPosition.getX() - (playerCollision.getSizeX() / 2); - if (Math.abs(translateDistance) < xPushAwayTolerance) { - playerCollision.translate(translateDistance, 0, 0); - } - - double bottomFaceYPos = b.getMiddleY() - (b.getSizeY() / 2); - translateDistance = bottomFaceYPos - relativePlayerPosition.getY() - (playerCollision.getSizeY() / 2); - if (Math.abs(translateDistance) < pushAwayTolerance) { - playerCollision.translate(0, translateDistance, 0); - } + if (!boundingBox.checkIntersection(x, y, z, playerCollision)) { + playerCollision.expand(-collisionExpansion, 0, -collisionExpansion); + continue; } - // Set the collision size back to normal - playerCollision.setSizeX(0.6); - playerCollision.setSizeZ(0.6); - } + boundingBox = boundingBox.clone(); + boundingBox.translate(x, y, z); - return true; + // The ULP should give an upper bound on the floating point error + double xULP = Math.ulp((float) Math.max(Math.abs(playerCollision.getMiddleX()) + playerCollision.getSizeX() / 2.0, Math.abs(x) + 1)); + double zULP = Math.ulp((float) Math.max(Math.abs(playerCollision.getMiddleZ()) + playerCollision.getSizeZ() / 2.0, Math.abs(z) + 1)); + double xPushAwayTolerance = Math.max(pushAwayTolerance, xULP), zPushAwayTolerance = Math.max(pushAwayTolerance, zULP); + + boundingBox.pushOutOfBoundingBox(playerCollision, Direction.NORTH, zPushAwayTolerance); + boundingBox.pushOutOfBoundingBox(playerCollision, Direction.SOUTH, zPushAwayTolerance); + boundingBox.pushOutOfBoundingBox(playerCollision, Direction.EAST, xPushAwayTolerance); + boundingBox.pushOutOfBoundingBox(playerCollision, Direction.WEST, xPushAwayTolerance); + boundingBox.pushOutOfBoundingBox(playerCollision, Direction.UP, pushAwayTolerance); + boundingBox.pushOutOfBoundingBox(playerCollision, Direction.DOWN, pushAwayTolerance); + } + } + + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { } public boolean checkIntersection(double x, double y, double z, BoundingBox playerCollision) { @@ -184,4 +128,4 @@ public class BlockCollision { } return true; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/BellCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/BellCollision.java new file mode 100644 index 000000000..954689c0a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/BellCollision.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 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.geyser.translator.collision.fixes; + +import lombok.EqualsAndHashCode; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; + +@EqualsAndHashCode(callSuper = true) +@CollisionRemapper(regex = "^bell$", passDefaultBoxes = true) +public class BellCollision extends BlockCollision { + private final static double MAX_PUSH_DISTANCE = 0.1875 + CollisionManager.COLLISION_TOLERANCE * 1.01; + + private final boolean standing; + + public BellCollision(BlockState state, BoundingBox[] boxes) { + super(boxes); + + this.standing = state.getValue(Properties.BELL_ATTACHMENT).equals("floor"); + } + + @Override + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { + if (!this.standing) { + return; + } + + // Check for bell collision bug (bell on Java is 0.1875 block higher than Bedrock) + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.UP, MAX_PUSH_DISTANCE); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ChestCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ChestCollision.java new file mode 100644 index 000000000..21c506d7b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ChestCollision.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 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.geyser.translator.collision.fixes; + +import lombok.EqualsAndHashCode; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.Axis; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; + +@EqualsAndHashCode(callSuper = true) +@CollisionRemapper(regex = "^chest$", passDefaultBoxes = true) +public class ChestCollision extends BlockCollision { + public ChestCollision(BlockState state, BoundingBox[] boxes) { + super(boxes); + } + + @Override + public void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox playerCollision) { + super.correctPosition(session, x, y, z, playerCollision); + + final SessionPlayerEntity player = session.getPlayerEntity(); + + // Player haven't even fall on the blocks yet, no need to move them down. + if (!player.isCollidingVertically()) { + return; + } + + final double collisionExpansion = CollisionManager.COLLISION_TOLERANCE * 2; + // Slightly expand the collision so we can see if the player is actually colliding with the block or not. + playerCollision.setSizeY(playerCollision.getSizeY() + collisionExpansion); + + double beforeYVelocity = player.getLastTickEndVelocity().getY(); + // If the player is already colliding with the block or player velocity y is larger than 0 then player likely don't need to be correct. + if (beforeYVelocity > 0 || playerCollision.getMin(Axis.Y) - player.position().getY() > 0 || this.checkIntersection(x, y, z, playerCollision)) { + playerCollision.setSizeY(playerCollision.getSizeX() - collisionExpansion); + return; + } + playerCollision.setSizeY(playerCollision.getSizeX() - collisionExpansion); + + BoundingBox previous = playerCollision.clone(); + previous.setMiddleX(player.getPosition().getX()); + previous.setMiddleY(player.position().getY() + (playerCollision.getSizeY() / 2)); + previous.setMiddleZ(player.getPosition().getZ()); + + // Check for chest bug (chest are 0.875 blocks thick on Java but 0.95 blocks thick on Bedrock) + // We grab the player velocity from last tick then apply collision on it, if the player can still fall then we correct + // their position to fall down. If not then just use the player current position. Also do the same when player is jumping, if player + // haven't collided yet, then correct them. Resolve #3277 and #4955 + double yVelocity = Math.max(beforeYVelocity, this.computeCollisionOffset(x, y, z, previous, Axis.Y, beforeYVelocity)); + // Player velocity is close enough, no need to correct, avoid moving player position silently if possible. Also don't move the player upwards. + if (Math.abs(beforeYVelocity - yVelocity) <= CollisionManager.COLLISION_TOLERANCE || yVelocity > CollisionManager.COLLISION_TOLERANCE) { + return; + } + + previous.translate(0, yVelocity, 0); + playerCollision.setMiddleY(previous.getMiddleY()); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ConduitCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ConduitCollision.java new file mode 100644 index 000000000..273a8f054 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ConduitCollision.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 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.geyser.translator.collision.fixes; + +import lombok.EqualsAndHashCode; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; + +@EqualsAndHashCode(callSuper = true) +@CollisionRemapper(regex = "^conduit$", passDefaultBoxes = true) +public class ConduitCollision extends BlockCollision { + private final static double MAX_PUSH_DISTANCE = 0.1875 + CollisionManager.COLLISION_TOLERANCE * 1.01; + + public ConduitCollision(BlockState state, BoundingBox[] boxes) { + super(boxes); + } + + @Override + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { + // Check for conduit bug (conduit is lifted from the ground on Java unlike Bedrock where conduit is placed on the ground) + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.UP, MAX_PUSH_DISTANCE); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/DoorCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/DoorCollision.java similarity index 67% rename from core/src/main/java/org/geysermc/geyser/translator/collision/DoorCollision.java rename to core/src/main/java/org/geysermc/geyser/translator/collision/fixes/DoorCollision.java index 33c85ce07..58682dd00 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/collision/DoorCollision.java +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/DoorCollision.java @@ -23,17 +23,23 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.translator.collision; +package org.geysermc.geyser.translator.collision.fixes; import lombok.EqualsAndHashCode; import org.geysermc.geyser.level.block.property.Properties; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; @EqualsAndHashCode(callSuper = true) @CollisionRemapper(regex = "_door$", usesParams = true, passDefaultBoxes = true) public class DoorCollision extends BlockCollision { + private final static double MAX_PUSH_DISTANCE = 0.005 + CollisionManager.COLLISION_TOLERANCE * 1.01; + /** * 1 = north * 2 = east @@ -59,26 +65,13 @@ public class DoorCollision extends BlockCollision { } @Override - public boolean correctPosition(GeyserSession session, int x, int y, int z, BoundingBox playerCollision) { - boolean result = super.correctPosition(session, x, y, z, playerCollision); - // Hack to prevent false positives - playerCollision.setSizeX(playerCollision.getSizeX() - 0.0001); - playerCollision.setSizeY(playerCollision.getSizeY() - 0.0001); - playerCollision.setSizeZ(playerCollision.getSizeZ() - 0.0001); - + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { // Check for door bug (doors are 0.1875 blocks thick on Java but 0.1825 blocks thick on Bedrock) - if (this.checkIntersection(x, y, z, playerCollision)) { - switch (facing) { - case 1 -> playerCollision.setMiddleZ(z + 0.5125); // North - case 2 -> playerCollision.setMiddleX(x + 0.5125); // East - case 3 -> playerCollision.setMiddleZ(z + 0.4875); // South - case 4 -> playerCollision.setMiddleX(x + 0.4875); // West - } + switch (this.facing) { + case 1 -> blockCollision.pushOutOfBoundingBox(playerCollision, Direction.NORTH, MAX_PUSH_DISTANCE); + case 2 -> blockCollision.pushOutOfBoundingBox(playerCollision, Direction.EAST, MAX_PUSH_DISTANCE); + case 3 -> blockCollision.pushOutOfBoundingBox(playerCollision, Direction.SOUTH, MAX_PUSH_DISTANCE); + case 4 -> blockCollision.pushOutOfBoundingBox(playerCollision, Direction.WEST, MAX_PUSH_DISTANCE); } - - playerCollision.setSizeX(playerCollision.getSizeX() + 0.0001); - playerCollision.setSizeY(playerCollision.getSizeY() + 0.0001); - playerCollision.setSizeZ(playerCollision.getSizeZ() + 0.0001); - return result; } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/EndPortalCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/EndPortalCollision.java new file mode 100644 index 000000000..53c26721e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/EndPortalCollision.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 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.geyser.translator.collision.fixes; + +import lombok.EqualsAndHashCode; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; + +@EqualsAndHashCode(callSuper = true) +@CollisionRemapper(regex = "^end_portal_frame$", passDefaultBoxes = true) +public class EndPortalCollision extends BlockCollision { + private final static double MAX_PUSH_DISTANCE = 0.1875 + CollisionManager.COLLISION_TOLERANCE * 1.01; + + private final boolean eye; + + public EndPortalCollision(BlockState state, BoundingBox[] boxes) { + super(boxes); + + this.eye = state.getValue(Properties.EYE); + } + + @Override + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { + if (!this.eye) { + return; + } + + // Check for end portal frame bug (BE don't have the eye collision when the end portal frame contain an eye unlike JE) + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.UP, MAX_PUSH_DISTANCE); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/GlassPaneAndIronBarsCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/GlassPaneAndIronBarsCollision.java similarity index 61% rename from core/src/main/java/org/geysermc/geyser/translator/collision/GlassPaneAndIronBarsCollision.java rename to core/src/main/java/org/geysermc/geyser/translator/collision/fixes/GlassPaneAndIronBarsCollision.java index 35874348c..f32303077 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/collision/GlassPaneAndIronBarsCollision.java +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/GlassPaneAndIronBarsCollision.java @@ -23,17 +23,23 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.translator.collision; +package org.geysermc.geyser.translator.collision.fixes; import lombok.EqualsAndHashCode; import org.geysermc.geyser.level.block.property.Properties; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; @EqualsAndHashCode(callSuper = true) @CollisionRemapper(regex = "glass_pane$|iron_bars$", usesParams = true, passDefaultBoxes = true) public class GlassPaneAndIronBarsCollision extends BlockCollision { + private final static double MAX_PUSH_DISTANCE = 0.0625 + CollisionManager.COLLISION_TOLERANCE * 1.01; + /** * 1 = north * 2 = east @@ -68,46 +74,23 @@ public class GlassPaneAndIronBarsCollision extends BlockCollision { } @Override - public boolean correctPosition(GeyserSession session, int x, int y, int z, BoundingBox playerCollision) { - boolean result = super.correctPosition(session, x, y, z, playerCollision); - playerCollision.setSizeX(playerCollision.getSizeX() - 0.0001); - playerCollision.setSizeY(playerCollision.getSizeY() - 0.0001); - playerCollision.setSizeZ(playerCollision.getSizeZ() - 0.0001); - - if (this.checkIntersection(x, y, z, playerCollision)) { - double newMiddleX = x; - double newMiddleZ = z; - - switch (facing) { - case 1 -> newMiddleZ += 0.8625; // North - case 2 -> newMiddleX += 0.1375; // East - case 3 -> newMiddleZ += 0.1375; // South - case 4 -> newMiddleX += 0.8625; // West - case 5 -> { // North, East - newMiddleZ += 0.8625; - newMiddleX += 0.1375; - } - case 6 -> { // East, South - newMiddleX += 0.1375; - newMiddleZ += 0.1375; - } - case 7 -> { // South, West - newMiddleZ += 0.1375; - newMiddleX += 0.8625; - } - case 8 -> { // West, North - newMiddleX += 0.8625; - newMiddleZ += 0.8625; - } - } - - playerCollision.setMiddleX(newMiddleX); - playerCollision.setMiddleZ(newMiddleZ); + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { + // Check for glass pane/iron bars bug (pane/iron bars is 0.5 blocks thick on Bedrock but 0.5625 on Java when only 1 side is connected). + // Also, we want to flip the direction since the direction here is indicating the block side the glass is connected to. + if (this.facing == 2 || this.facing == 6 || this.facing == 5) { // East + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.WEST, MAX_PUSH_DISTANCE); } - playerCollision.setSizeX(playerCollision.getSizeX() + 0.0001); - playerCollision.setSizeY(playerCollision.getSizeY() + 0.0001); - playerCollision.setSizeZ(playerCollision.getSizeZ() + 0.0001); - return result; + if (this.facing == 1 || this.facing == 5 || this.facing == 8) { // North. + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.SOUTH, MAX_PUSH_DISTANCE); + } + + if (this.facing == 3 || this.facing == 6 || this.facing == 7) { // South + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.NORTH, MAX_PUSH_DISTANCE); + } + + if (this.facing == 4 || this.facing == 7 || this.facing == 8) { // West + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.EAST, MAX_PUSH_DISTANCE); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/LanternCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/LanternCollision.java new file mode 100644 index 000000000..2cc353fec --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/LanternCollision.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 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.geyser.translator.collision.fixes; + +import lombok.EqualsAndHashCode; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; + +@EqualsAndHashCode(callSuper = true) +@CollisionRemapper(regex = "^lantern$|^soul_lantern$", usesParams = true, passDefaultBoxes = true) +public class LanternCollision extends BlockCollision { + private final static double MAX_PUSH_DISTANCE = 0.0625 + CollisionManager.COLLISION_TOLERANCE * 1.01; + + private final boolean hanging; + + public LanternCollision(BlockState state, BoundingBox[] boxes) { + super(boxes); + + this.hanging = state.getValue(Properties.HANGING); + } + + @Override + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { + // Check for lantern collision (lantern is 0.0625 block higher on Java) + if (this.hanging) { + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.DOWN, MAX_PUSH_DISTANCE); + } else { + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.UP, MAX_PUSH_DISTANCE); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/ScaffoldingCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ScaffoldingCollision.java similarity index 71% rename from core/src/main/java/org/geysermc/geyser/translator/collision/ScaffoldingCollision.java rename to core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ScaffoldingCollision.java index 7449987c6..7a0265303 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/collision/ScaffoldingCollision.java +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/ScaffoldingCollision.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 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 @@ -23,12 +23,17 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.translator.collision; +package org.geysermc.geyser.translator.collision.fixes; import lombok.EqualsAndHashCode; +import org.geysermc.geyser.level.block.property.Properties; import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.Axis; import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; /** * In order for scaffolding to work on Bedrock, entity flags need to be sent to the player @@ -36,8 +41,12 @@ import org.geysermc.geyser.session.GeyserSession; @EqualsAndHashCode(callSuper = true) @CollisionRemapper(regex = "^scaffolding$", usesParams = true, passDefaultBoxes = true) public class ScaffoldingCollision extends BlockCollision { + private final boolean bottom; + public ScaffoldingCollision(BlockState state, BoundingBox[] defaultBoxes) { super(defaultBoxes); + + this.bottom = state.getValue(Properties.BOTTOM) && state.getValue(Properties.STABILITY_DISTANCE) != 0; } @Override @@ -51,6 +60,17 @@ public class ScaffoldingCollision extends BlockCollision { playerCollision.setSizeY(playerCollision.getSizeY() + 0.001); playerCollision.setMiddleY(playerCollision.getMiddleY() - 0.002); + boolean canStandOn = playerCollision.getMin(Axis.Y) >= y + 1 && !session.isSneaking(); + + // If these condition are met, then the scaffolding will have a bottom collision that is 0.125 block height. + // However, this is not the case in Bedrock, so we push the player up by 0.125 blocks so player won't get setback. + if (!canStandOn && this.bottom && playerCollision.getMin(Axis.Y) >= y) { + double distance = y + 0.125 - (playerCollision.getMin(Axis.Y)) ; + if (Math.abs(distance) < 0.125F + CollisionManager.COLLISION_TOLERANCE * 1.01F) { + playerCollision.translate(0, distance, 0); + } + } + if (intersected) { session.getCollisionManager().setTouchingScaffolding(true); session.getCollisionManager().setOnScaffolding(true); diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/SeaPickleCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/SeaPickleCollision.java new file mode 100644 index 000000000..ab407a71e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/SeaPickleCollision.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 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.geyser.translator.collision.fixes; + +import lombok.EqualsAndHashCode; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.Axis; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; + +@EqualsAndHashCode(callSuper = true) +@CollisionRemapper(regex = "^sea_pickle$", passDefaultBoxes = true) +public class SeaPickleCollision extends BlockCollision { + public SeaPickleCollision(BlockState state, BoundingBox[] boxes) { + super(boxes); + } + + @Override + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { + // Check for sea pickle bug (sea pickle have no collision on Bedrock but does on Java). + double maxY = blockCollision.getMax(Axis.Y) - y; + blockCollision.pushOutOfBoundingBox(playerCollision, Direction.UP, maxY + CollisionManager.COLLISION_TOLERANCE * 1.01F); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/TrapdoorCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/TrapdoorCollision.java similarity index 60% rename from core/src/main/java/org/geysermc/geyser/translator/collision/TrapdoorCollision.java rename to core/src/main/java/org/geysermc/geyser/translator/collision/fixes/TrapdoorCollision.java index 909761166..67330c040 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/collision/TrapdoorCollision.java +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/fixes/TrapdoorCollision.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 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 @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.translator.collision; +package org.geysermc.geyser.translator.collision.fixes; import lombok.EqualsAndHashCode; import org.geysermc.geyser.level.block.property.Properties; @@ -32,10 +32,14 @@ import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.level.physics.Direction; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.CollisionRemapper; @EqualsAndHashCode(callSuper = true) @CollisionRemapper(regex = "_trapdoor$", usesParams = true, passDefaultBoxes = true) public class TrapdoorCollision extends BlockCollision { + private final static double MAX_PUSH_DISTANCE = 0.005 + CollisionManager.COLLISION_TOLERANCE * 1.01; + private final Direction facing; public TrapdoorCollision(BlockState state, BoundingBox[] defaultBoxes) { @@ -52,32 +56,8 @@ public class TrapdoorCollision extends BlockCollision { } @Override - public boolean correctPosition(GeyserSession session, int x, int y, int z, BoundingBox playerCollision) { - boolean result = super.correctPosition(session, x, y, z, playerCollision); - // Check for door bug (doors are 0.1875 blocks thick on Java but 0.1825 blocks thick on Bedrock) - if (this.checkIntersection(x, y, z, playerCollision)) { - switch (facing) { - case NORTH: - playerCollision.setMiddleZ(z + 0.5125); - break; - case EAST: - playerCollision.setMiddleX(x + 0.5125); - break; - case SOUTH: - playerCollision.setMiddleZ(z + 0.4875); - break; - case WEST: - playerCollision.setMiddleX(x + 0.4875); - break; - case UP: - // Up-facing trapdoors are handled by the step-up check - break; - case DOWN: - // (top y of trap door) - (trap door thickness) = top y of player - playerCollision.setMiddleY(y + 1 - (3.0 / 16.0) - playerCollision.getSizeY() / 2.0 - CollisionManager.COLLISION_TOLERANCE); - break; - } - } - return result; + protected void correctPosition(GeyserSession session, int x, int y, int z, BoundingBox blockCollision, BoundingBox playerCollision) { + // Check for trapdoor bug (trapdoors are 0.1875 blocks thick on Java but 0.1825 blocks thick on Bedrock) + blockCollision.pushOutOfBoundingBox(playerCollision, facing, MAX_PUSH_DISTANCE); } }