aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/1018-Collision-optimisations.patch
diff options
context:
space:
mode:
authorTonytheMacaroni <[email protected]>2024-03-20 17:33:34 -0400
committerGitHub <[email protected]>2024-03-20 22:33:34 +0100
commit41ffa0cf8c26f6137a0d21d45067fee6d5896edf (patch)
treee69ade6383d83730724af6dff5145093716c9529 /patches/server/1018-Collision-optimisations.patch
parentde620b8480372e2896633f36e04e15dd01443265 (diff)
downloadPaper-41ffa0cf8c26f6137a0d21d45067fee6d5896edf.tar.gz
Paper-41ffa0cf8c26f6137a0d21d45067fee6d5896edf.zip
Expose power on fireballs (#10302)
Diffstat (limited to 'patches/server/1018-Collision-optimisations.patch')
-rw-r--r--patches/server/1018-Collision-optimisations.patch4673
1 files changed, 4673 insertions, 0 deletions
diff --git a/patches/server/1018-Collision-optimisations.patch b/patches/server/1018-Collision-optimisations.patch
new file mode 100644
index 0000000000..c57de0cfdf
--- /dev/null
+++ b/patches/server/1018-Collision-optimisations.patch
@@ -0,0 +1,4673 @@
+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..ee0331a6bc40cdde08d926fd8eb1dc642630c2e5
+--- /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.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 073c717bb676b9e99aada00c349fb7eee91df1e7..2a9fc1f1dfc0c5894c1e74dad5a79ae9b02ac74f 100644
+--- a/src/main/java/net/minecraft/core/Direction.java
++++ b/src/main/java/net/minecraft/core/Direction.java
+@@ -57,6 +57,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(int id, int idOpposite, int idHorizontal, String name, Direction.AxisDirection direction, Direction.Axis axis, Vec3i vector) {
+ this.data3d = id;
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index acc9858e0cf10cb2aae0554037096411a208bd05..c99d2f2d64b73179e4e27b63030e26a07953041b 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -496,7 +496,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;
+ }
+ }
+@@ -504,7 +504,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 594cb6ce4bfa6c42212000a1ed983ea95ee2c4bf..97b0119ac71284b3a223c089bec26d87a01d3b25 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -936,7 +936,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 ec01a97cac653fb1a2065289fe5b0c6824732bcb..be2220457e941803b14d78c550d0db777c79a886 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -1250,9 +1250,44 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S
+ 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());
+ }
+@@ -1432,32 +1467,82 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S
+ }
+
+ 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) {
+@@ -2707,11 +2792,70 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S
+ 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 bbe299afd361a107e3936c8ea1a62067fcac9b7e..eadcebd7845ee716e33c0ac0544502da1a6c5941 100644
+--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
+@@ -354,7 +354,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 ffa4f34d964fbcc53e2dfe11677832db21a6eb93..7618364e5373fe17cfe45a5a4ee9ab25e591581c 100644
+--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java
++++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java
+@@ -86,7 +86,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 a3eaf80b020c3bbc0306c5d17659ee661dfd275b..1b6f72932fbdd567a1534bcf15e8a610b00f974d 100644
+--- a/src/main/java/net/minecraft/world/level/BlockCollisions.java
++++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java
+@@ -105,7 +105,7 @@ public class BlockCollisions<T> extends AbstractIterator<T> {
+
+ 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.0D, (double)j + 1.0D, (double)k + 1.0D)) {
++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(this.box, (double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { // Paper - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil
+ continue;
+ }
+
+diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java
+index 86a4f30c8784c602436ecf1c78efb0bdca4b7089..b0bea28e9261767c60d30fb0e76f4f3af8a5634e 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 c476e37df8a75d77f5093b2a449e04f25ef2c2dd..5d66aadae51db1ae760812849bfc8740b82af9a9 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((Entity)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 cc888bbcd6a50124fa553bc4a8ffd1e8885d3856..f42dd9602805e9d538506ee4e3eac7e2811a3da6 100644
+--- a/src/main/java/net/minecraft/world/level/EntityGetter.java
++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
+@@ -45,17 +45,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) {
+@@ -63,23 +82,41 @@ public interface EntityGetter {
+ }
+
+ default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) {
+- if (box.getSize() < 1.0E-7D) {
+- 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-7D), predicate);
+- if (list.isEmpty()) {
+- return List.of();
+- } else {
+- ImmutableList.Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size());
+-
+- for(Entity entity2 : list) {
+- builder.add(Shapes.create(entity2.getBoundingBox()));
+- }
++ entities = this.getHardCollidingEntities(entity, box, null);
++ }
+
+- return builder.build();
++ final List<VoxelShape> ret = new java.util.ArrayList<>(Math.min(25, entities.size()));
++
++ 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 7693163f73ea2dc9cf357893e1545b11b2049aec..3afb280e401f8f1b22bf91472b0b70c4716ac95b 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -294,6 +294,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
+@@ -335,6 +339,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;
+@@ -958,7 +1322,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 b60a52788e73de3dcb086c1a4628466b25c9d3ef..22036ed3ea0629bc12981a8d91a03e55cc2117d6 100644
+--- a/src/main/java/net/minecraft/world/level/block/Block.java
++++ b/src/main/java/net/minecraft/world/level/block/Block.java
+@@ -284,7 +284,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 boolean propagatesSkylightDown(BlockState state, BlockGetter world, BlockPos pos) {
+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 e493b34aa8726ed48f8e5db2ae8ea561cc5b1f75..2892e586146cbc560f0bcf4b9af6d0575cb0a82e 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
+@@ -882,6 +882,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;
+@@ -930,6 +934,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());
+@@ -941,6 +991,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 eb05c01e85825cbd5b7cf43bc6d261db0b871b92..796bbef3544e06b8e7aac7e8ac5f740a2613f4bd 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
+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 a98ab20814cc29a25e9d29adfbb7e70d46768df2..6d8ff6c06af5545634f255ed17dc1e489ece2548 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 b8443953de15066f32f629c0dd7e24bad750f558..67d595f75e0c3bffdb27b85b25ccd1f0bf1427d5 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));
+ }
+@@ -305,7 +316,7 @@ public class AABB {
+ }
+
+ @Nullable
+- private static Direction getDirection(AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ) {
++ public static Direction getDirection(AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ) { // Paper - optimise collisions - public
+ if (deltaX > 1.0E-7D) {
+ approachDirection = clipPoint(traceDistanceResult, approachDirection, deltaX, deltaY, deltaZ, box.minX, box.minY, box.maxY, box.minZ, box.maxZ, Direction.WEST, intersectingVector.x, intersectingVector.y, intersectingVector.z);
+ } else if (deltaX < -1.0E-7D) {
+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 9d627b8e6bf3140b894d38b9a720896e2d776369..a232b9396a41c11579a4d691b05717b16473513e 100644
+--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
++++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
+@@ -15,7 +15,7 @@ public class ArrayVoxelShape extends VoxelShape {
+ this(shape, (DoubleList)DoubleArrayList.wrap(Arrays.copyOf(xPoints, shape.getXSize() + 1)), (DoubleList)DoubleArrayList.wrap(Arrays.copyOf(yPoints, shape.getYSize() + 1)), (DoubleList)DoubleArrayList.wrap(Arrays.copyOf(zPoints, shape.getZSize() + 1)));
+ }
+
+- 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;
+@@ -27,6 +27,7 @@ public class ArrayVoxelShape extends VoxelShape {
+ } else {
+ throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape."));
+ }
++ this.initCache(); // Paper - optimise collisions
+ }
+
+ @Override
+@@ -42,4 +43,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 c25f409d63a50c5de1434db1d6b298935f106221..6f532d9aa613ecb0f5695b108ec6d7ed3598ca82 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);
+@@ -150,46 +150,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;
+ }
+- } 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;
++
++ int lastSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex);
++ if (lastSetZ == -1) {
++ lastSetZ = endIndex;
+ }
+
+- while(bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) {
+- for(int o = j; o <= m; ++o) {
+- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1);
++ 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);
++ }
++
++ // 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 68e89dbd79171627046e89699057964e44c40e7d..110405e6e70d980d3e09f04d79562b32a7413071 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 b27ed92b2a87d4c20c1aa300202adfab896c99ea..d0a4547f95ee24283af77cfa130979d05915292f 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 9176735c08a75854209f24113b0e78332249dc4d..17785f7c709073a01928e8e6a57702f8357900f4 100644
+--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
+@@ -16,13 +16,43 @@ public final class Shapes {
+ public static final double EPSILON = 1.0E-7D;
+ public static final double BIG_EPSILON = 1.0E-6D;
+ 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, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
+ private static final VoxelShape EMPTY = new ArrayVoxelShape(new BitSetDiscreteVoxelShape(0, 0, 0), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D})), (DoubleList)(new DoubleArrayList(new double[]{0.0D})));
+
++ // 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;
+ }
+@@ -41,22 +71,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-7D) && !(maxY - minY < 1.0E-7D) && !(maxZ - minZ < 1.0E-7D)) {
+- int i = findBits(minX, maxX);
+- int j = findBits(minY, maxY);
+- int k = findBits(minZ, maxZ);
+- if (i >= 0 && j >= 0 && k >= 0) {
+- 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 {
+- 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 CubeVoxelShape(bitSetDiscreteVoxelShape);
++ 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 {
+- return new ArrayVoxelShape(BLOCK.shape, (DoubleList)DoubleArrayList.wrap(new double[]{minX, maxX}), (DoubleList)DoubleArrayList.wrap(new double[]{minY, maxY}), (DoubleList)DoubleArrayList.wrap(new double[]{minZ, maxZ}));
++ 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 })
++ );
+ }
++ // Paper end - optimise collisions
+ } else {
+ return empty();
+ }
+@@ -95,67 +142,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-7D) {
+- return bl3 || bl4;
+- }
+-
+- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7D) {
+- 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(IndexMerger mergedX, IndexMerger mergedY, IndexMerger mergedZ, DiscreteVoxelShape shape1, DiscreteVoxelShape shape2, BooleanOp predicate) {
+@@ -181,69 +214,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;
+- } 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.0D, 1.0E-7D) && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0D, 1.0E-7D) && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp);
+ }
++
++ // 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;
++ }
++ 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.0D, 1.0E-7D);
+- i = shape.shape.getSize(axis) - 1;
+- } else {
+- bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0D, 1.0E-7D);
+- 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.0D, 1.0E-7D)) {
+- 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.0D, 1.0E-7D)) {
+- 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) {
+- if (one != block() && two != block()) {
+- if (one.isEmpty() && two.isEmpty()) {
+- return false;
+- } else {
+- return !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST);
+- }
+- } else {
++ // 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 cf469f9daa81da8bc330c9cac7e813db87f9f9af..d9256710e815a5cb55409a80d59df2029b98c0d7 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 15e2dfa9a17b4f19768c0cde0ad8031f0122cd33..6bd6385ad82481a099f3556ed2dbd3744888fc34 100644
+--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
+@@ -16,30 +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;
++ 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() {
+@@ -53,19 +461,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(this.shape, (DoubleList)(new OffsetDoubleList(this.getCoords(Direction.Axis.X), x)), (DoubleList)(new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y)), (DoubleList)(new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z))));
++ // Paper start - optimise collisions
++ if (this.isEmpty) {
++ return Shapes.empty();
++ }
++
++ final ArrayVoxelShape ret = new ArrayVoxelShape(
++ this.shape,
++ 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) {
+@@ -83,12 +578,43 @@ public abstract class VoxelShape {
+ }, true);
+ }
+
++ // 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) {
+@@ -115,37 +641,85 @@ public abstract class VoxelShape {
+ }) - 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-7D) {
+- return null;
+- } else {
+- Vec3 vec32 = start.add(vec3.scale(0.001D));
+- 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) {
+@@ -180,7 +754,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) {