aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/unapplied/server/1025-Collision-optimisations.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/unapplied/server/1025-Collision-optimisations.patch')
-rw-r--r--patches/unapplied/server/1025-Collision-optimisations.patch4726
1 files changed, 0 insertions, 4726 deletions
diff --git a/patches/unapplied/server/1025-Collision-optimisations.patch b/patches/unapplied/server/1025-Collision-optimisations.patch
deleted file mode 100644
index 0c6a99e1b7..0000000000
--- a/patches/unapplied/server/1025-Collision-optimisations.patch
+++ /dev/null
@@ -1,4726 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <[email protected]>
-Date: Mon, 4 May 2020 10:06:24 -0700
-Subject: [PATCH] Collision optimisations
-
-The collision patch has been designed with the assumption that
-most shapes are either a single AABB or an ArrayVoxelShape
-(typical voxel bitset representation). Like previously,
-single AABB shapes are treated as AABBs. Unlike previously, the
-VoxelShape class has been changed to carry shape data that
-ArrayVoxelShape would, except in a discrete manner rather
-than abstracted away (not hidden behind DoubleList and
-the poorly named DiscreteVoxelShape).
-
-VoxelShape now carries three important states:
- 1. The voxel bitset + its sizes for the X, Y, and Z axis
- 2. The voxel coordinates (represented as an array and an offset per axis)
- 3. Single AABB representation, if possible
-
-Note that if the single AABB representation is present,
-it is used instead of the voxel bitset representation as
-the single AABB representation is a special case of the
-voxel bitset representation and can be optimised as such.
-
-This effectively turns every VoxelShape instance, regardless of
-actual class, into a typical voxel bitset representation.
-This allows all VoxelShape operations to be optimised
-for voxel bitset representations without dealing with the
-abstraction and indirection that was imposed on VoxelShape
-by Mojang. The patch now effectively optimises all VoxelShape
-operations. Below is a list of some of the operations optimised:
- - Shape merging/ORing
- - Shape optimisation
- - Occlusion checking
- - Non-single AABB VoxelShape collisions/intersection
- - Shape raytracing
- - Empty VoxelShape testing
-
-This patch also includes optimisations for raytracing,
-which mostly boil down to removing indirection caused by the
-interface BlockGetter which allows chunk caching.
-
-diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java
-index be668387f65a633c6ac497fca632a4767a1bf3a2..e08f4e39db4ee3fed62e37364d17dcc5c5683504 100644
---- a/src/main/java/io/papermc/paper/util/CachedLists.java
-+++ b/src/main/java/io/papermc/paper/util/CachedLists.java
-@@ -1,8 +1,57 @@
- package io.papermc.paper.util;
-
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.phys.AABB;
-+import org.bukkit.Bukkit;
-+import org.bukkit.craftbukkit.util.UnsafeList;
-+import java.util.List;
-+
- public final class CachedLists {
-
-- public static void reset() {
-+ // Paper start - optimise collisions
-+ static final UnsafeList<AABB> TEMP_COLLISION_LIST = new UnsafeList<>(1024);
-+ static boolean tempCollisionListInUse;
-+
-+ public static UnsafeList<AABB> getTempCollisionList() {
-+ if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) {
-+ return new UnsafeList<>(16);
-+ }
-+ tempCollisionListInUse = true;
-+ return TEMP_COLLISION_LIST;
-+ }
-+
-+ public static void returnTempCollisionList(List<AABB> list) {
-+ if (list != TEMP_COLLISION_LIST) {
-+ return;
-+ }
-+ ((UnsafeList)list).setSize(0);
-+ tempCollisionListInUse = false;
-+ }
-
-+ static final UnsafeList<Entity> TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024);
-+ static boolean tempGetEntitiesListInUse;
-+
-+ public static UnsafeList<Entity> getTempGetEntitiesList() {
-+ if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) {
-+ return new UnsafeList<>(16);
-+ }
-+ tempGetEntitiesListInUse = true;
-+ return TEMP_GET_ENTITIES_LIST;
-+ }
-+
-+ public static void returnTempGetEntitiesList(List<Entity> list) {
-+ if (list != TEMP_GET_ENTITIES_LIST) {
-+ return;
-+ }
-+ ((UnsafeList)list).setSize(0);
-+ tempGetEntitiesListInUse = false;
-+ }
-+ // Paper end - optimise collisions
-+
-+ public static void reset() {
-+ // Paper start - optimise collisions
-+ TEMP_COLLISION_LIST.completeReset();
-+ TEMP_GET_ENTITIES_LIST.completeReset();
-+ // Paper end - optimise collisions
- }
- }
-diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ee8e9c0e3690e78f3cc621ddfca89ea4256d4803
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java
-@@ -0,0 +1,1851 @@
-+package io.papermc.paper.util;
-+
-+import io.papermc.paper.util.collisions.CachedShapeData;
-+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
-+import it.unimi.dsi.fastutil.doubles.DoubleList;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.item.Item;
-+import net.minecraft.world.level.CollisionGetter;
-+import net.minecraft.world.level.EntityGetter;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.border.WorldBorder;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.PalettedContainer;
-+import net.minecraft.world.level.material.FluidState;
-+import net.minecraft.world.phys.AABB;
-+import net.minecraft.world.phys.Vec3;
-+import net.minecraft.world.phys.shapes.ArrayVoxelShape;
-+import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
-+import net.minecraft.world.phys.shapes.BooleanOp;
-+import net.minecraft.world.phys.shapes.CollisionContext;
-+import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
-+import net.minecraft.world.phys.shapes.EntityCollisionContext;
-+import net.minecraft.world.phys.shapes.OffsetDoubleList;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.Arrays;
-+import java.util.List;
-+import java.util.function.BiPredicate;
-+import java.util.function.Predicate;
-+
-+public final class CollisionUtil {
-+
-+ public static final double COLLISION_EPSILON = 1.0E-7;
-+ public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 });
-+
-+ public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) {
-+ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON;
-+ }
-+
-+ public static boolean isEmpty(final AABB aabb) {
-+ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON;
-+ }
-+
-+ public static boolean isEmpty(final double minX, final double minY, final double minZ,
-+ final double maxX, final double maxY, final double maxZ) {
-+ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON;
-+ }
-+
-+ public static AABB getBoxForChunk(final int chunkX, final int chunkZ) {
-+ double x = (double)(chunkX << 4);
-+ double z = (double)(chunkZ << 4);
-+ // use a bounding box bigger than the chunk to prevent entities from entering it on move
-+ return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON,
-+ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON), false);
-+ }
-+
-+ /*
-+ A couple of rules for VoxelShape collisions:
-+ Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement
-+ checks.
-+ If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite
-+ direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code
-+ will automatically round it to 0.
-+ */
-+
-+ public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1,
-+ final double maxY1, final double maxZ1, final double minX2, final double minY2,
-+ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) {
-+ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON &&
-+ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON &&
-+ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON;
-+ }
-+
-+ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ,
-+ final double maxX, final double maxY, final double maxZ) {
-+ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON &&
-+ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON &&
-+ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON;
-+ }
-+
-+ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) {
-+ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON &&
-+ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON &&
-+ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON;
-+ }
-+
-+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideX(final AABB target, final AABB source, final double source_move) {
-+ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON &&
-+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideY(final AABB target, final AABB source, final double source_move) {
-+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
-+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideZ(final AABB target, final AABB source, final double source_move) {
-+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
-+ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) {
-+ if (source_move >= 0.0) {
-+ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision
-+ if (max_move < -COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.min(max_move, source_move);
-+ } else {
-+ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision
-+ if (max_move > COLLISION_EPSILON) {
-+ return source_move;
-+ }
-+ return Math.max(max_move, source_move);
-+ }
-+ }
-+ return source_move;
-+ }
-+
-+ // startIndex and endIndex inclusive
-+ // assumes indices are in range of array
-+ private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) {
-+ do {
-+ final int middle = (startIndex + endIndex) >>> 1;
-+ final double middleVal = values[middle];
-+
-+ if (value < middleVal) {
-+ endIndex = middle - 1;
-+ } else {
-+ startIndex = middle + 1;
-+ }
-+ } while (startIndex <= endIndex);
-+
-+ return startIndex - 1;
-+ }
-+
-+ public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) {
-+ if (voxel.isEmpty()) {
-+ return false;
-+ }
-+
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = voxel.offsetX();
-+ final double off_y = voxel.offsetY();
-+ final double off_z = voxel.offsetZ();
-+
-+ final double[] coords_x = voxel.rootCoordinatesX();
-+ final double[] coords_y = voxel.rootCoordinatesY();
-+ final double[] coords_z = voxel.rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
-+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_x = Math.max(
-+ 0,
-+ findFloor(coords_x, (aabb.minX - off_x) + COLLISION_EPSILON, 0, size_x)
-+ );
-+ if (floor_min_x >= size_x) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int ceil_max_x = Math.min(
-+ size_x,
-+ findFloor(coords_x, (aabb.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
-+ );
-+ if (floor_min_x >= ceil_max_x) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int floor_min_y = Math.max(
-+ 0,
-+ findFloor(coords_y, (aabb.minY - off_y) + COLLISION_EPSILON, 0, size_y)
-+ );
-+ if (floor_min_y >= size_y) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int ceil_max_y = Math.min(
-+ size_y,
-+ findFloor(coords_y, (aabb.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
-+ );
-+ if (floor_min_y >= ceil_max_y) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int floor_min_z = Math.max(
-+ 0,
-+ findFloor(coords_z, (aabb.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
-+ );
-+ if (floor_min_z >= size_z) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final int ceil_max_z = Math.min(
-+ size_z,
-+ findFloor(coords_z, (aabb.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
-+ );
-+ if (floor_min_z >= ceil_max_z) {
-+ // cannot intersect
-+ return false;
-+ }
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ // check bitset to check if any shapes in range are full
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return true;
-+ }
-+ }
-+ }
-+ }
-+
-+ return false;
-+ }
-+
-+ // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideX(final VoxelShape target, final AABB source, final double source_move) {
-+ final AABB single_aabb = target.getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return collideX(single_aabb, source, source_move);
-+ }
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = target.offsetX();
-+ final double off_y = target.offsetY();
-+ final double off_z = target.offsetZ();
-+
-+ final double[] coords_x = target.rootCoordinatesX();
-+ final double[] coords_y = target.rootCoordinatesY();
-+ final double[] coords_z = target.rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = target.getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+
-+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
-+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_y = Math.max(
-+ 0,
-+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y)
-+ );
-+ if (floor_min_y >= size_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_y = Math.min(
-+ size_y,
-+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
-+ );
-+ if (floor_min_y >= ceil_max_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int floor_min_z = Math.max(
-+ 0,
-+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
-+ );
-+ if (floor_min_z >= size_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_z = Math.min(
-+ size_z,
-+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
-+ );
-+ if (floor_min_z >= ceil_max_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ if (source_move > 0.0) {
-+ final double source_max = source.maxX - off_x;
-+ final int ceil_max_x = findFloor(
-+ coords_x, source_max - COLLISION_EPSILON, 0, size_x
-+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index size on the collision axis for forward movement
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) {
-+ double max_dist = coords_x[curr_x] - source_max;
-+ if (max_dist >= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.min(max_dist, source_move);
-+ }
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ } else {
-+ final double source_min = source.minX - off_x;
-+ final int floor_min_x = findFloor(
-+ coords_x, source_min + COLLISION_EPSILON, 0, size_x
-+ );
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
-+
-+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
-+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
-+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
-+ final int mul_x = size_y*size_z;
-+ for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) {
-+ double max_dist = coords_x[curr_x + 1] - source_min;
-+ if (max_dist <= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.max(max_dist, source_move);
-+ }
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ }
-+ }
-+
-+ public static double collideY(final VoxelShape target, final AABB source, final double source_move) {
-+ final AABB single_aabb = target.getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return collideY(single_aabb, source, source_move);
-+ }
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = target.offsetX();
-+ final double off_y = target.offsetY();
-+ final double off_z = target.offsetZ();
-+
-+ final double[] coords_x = target.rootCoordinatesX();
-+ final double[] coords_y = target.rootCoordinatesY();
-+ final double[] coords_z = target.rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = target.getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+
-+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
-+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_x = Math.max(
-+ 0,
-+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x)
-+ );
-+ if (floor_min_x >= size_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_x = Math.min(
-+ size_x,
-+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
-+ );
-+ if (floor_min_x >= ceil_max_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int floor_min_z = Math.max(
-+ 0,
-+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
-+ );
-+ if (floor_min_z >= size_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_z = Math.min(
-+ size_z,
-+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
-+ );
-+ if (floor_min_z >= ceil_max_z) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ if (source_move > 0.0) {
-+ final double source_max = source.maxY - off_y;
-+ final int ceil_max_y = findFloor(
-+ coords_y, source_max - COLLISION_EPSILON, 0, size_y
-+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index size on the collision axis for forward movement
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) {
-+ double max_dist = coords_y[curr_y] - source_max;
-+ if (max_dist >= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.min(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ } else {
-+ final double source_min = source.minY - off_y;
-+ final int floor_min_y = findFloor(
-+ coords_y, source_min + COLLISION_EPSILON, 0, size_y
-+ );
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
-+
-+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
-+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
-+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
-+ final int mul_x = size_y*size_z;
-+ for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) {
-+ double max_dist = coords_y[curr_y + 1] - source_min;
-+ if (max_dist <= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.max(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ }
-+ }
-+
-+ public static double collideZ(final VoxelShape target, final AABB source, final double source_move) {
-+ final AABB single_aabb = target.getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return collideZ(single_aabb, source, source_move);
-+ }
-+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
-+
-+ // offsets that should be applied to coords
-+ final double off_x = target.offsetX();
-+ final double off_y = target.offsetY();
-+ final double off_z = target.offsetZ();
-+
-+ final double[] coords_x = target.rootCoordinatesX();
-+ final double[] coords_y = target.rootCoordinatesY();
-+ final double[] coords_z = target.rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = target.getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: voxel bitset with set index (x, y, z) indicates that
-+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
-+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
-+
-+
-+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
-+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
-+ // note: for intersection, one we find the floor of the min we can use that as the start index
-+ // for the next check as source max >= source min
-+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
-+ // as this implies that coords[coords.length - 1] < source min
-+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
-+
-+ final int floor_min_x = Math.max(
-+ 0,
-+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x)
-+ );
-+ if (floor_min_x >= size_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_x = Math.min(
-+ size_x,
-+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
-+ );
-+ if (floor_min_x >= ceil_max_x) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int floor_min_y = Math.max(
-+ 0,
-+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y)
-+ );
-+ if (floor_min_y >= size_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ final int ceil_max_y = Math.min(
-+ size_y,
-+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
-+ );
-+ if (floor_min_y >= ceil_max_y) {
-+ // cannot intersect
-+ return source_move;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ if (source_move > 0.0) {
-+ final double source_max = source.maxZ - off_z;
-+ final int ceil_max_z = findFloor(
-+ coords_z, source_max - COLLISION_EPSILON, 0, size_z
-+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index size on the collision axis for forward movement
-+
-+ final int mul_x = size_y*size_z;
-+ for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) {
-+ double max_dist = coords_z[curr_z] - source_max;
-+ if (max_dist >= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.min(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ } else {
-+ final double source_min = source.minZ - off_z;
-+ final int floor_min_z = findFloor(
-+ coords_z, source_min + COLLISION_EPSILON, 0, size_z
-+ );
-+
-+ // note: only the order of the first loop matters
-+
-+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
-+
-+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
-+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
-+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
-+ final int mul_x = size_y*size_z;
-+ for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) {
-+ double max_dist = coords_z[curr_z + 1] - source_min;
-+ if (max_dist <= source_move) {
-+ // if we reach here, then we will never have a case where
-+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
-+ // thus, we can return immediately
-+
-+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
-+ // a calculated max index based off of source_move - so it would be possible to check
-+ // the whole intersected shape for collisions when we didn't need to!
-+ return source_move;
-+ }
-+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
-+ max_dist = Math.max(max_dist, source_move);
-+ }
-+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
-+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
-+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
-+ // note: JLS states long shift operators ANDS shift by 63
-+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
-+ return max_dist;
-+ }
-+ }
-+ }
-+ }
-+
-+ return source_move;
-+ }
-+ }
-+
-+ // does not use epsilon
-+ public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) {
-+ return strictlyContains(voxel, point.x, point.y, point.z);
-+ }
-+
-+ // does not use epsilon
-+ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) {
-+ final AABB single_aabb = voxel.getSingleAABBRepresentation();
-+ if (single_aabb != null) {
-+ return single_aabb.contains(x, y, z);
-+ }
-+
-+ if (voxel.isEmpty()) {
-+ // bitset is clear, no point in searching
-+ return false;
-+ }
-+
-+ // offset input
-+ x -= voxel.offsetX();
-+ y -= voxel.offsetY();
-+ z -= voxel.offsetZ();
-+
-+ final double[] coords_x = voxel.rootCoordinatesX();
-+ final double[] coords_y = voxel.rootCoordinatesY();
-+ final double[] coords_z = voxel.rootCoordinatesZ();
-+
-+ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData();
-+
-+ // note: size = coords.length - 1
-+ final int size_x = cached_shape_data.sizeX();
-+ final int size_y = cached_shape_data.sizeY();
-+ final int size_z = cached_shape_data.sizeZ();
-+
-+ // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max.
-+ // specifically, it cannot collide on the max bounds of the shape
-+
-+ final int index_x = findFloor(coords_x, x, 0, size_x);
-+ if (index_x < 0 || index_x >= size_x) {
-+ return false;
-+ }
-+
-+ final int index_y = findFloor(coords_y, y, 0, size_y);
-+ if (index_y < 0 || index_y >= size_y) {
-+ return false;
-+ }
-+
-+ final int index_z = findFloor(coords_z, z, 0, size_z);
-+ if (index_z < 0 || index_z >= size_z) {
-+ return false;
-+ }
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ final int index = index_z + index_y*size_z + index_x*(size_z*size_y);
-+
-+ final long[] bitset = cached_shape_data.voxelSet();
-+
-+ return (bitset[index >>> 6] & (1L << index)) != 0L;
-+ }
-+
-+ private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) {
-+ // idx ff -> 0
-+ // idx ft -> 1
-+ // idx tf -> 2
-+ // idx tt -> 3
-+ return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3);
-+ }
-+
-+ private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond,
-+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY,
-+ final MergedVoxelCoordinateList mergedZ,
-+ final int booleanOp) {
-+ final int sizeX = mergedX.voxels;
-+ final int sizeY = mergedY.voxels;
-+ final int sizeZ = mergedZ.voxels;
-+
-+ final long[] s1Voxels = shapeDataFirst.voxelSet();
-+ final long[] s2Voxels = shapeDataSecond.voxelSet();
-+
-+ final int s1Mul1 = shapeDataFirst.sizeZ();
-+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
-+
-+ final int s2Mul1 = shapeDataSecond.sizeZ();
-+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
-+
-+ // note: indices may contain -1, but nothing > size
-+ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ);
-+
-+ boolean empty = true;
-+
-+ int mergedIdx = 0;
-+ for (int idxX = 0; idxX < sizeX; ++idxX) {
-+ final int s1x = mergedX.firstIndices[idxX];
-+ final int s2x = mergedX.secondIndices[idxX];
-+ boolean setX = false;
-+ for (int idxY = 0; idxY < sizeY; ++idxY) {
-+ final int s1y = mergedY.firstIndices[idxY];
-+ final int s2y = mergedY.secondIndices[idxY];
-+ boolean setY = false;
-+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
-+ final int s1z = mergedZ.firstIndices[idxZ];
-+ final int s2z = mergedZ.secondIndices[idxZ];
-+
-+ int idx;
-+
-+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
-+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
-+
-+ // idx ff -> 0
-+ // idx ft -> 1
-+ // idx tf -> 2
-+ // idx tt -> 3
-+
-+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
-+ setY |= res;
-+ setX |= res;
-+
-+ if (res) {
-+ empty = false;
-+ // inline and optimize fill operation
-+ ret.zMin = Math.min(ret.zMin, idxZ);
-+ ret.zMax = Math.max(ret.zMax, idxZ + 1);
-+ ret.storage.set(mergedIdx);
-+ }
-+
-+ ++mergedIdx;
-+ }
-+ if (setY) {
-+ ret.yMin = Math.min(ret.yMin, idxY);
-+ ret.yMax = Math.max(ret.yMax, idxY + 1);
-+ }
-+ }
-+ if (setX) {
-+ ret.xMin = Math.min(ret.xMin, idxX);
-+ ret.xMax = Math.max(ret.xMax, idxX + 1);
-+ }
-+ }
-+
-+ return empty ? null : ret;
-+ }
-+
-+ private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond,
-+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY,
-+ final MergedVoxelCoordinateList mergedZ,
-+ final int booleanOp) {
-+ final int sizeX = mergedX.voxels;
-+ final int sizeY = mergedY.voxels;
-+ final int sizeZ = mergedZ.voxels;
-+
-+ final long[] s1Voxels = shapeDataFirst.voxelSet();
-+ final long[] s2Voxels = shapeDataSecond.voxelSet();
-+
-+ final int s1Mul1 = shapeDataFirst.sizeZ();
-+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
-+
-+ final int s2Mul1 = shapeDataSecond.sizeZ();
-+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
-+
-+ // note: indices may contain -1, but nothing > size
-+ for (int idxX = 0; idxX < sizeX; ++idxX) {
-+ final int s1x = mergedX.firstIndices[idxX];
-+ final int s2x = mergedX.secondIndices[idxX];
-+ for (int idxY = 0; idxY < sizeY; ++idxY) {
-+ final int s1y = mergedY.firstIndices[idxY];
-+ final int s2y = mergedY.secondIndices[idxY];
-+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
-+ final int s1z = mergedZ.firstIndices[idxZ];
-+ final int s2z = mergedZ.secondIndices[idxZ];
-+
-+ int idx;
-+
-+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
-+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
-+
-+ // idx ff -> 0
-+ // idx ft -> 1
-+ // idx tf -> 2
-+ // idx tt -> 3
-+
-+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
-+
-+ if (res) {
-+ return false;
-+ }
-+ }
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
-+ return joinUnoptimized(first, second, operator).optimize();
-+ }
-+
-+ public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
-+ final boolean ff = operator.apply(false, false);
-+ if (ff) {
-+ // technically, should be an infinite box but that's clearly an error
-+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
-+ }
-+
-+ final boolean tt = operator.apply(true, true);
-+
-+ if (first == second) {
-+ return tt ? first : Shapes.empty();
-+ }
-+
-+ final boolean ft = operator.apply(false, true);
-+ final boolean tf = operator.apply(true, false);
-+
-+ if (first.isEmpty()) {
-+ return ft ? second : Shapes.empty();
-+ }
-+ if (second.isEmpty()) {
-+ return tf ? first : Shapes.empty();
-+ }
-+
-+ if (!tt) {
-+ // try to check for no intersection, since tt = false
-+ final AABB aabbF = first.getSingleAABBRepresentation();
-+ final AABB aabbS = second.getSingleAABBRepresentation();
-+
-+ final boolean intersect;
-+
-+ final boolean hasAABBF = aabbF != null;
-+ final boolean hasAABBS = aabbS != null;
-+ if (hasAABBF | hasAABBS) {
-+ if (hasAABBF & hasAABBS) {
-+ intersect = voxelShapeIntersect(aabbF, aabbS);
-+ } else if (hasAABBF) {
-+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
-+ } else {
-+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
-+ }
-+ } else {
-+ // expect cached bounds
-+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
-+ }
-+
-+ if (!intersect) {
-+ if (!tf & !ft) {
-+ return Shapes.empty();
-+ }
-+ if (!tf | !ft) {
-+ return tf ? first : second;
-+ }
-+ }
-+ }
-+
-+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge(
-+ first.rootCoordinatesX(), first.offsetX(),
-+ second.rootCoordinatesX(), second.offsetX(),
-+ ft, tf
-+ );
-+ if (mergedX == MergedVoxelCoordinateList.EMPTY) {
-+ return Shapes.empty();
-+ }
-+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge(
-+ first.rootCoordinatesY(), first.offsetY(),
-+ second.rootCoordinatesY(), second.offsetY(),
-+ ft, tf
-+ );
-+ if (mergedY == MergedVoxelCoordinateList.EMPTY) {
-+ return Shapes.empty();
-+ }
-+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge(
-+ first.rootCoordinatesZ(), first.offsetZ(),
-+ second.rootCoordinatesZ(), second.offsetZ(),
-+ ft, tf
-+ );
-+ if (mergedZ == MergedVoxelCoordinateList.EMPTY) {
-+ return Shapes.empty();
-+ }
-+
-+ final CachedShapeData shapeDataFirst = first.getCachedVoxelData();
-+ final CachedShapeData shapeDataSecond = second.getCachedVoxelData();
-+
-+ final BitSetDiscreteVoxelShape mergedShape = merge(
-+ shapeDataFirst, shapeDataSecond,
-+ mergedX, mergedY, mergedZ,
-+ makeBitset(ft, tf, tt)
-+ );
-+
-+ if (mergedShape == null) {
-+ return Shapes.empty();
-+ }
-+
-+ return new ArrayVoxelShape(
-+ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords()
-+ );
-+ }
-+
-+ public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
-+ final boolean ff = operator.apply(false, false);
-+ if (ff) {
-+ // technically, should be an infinite box but that's clearly an error
-+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
-+ }
-+ final boolean firstEmpty = first.isEmpty();
-+ final boolean secondEmpty = second.isEmpty();
-+ if (firstEmpty | secondEmpty) {
-+ return operator.apply(!firstEmpty, !secondEmpty);
-+ }
-+
-+ final boolean tt = operator.apply(true, true);
-+
-+ if (first == second) {
-+ return tt;
-+ }
-+
-+ final boolean ft = operator.apply(false, true);
-+ final boolean tf = operator.apply(true, false);
-+
-+ // try to check intersection
-+ final AABB aabbF = first.getSingleAABBRepresentation();
-+ final AABB aabbS = second.getSingleAABBRepresentation();
-+
-+ final boolean intersect;
-+
-+ final boolean hasAABBF = aabbF != null;
-+ final boolean hasAABBS = aabbS != null;
-+ if (hasAABBF | hasAABBS) {
-+ if (hasAABBF & hasAABBS) {
-+ intersect = voxelShapeIntersect(aabbF, aabbS);
-+ } else if (hasAABBF) {
-+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
-+ } else {
-+ // hasAABBS -> true
-+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
-+ }
-+
-+ if (!intersect) {
-+ // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty
-+ return tf | ft;
-+ } else if (tt) {
-+ // intersect = true && tt = true -> non-empty merged shape
-+ return true;
-+ }
-+ } else {
-+ // expect cached bounds
-+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
-+ if (!intersect) {
-+ // is only non-empty if we take from first or second, as there is no intersection
-+ return tf | ft;
-+ }
-+ }
-+
-+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge(
-+ first.rootCoordinatesX(), first.offsetX(),
-+ second.rootCoordinatesX(), second.offsetX(),
-+ ft, tf
-+ );
-+ if (mergedX == MergedVoxelCoordinateList.EMPTY) {
-+ return false;
-+ }
-+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge(
-+ first.rootCoordinatesY(), first.offsetY(),
-+ second.rootCoordinatesY(), second.offsetY(),
-+ ft, tf
-+ );
-+ if (mergedY == MergedVoxelCoordinateList.EMPTY) {
-+ return false;
-+ }
-+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge(
-+ first.rootCoordinatesZ(), first.offsetZ(),
-+ second.rootCoordinatesZ(), second.offsetZ(),
-+ ft, tf
-+ );
-+ if (mergedZ == MergedVoxelCoordinateList.EMPTY) {
-+ return false;
-+ }
-+
-+ final CachedShapeData shapeDataFirst = first.getCachedVoxelData();
-+ final CachedShapeData shapeDataSecond = second.getCachedVoxelData();
-+
-+ return !isMergeEmpty(
-+ shapeDataFirst, shapeDataSecond,
-+ mergedX, mergedY, mergedZ,
-+ makeBitset(ft, tf, tt)
-+ );
-+ }
-+
-+ private static final class MergedVoxelCoordinateList {
-+
-+ private static final int[][] SIMPLE_INDICES_CACHE = new int[64][];
-+ static {
-+ for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) {
-+ SIMPLE_INDICES_CACHE[i] = getIndices(i);
-+ }
-+ }
-+
-+ private static final MergedVoxelCoordinateList EMPTY = new MergedVoxelCoordinateList(
-+ new double[] { 0.0 }, 0.0, new int[0], new int[0], 0
-+ );
-+
-+ private static int[] getIndices(final int length) {
-+ final int[] ret = new int[length];
-+
-+ for (int i = 1; i < length; ++i) {
-+ ret[i] = i;
-+ }
-+
-+ return ret;
-+ }
-+
-+ // indices above voxel size are always set to -1
-+ public final double[] coordinates;
-+ public final double coordinateOffset;
-+ public final int[] firstIndices;
-+ public final int[] secondIndices;
-+ public final int voxels;
-+
-+ private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset,
-+ final int[] firstIndices, final int[] secondIndices, final int voxels) {
-+ this.coordinates = coordinates;
-+ this.coordinateOffset = coordinateOffset;
-+ this.firstIndices = firstIndices;
-+ this.secondIndices = secondIndices;
-+ this.voxels = voxels;
-+ }
-+
-+ public DoubleList wrapCoords() {
-+ if (this.coordinateOffset == 0.0) {
-+ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1);
-+ }
-+ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset);
-+ }
-+
-+ // assume coordinates.length > 1
-+ public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) {
-+ final int voxels = coordinates.length - 1;
-+ final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels);
-+
-+ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels);
-+ }
-+
-+ // assume coordinates.length > 1
-+ public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset,
-+ final double[] secondCoordinates, final double secondOffset,
-+ final boolean ft, final boolean tf) {
-+ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) {
-+ return getForSingle(firstCoordinates, firstOffset);
-+ }
-+
-+ final int firstCount = firstCoordinates.length;
-+ final int secondCount = secondCoordinates.length;
-+
-+ final int voxelsFirst = firstCount - 1;
-+ final int voxelsSecond = secondCount - 1;
-+
-+ final int maxCount = firstCount + secondCount;
-+
-+ final double[] coordinates = new double[maxCount];
-+ final int[] firstIndices = new int[maxCount];
-+ final int[] secondIndices = new int[maxCount];
-+
-+ final boolean notTF = !tf;
-+ final boolean notFT = !ft;
-+
-+ int firstIndex = 0;
-+ int secondIndex = 0;
-+ int resultSize = 0;
-+
-+ // note: operations on NaN are false
-+ double last = Double.NaN;
-+
-+ for (;;) {
-+ final boolean noneLeftFirst = firstIndex >= firstCount;
-+ final boolean noneLeftSecond = secondIndex >= secondCount;
-+
-+ if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) {
-+ break;
-+ }
-+
-+ final boolean firstZero = firstIndex == 0;
-+ final boolean secondZero = secondIndex == 0;
-+
-+ final double select;
-+
-+ if (noneLeftFirst) {
-+ // noneLeftSecond -> false
-+ // notFT -> false
-+ select = secondCoordinates[secondIndex] + secondOffset;
-+ ++secondIndex;
-+ } else if (noneLeftSecond) {
-+ // noneLeftFirst -> false
-+ // notTF -> false
-+ select = firstCoordinates[firstIndex] + firstOffset;
-+ ++firstIndex;
-+ } else {
-+ // noneLeftFirst | noneLeftSecond -> false
-+ // notTF -> ??
-+ // notFT -> ??
-+ final boolean breakFirst = notTF & secondZero;
-+ final boolean breakSecond = notFT & firstZero;
-+
-+ final double first = firstCoordinates[firstIndex] + firstOffset;
-+ final double second = secondCoordinates[secondIndex] + secondOffset;
-+ final boolean useFirst = first < (second + COLLISION_EPSILON);
-+ final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond);
-+
-+ select = useFirst ? first : second;
-+ firstIndex += useFirst ? 1 : 0;
-+ secondIndex += 1 ^ (useFirst ? 1 : 0);
-+
-+ if (cont) {
-+ continue;
-+ }
-+ }
-+
-+ int prevFirst = firstIndex - 1;
-+ prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst;
-+ int prevSecond = secondIndex - 1;
-+ prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond;
-+
-+ if (last >= (select - COLLISION_EPSILON)) {
-+ // note: any operations on NaN is false
-+ firstIndices[resultSize - 1] = prevFirst;
-+ secondIndices[resultSize - 1] = prevSecond;
-+ } else {
-+ firstIndices[resultSize] = prevFirst;
-+ secondIndices[resultSize] = prevSecond;
-+ coordinates[resultSize] = select;
-+
-+ ++resultSize;
-+ last = select;
-+ }
-+ }
-+
-+ return resultSize <= 1 ? EMPTY : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1);
-+ }
-+ }
-+
-+ public static AABB offsetX(final AABB box, final double dx) {
-+ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB offsetY(final AABB box, final double dy) {
-+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
-+ }
-+
-+ public static AABB offsetZ(final AABB box, final double dz) {
-+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false);
-+ }
-+
-+ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0
-+ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ);
-+ }
-+
-+ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
-+ }
-+
-+ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0
-+ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0
-+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false);
-+ }
-+
-+ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0
-+ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0
-+ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0
-+ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0
-+ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
-+ }
-+
-+ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0
-+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false);
-+ }
-+
-+ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0
-+ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false);
-+ }
-+
-+ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0
-+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false);
-+ }
-+
-+ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ if (Math.abs(value) < COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideX(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ if (Math.abs(value) < COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideY(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ if (Math.abs(value) < COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ final AABB target = potentialCollisions.get(i);
-+ value = collideZ(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ if (Math.abs(value) < COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ final VoxelShape target = potentialCollisions.get(i);
-+ value = collideX(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ if (Math.abs(value) < COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ final VoxelShape target = potentialCollisions.get(i);
-+ value = collideY(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
-+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
-+ if (Math.abs(value) < COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ final VoxelShape target = potentialCollisions.get(i);
-+ value = collideZ(target, currentBoundingBox, value);
-+ }
-+
-+ return value;
-+ }
-+
-+ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<VoxelShape> potentialCollisions) {
-+ double x = moveVector.x;
-+ double y = moveVector.y;
-+ double z = moveVector.z;
-+
-+ if (y != 0.0) {
-+ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions);
-+ if (y != 0.0) {
-+ axisalignedbb = offsetY(axisalignedbb, y);
-+ }
-+ }
-+
-+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
-+
-+ if (xSmaller && z != 0.0) {
-+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ if (z != 0.0) {
-+ axisalignedbb = offsetZ(axisalignedbb, z);
-+ }
-+ }
-+
-+ if (x != 0.0) {
-+ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions);
-+ if (!xSmaller && x != 0.0) {
-+ axisalignedbb = offsetX(axisalignedbb, x);
-+ }
-+ }
-+
-+ if (!xSmaller && z != 0.0) {
-+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ }
-+
-+ return new Vec3(x, y, z);
-+ }
-+
-+ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<AABB> potentialCollisions) {
-+ double x = moveVector.x;
-+ double y = moveVector.y;
-+ double z = moveVector.z;
-+
-+ if (y != 0.0) {
-+ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions);
-+ if (y != 0.0) {
-+ axisalignedbb = offsetY(axisalignedbb, y);
-+ }
-+ }
-+
-+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
-+
-+ if (xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ if (z != 0.0) {
-+ axisalignedbb = offsetZ(axisalignedbb, z);
-+ }
-+ }
-+
-+ if (x != 0.0) {
-+ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions);
-+ if (!xSmaller && x != 0.0) {
-+ axisalignedbb = offsetX(axisalignedbb, x);
-+ }
-+ }
-+
-+ if (!xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
-+ }
-+
-+ return new Vec3(x, y, z);
-+ }
-+
-+ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb,
-+ final List<VoxelShape> voxels,
-+ final List<AABB> aabbs) {
-+ if (voxels.isEmpty()) {
-+ // fast track only AABBs
-+ return performAABBCollisions(moveVector, axisalignedbb, aabbs);
-+ }
-+
-+ double x = moveVector.x;
-+ double y = moveVector.y;
-+ double z = moveVector.z;
-+
-+ if (y != 0.0) {
-+ y = performAABBCollisionsY(axisalignedbb, y, aabbs);
-+ y = performVoxelCollisionsY(axisalignedbb, y, voxels);
-+ if (y != 0.0) {
-+ axisalignedbb = offsetY(axisalignedbb, y);
-+ }
-+ }
-+
-+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
-+
-+ if (xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
-+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
-+ if (z != 0.0) {
-+ axisalignedbb = offsetZ(axisalignedbb, z);
-+ }
-+ }
-+
-+ if (x != 0.0) {
-+ x = performAABBCollisionsX(axisalignedbb, x, aabbs);
-+ x = performVoxelCollisionsX(axisalignedbb, x, voxels);
-+ if (!xSmaller && x != 0.0) {
-+ axisalignedbb = offsetX(axisalignedbb, x);
-+ }
-+ }
-+
-+ if (!xSmaller && z != 0.0) {
-+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
-+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
-+ }
-+
-+ return new Vec3(x, y, z);
-+ }
-+
-+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) {
-+ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ);
-+ }
-+
-+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX,
-+ final double boxMinZ, final double boxMaxZ) {
-+ // border size is rounded like the collide voxel shape of the border
-+ final double borderMinX = Math.floor(worldborder.getMinX()); // -X
-+ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X
-+
-+ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z
-+ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z
-+
-+ // inverted check for world border enclosing the specified box expanded by -EPSILON
-+ return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON ||
-+ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON;
-+ }
-+
-+ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */
-+ private static double min(final double x, final double y) {
-+ return x < y ? x : y;
-+ }
-+
-+ private static double max(final double x, final double y) {
-+ return x > y ? x : y;
-+ }
-+
-+ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0;
-+ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1;
-+ public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2;
-+ public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3;
-+
-+ public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb,
-+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB,
-+ final int collisionFlags, final BiPredicate<BlockState, BlockPos> predicate) {
-+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0;
-+ boolean ret = false;
-+
-+ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) {
-+ final WorldBorder worldBorder = world.getWorldBorder();
-+ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ final VoxelShape borderShape = worldBorder.getCollisionShape();
-+ intoVoxel.add(borderShape);
-+ ret = true;
-+ }
-+ }
-+ }
-+
-+ final int minSection = world.minSection;
-+
-+ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
-+ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
-+
-+ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1);
-+ final int maxBlockY = Math.min((world.maxSection << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1);
-+
-+ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
-+ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
-+
-+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
-+ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity);
-+
-+ // special cases:
-+ if (minBlockY > maxBlockY) {
-+ // no point in checking
-+ return ret;
-+ }
-+
-+ final int minChunkX = minBlockX >> 4;
-+ final int maxChunkX = maxBlockX >> 4;
-+
-+ final int minChunkY = minBlockY >> 4;
-+ final int maxChunkY = maxBlockY >> 4;
-+
-+ final int minChunkZ = minBlockZ >> 4;
-+ final int maxChunkZ = maxBlockZ >> 4;
-+
-+ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0;
-+ final ServerChunkCache chunkSource = (ServerChunkCache)world.getChunkSource();
-+
-+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
-+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ final ChunkAccess chunk = loadChunks ? chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, true) : chunkSource.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
-+
-+ if (chunk == null) {
-+ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ));
-+ ret = true;
-+ }
-+ }
-+ continue;
-+ }
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+
-+ // bound y
-+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-+ final int sectionIdx = currChunkY - minSection;
-+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
-+ continue;
-+ }
-+ final LevelChunkSection section = sections[sectionIdx];
-+ if (section == null || section.hasOnlyAir()) {
-+ // empty
-+ continue;
-+ }
-+
-+ final boolean hasSpecial = section.getSpecialCollidingBlocks() != 0;
-+ final int sectionAdjust = !hasSpecial ? 1 : 0;
-+
-+ final PalettedContainer<BlockState> blocks = section.states;
-+
-+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0;
-+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15;
-+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0;
-+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15;
-+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0;
-+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15;
-+
-+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
-+ final int blockY = currY | (currChunkY << 4);
-+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
-+ final int blockZ = currZ | (currChunkZ << 4);
-+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
-+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8);
-+ final int blockX = currX | (currChunkX << 4);
-+
-+ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
-+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
-+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0;
-+ if (edgeCount == 3) {
-+ continue;
-+ }
-+
-+ final BlockState blockData = blocks.get(localBlockIndex);
-+
-+ if (blockData.emptyCollisionShape()) {
-+ continue;
-+ }
-+
-+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) {
-+ VoxelShape blockCollision = blockData.getConstantCollisionShape();
-+
-+ if (blockCollision == null) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape);
-+ }
-+
-+ AABB singleAABB = blockCollision.getSingleAABBRepresentation();
-+ if (singleAABB != null) {
-+ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ);
-+ if (!voxelShapeIntersect(aabb, singleAABB)) {
-+ continue;
-+ }
-+
-+ if (predicate != null) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ if (!predicate.test(blockData, mutablePos)) {
-+ continue;
-+ }
-+ }
-+
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ ret = true;
-+ intoAABB.add(singleAABB);
-+ continue;
-+ }
-+ }
-+
-+ if (blockCollision.isEmpty()) {
-+ continue;
-+ }
-+
-+ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
-+
-+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) {
-+ continue;
-+ }
-+
-+ if (predicate != null) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ if (!predicate.test(blockData, mutablePos)) {
-+ continue;
-+ }
-+ }
-+
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ ret = true;
-+ intoVoxel.add(blockCollisionOffset);
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb,
-+ final List<AABB> into, final int collisionFlags, final Predicate<Entity> predicate) {
-+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0;
-+ if (!(getter instanceof EntityGetter entityGetter)) {
-+ return false;
-+ }
-+
-+ boolean ret = false;
-+
-+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
-+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
-+ // specifically with boat collisions.
-+ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON);
-+ final List<Entity> entities;
-+ if (entity != null && entity.hardCollides()) {
-+ entities = entityGetter.getEntities(entity, aabb, predicate);
-+ } else {
-+ entities = entityGetter.getHardCollidingEntities(entity, aabb, predicate);
-+ }
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isSpectator()) {
-+ continue;
-+ }
-+
-+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
-+ if (checkOnly) {
-+ return true;
-+ } else {
-+ into.add(otherEntity.getBoundingBox());
-+ ret = true;
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb,
-+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, final int collisionFlags,
-+ final BiPredicate<BlockState, BlockPos> blockPredicate,
-+ final Predicate<Entity> entityPredicate) {
-+ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) {
-+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
-+ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
-+ } else {
-+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
-+ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
-+ }
-+ }
-+
-+ public static final class LazyEntityCollisionContext extends EntityCollisionContext {
-+
-+ private CollisionContext delegate;
-+ private boolean delegated;
-+
-+ public LazyEntityCollisionContext(final Entity entity) {
-+ super(false, 0.0, null, null, entity);
-+ }
-+
-+ public boolean isDelegated() {
-+ final boolean delegated = this.delegated;
-+ this.delegated = false;
-+ return delegated;
-+ }
-+
-+ public CollisionContext getDelegate() {
-+ this.delegated = true;
-+ final Entity entity = this.getEntity();
-+ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate;
-+ }
-+
-+ @Override
-+ public boolean isDescending() {
-+ return this.getDelegate().isDescending();
-+ }
-+
-+ @Override
-+ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) {
-+ return this.getDelegate().isAbove(shape, pos, defaultValue);
-+ }
-+
-+ @Override
-+ public boolean isHoldingItem(final Item item) {
-+ return this.getDelegate().isHoldingItem(item);
-+ }
-+
-+ @Override
-+ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) {
-+ return this.getDelegate().canStandOnFluid(state, fluidState);
-+ }
-+ }
-+
-+ private CollisionUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..1cb96b09375770f92f3e494ce2f28d9ff8699581
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java
-@@ -0,0 +1,10 @@
-+package io.papermc.paper.util.collisions;
-+
-+public record CachedShapeData(
-+ int sizeX, int sizeY, int sizeZ,
-+ long[] voxelSet,
-+ int minFullX, int minFullY, int minFullZ,
-+ int maxFullX, int maxFullY, int maxFullZ,
-+ boolean isEmpty, boolean hasSingleAABB
-+) {
-+}
-diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..85c448a775f60ca4b4a4f2baf17487ef45bdd383
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java
-@@ -0,0 +1,39 @@
-+package io.papermc.paper.util.collisions;
-+
-+import net.minecraft.world.phys.AABB;
-+import java.util.ArrayList;
-+import java.util.List;
-+
-+public record CachedToAABBs(
-+ List<AABB> aabbs,
-+ boolean isOffset,
-+ double offX, double offY, double offZ
-+) {
-+
-+ public CachedToAABBs removeOffset() {
-+ final List<AABB> toOffset = this.aabbs;
-+ final double offX = this.offX;
-+ final double offY = this.offY;
-+ final double offZ = this.offZ;
-+
-+ final List<AABB> ret = new ArrayList<>(toOffset.size());
-+
-+ for (int i = 0, len = toOffset.size(); i < len; ++i) {
-+ ret.add(toOffset.get(i).move(offX, offY, offZ));
-+ }
-+
-+ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
-+ }
-+
-+ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) {
-+ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) {
-+ return cache;
-+ }
-+
-+ final double resX = cache.offX + offX;
-+ final double resY = cache.offY + offY;
-+ final double resZ = cache.offZ + offZ;
-+
-+ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
-+ }
-+}
-diff --git a/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ff9d2dad39dcc02b2371458b7b5f64c6090e8012
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java
-@@ -0,0 +1,109 @@
-+package io.papermc.paper.util.collisions;
-+
-+import java.util.Objects;
-+
-+public final class FlatBitsetUtil {
-+
-+ private static final int LOG2_LONG = 6;
-+ private static final long ALL_SET = -1L;
-+ private static final int BITS_PER_LONG = Long.SIZE;
-+
-+ // from inclusive
-+ // to exclusive
-+ public static int firstSet(final long[] bitset, final int from, final int to) {
-+ if ((from | to | (to - from)) < 0) {
-+ throw new IndexOutOfBoundsException();
-+ }
-+
-+ int bitsetIdx = from >>> LOG2_LONG;
-+ int bitIdx = from & ~(BITS_PER_LONG - 1);
-+
-+ long tmp = bitset[bitsetIdx] & (ALL_SET << from);
-+ for (;;) {
-+ if (tmp != 0L) {
-+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
-+ return ret >= to ? -1 : ret;
-+ }
-+
-+ bitIdx += BITS_PER_LONG;
-+
-+ if (bitIdx >= to) {
-+ return -1;
-+ }
-+
-+ tmp = bitset[++bitsetIdx];
-+ }
-+ }
-+
-+ // from inclusive
-+ // to exclusive
-+ public static int firstClear(final long[] bitset, final int from, final int to) {
-+ if ((from | to | (to - from)) < 0) {
-+ throw new IndexOutOfBoundsException();
-+ }
-+ // like firstSet, but invert the bitset
-+
-+ int bitsetIdx = from >>> LOG2_LONG;
-+ int bitIdx = from & ~(BITS_PER_LONG - 1);
-+
-+ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
-+ for (;;) {
-+ if (tmp != 0L) {
-+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
-+ return ret >= to ? -1 : ret;
-+ }
-+
-+ bitIdx += BITS_PER_LONG;
-+
-+ if (bitIdx >= to) {
-+ return -1;
-+ }
-+
-+ tmp = ~bitset[++bitsetIdx];
-+ }
-+ }
-+
-+ // from inclusive
-+ // to exclusive
-+ public static void clearRange(final long[] bitset, final int from, int to) {
-+ if ((from | to | (to - from)) < 0) {
-+ throw new IndexOutOfBoundsException();
-+ }
-+
-+ if (from == to) {
-+ return;
-+ }
-+
-+ --to;
-+
-+ final int fromBitsetIdx = from >>> LOG2_LONG;
-+ final int toBitsetIdx = to >>> LOG2_LONG;
-+
-+ final long keepFirst = ~(ALL_SET << from);
-+ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
-+
-+ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
-+
-+ if (fromBitsetIdx == toBitsetIdx) {
-+ // special case: need to keep both first and last
-+ bitset[fromBitsetIdx] &= (keepFirst | keepLast);
-+ } else {
-+ bitset[fromBitsetIdx] &= keepFirst;
-+
-+ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
-+ bitset[i] = 0L;
-+ }
-+
-+ bitset[toBitsetIdx] &= keepLast;
-+ }
-+ }
-+
-+ // from inclusive
-+ // to exclusive
-+ public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
-+ return firstClear(bitset, from, to) == -1;
-+ }
-+
-+
-+ private FlatBitsetUtil() {}
-+}
-diff --git a/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..1f42bdfdb052056e62a939ab0d1944f8a064fe6c
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java
-@@ -0,0 +1,10 @@
-+package io.papermc.paper.util.collisions;
-+
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+
-+public record MergedORCache(
-+ VoxelShape key,
-+ VoxelShape result
-+) {
-+
-+}
-diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java
-index 03c45ee77276462818a6f774b5945b25924aa3f0..ab289a6ca85459e03acb2089c6b9e931caa9c873 100644
---- a/src/main/java/net/minecraft/core/Direction.java
-+++ b/src/main/java/net/minecraft/core/Direction.java
-@@ -60,6 +60,21 @@ public enum Direction implements StringRepresentable {
- private final int adjY;
- private final int adjZ;
- // Paper end - Perf: Inline shift direction fields
-+ // Paper start - optimise collisions
-+ private static final int RANDOM_OFFSET = 2017601568;
-+ private Direction opposite;
-+ static {
-+ for (final Direction direction : VALUES) {
-+ direction.opposite = from3DDataValue(direction.oppositeIndex);
-+ }
-+ }
-+
-+ private final int id = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(this.ordinal() + RANDOM_OFFSET) + RANDOM_OFFSET);
-+
-+ public final int uniqueId() {
-+ return this.id;
-+ }
-+ // Paper end - optimise collisions
-
- private Direction(
- final int id,
-diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-index 54023123e8f46671968f23f05590dd18ed5c2024..09a80b6816e50ea0d733725c59164520136308d6 100644
---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
-+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
-@@ -523,7 +523,7 @@ public class ServerPlayer extends Player {
-
- if (blockposition1 != null) {
- this.moveTo(blockposition1, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored
-- if (world.noCollision((Entity) this)) {
-+ if (world.noCollision(this, this.getBoundingBox(), true)) { // Paper - make sure this loads chunks, we default to NOT loading now
- break;
- }
- }
-@@ -531,7 +531,7 @@ public class ServerPlayer extends Player {
- } else {
- this.moveTo(blockposition, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored
-
-- while (!world.noCollision((Entity) this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) {
-+ while (!world.noCollision(this, this.getBoundingBox(), true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now
- this.setPos(this.getX(), this.getY() + 1.0D, this.getZ());
- }
- }
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 60ba289e724463129dfb27aa5e3b6daf3dd7386e..68446b7532dfbda303293aa9e756644c6fcdffca 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -935,7 +935,7 @@ public abstract class PlayerList {
- entityplayer1.forceSetPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
-
- worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper
-- while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) {
-+ while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Paper - make sure this loads chunks, we default to NOT loading now
- // CraftBukkit end
- entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ());
- }
-diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 644cbdc15cdedb18f5b0eb9d4a2249ba131f89e5..9153c15cd2b8a3811d5f1c16ac2221aea7c3aacd 100644
---- a/src/main/java/net/minecraft/world/entity/Entity.java
-+++ b/src/main/java/net/minecraft/world/entity/Entity.java
-@@ -1260,9 +1260,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- float f = this.getBlockSpeedFactor();
-
- this.setDeltaMovement(this.getDeltaMovement().multiply((double) f, 1.0D, (double) f));
-- if (this.level().getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata2) -> {
-- return iblockdata2.is(BlockTags.FIRE) || iblockdata2.is(Blocks.LAVA);
-- })) {
-+ // Paper start - remove expensive streams from here
-+ boolean noneMatch = true;
-+ AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D);
-+ {
-+ int minX = Mth.floor(fireSearchBox.minX);
-+ int minY = Mth.floor(fireSearchBox.minY);
-+ int minZ = Mth.floor(fireSearchBox.minZ);
-+ int maxX = Mth.floor(fireSearchBox.maxX);
-+ int maxY = Mth.floor(fireSearchBox.maxY);
-+ int maxZ = Mth.floor(fireSearchBox.maxZ);
-+ fire_search_loop:
-+ for (int fz = minZ; fz <= maxZ; ++fz) {
-+ for (int fx = minX; fx <= maxX; ++fx) {
-+ for (int fy = minY; fy <= maxY; ++fy) {
-+ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4);
-+ if (chunk == null) {
-+ // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true
-+ // even if we're in lava/fire
-+ noneMatch = true;
-+ break fire_search_loop;
-+ }
-+ if (!noneMatch) {
-+ // don't do get type, we already know we're in fire - we just need to check the chunks
-+ // loaded state
-+ continue;
-+ }
-+
-+ BlockState type = chunk.getBlockStateFinal(fx, fy, fz);
-+ if (type.is(BlockTags.FIRE) || type.is(Blocks.LAVA)) {
-+ noneMatch = false;
-+ // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded
-+ }
-+ }
-+ }
-+ }
-+ }
-+ if (noneMatch) {
-+ // Paper end - remove expensive streams from here
- if (this.remainingFireTicks <= 0) {
- this.setRemainingFireTicks(-this.getFireImmuneTicks());
- }
-@@ -1442,32 +1477,82 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- }
-
- private Vec3 collide(Vec3 movement) {
-- AABB axisalignedbb = this.getBoundingBox();
-- List<VoxelShape> list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement));
-- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list);
-- boolean flag = movement.x != vec3d1.x;
-- boolean flag1 = movement.y != vec3d1.y;
-- boolean flag2 = movement.z != vec3d1.z;
-- boolean flag3 = this.onGround() || flag1 && movement.y < 0.0D;
-+ // Paper start - optimise collisions
-+ final boolean xZero = movement.x == 0.0;
-+ final boolean yZero = movement.y == 0.0;
-+ final boolean zZero = movement.z == 0.0;
-+ if (xZero & yZero & zZero) {
-+ return movement;
-+ }
-+
-+ final Level world = this.level;
-+ final AABB currBoundingBox = this.getBoundingBox();
-+
-+ if (io.papermc.paper.util.CollisionUtil.isEmpty(currBoundingBox)) {
-+ return movement;
-+ }
-+
-+ final List<AABB> potentialCollisionsBB = new java.util.ArrayList<>();
-+ final List<VoxelShape> potentialCollisionsVoxel = new java.util.ArrayList<>();
-+ final double stepHeight = (double)this.maxUpStep();
-+ final AABB collisionBox;
-+ final boolean onGround = this.onGround;
-+
-+ if (xZero & zZero) {
-+ if (movement.y > 0.0) {
-+ collisionBox = io.papermc.paper.util.CollisionUtil.cutUpwards(currBoundingBox, movement.y);
-+ } else {
-+ collisionBox = io.papermc.paper.util.CollisionUtil.cutDownwards(currBoundingBox, movement.y);
-+ }
-+ } else {
-+ // note: xZero == false or zZero == false
-+ if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) {
-+ // don't bother getting the collisions if we don't need them.
-+ if (movement.y <= 0.0) {
-+ collisionBox = io.papermc.paper.util.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight);
-+ } else {
-+ collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z);
-+ }
-+ } else {
-+ collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z);
-+ }
-+ }
-+
-+ io.papermc.paper.util.CollisionUtil.getCollisions(
-+ world, this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB,
-+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
-+ null, null
-+ );
-+
-+ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) {
-+ return movement;
-+ }
-
-- if (this.maxUpStep() > 0.0F && flag3 && (flag || flag2)) {
-- Vec3 vec3d2 = Entity.collideBoundingBox(this, new Vec3(movement.x, (double) this.maxUpStep(), movement.z), axisalignedbb, this.level(), list);
-- Vec3 vec3d3 = Entity.collideBoundingBox(this, new Vec3(0.0D, (double) this.maxUpStep(), 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level(), list);
-+ final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
-
-- if (vec3d3.y < (double) this.maxUpStep()) {
-- Vec3 vec3d4 = Entity.collideBoundingBox(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level(), list).add(vec3d3);
-+ if (stepHeight > 0.0
-+ && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0))
-+ && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) {
-+ Vec3 vec3d2 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
-+ final Vec3 vec3d3 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB);
-+
-+ if (vec3d3.y < stepHeight) {
-+ final Vec3 vec3d4 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3);
-
- if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) {
- vec3d2 = vec3d4;
- }
- }
-
-- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) {
-- return vec3d2.add(Entity.collideBoundingBox(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level(), list));
-+ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) {
-+ return vec3d2.add(io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB));
- }
-- }
-
-- return vec3d1;
-+ return limitedMoveVector;
-+ } else {
-+ return limitedMoveVector;
-+ }
-+ // Paper end - optimise collisions
- }
-
- public static Vec3 collideBoundingBox(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, List<VoxelShape> collisions) {
-@@ -2734,11 +2819,70 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- float f = this.dimensions.width() * 0.8F;
- AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f);
-
-- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> {
-- BlockState iblockdata = this.level().getBlockState(blockposition);
-+ // Paper start - optimise collisions
-+ if (io.papermc.paper.util.CollisionUtil.isEmpty(axisalignedbb)) {
-+ return false;
-+ }
-
-- return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND);
-- });
-+ final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos();
-+
-+ final int minX = Mth.floor(axisalignedbb.minX);
-+ final int minY = Mth.floor(axisalignedbb.minY);
-+ final int minZ = Mth.floor(axisalignedbb.minZ);
-+ final int maxX = Mth.floor(axisalignedbb.maxX);
-+ final int maxY = Mth.floor(axisalignedbb.maxY);
-+ final int maxZ = Mth.floor(axisalignedbb.maxZ);
-+
-+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.level.getChunkSource();
-+
-+ long lastChunkKey = ChunkPos.INVALID_CHUNK_POS;
-+ net.minecraft.world.level.chunk.LevelChunk lastChunk = null;
-+ for (int fz = minZ; fz <= maxZ; ++fz) {
-+ tempPos.setZ(fz);
-+ for (int fx = minX; fx <= maxX; ++fx) {
-+ final int newChunkX = fx >> 4;
-+ final int newChunkZ = fz >> 4;
-+ final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ?
-+ lastChunk : (lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ));
-+ tempPos.setX(fx);
-+ if (chunk == null) {
-+ continue;
-+ }
-+ for (int fy = minY; fy <= maxY; ++fy) {
-+ tempPos.setY(fy);
-+
-+ final BlockState state = chunk.getBlockState(tempPos);
-+
-+ if (state.emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) {
-+ continue;
-+ }
-+
-+ // Yes, it does not use the Entity context stuff.
-+ final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos);
-+
-+ if (collisionShape.isEmpty()) {
-+ continue;
-+ }
-+
-+ final AABB toCollide = axisalignedbb.move(-(double)fx, -(double)fy, -(double)fz);
-+
-+ final AABB singleAABB = collisionShape.getSingleAABBRepresentation();
-+ if (singleAABB != null) {
-+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) {
-+ return true;
-+ }
-+ continue;
-+ }
-+
-+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) {
-+ return true;
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ // Paper end - optimise collisions
-+ return false;
- }
- }
-
-diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
-index 095a678e3ff7b2bd713fe5bc8542b35ac91d159c..a02ca704e98ef42f32c3c50b111ee3537f60bf7b 100644
---- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
-+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
-@@ -360,7 +360,7 @@ public class ArmorStand extends LivingEntity {
- @Override
- protected void pushEntities() {
- if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups
-- List<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS);
-+ List<AbstractMinecart> list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); // Paper - optimise collisions
- Iterator iterator = list.iterator();
-
- while (iterator.hasNext()) {
-diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java
-index f0127f1b55999aa4a841341ad02cbcde45702b50..ef8911f7bcf6a97496675abb4689bb09cf322e85 100644
---- a/src/main/java/net/minecraft/world/entity/monster/Spider.java
-+++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java
-@@ -82,7 +82,7 @@ public class Spider extends Monster {
- public void tick() {
- super.tick();
- if (!this.level().isClientSide) {
-- this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing)); // Paper - Add config option for spider worldborder climbing
-+ this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing || !io.papermc.paper.util.CollisionUtil.isCollidingWithBorder(this.level().getWorldBorder(), this.getBoundingBox().inflate(io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)))); // Paper - Add config option for spider worldborder climbing & Inflate by +EPSILON as collision will just barely place us outside border
- }
-
- }
-diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java
-index cd89623a44f02d7db77f0d0f87545cf80841f403..48710a60561824a3670ebef3601f284dd7089481 100644
---- a/src/main/java/net/minecraft/world/level/BlockCollisions.java
-+++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java
-@@ -99,7 +99,7 @@ public class BlockCollisions<T> extends AbstractIterator<T> {
- // Paper end
- VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context);
- if (voxelShape == Shapes.block()) {
-- if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) {
-+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(this.box, i, j, k, i + 1.0, j + 1.0, k + 1.0)) { // Paper - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil
- return this.resultProvider.apply(this.pos, voxelShape.move((double)i, (double)j, (double)k));
- }
- } else {
-diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java
-index 3fa2964b979053ecbefc946c7fe76828de86d8f1..2634ba06147c936b0fae08a190c7820c832e6b23 100644
---- a/src/main/java/net/minecraft/world/level/ClipContext.java
-+++ b/src/main/java/net/minecraft/world/level/ClipContext.java
-@@ -17,8 +17,8 @@ public class ClipContext {
-
- private final Vec3 from;
- private final Vec3 to;
-- private final ClipContext.Block block;
-- private final ClipContext.Fluid fluid;
-+ public final ClipContext.Block block; // Paper - optimise collisions - public
-+ public final ClipContext.Fluid fluid; // Paper - optimise collisions - public
- private final CollisionContext collisionContext;
-
- public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) {
-diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java
-index 1ad0c976c6e2d6d31397dff850a9de7c16d16fba..dc877fe2e3c53b353baa59c125232e425fee67d7 100644
---- a/src/main/java/net/minecraft/world/level/CollisionGetter.java
-+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java
-@@ -35,6 +35,12 @@ public interface CollisionGetter extends BlockGetter {
- return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox()));
- }
-
-+ // Paper start - optimise collisions
-+ default boolean noCollision(Entity entity, AABB box, boolean loadChunks) {
-+ return this.noCollision(entity, box);
-+ }
-+ // Paper end - optimise collisions
-+
- default boolean noCollision(AABB box) {
- return this.noCollision(null, box);
- }
-diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
-index 9a28912f52824acdc80a62243b136e6f365bf567..21843501355a0c0c8d594e3e5312e97861c9a777 100644
---- a/src/main/java/net/minecraft/world/level/EntityGetter.java
-+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
-@@ -46,20 +46,36 @@ public interface EntityGetter {
- }
-
- default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) {
-+ // Paper start - optimise collisions
- if (shape.isEmpty()) {
-- return true;
-- } else {
-- for (Entity entity : this.getEntities(except, shape.bounds())) {
-- if (!entity.isRemoved()
-- && entity.blocksBuilding
-- && (except == null || !entity.isPassengerOfSameVehicle(except))
-- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) {
-- return false;
-+ return false;
-+ }
-+
-+ final AABB singleAABB = shape.getSingleAABBRepresentation();
-+ final List<Entity> entities = this.getEntities(
-+ except,
-+ singleAABB == null ? shape.bounds() : singleAABB.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)
-+ );
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (except != null && otherEntity.isPassengerOfSameVehicle(except))) {
-+ continue;
-+ }
-+
-+ if (singleAABB == null) {
-+ final AABB entityBB = otherEntity.getBoundingBox();
-+ if (io.papermc.paper.util.CollisionUtil.isEmpty(entityBB) || !io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(shape, entityBB)) {
-+ continue;
- }
- }
-
-- return true;
-+ return false;
- }
-+
-+ return true;
-+ // Paper end - optimise collisions
- }
-
- default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box) {
-@@ -67,23 +83,41 @@ public interface EntityGetter {
- }
-
- default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) {
-- if (box.getSize() < 1.0E-7) {
-- return List.of();
-+ // Paper start - optimise collisions
-+ // first behavior change is to correctly check for empty AABB
-+ if (io.papermc.paper.util.CollisionUtil.isEmpty(box)) {
-+ // reduce indirection by always returning type with same class
-+ return new java.util.ArrayList<>();
-+ }
-+
-+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
-+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
-+ // specifically with boat collisions.
-+ box = box.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON);
-+
-+ final List<Entity> entities;
-+ if (entity != null && entity.hardCollides()) {
-+ entities = this.getEntities(entity, box, null);
- } else {
-- Predicate<Entity> predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith);
-- List<Entity> list = this.getEntities(entity, box.inflate(1.0E-7), predicate);
-- if (list.isEmpty()) {
-- return List.of();
-- } else {
-- Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size());
--
-- for (Entity entity2 : list) {
-- builder.add(Shapes.create(entity2.getBoundingBox()));
-- }
-+ entities = this.getHardCollidingEntities(entity, box, null);
-+ }
-+
-+ final List<VoxelShape> ret = new java.util.ArrayList<>(Math.min(25, entities.size()));
-
-- return builder.build();
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isSpectator()) {
-+ continue;
-+ }
-+
-+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
-+ ret.add(Shapes.create(otherEntity.getBoundingBox()));
- }
- }
-+
-+ return ret;
-+ // Paper end - optimise collisions
- }
-
- // Paper start - Affects Spawning API
-diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 4001bec504b8fcda1eec03f49abd4e4d82cf6336..07281d73fcbca2ea5f8bce25f6bf961d258bf8a0 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -287,6 +287,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
- this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
- this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
-+ // Paper start - optimise collisions
-+ this.minSection = io.papermc.paper.util.WorldUtil.getMinSection(this);
-+ this.maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(this);
-+ // Paper end - optimise collisions
- }
-
- // Paper start - Cancel hit for vanished players
-@@ -328,6 +332,366 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- return true;
- }
- // Paper end - Cancel hit for vanished players
-+ // Paper start - optimise collisions
-+ public final int minSection;
-+ public final int maxSection;
-+
-+ @Override
-+ public final boolean isUnobstructed(final Entity entity) {
-+ final AABB boundingBox = entity.getBoundingBox();
-+ if (io.papermc.paper.util.CollisionUtil.isEmpty(boundingBox)) {
-+ return false;
-+ }
-+
-+ final List<Entity> entities = this.getEntities(
-+ entity,
-+ boundingBox.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON),
-+ null
-+ );
-+
-+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final Entity otherEntity = entities.get(i);
-+
-+ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) {
-+ continue;
-+ }
-+
-+ return false;
-+ }
-+
-+ return true;
-+ }
-+
-+ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) {
-+ final Vec3 to = clipContext.getTo();
-+ final Vec3 from = clipContext.getFrom();
-+
-+ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z));
-+ }
-+
-+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState();
-+
-+ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level,
-+ final ClipContext clipContext) {
-+ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
-+ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
-+ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
-+
-+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
-+ return miss(clipContext);
-+ }
-+
-+ final double toXAdj = to.x - adjX;
-+ final double toYAdj = to.y - adjY;
-+ final double toZAdj = to.z - adjZ;
-+ final double fromXAdj = from.x + adjX;
-+ final double fromYAdj = from.y + adjY;
-+ final double fromZAdj = from.z + adjZ;
-+
-+ int currX = Mth.floor(fromXAdj);
-+ int currY = Mth.floor(fromYAdj);
-+ int currZ = Mth.floor(fromZAdj);
-+
-+ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos();
-+
-+ final double diffX = toXAdj - fromXAdj;
-+ final double diffY = toYAdj - fromYAdj;
-+ final double diffZ = toZAdj - fromZAdj;
-+
-+ final double dxDouble = Math.signum(diffX);
-+ final double dyDouble = Math.signum(diffY);
-+ final double dzDouble = Math.signum(diffZ);
-+
-+ final int dx = (int)dxDouble;
-+ final int dy = (int)dyDouble;
-+ final int dz = (int)dzDouble;
-+
-+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
-+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
-+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
-+
-+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
-+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
-+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
-+
-+ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null;
-+ net.minecraft.world.level.chunk.PalettedContainer<BlockState> lastSection = null;
-+ int lastChunkX = Integer.MIN_VALUE;
-+ int lastChunkY = Integer.MIN_VALUE;
-+ int lastChunkZ = Integer.MIN_VALUE;
-+
-+ final int minSection = level.minSection;
-+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)level.getChunkSource();
-+
-+ for (;;) {
-+ currPos.set(currX, currY, currZ);
-+
-+ final int newChunkX = currX >> 4;
-+ final int newChunkY = currY >> 4;
-+ final int newChunkZ = currZ >> 4;
-+
-+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
-+ final int chunkYDiff = newChunkY ^ lastChunkY;
-+
-+ if ((chunkDiff | chunkYDiff) != 0) {
-+ if (chunkDiff != 0) {
-+ LevelChunk chunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ);
-+ lastChunk = chunk == null ? null : chunk.getSections(); // diff: don't load chunks for this
-+ }
-+ final int sectionY = newChunkY - minSection;
-+ lastSection = lastChunk != null && sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null;
-+
-+ lastChunkX = newChunkX;
-+ lastChunkY = newChunkY;
-+ lastChunkZ = newChunkZ;
-+ }
-+
-+ final BlockState blockState;
-+ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) {
-+ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos);
-+
-+ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState);
-+
-+ final net.minecraft.world.phys.shapes.VoxelShape fluidCollision;
-+ final FluidState fluidState;
-+ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) {
-+ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos);
-+
-+ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos);
-+
-+ if (fluidHit != null) {
-+ if (blockHit == null) {
-+ return fluidHit;
-+ }
-+
-+ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit;
-+ }
-+ }
-+
-+ if (blockHit != null) {
-+ return blockHit;
-+ }
-+ } // else: usually fall here
-+
-+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
-+ return miss(clipContext);
-+ }
-+
-+ // inc the smallest normalized coordinate
-+
-+ if (normalizedCurrX < normalizedCurrY) {
-+ if (normalizedCurrX < normalizedCurrZ) {
-+ currX += dx;
-+ normalizedCurrX += normalizedDiffX;
-+ } else {
-+ // x < y && x >= z <--> z < y && z <= x
-+ currZ += dz;
-+ normalizedCurrZ += normalizedDiffZ;
-+ }
-+ } else if (normalizedCurrY < normalizedCurrZ) {
-+ // y <= x && y < z
-+ currY += dy;
-+ normalizedCurrY += normalizedDiffY;
-+ } else {
-+ // y <= x && z <= y <--> z <= y && z <= x
-+ currZ += dz;
-+ normalizedCurrZ += normalizedDiffZ;
-+ }
-+ }
-+ }
-+
-+ @Override
-+ public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) {
-+ // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks
-+ return fastClip(clipContext.getFrom(), clipContext.getTo(), this, clipContext);
-+ }
-+
-+ @Override
-+ public final boolean noCollision(final Entity entity, final AABB box, final boolean loadChunks) {
-+ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
-+ if (entity != null) {
-+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER;
-+ }
-+ if (loadChunks) {
-+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_LOAD_CHUNKS;
-+ }
-+ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) {
-+ return false;
-+ }
-+
-+ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null);
-+ }
-+
-+ @Override
-+ public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) {
-+ return io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null,
-+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY,
-+ (final BlockState state, final BlockPos pos) -> {
-+ return state.isSuffocating(Level.this, pos);
-+ }
-+ );
-+ }
-+
-+ private static net.minecraft.world.phys.shapes.VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) {
-+ return net.minecraft.world.phys.shapes.Shapes.create(
-+ aabb.minX - x,
-+ aabb.minY - y,
-+ aabb.minZ - z,
-+
-+ aabb.maxX + x,
-+ aabb.maxY + y,
-+ aabb.maxZ + z
-+ );
-+ }
-+
-+ @Override
-+ public final java.util.Optional<Vec3> findFreePosition(final Entity entity, final net.minecraft.world.phys.shapes.VoxelShape boundsShape, final Vec3 fromPosition,
-+ final double rangeX, final double rangeY, final double rangeZ) {
-+ if (boundsShape.isEmpty()) {
-+ return java.util.Optional.empty();
-+ }
-+
-+ final double expandByX = rangeX * 0.5;
-+ final double expandByY = rangeY * 0.5;
-+ final double expandByZ = rangeZ * 0.5;
-+
-+ // note: it is useless to look at shapes outside of range / 2.0
-+ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ);
-+
-+ final List<AABB> aabbs = new java.util.ArrayList<>();
-+ final List<net.minecraft.world.phys.shapes.VoxelShape> voxels = new java.util.ArrayList<>();
-+
-+ io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
-+ this, entity, collectionVolume, voxels, aabbs,
-+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
-+ null
-+ );
-+
-+ // push voxels into aabbs
-+ for (int i = 0, len = voxels.size(); i < len; ++i) {
-+ aabbs.addAll(voxels.get(i).toAabbs());
-+ }
-+
-+ // expand AABBs
-+ final net.minecraft.world.phys.shapes.VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ);
-+ final net.minecraft.world.phys.shapes.VoxelShape[] rest = new net.minecraft.world.phys.shapes.VoxelShape[Math.max(0, aabbs.size() - 1)];
-+
-+ for (int i = 1, len = aabbs.size(); i < len; ++i) {
-+ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ);
-+ }
-+
-+ // use optimized implementation of ORing the shapes together
-+ final net.minecraft.world.phys.shapes.VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest);
-+
-+ // find free space
-+ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs()
-+ final net.minecraft.world.phys.shapes.VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(
-+ boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST
-+ );
-+
-+ return freeSpace.closestPointTo(fromPosition);
-+ }
-+
-+ @Override
-+ public final java.util.Optional<BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) {
-+ final int minBlockX = Mth.floor(aabb.minX - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
-+ final int maxBlockX = Mth.floor(aabb.maxX + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
-+
-+ final int minBlockY = Mth.floor(aabb.minY - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
-+ final int maxBlockY = Mth.floor(aabb.maxY + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
-+
-+ final int minBlockZ = Mth.floor(aabb.minZ - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
-+ final int maxBlockZ = Mth.floor(aabb.maxZ + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
-+
-+ io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext collisionContext = null;
-+
-+ final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
-+ BlockPos selected = null;
-+ double selectedDistance = Double.MAX_VALUE;
-+
-+ final Vec3 entityPos = entity.position();
-+
-+ LevelChunk lastChunk = null;
-+ int lastChunkX = Integer.MIN_VALUE;
-+ int lastChunkZ = Integer.MIN_VALUE;
-+
-+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource();
-+
-+ for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) {
-+ pos.setZ(currZ);
-+ for (int currX = minBlockX; currX <= maxBlockX; ++currX) {
-+ pos.setX(currX);
-+
-+ final int newChunkX = currX >> 4;
-+ final int newChunkZ = currZ >> 4;
-+
-+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
-+
-+ if (chunkDiff != 0) {
-+ lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ);
-+ }
-+
-+ if (lastChunk == null) {
-+ continue;
-+ }
-+ for (int currY = minBlockY; currY <= maxBlockY; ++currY) {
-+ int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) +
-+ ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) +
-+ ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0);
-+ if (edgeCount == 3) {
-+ continue;
-+ }
-+
-+ pos.setY(currY);
-+
-+ final double distance = pos.distToCenterSqr(entityPos);
-+ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) {
-+ continue;
-+ }
-+
-+ final BlockState state = lastChunk.getBlockState(currX, currY, currZ);
-+ if (state.emptyCollisionShape()) {
-+ continue;
-+ }
-+
-+ if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) {
-+ if (collisionContext == null) {
-+ collisionContext = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(entity);
-+ }
-+ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = state.getCollisionShape(lastChunk, pos, collisionContext);
-+ if (blockCollision.isEmpty()) {
-+ continue;
-+ }
-+
-+ // avoid VoxelShape#move by shifting the entity collision shape instead
-+ final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ);
-+
-+ final AABB singleAABB = blockCollision.getSingleAABBRepresentation();
-+ if (singleAABB != null) {
-+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) {
-+ continue;
-+ }
-+
-+ selected = pos.immutable();
-+ selectedDistance = distance;
-+ continue;
-+ }
-+
-+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) {
-+ continue;
-+ }
-+
-+ selected = pos.immutable();
-+ selectedDistance = distance;
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+
-+ return java.util.Optional.ofNullable(selected);
-+ }
-+ // Paper end - optimise collisions
- @Override
- public boolean isClientSide() {
- return this.isClientSide;
-@@ -949,7 +1313,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
- @Override
- public boolean noCollision(@Nullable Entity entity, AABB box) {
- if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false;
-- return LevelAccessor.super.noCollision(entity, box);
-+ // Paper start - optimise collisions
-+ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
-+ if (entity != null) {
-+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER;
-+ }
-+ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) {
-+ return false;
-+ }
-+
-+ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null);
-+ // Paper end - optimise collisions
- }
- // Paper end - Option to prevent armor stands from doing entity lookups
-
-diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
-index 054593fc0b8d13f6bf449cc20a1f7ddfd5f1d1f0..cf8b8c8efd1c9c81eb5f02d75bd75875eb66771f 100644
---- a/src/main/java/net/minecraft/world/level/block/Block.java
-+++ b/src/main/java/net/minecraft/world/level/block/Block.java
-@@ -280,7 +280,7 @@ public class Block extends BlockBehaviour implements ItemLike {
- }
-
- public static boolean isShapeFullBlock(VoxelShape shape) {
-- return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape);
-+ return shape.isFullBlock(); // Paper - optimise collisions
- }
-
- public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {}
-diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-index 55efd0d379bac79935f62446cd3479d1e59361a4..2034ca2edd3aff61d94416266e75402babd3e741 100644
---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-@@ -813,6 +813,10 @@ public abstract class BlockBehaviour implements FeatureElement {
- this.instrument = blockbase_info.instrument;
- this.replaceable = blockbase_info.replaceable;
- this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper
-+ // Paper start - optimise collisions
-+ this.id1 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET);
-+ this.id2 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET);
-+ // Paper end - optimise collisions
- }
- // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time
- private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData;
-@@ -861,6 +865,52 @@ public abstract class BlockBehaviour implements FeatureElement {
- return this.conditionallyFullOpaque;
- }
- // Paper end - starlight
-+ // Paper start - optimise collisions
-+ private static final int RANDOM_OFFSET = 704237939;
-+ private static final Direction[] DIRECTIONS_CACHED = Direction.values();
-+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger();
-+ private final int id1, id2;
-+ private boolean occludesFullBlock;
-+ private boolean emptyCollisionShape;
-+ private VoxelShape constantCollisionShape;
-+ private AABB constantAABBCollision;
-+ private static void initCaches(final VoxelShape shape) {
-+ shape.isFullBlock();
-+ shape.occludesFullBlock();
-+ shape.toAabbs();
-+ if (!shape.isEmpty()) {
-+ shape.bounds();
-+ }
-+ }
-+
-+ public final boolean hasCache() {
-+ return this.cache != null;
-+ }
-+
-+ public final boolean occludesFullBlock() {
-+ return this.occludesFullBlock;
-+ }
-+
-+ public final boolean emptyCollisionShape() {
-+ return this.emptyCollisionShape;
-+ }
-+
-+ public final int uniqueId1() {
-+ return this.id1;
-+ }
-+
-+ public final int uniqueId2() {
-+ return this.id2;
-+ }
-+
-+ public final VoxelShape getConstantCollisionShape() {
-+ return this.constantCollisionShape;
-+ }
-+
-+ public final AABB getConstantCollisionAABB() {
-+ return this.constantAABBCollision;
-+ }
-+ // Paper end - optimise collisions
-
- public void initCache() {
- this.fluidState = ((Block) this.owner).getFluidState(this.asState());
-@@ -872,6 +922,39 @@ public abstract class BlockBehaviour implements FeatureElement {
- this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light
-
- this.legacySolid = this.calculateSolid();
-+ // Paper start - optimise collisions
-+ if (this.cache != null) {
-+ final VoxelShape collisionShape = this.cache.collisionShape;
-+ try {
-+ this.constantCollisionShape = this.getCollisionShape(null, null, null);
-+ this.constantAABBCollision = this.constantCollisionShape == null ? null : this.constantCollisionShape.getSingleAABBRepresentation();
-+ } catch (final Throwable throwable) {
-+ this.constantCollisionShape = null;
-+ this.constantAABBCollision = null;
-+ }
-+ this.occludesFullBlock = collisionShape.occludesFullBlock();
-+ this.emptyCollisionShape = collisionShape.isEmpty();
-+ // init caches
-+ initCaches(collisionShape);
-+ if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) {
-+ for (final Direction direction : DIRECTIONS_CACHED) {
-+ // initialise the directional face shape cache as well
-+ final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction);
-+ initCaches(shape);
-+ }
-+ }
-+ if (this.cache.occlusionShapes != null) {
-+ for (final VoxelShape shape : this.cache.occlusionShapes) {
-+ initCaches(shape);
-+ }
-+ }
-+ } else {
-+ this.occludesFullBlock = false;
-+ this.emptyCollisionShape = false;
-+ this.constantCollisionShape = null;
-+ this.constantAABBCollision = null;
-+ }
-+ // Paper end - optimise collisions
- }
-
- public Block getBlock() {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-index 8cd6c1d838e0332125fde3fc36475034aa4effa0..a2a5aef769ee8bb638a5a9f3da9812fa4a85dda5 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-@@ -26,6 +26,22 @@ public class LevelChunkSection {
- // CraftBukkit start - read/write
- private PalettedContainer<Holder<Biome>> biomes;
- public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper
-+ // Paper start - optimise collisions
-+ private int specialCollidingBlocks;
-+
-+ private void updateBlockCallback(final int x, final int y, final int z, final BlockState oldState, final BlockState newState) {
-+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(newState)) {
-+ ++this.specialCollidingBlocks;
-+ }
-+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(oldState)) {
-+ --this.specialCollidingBlocks;
-+ }
-+ }
-+
-+ public final int getSpecialCollidingBlocks() {
-+ return this.specialCollidingBlocks;
-+ }
-+ // Paper end - optimise collisions
-
- public LevelChunkSection(PalettedContainer<BlockState> datapaletteblock, PalettedContainer<Holder<Biome>> palettedcontainerro) {
- // CraftBukkit end
-@@ -62,8 +78,8 @@ public class LevelChunkSection {
- return this.setBlockState(x, y, z, state, true);
- }
-
-- public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) {
-- BlockState iblockdata1;
-+ public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { // Paper - state -> new state
-+ BlockState iblockdata1; // Paper - iblockdata1 -> oldState
-
- if (lock) {
- iblockdata1 = (BlockState) this.states.getAndSet(x, y, z, state);
-@@ -102,6 +118,7 @@ public class LevelChunkSection {
- ++this.tickingFluidCount;
- }
-
-+ this.updateBlockCallback(x, y, z, iblockdata1, state); // Paper - optimise collisions
- return iblockdata1;
- }
-
-@@ -147,6 +164,11 @@ public class LevelChunkSection {
- }
- }
-
-+ // Paper start - optimise collisions
-+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(iblockdata)) {
-+ ++this.specialCollidingBlocks;
-+ }
-+ // Paper end - optimise collisions
- });
- }
- // Paper end - unfuck this
-diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-index 1c0712295695727ee9c4d430d4157b8e17cbd71f..c2943d892b067b3f1fb3b93301a092e912d71f08 100644
---- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-+++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-@@ -240,6 +240,17 @@ public abstract class FlowingFluid extends Fluid {
- }
-
- private boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) {
-+ // Paper start - optimise collisions
-+ if (state.emptyCollisionShape() & fromState.emptyCollisionShape()) {
-+ // don't even try to cache simple cases
-+ return true;
-+ }
-+
-+ if (state.occludesFullBlock() | fromState.occludesFullBlock()) {
-+ // don't even try to cache simple cases
-+ return false;
-+ }
-+ // Paper end - optimise collisions
- Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap;
-
- if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) {
-diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java
-index 62752e28a68400f0e1a44f0196f0e51e3dd702b8..92394960fc76886f393cba02ac33c57739a4b383 100644
---- a/src/main/java/net/minecraft/world/phys/AABB.java
-+++ b/src/main/java/net/minecraft/world/phys/AABB.java
-@@ -25,6 +25,17 @@ public class AABB {
- this.maxZ = Math.max(z1, z2);
- }
-
-+ // Paper start
-+ public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) {
-+ this.minX = minX;
-+ this.minY = minY;
-+ this.minZ = minZ;
-+ this.maxX = maxX;
-+ this.maxY = maxY;
-+ this.maxZ = maxZ;
-+ }
-+ // Paper end
-+
- public AABB(BlockPos pos) {
- this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1));
- }
-@@ -321,7 +332,7 @@ public class AABB {
- }
-
- @Nullable
-- private static Direction getDirection(
-+ public static Direction getDirection( // Paper - optimise collisions - public
- AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ
- ) {
- if (deltaX > 1.0E-7) {
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-index fc7f986812bdf74e0aea3bd09a1d53ba6def697f..0583d40a235aaecd9d6081486bbfb7355709a5ac 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
-@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape {
- );
- }
-
-- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) {
-+ public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public
- super(shape);
- int i = shape.getXSize() + 1;
- int j = shape.getYSize() + 1;
-@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape {
- new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")
- );
- }
-+ this.initCache(); // Paper - optimise collisions
- }
-
- @Override
-@@ -49,4 +50,5 @@ public class ArrayVoxelShape extends VoxelShape {
- throw new IllegalArgumentException();
- }
- }
-+
- }
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..a71421947b161697d43bd8602e2af95aa272ed3f 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
-@@ -4,13 +4,13 @@ import java.util.BitSet;
- import net.minecraft.core.Direction;
-
- public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
-- private final BitSet storage;
-- private int xMin;
-- private int yMin;
-- private int zMin;
-- private int xMax;
-- private int yMax;
-- private int zMax;
-+ public final BitSet storage; // Paper - optimise collisions - public
-+ public int xMin; // Paper - optimise collisions - public
-+ public int yMin; // Paper - optimise collisions - public
-+ public int zMin; // Paper - optimise collisions - public
-+ public int xMax; // Paper - optimise collisions - public
-+ public int yMax; // Paper - optimise collisions - public
-+ public int zMax; // Paper - optimise collisions - public
-
- public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
- super(sizeX, sizeY, sizeZ);
-@@ -151,45 +151,106 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
- }
-
- protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) {
-- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet);
-+ // Paper start - optimise collisions
-+ // called with the shape of a VoxelShape, so we can expect the cache to exist
-+ final io.papermc.paper.util.collisions.CachedShapeData cache = voxelSet.getOrCreateCachedShapeData();
-+
-+ final int sizeX = cache.sizeX();
-+ final int sizeY = cache.sizeY();
-+ final int sizeZ = cache.sizeZ();
-+
-+ int indexX;
-+ int indexY = 0;
-+ int indexZ;
-+
-+ int incY = sizeZ;
-+ int incX = sizeZ*sizeY;
-+
-+ long[] bitset = cache.voxelSet();
-+
-+ // index = z + y*size_z + x*(size_z*size_y)
-+
-+ if (!coalesce) {
-+ // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply
-+ // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid
-+ // the multiplication
-+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
-+ indexX = indexY;
-+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
-+ indexZ = indexX;
-+ for (int z = 0; z < sizeZ; ++z, ++indexZ) {
-+ if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) {
-+ callback.consume(x, y, z, x + 1, y + 1, z + 1);
-+ }
-+ }
-+ }
-+ }
-+ } else {
-+ // same notes about loop order as the above
-+ // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize())
-
-- for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) {
-- for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) {
-- int k = -1;
-+ // only clone when we may write to it
-+ bitset = bitset.clone();
-
-- for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) {
-- if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) {
-- if (coalesce) {
-- if (k == -1) {
-- k = l;
-- }
-- } else {
-- callback.consume(j, i, l, j + 1, i + 1, l + 1);
-+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
-+ indexX = indexY;
-+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
-+ for (int zIdx = indexX, endIndex = indexX + sizeZ; zIdx < endIndex;) {
-+ final int firstSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstSet(bitset, zIdx, endIndex);
-+
-+ if (firstSetZ == -1) {
-+ break;
-+ }
-+
-+ int lastSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex);
-+ if (lastSetZ == -1) {
-+ lastSetZ = endIndex;
- }
-- } else if (k != -1) {
-- int m = j;
-- int n = i;
-- bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i);
--
-- while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) {
-- bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i);
-- m++;
-+
-+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ);
-+
-+ // try to merge neighbouring on the X axis
-+ int endX = x + 1; // exclusive
-+ for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX;
-+ endX < sizeX && io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd);
-+ neighbourIdxStart += incX, neighbourIdxEnd += incX) {
-+
-+ ++endX;
-+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd);
- }
-
-- while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) {
-- for (int o = j; o <= m; o++) {
-- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1);
-+ // try to merge neighbouring on the Y axis
-+
-+ int endY; // exclusive
-+ int firstSetZY, lastSetZY;
-+ y_merge:
-+ for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY;
-+ firstSetZY += incY, lastSetZY += incY) {
-+
-+ // test the whole XZ range
-+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
-+ ++testX, start += incX, end += incX) {
-+ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, start, end)) {
-+ break y_merge;
-+ }
- }
-
-- n++;
-+ ++endY;
-+
-+ // passed, so we can clear it
-+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
-+ ++testX, start += incX, end += incX) {
-+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, start, end);
-+ }
- }
-
-- callback.consume(j, i, k, m + 1, n + 1, l);
-- k = -1;
-+ callback.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX);
-+ zIdx = lastSetZ;
- }
- }
- }
- }
-+ // Paper end - optimise collisions
- }
-
- private boolean isZStripFull(int z1, int z2, int x, int y) {
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-index 32632368f06b79f53342fde060bbcd1b7c64767a..b9af1d14c7815c99273bce8165cf384d669c1a75 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
-@@ -7,6 +7,7 @@ import net.minecraft.util.Mth;
- public final class CubeVoxelShape extends VoxelShape {
- protected CubeVoxelShape(DiscreteVoxelShape voxels) {
- super(voxels);
-+ this.initCache(); // Paper - optimise collisions
- }
-
- @Override
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-index 01693ba050b12b9debcdaefceeff9cbcd503b369..a7af766f1c28f2c9ca2a430808ac4c07fe24df68 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-@@ -9,6 +9,71 @@ public abstract class DiscreteVoxelShape {
- protected final int ySize;
- protected final int zSize;
-
-+ // Paper start - optimise collisions
-+ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData;
-+
-+ public final io.papermc.paper.util.collisions.CachedShapeData getOrCreateCachedShapeData() {
-+ if (this.cachedShapeData != null) {
-+ return this.cachedShapeData;
-+ }
-+
-+ final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this;
-+
-+ final int sizeX = discreteVoxelShape.getXSize();
-+ final int sizeY = discreteVoxelShape.getYSize();
-+ final int sizeZ = discreteVoxelShape.getZSize();
-+
-+ final int maxIndex = sizeX * sizeY * sizeZ; // exclusive
-+
-+ final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6;
-+ long[] voxelSet;
-+
-+ final boolean isEmpty = discreteVoxelShape.isEmpty();
-+
-+ if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) {
-+ voxelSet = bitsetShape.storage.toLongArray();
-+ if (voxelSet.length < longsRequired) {
-+ // happens when the later long values are 0L, so we need to resize
-+ voxelSet = java.util.Arrays.copyOf(voxelSet, longsRequired);
-+ }
-+ } else {
-+ voxelSet = new long[longsRequired];
-+ if (!isEmpty) {
-+ final int mulX = sizeZ * sizeY;
-+ for (int x = 0; x < sizeX; ++x) {
-+ for (int y = 0; y < sizeY; ++y) {
-+ for (int z = 0; z < sizeZ; ++z) {
-+ if (discreteVoxelShape.isFull(x, y, z)) {
-+ // index = z + y*size_z + x*(size_z*size_y)
-+ final int index = z + y * sizeZ + x * mulX;
-+
-+ voxelSet[index >>> 6] |= 1L << index;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0);
-+
-+ final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X);
-+ final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y);
-+ final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z);
-+
-+ final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X);
-+ final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y);
-+ final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z);
-+
-+ return this.cachedShapeData = new io.papermc.paper.util.collisions.CachedShapeData(
-+ sizeX, sizeY, sizeZ, voxelSet,
-+ minFullX, minFullY, minFullZ,
-+ maxFullX, maxFullY, maxFullZ,
-+ isEmpty, hasSingleAABB
-+ );
-+ }
-+ // Paper end - optimise collisions
-+
- protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
- if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) {
- this.xSize = sizeX;
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
-@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
- import it.unimi.dsi.fastutil.doubles.DoubleList;
-
- public class OffsetDoubleList extends AbstractDoubleList {
-- private final DoubleList delegate;
-- private final double offset;
-+ public final DoubleList delegate; // Paper - optimise collisions - public
-+ public final double offset; // Paper - optimise collisions - public
-
- public OffsetDoubleList(DoubleList oldList, double offset) {
- this.delegate = oldList;
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-index 86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc..fbf1a559aefe444410b63a773374e011e4964e16 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-@@ -16,9 +16,15 @@ public final class Shapes {
- public static final double EPSILON = 1.0E-7;
- public static final double BIG_EPSILON = 1.0E-6;
- private static final VoxelShape BLOCK = Util.make(() -> {
-- DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1);
-- discreteVoxelShape.fill(0, 0, 0);
-- return new CubeVoxelShape(discreteVoxelShape);
-+ // Paper start - optimise collisions - force arrayvoxelshape
-+ final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1);
-+ shape.fill(0, 0, 0);
-+
-+ return new ArrayVoxelShape(
-+ shape,
-+ io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE
-+ );
-+ // Paper end - optimise collisions - force arrayvoxelshape
- });
- public static final VoxelShape INFINITY = box(
- Double.NEGATIVE_INFINITY,
-@@ -35,6 +41,30 @@ public final class Shapes {
- new DoubleArrayList(new double[]{0.0})
- );
-
-+ // Paper start - optimise collisions - force arrayvoxelshape
-+ private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] {
-+ DoubleArrayList.wrap(generateCubeParts(1 << 0)),
-+ DoubleArrayList.wrap(generateCubeParts(1 << 1)),
-+ DoubleArrayList.wrap(generateCubeParts(1 << 2)),
-+ DoubleArrayList.wrap(generateCubeParts(1 << 3))
-+ };
-+
-+ private static double[] generateCubeParts(final int parts) {
-+ // note: parts is a power of two, so we do not need to worry about loss of precision here
-+ // note: parts is from [2^0, 2^3]
-+ final double inc = 1.0 / (double)parts;
-+
-+ final double[] ret = new double[parts + 1];
-+ double val = 0.0;
-+ for (int i = 0; i <= parts; ++i) {
-+ ret[i] = val;
-+ val += inc;
-+ }
-+
-+ return ret;
-+ }
-+ // Paper end - optimise collisions - force arrayvoxelshape
-+
- public static VoxelShape empty() {
- return EMPTY;
- }
-@@ -53,35 +83,39 @@ public final class Shapes {
-
- public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
- if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) {
-- int i = findBits(minX, maxX);
-- int j = findBits(minY, maxY);
-- int k = findBits(minZ, maxZ);
-- if (i < 0 || j < 0 || k < 0) {
-- return new ArrayVoxelShape(
-- BLOCK.shape,
-- DoubleArrayList.wrap(new double[]{minX, maxX}),
-- DoubleArrayList.wrap(new double[]{minY, maxY}),
-- DoubleArrayList.wrap(new double[]{minZ, maxZ})
-- );
-- } else if (i == 0 && j == 0 && k == 0) {
-- return block();
-+ // Paper start - optimise collisions
-+ // force ArrayVoxelShape in every case
-+ final int bitsX = findBits(minX, maxX);
-+ final int bitsY = findBits(minY, maxY);
-+ final int bitsZ = findBits(minZ, maxZ);
-+ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) {
-+ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) {
-+ return BLOCK;
-+ } else {
-+ final int sizeX = 1 << bitsX;
-+ final int sizeY = 1 << bitsY;
-+ final int sizeZ = 1 << bitsZ;
-+ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds(
-+ sizeX, sizeY, sizeZ,
-+ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ),
-+ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ)
-+ );
-+ return new ArrayVoxelShape(
-+ shape,
-+ PARTS_BY_BITS[bitsX],
-+ PARTS_BY_BITS[bitsY],
-+ PARTS_BY_BITS[bitsZ]
-+ );
-+ }
- } else {
-- int l = 1 << i;
-- int m = 1 << j;
-- int n = 1 << k;
-- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(
-- l,
-- m,
-- n,
-- (int)Math.round(minX * (double)l),
-- (int)Math.round(minY * (double)m),
-- (int)Math.round(minZ * (double)n),
-- (int)Math.round(maxX * (double)l),
-- (int)Math.round(maxY * (double)m),
-- (int)Math.round(maxZ * (double)n)
-+ return new ArrayVoxelShape(
-+ BLOCK.shape,
-+ minX == 0.0 && maxX == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }),
-+ minY == 0.0 && maxY == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }),
-+ minZ == 0.0 && maxZ == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ })
- );
-- return new CubeVoxelShape(bitSetDiscreteVoxelShape);
- }
-+ // Paper end - optimise collisions
- } else {
- return empty();
- }
-@@ -120,79 +154,53 @@ public final class Shapes {
- }
-
- public static VoxelShape or(VoxelShape first, VoxelShape... others) {
-- return Arrays.stream(others).reduce(first, Shapes::or);
-+ // Paper start - optimise collisions
-+ int size = others.length;
-+ if (size == 0) {
-+ return first;
-+ }
-+
-+ // reduce complexity of joins by splitting the merges
-+
-+ // add extra slot for first shape
-+ ++size;
-+ final VoxelShape[] tmp = Arrays.copyOf(others, size);
-+ // insert first shape
-+ tmp[size - 1] = first;
-+
-+ while (size > 1) {
-+ int newSize = 0;
-+ for (int i = 0; i < size; i += 2) {
-+ final int next = i + 1;
-+ if (next >= size) {
-+ // nothing to merge with, so leave it for next iteration
-+ tmp[newSize++] = tmp[i];
-+ break;
-+ } else {
-+ // merge with adjacent
-+ final VoxelShape one = tmp[i];
-+ final VoxelShape second = tmp[next];
-+
-+ tmp[newSize++] = Shapes.or(one, second);
-+ }
-+ }
-+ size = newSize;
-+ }
-+
-+ return tmp[0];
-+ // Paper end - optimise collisions
- }
-
- public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) {
-- return joinUnoptimized(first, second, function).optimize();
-+ return io.papermc.paper.util.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions
- }
-
- public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) {
-- if (function.apply(false, false)) {
-- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
-- } else if (one == two) {
-- return function.apply(true, true) ? one : empty();
-- } else {
-- boolean bl = function.apply(true, false);
-- boolean bl2 = function.apply(false, true);
-- if (one.isEmpty()) {
-- return bl2 ? two : empty();
-- } else if (two.isEmpty()) {
-- return bl ? one : empty();
-- } else {
-- IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2);
-- IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2);
-- IndexMerger indexMerger3 = createIndexMerger(
-- (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2
-- );
-- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join(
-- one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function
-- );
-- return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger
-- && indexMerger2 instanceof DiscreteCubeMerger
-- && indexMerger3 instanceof DiscreteCubeMerger
-- ? new CubeVoxelShape(bitSetDiscreteVoxelShape)
-- : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList()));
-- }
-- }
-+ return io.papermc.paper.util.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions
- }
-
- public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) {
-- if (predicate.apply(false, false)) {
-- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
-- } else {
-- boolean bl = shape1.isEmpty();
-- boolean bl2 = shape2.isEmpty();
-- if (!bl && !bl2) {
-- if (shape1 == shape2) {
-- return predicate.apply(true, true);
-- } else {
-- boolean bl3 = predicate.apply(true, false);
-- boolean bl4 = predicate.apply(false, true);
--
-- for (Direction.Axis axis : AxisCycle.AXIS_VALUES) {
-- if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) {
-- return bl3 || bl4;
-- }
--
-- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) {
-- return bl3 || bl4;
-- }
-- }
--
-- IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4);
-- IndexMerger indexMerger2 = createIndexMerger(
-- indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4
-- );
-- IndexMerger indexMerger3 = createIndexMerger(
-- (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4
-- );
-- return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate);
-- }
-- } else {
-- return predicate.apply(!bl, !bl2);
-- }
-- }
-+ return io.papermc.paper.util.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions
- }
-
- private static boolean joinIsNotEmpty(
-@@ -220,69 +228,119 @@ public final class Shapes {
- }
-
- public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) {
-- if (shape == block() && neighbor == block()) {
-+ // Paper start - optimise collisions
-+ final boolean firstBlock = shape == BLOCK;
-+ final boolean secondBlock = neighbor == BLOCK;
-+
-+ if (firstBlock & secondBlock) {
- return true;
-- } else if (neighbor.isEmpty()) {
-+ }
-+
-+ if (shape.isEmpty() | neighbor.isEmpty()) {
-+ return false;
-+ }
-+
-+ // we optimise getOpposite, so we can use it
-+ // secondly, use our cache to retrieve sliced shape
-+ final VoxelShape newFirst = shape.getFaceShapeClamped(direction);
-+ if (newFirst.isEmpty()) {
- return false;
-- } else {
-- Direction.Axis axis = direction.getAxis();
-- Direction.AxisDirection axisDirection = direction.getAxisDirection();
-- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor;
-- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape;
-- BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND;
-- return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)
-- && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)
-- && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp);
- }
-+ final VoxelShape newSecond = neighbor.getFaceShapeClamped(direction.getOpposite());
-+ if (newSecond.isEmpty()) {
-+ return false;
-+ }
-+
-+ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST);
-+ // Paper end - optimise collisions
- }
-
- public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) {
-- if (shape == block()) {
-- return block();
-- } else {
-- Direction.Axis axis = direction.getAxis();
-- boolean bl;
-- int i;
-- if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
-- bl = DoubleMath.fuzzyEquals(shape.max(axis), 1.0, 1.0E-7);
-- i = shape.shape.getSize(axis) - 1;
-- } else {
-- bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0, 1.0E-7);
-- i = 0;
-- }
-+ return shape.getFaceShapeClamped(direction); // Paper - optimise collisions
-+ }
-
-- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i));
-- }
-+ // Paper start - optimise collisions
-+ private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) {
-+ // if the combined bounds of the two shapes cannot occlude, then neither can the merged
-+ final AABB bounds1 = shape1.bounds();
-+ final AABB bounds2 = shape2.bounds();
-+
-+ final double minX = Math.min(bounds1.minX, bounds2.minX);
-+ final double minY = Math.min(bounds1.minY, bounds2.minY);
-+ final double minZ = Math.min(bounds1.minZ, bounds2.minZ);
-+
-+ final double maxX = Math.max(bounds1.maxX, bounds2.maxX);
-+ final double maxY = Math.max(bounds1.maxY, bounds2.maxY);
-+ final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ);
-+
-+ return (minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
-+ (minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
-+ (minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON));
- }
-+ // Paper end - optimise collisions
-
- public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) {
-- if (one != block() && two != block()) {
-- Direction.Axis axis = direction.getAxis();
-- Direction.AxisDirection axisDirection = direction.getAxisDirection();
-- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two;
-- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one;
-- if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) {
-- voxelShape = empty();
-- }
-+ // Paper start - optimise collisions
-+ // see if any of the shapes on their own occludes, only if cached
-+ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) {
-+ return true;
-+ }
-
-- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) {
-- voxelShape2 = empty();
-- }
-+ if (one.isEmpty() & two.isEmpty()) {
-+ return false;
-+ }
-
-- return !joinIsNotEmpty(
-- block(),
-- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR),
-- BooleanOp.ONLY_FIRST
-- );
-- } else {
-+ // we optimise getOpposite, so we can use it
-+ // secondly, use our cache to retrieve sliced shape
-+ final VoxelShape newFirst = one.getFaceShapeClamped(direction);
-+ final VoxelShape newSecond = two.getFaceShapeClamped(direction.getOpposite());
-+
-+ // see if any of the shapes on their own occludes, only if cached
-+ if (newFirst.occludesFullBlockIfCached() || newSecond.occludesFullBlockIfCached()) {
- return true;
- }
-+
-+ final boolean firstEmpty = newFirst.isEmpty();
-+ final boolean secondEmpty = newSecond.isEmpty();
-+
-+ if (firstEmpty & secondEmpty) {
-+ return false;
-+ }
-+
-+ if (firstEmpty | secondEmpty) {
-+ return secondEmpty ? newFirst.occludesFullBlock() : newSecond.occludesFullBlock();
-+ }
-+
-+ if (newFirst == newSecond) {
-+ return newFirst.occludesFullBlock();
-+ }
-+
-+ return mergedMayOccludeBlock(newFirst, newSecond) && newFirst.orUnoptimized(newSecond).occludesFullBlock();
-+ // Paper end - optimise collisions
- }
-
- public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) {
-- return one == block()
-- || two == block()
-- || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST);
-+ // Paper start - optimise collisions
-+ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) {
-+ return true;
-+ }
-+
-+ final boolean s1Empty = one.isEmpty();
-+ final boolean s2Empty = two.isEmpty();
-+ if (s1Empty & s2Empty) {
-+ return false;
-+ }
-+
-+ if (s1Empty | s2Empty) {
-+ return s2Empty ? one.occludesFullBlock() : two.occludesFullBlock();
-+ }
-+
-+ if (one == two) {
-+ return one.occludesFullBlock();
-+ }
-+
-+ return mergedMayOccludeBlock(one, two) && (one.orUnoptimized(two)).occludesFullBlock();
-+ // Paper end - optimise collisions
- }
-
- @VisibleForTesting
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
-index 53aa193f33a1a15376a59b8d6dd8cbc6cbec168b..a745ff8d115e1d0da6138e4f06726e0737bb1600 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
-@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape {
- super(makeSlice(shape.shape, axis, sliceWidth));
- this.delegate = shape;
- this.axis = axis;
-+ this.initCache(); // Paper - optimise collisions
- }
-
- private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) {
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-index 2936c56e5690b42518010698e5177755422e4c5d..e6b17f32f2b6930739a98c6139442383c1847add 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-@@ -16,37 +16,438 @@ import net.minecraft.world.phys.BlockHitResult;
- import net.minecraft.world.phys.Vec3;
-
- public abstract class VoxelShape {
-- protected final DiscreteVoxelShape shape;
-+ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public
- @Nullable
- private VoxelShape[] faces;
-
-- VoxelShape(DiscreteVoxelShape voxels) {
-+ // Paper start - optimise collisions
-+ private double offsetX;
-+ private double offsetY;
-+ private double offsetZ;
-+ @Nullable private AABB singleAABBRepresentation;
-+ private double[] rootCoordinatesX;
-+ private double[] rootCoordinatesY;
-+ private double[] rootCoordinatesZ;
-+
-+ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData;
-+ private boolean isEmpty;
-+
-+ private io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs;
-+ private AABB cachedBounds;
-+
-+ private Boolean isFullBlock;
-+
-+ private Boolean occludesFullBlock;
-+
-+ // must be power of two
-+ private static final int MERGED_CACHE_SIZE = 16;
-+
-+ private io.papermc.paper.util.collisions.MergedORCache[] mergedORCache;
-+
-+ public final double offsetX() {
-+ return this.offsetX;
-+ }
-+
-+ public final double offsetY() {
-+ return this.offsetY;
-+ }
-+
-+ public final double offsetZ() {
-+ return this.offsetZ;
-+ }
-+
-+ public final AABB getSingleAABBRepresentation() {
-+ return this.singleAABBRepresentation;
-+ }
-+
-+ public final double[] rootCoordinatesX() {
-+ return this.rootCoordinatesX;
-+ }
-+
-+ public final double[] rootCoordinatesY() {
-+ return this.rootCoordinatesY;
-+ }
-+
-+ public final double[] rootCoordinatesZ() {
-+ return this.rootCoordinatesZ;
-+ }
-+
-+ private static double[] extractRawArray(final DoubleList list) {
-+ if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) {
-+ final double[] raw = rawList.elements();
-+ final int expected = rawList.size();
-+ if (raw.length == expected) {
-+ return raw;
-+ } else {
-+ return java.util.Arrays.copyOf(raw, expected);
-+ }
-+ } else {
-+ return list.toDoubleArray();
-+ }
-+ }
-+
-+ public final void initCache() {
-+ this.cachedShapeData = this.shape.getOrCreateCachedShapeData();
-+ this.isEmpty = this.cachedShapeData.isEmpty();
-+
-+ final DoubleList xList = this.getCoords(Direction.Axis.X);
-+ final DoubleList yList = this.getCoords(Direction.Axis.Y);
-+ final DoubleList zList = this.getCoords(Direction.Axis.Z);
-+
-+ if (xList instanceof OffsetDoubleList offsetDoubleList) {
-+ this.offsetX = offsetDoubleList.offset;
-+ this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate);
-+ } else {
-+ this.rootCoordinatesX = extractRawArray(xList);
-+ }
-+
-+ if (yList instanceof OffsetDoubleList offsetDoubleList) {
-+ this.offsetY = offsetDoubleList.offset;
-+ this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate);
-+ } else {
-+ this.rootCoordinatesY = extractRawArray(yList);
-+ }
-+
-+ if (zList instanceof OffsetDoubleList offsetDoubleList) {
-+ this.offsetZ = offsetDoubleList.offset;
-+ this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate);
-+ } else {
-+ this.rootCoordinatesZ = extractRawArray(zList);
-+ }
-+
-+ if (this.cachedShapeData.hasSingleAABB()) {
-+ this.singleAABBRepresentation = new AABB(
-+ this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ,
-+ this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ
-+ );
-+ this.cachedBounds = this.singleAABBRepresentation;
-+ }
-+ }
-+
-+ public final io.papermc.paper.util.collisions.CachedShapeData getCachedVoxelData() {
-+ return this.cachedShapeData;
-+ }
-+
-+ private VoxelShape[] faceShapeClampedCache;
-+
-+ public final VoxelShape getFaceShapeClamped(final Direction direction) {
-+ if (this.isEmpty) {
-+ return (VoxelShape)(Object)this;
-+ }
-+ if ((VoxelShape)(Object)this == Shapes.block()) {
-+ return (VoxelShape)(Object)this;
-+ }
-+
-+ VoxelShape[] cache = this.faceShapeClampedCache;
-+ if (cache != null) {
-+ final VoxelShape ret = cache[direction.ordinal()];
-+ if (ret != null) {
-+ return ret;
-+ }
-+ }
-+
-+
-+ if (cache == null) {
-+ this.faceShapeClampedCache = cache = new VoxelShape[6];
-+ }
-+
-+ final Direction.Axis axis = direction.getAxis();
-+
-+ final VoxelShape ret;
-+
-+ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
-+ if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) {
-+ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1));
-+ } else {
-+ ret = Shapes.empty();
-+ }
-+ } else {
-+ if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) {
-+ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0));
-+ } else {
-+ ret = Shapes.empty();
-+ }
-+ }
-+
-+ cache[direction.ordinal()] = ret;
-+
-+ return ret;
-+ }
-+
-+ private static VoxelShape tryForceBlock(final VoxelShape other) {
-+ if (other == Shapes.block()) {
-+ return other;
-+ }
-+
-+ final AABB otherAABB = other.getSingleAABBRepresentation();
-+ if (otherAABB == null) {
-+ return other;
-+ }
-+
-+ if (Shapes.block().getSingleAABBRepresentation().equals(otherAABB)) {
-+ return Shapes.block();
-+ }
-+
-+ return other;
-+ }
-+
-+ private boolean computeOccludesFullBlock() {
-+ if (this.isEmpty) {
-+ this.occludesFullBlock = Boolean.FALSE;
-+ return false;
-+ }
-+
-+ if (this.isFullBlock()) {
-+ this.occludesFullBlock = Boolean.TRUE;
-+ return true;
-+ }
-+
-+ final AABB singleAABB = this.singleAABBRepresentation;
-+ if (singleAABB != null) {
-+ // check if the bounding box encloses the full cube
-+ final boolean ret =
-+ (singleAABB.minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
-+ (singleAABB.minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
-+ (singleAABB.minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON));
-+ this.occludesFullBlock = Boolean.valueOf(ret);
-+ return ret;
-+ }
-+
-+ final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST);
-+ this.occludesFullBlock = Boolean.valueOf(ret);
-+ return ret;
-+ }
-+
-+ public final boolean occludesFullBlock() {
-+ final Boolean ret = this.occludesFullBlock;
-+ if (ret != null) {
-+ return ret.booleanValue();
-+ }
-+
-+ return this.computeOccludesFullBlock();
-+ }
-+
-+ public final boolean occludesFullBlockIfCached() {
-+ final Boolean ret = this.occludesFullBlock;
-+ return ret != null ? ret.booleanValue() : false;
-+ }
-+
-+ private static int hash(final VoxelShape key) {
-+ return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key));
-+ }
-+
-+ public final VoxelShape orUnoptimized(final VoxelShape other) {
-+ // don't cache simple cases
-+ if (((VoxelShape)(Object)this) == other) {
-+ return other;
-+ }
-+
-+ if (this.isEmpty) {
-+ return other;
-+ }
-+
-+ if (other.isEmpty()) {
-+ return (VoxelShape)(Object)this;
-+ }
-+
-+ // try this cache first
-+ final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1);
-+ final io.papermc.paper.util.collisions.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey];
-+ if (cached != null && cached.key() == other) {
-+ return cached.result();
-+ }
-+
-+ // try other cache
-+ final int otherCacheKey = hash(this) & (MERGED_CACHE_SIZE - 1);
-+ final io.papermc.paper.util.collisions.MergedORCache otherCache = other.mergedORCache == null ? null : other.mergedORCache[otherCacheKey];
-+ if (otherCache != null && otherCache.key() == this) {
-+ return otherCache.result();
-+ }
-+
-+ // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases
-+ final VoxelShape result = Shapes.joinUnoptimized(this, other, BooleanOp.OR);
-+
-+ if (cached != null && otherCache == null) {
-+ // try to use second cache instead of replacing an entry in this cache
-+ if (other.mergedORCache == null) {
-+ other.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE];
-+ }
-+ other.mergedORCache[otherCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(this, result);
-+ } else {
-+ // line is not occupied or other cache line is full
-+ // always bias to replace this cache, as this cache is the first we check
-+ if (this.mergedORCache == null) {
-+ this.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE];
-+ }
-+ this.mergedORCache[thisCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(other, result);
-+ }
-+
-+ return result;
-+ }
-+
-+ private boolean computeFullBlock() {
-+ Boolean ret;
-+ if (this.isEmpty) {
-+ ret = Boolean.FALSE;
-+ } else if ((VoxelShape)(Object)this == Shapes.block()) {
-+ ret = Boolean.TRUE;
-+ } else {
-+ final AABB singleAABB = this.singleAABBRepresentation;
-+ if (singleAABB == null) {
-+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
-+ final int sMinX = shapeData.minFullX();
-+ final int sMinY = shapeData.minFullY();
-+ final int sMinZ = shapeData.minFullZ();
-+
-+ final int sMaxX = shapeData.maxFullX();
-+ final int sMaxY = shapeData.maxFullY();
-+ final int sMaxZ = shapeData.maxFullZ();
-+
-+ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+
-+ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) {
-+
-+ // index = z + y*sizeZ + x*(sizeZ*sizeY)
-+
-+ final int sizeY = shapeData.sizeY();
-+ final int sizeZ = shapeData.sizeZ();
-+
-+ final long[] bitset = shapeData.voxelSet();
-+
-+ ret = Boolean.TRUE;
-+
-+ check_full:
-+ for (int x = sMinX; x < sMaxX; ++x) {
-+ for (int y = sMinY; y < sMaxY; ++y) {
-+ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY);
-+ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) {
-+ ret = Boolean.FALSE;
-+ break check_full;
-+ }
-+ }
-+ }
-+ } else {
-+ ret = Boolean.FALSE;
-+ }
-+ } else {
-+ ret = Boolean.valueOf(
-+ Math.abs(singleAABB.minX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(singleAABB.minY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(singleAABB.minZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+
-+ Math.abs(1.0 - singleAABB.maxX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - singleAABB.maxY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
-+ Math.abs(1.0 - singleAABB.maxZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON
-+ );
-+ }
-+ }
-+
-+ this.isFullBlock = ret;
-+
-+ return ret.booleanValue();
-+ }
-+
-+ public boolean isFullBlock() {
-+ final Boolean ret = this.isFullBlock;
-+
-+ if (ret != null) {
-+ return ret.booleanValue();
-+ }
-+
-+ return this.computeFullBlock();
-+ }
-+ // Paper end - optimise collisions
-+
-+ protected VoxelShape(DiscreteVoxelShape voxels) { // Paper - protected
- this.shape = voxels;
- }
-
- public double min(Direction.Axis axis) {
-- int i = this.shape.firstFull(axis);
-- return i >= this.shape.getSize(axis) ? Double.POSITIVE_INFINITY : this.get(axis, i);
-+ // Paper start - optimise collisions
-+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
-+ switch (axis) {
-+ case X: {
-+ final int idx = shapeData.minFullX();
-+ return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
-+ }
-+ case Y: {
-+ final int idx = shapeData.minFullY();
-+ return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
-+ }
-+ case Z: {
-+ final int idx = shapeData.minFullZ();
-+ return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
-+ }
-+ default: {
-+ // should never get here
-+ return Double.POSITIVE_INFINITY;
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- public double max(Direction.Axis axis) {
-- int i = this.shape.lastFull(axis);
-- return i <= 0 ? Double.NEGATIVE_INFINITY : this.get(axis, i);
-+ // Paper start - optimise collisions
-+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
-+ switch (axis) {
-+ case X: {
-+ final int idx = shapeData.maxFullX();
-+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
-+ }
-+ case Y: {
-+ final int idx = shapeData.maxFullY();
-+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
-+ }
-+ case Z: {
-+ final int idx = shapeData.maxFullZ();
-+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
-+ }
-+ default: {
-+ // should never get here
-+ return Double.NEGATIVE_INFINITY;
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- public AABB bounds() {
-- if (this.isEmpty()) {
-- throw (UnsupportedOperationException)Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
-- } else {
-- return new AABB(
-- this.min(Direction.Axis.X),
-- this.min(Direction.Axis.Y),
-- this.min(Direction.Axis.Z),
-- this.max(Direction.Axis.X),
-- this.max(Direction.Axis.Y),
-- this.max(Direction.Axis.Z)
-- );
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
-+ throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
-+ }
-+ AABB cached = this.cachedBounds;
-+ if (cached != null) {
-+ return cached;
- }
-+
-+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
-+
-+ final double[] coordsX = this.rootCoordinatesX;
-+ final double[] coordsY = this.rootCoordinatesY;
-+ final double[] coordsZ = this.rootCoordinatesZ;
-+
-+ final double offX = this.offsetX;
-+ final double offY = this.offsetY;
-+ final double offZ = this.offsetZ;
-+
-+ // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices
-+ cached = new AABB(
-+ coordsX[shapeData.minFullX()] + offX,
-+ coordsY[shapeData.minFullY()] + offY,
-+ coordsZ[shapeData.minFullZ()] + offZ,
-+
-+ coordsX[shapeData.maxFullX()] + offX,
-+ coordsY[shapeData.maxFullY()] + offY,
-+ coordsZ[shapeData.maxFullZ()] + offZ
-+ );
-+
-+ this.cachedBounds = cached;
-+ return cached;
-+ // Paper end - optimise collisions
- }
-
- public VoxelShape singleEncompassing() {
-@@ -69,28 +470,106 @@ public abstract class VoxelShape {
- protected abstract DoubleList getCoords(Direction.Axis axis);
-
- public boolean isEmpty() {
-- return this.shape.isEmpty();
-+ return this.isEmpty; // Paper - optimise collisions
-+ }
-+
-+ // Paper start - optimise collisions
-+ private static DoubleList offsetList(final DoubleList src, final double by) {
-+ if (src instanceof OffsetDoubleList offsetDoubleList) {
-+ return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset);
-+ }
-+ return new OffsetDoubleList(src, by);
- }
-+ // Paper end - optimise collisions
-
- public VoxelShape move(double x, double y, double z) {
-- return (VoxelShape)(this.isEmpty()
-- ? Shapes.empty()
-- : new ArrayVoxelShape(
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
-+ return Shapes.empty();
-+ }
-+
-+ final ArrayVoxelShape ret = new ArrayVoxelShape(
- this.shape,
-- new OffsetDoubleList(this.getCoords(Direction.Axis.X), x),
-- new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y),
-- new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z)
-- ));
-+ offsetList(this.getCoords(Direction.Axis.X), x),
-+ offsetList(this.getCoords(Direction.Axis.Y), y),
-+ offsetList(this.getCoords(Direction.Axis.Z), z)
-+ );
-+
-+ final io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
-+ if (cachedToAABBs != null) {
-+ ((VoxelShape)ret).cachedToAABBs = io.papermc.paper.util.collisions.CachedToAABBs.offset(cachedToAABBs, x, y, z);
-+ }
-+
-+ return ret;
-+ // Paper end - optimise collisions
- }
-
- public VoxelShape optimize() {
-- VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()};
-- this.forAllBoxes(
-- (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized(
-- voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR
-- )
-- );
-- return voxelShapes[0];
-+ // Paper start - optimise collisions
-+ // Optimise merge strategy to increase the number of simple joins, and additionally forward the toAabbs cache
-+ // to result
-+ if (this.isEmpty) {
-+ return Shapes.empty();
-+ }
-+
-+ if (this.singleAABBRepresentation != null) {
-+ // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block()
-+ return this.isFullBlock() ? Shapes.block() : this;
-+ }
-+
-+ final List<AABB> aabbs = this.toAabbs();
-+
-+ if (aabbs.size() == 1) {
-+ final AABB singleAABB = aabbs.get(0);
-+ final VoxelShape ret = Shapes.create(singleAABB);
-+
-+ // forward AABB cache
-+ if (ret.cachedToAABBs == null) {
-+ ret.cachedToAABBs = this.cachedToAABBs;
-+ }
-+
-+ return ret;
-+ } else {
-+ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn)
-+
-+ // set up flat array so that this merge is done in-place
-+ final VoxelShape[] tmp = new VoxelShape[aabbs.size()];
-+
-+ // initialise as unmerged
-+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
-+ tmp[i] = Shapes.create(aabbs.get(i));
-+ }
-+
-+ int size = aabbs.size();
-+ while (size > 1) {
-+ int newSize = 0;
-+ for (int i = 0; i < size; i += 2) {
-+ final int next = i + 1;
-+ if (next >= size) {
-+ // nothing to merge with, so leave it for next iteration
-+ tmp[newSize++] = tmp[i];
-+ break;
-+ } else {
-+ // merge with adjacent
-+ final VoxelShape first = tmp[i];
-+ final VoxelShape second = tmp[next];
-+
-+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR);
-+ }
-+ }
-+ size = newSize;
-+ }
-+
-+ final VoxelShape ret = tmp[0];
-+
-+ // forward AABB cache
-+ if (ret.cachedToAABBs == null) {
-+ ret.cachedToAABBs = this.cachedToAABBs;
-+ }
-+
-+ return ret;
-+ }
-+ // Paper end - optimise collisions
- }
-
- public void forAllEdges(Shapes.DoubleLineConsumer consumer) {
-@@ -126,10 +605,43 @@ public abstract class VoxelShape {
- );
- }
-
-+ // Paper start - optimise collisions
-+ private List<AABB> toAabbsUncached() {
-+ final List<AABB> ret = new java.util.ArrayList<>();
-+ if (this.singleAABBRepresentation != null) {
-+ ret.add(this.singleAABBRepresentation);
-+ } else {
-+ this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> {
-+ ret.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ));
-+ });
-+ }
-+
-+ // cache result
-+ this.cachedToAABBs = new io.papermc.paper.util.collisions.CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
-+
-+ return ret;
-+ }
-+ // Paper end - optimise collisions
-+
- public List<AABB> toAabbs() {
-- List<AABB> list = Lists.newArrayList();
-- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2)));
-- return list;
-+ // Paper start - optimise collisions
-+ io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
-+ if (cachedToAABBs != null) {
-+ if (!cachedToAABBs.isOffset()) {
-+ return cachedToAABBs.aabbs();
-+ }
-+
-+ // all we need to do is offset the cache
-+ cachedToAABBs = cachedToAABBs.removeOffset();
-+ // update cache
-+ this.cachedToAABBs = cachedToAABBs;
-+
-+ return cachedToAABBs.aabbs();
-+ }
-+
-+ // make new cache
-+ return this.toAabbsUncached();
-+ // Paper end - optimise collisions
- }
-
- public double min(Direction.Axis axis, double from, double to) {
-@@ -154,43 +666,85 @@ public abstract class VoxelShape {
- return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1;
- }
-
-+ // Paper start - optimise collisions
-+ /**
-+ * Copy of AABB#clip but for one AABB
-+ */
-+ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) {
-+ final double[] minDistanceArr = new double[] { 1.0 };
-+ final double diffX = to.x - from.x;
-+ final double diffY = to.y - from.y;
-+ final double diffZ = to.z - from.z;
-+
-+ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ);
-+
-+ if (direction == null) {
-+ return null;
-+ }
-+
-+ final double minDistance = minDistanceArr[0];
-+ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false);
-+ }
-+ // Paper end - optimise collisions
-+
- @Nullable
- public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) {
-- if (this.isEmpty()) {
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
- return null;
-- } else {
-- Vec3 vec3 = end.subtract(start);
-- if (vec3.lengthSqr() < 1.0E-7) {
-- return null;
-- } else {
-- Vec3 vec32 = start.add(vec3.scale(0.001));
-- return this.shape
-- .isFullWide(
-- this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()),
-- this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()),
-- this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ())
-- )
-- ? new BlockHitResult(vec32, Direction.getNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true)
-- : AABB.clip(this.toAabbs(), start, end, pos);
-+ }
-+
-+ final Vec3 directionOpposite = end.subtract(start);
-+ if (directionOpposite.lengthSqr() < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) {
-+ return null;
-+ }
-+
-+ final Vec3 fromBehind = start.add(directionOpposite.scale(0.001));
-+ final double fromBehindOffsetX = fromBehind.x - (double)pos.getX();
-+ final double fromBehindOffsetY = fromBehind.y - (double)pos.getY();
-+ final double fromBehindOffsetZ = fromBehind.z - (double)pos.getZ();
-+
-+ final AABB singleAABB = this.singleAABBRepresentation;
-+ if (singleAABB != null) {
-+ if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-+ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true);
- }
-+ return clip(singleAABB, start, end, pos);
- }
-+
-+ if (io.papermc.paper.util.CollisionUtil.strictlyContains(this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-+ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true);
-+ }
-+
-+ return AABB.clip(this.toAabbs(), start, end, pos);
-+ // Paper end - optimise collisions
- }
-
- public Optional<Vec3> closestPointTo(Vec3 target) {
-- if (this.isEmpty()) {
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
- return Optional.empty();
-- } else {
-- Vec3[] vec3s = new Vec3[1];
-- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> {
-- double d = Mth.clamp(target.x(), minX, maxX);
-- double e = Mth.clamp(target.y(), minY, maxY);
-- double f = Mth.clamp(target.z(), minZ, maxZ);
-- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) {
-- vec3s[0] = new Vec3(d, e, f);
-- }
-- });
-- return Optional.of(vec3s[0]);
- }
-+
-+ Vec3 ret = null;
-+ double retDistance = Double.MAX_VALUE;
-+
-+ final List<AABB> aabbs = this.toAabbs();
-+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
-+ final AABB aabb = aabbs.get(i);
-+ final double x = Mth.clamp(target.x, aabb.minX, aabb.maxX);
-+ final double y = Mth.clamp(target.y, aabb.minY, aabb.maxY);
-+ final double z = Mth.clamp(target.z, aabb.minZ, aabb.maxZ);
-+
-+ double dist = target.distanceToSqr(x, y, z);
-+ if (dist < retDistance) {
-+ ret = new Vec3(x, y, z);
-+ retDistance = dist;
-+ }
-+ }
-+
-+ return Optional.ofNullable(ret);
-+ // Paper end - optimise collisions
- }
-
- public VoxelShape getFaceShape(Direction facing) {
-@@ -227,7 +781,28 @@ public abstract class VoxelShape {
- }
-
- public double collide(Direction.Axis axis, AABB box, double maxDist) {
-- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist);
-+ // Paper start - optimise collisions
-+ if (this.isEmpty) {
-+ return maxDist;
-+ }
-+ if (Math.abs(maxDist) < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) {
-+ return 0.0;
-+ }
-+ switch (axis) {
-+ case X: {
-+ return io.papermc.paper.util.CollisionUtil.collideX(this, box, maxDist);
-+ }
-+ case Y: {
-+ return io.papermc.paper.util.CollisionUtil.collideY(this, box, maxDist);
-+ }
-+ case Z: {
-+ return io.papermc.paper.util.CollisionUtil.collideZ(this, box, maxDist);
-+ }
-+ default: {
-+ throw new RuntimeException("Unknown axis: " + axis);
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) {