diff --git a/patches/server/0027-fixup-Highly-optimise-single-and-multi-AABB-VoxelSha.patch b/patches/server/0027-fixup-Highly-optimise-single-and-multi-AABB-VoxelSha.patch new file mode 100644 index 0000000000..e3191eaf5c --- /dev/null +++ b/patches/server/0027-fixup-Highly-optimise-single-and-multi-AABB-VoxelSha.patch @@ -0,0 +1,5400 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 12 Sep 2023 04:42:56 -0700 +Subject: [PATCH] fixup! Highly optimise single and multi-AABB VoxelShapes and + collisions + + +diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java +index 77d369ca6be814d1f5a19ec37b44800529e6cda3..bfb1de19f53d5d7c7b65e25a606fabfa416706b3 100644 +--- a/src/main/java/io/papermc/paper/util/CollisionUtil.java ++++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java +@@ -1,30 +1,37 @@ + package io.papermc.paper.util; + +-import io.papermc.paper.voxel.AABBVoxelShape; ++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.server.level.ServerLevel; +-import net.minecraft.server.level.WorldGenRegion; + 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.FlowingFluid; + 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; +@@ -32,23 +39,19 @@ import java.util.function.Predicate; + public final class CollisionUtil { + + public static final double COLLISION_EPSILON = 1.0E-7; +- +- public static final long KNOWN_EMPTY_BLOCK = 0b00; // known to always have voxelshape of empty +- public static final long KNOWN_FULL_BLOCK = 0b01; // known to always have voxelshape of full cube +- public static final long KNOWN_UNKNOWN_BLOCK = 0b10; // must read the actual block state for info +- public static final long KNOWN_SPECIAL_BLOCK = 0b11; // caller must check this block for special collisions ++ 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.shapeExceedsCube() || block.getBlock() == Blocks.MOVING_PISTON; ++ 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; ++ 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; ++ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON; + } + + public static AABB getBoxForChunk(final int chunkX, final int chunkZ) { +@@ -89,11 +92,8 @@ public final class CollisionUtil { + (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_move == 0.0) { +- return 0.0; +- } +- + 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) { +@@ -113,11 +113,8 @@ public final class CollisionUtil { + 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_move == 0.0) { +- return 0.0; +- } +- + 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) { +@@ -137,28 +134,1160 @@ public final class CollisionUtil { + return source_move; + } + +- public static double collideZ(final AABB target, final AABB source, final double source_move) { +- if (source_move == 0.0) { +- return 0.0; +- } ++ // 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 ((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; ++ 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 Math.max(max_move, source_move); + } ++ ++ return resultSize <= 1 ? EMPTY : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); + } +- return source_move; + } + + public static AABB offsetX(final AABB box, final double dx) { +@@ -178,7 +1307,7 @@ public final class CollisionUtil { + } + + 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, false); ++ 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 +@@ -221,8 +1350,11 @@ public final class CollisionUtil { + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false); + } + +- public static double performCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List 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); + } +@@ -230,8 +1362,11 @@ public final class CollisionUtil { + return value; + } + +- public static double performCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List 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); + } +@@ -239,8 +1374,11 @@ public final class CollisionUtil { + return value; + } + +- public static double performCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List 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); + } +@@ -248,13 +1386,49 @@ public final class CollisionUtil { + return value; + } + +- public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { ++ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List 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 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 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 potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { +- y = performCollisionsY(axisalignedbb, y, potentialCollisions); ++ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } +@@ -263,100 +1437,105 @@ public final class CollisionUtil { + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { +- z = performCollisionsZ(axisalignedbb, z, potentialCollisions); ++ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { +- x = performCollisionsX(axisalignedbb, x, potentialCollisions); ++ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { +- z = performCollisionsZ(axisalignedbb, z, potentialCollisions); ++ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); + } + + return new Vec3(x, y, z); + } + +- public static boolean addBoxesToIfIntersects(final VoxelShape shape, final AABB aabb, final List list) { +- if (shape instanceof AABBVoxelShape) { +- final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape; +- if (voxelShapeIntersect(shapeCasted.aabb, aabb) && !isEmpty(shapeCasted.aabb)) { +- list.add(shapeCasted.aabb); +- return true; +- } +- return false; +- } else if (shape instanceof ArrayVoxelShape) { +- final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape; +- // this can be optimised by checking an "overall shape" first, but not needed +- +- final double offX = shapeCasted.getOffsetX(); +- final double offY = shapeCasted.getOffsetY(); +- final double offZ = shapeCasted.getOffsetZ(); +- +- boolean ret = false; +- +- for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) { +- final double minX, minY, minZ, maxX, maxY, maxZ; +- if (voxelShapeIntersect(aabb, minX = boundingBox.minX + offX, minY = boundingBox.minY + offY, minZ = boundingBox.minZ + offZ, +- maxX = boundingBox.maxX + offX, maxY = boundingBox.maxY + offY, maxZ = boundingBox.maxZ + offZ) +- && !isEmpty(minX, minY, minZ, maxX, maxY, maxZ)) { +- list.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ, false)); +- ret = true; +- } ++ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List 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); + } ++ } + +- return ret; +- } else { +- final List boxes = shape.toAabbs(); ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); + +- boolean ret = false; ++ if (xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); ++ } ++ } + +- for (int i = 0, len = boxes.size(); i < len; ++i) { +- final AABB box = boxes.get(i); +- if (voxelShapeIntersect(box, aabb) && !isEmpty(box)) { +- list.add(box); +- ret = true; +- } ++ if (x != 0.0) { ++ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions); ++ if (!xSmaller && x != 0.0) { ++ axisalignedbb = offsetX(axisalignedbb, x); + } ++ } + +- return ret; ++ if (!xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); + } ++ ++ return new Vec3(x, y, z); + } + +- public static void addBoxesTo(final VoxelShape shape, final List list) { +- if (shape instanceof AABBVoxelShape) { +- final AABBVoxelShape shapeCasted = (AABBVoxelShape)shape; +- if (!isEmpty(shapeCasted.aabb)) { +- list.add(shapeCasted.aabb); ++ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, ++ final List voxels, ++ final List 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); + } +- } else if (shape instanceof ArrayVoxelShape) { +- final ArrayVoxelShape shapeCasted = (ArrayVoxelShape)shape; ++ } + +- final double offX = shapeCasted.getOffsetX(); +- final double offY = shapeCasted.getOffsetY(); +- final double offZ = shapeCasted.getOffsetZ(); ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); + +- for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) { +- final AABB box = boundingBox.move(offX, offY, offZ); +- if (!isEmpty(box)) { +- list.add(box); +- } ++ if (xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); ++ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); + } +- } else { +- final List boxes = shape.toAabbs(); +- for (int i = 0, len = boxes.size(); i < len; ++i) { +- final AABB box = boxes.get(i); +- if (!isEmpty(box)) { +- list.add(box); +- } ++ } ++ ++ 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 isAlmostCollidingOnBorder(final WorldBorder worldborder, final AABB boundingBox) { +@@ -403,97 +1582,80 @@ public final class CollisionUtil { + return boxMinX < borderMinX || boxMaxX > borderMaxX || boxMinZ < borderMinZ || boxMaxZ > borderMaxZ; + } + +- public static boolean getCollisionsForBlocksOrWorldBorder(final CollisionGetter getter, final Entity entity, final AABB aabb, +- final List into, final boolean loadChunks, final boolean collidesWithUnloaded, +- final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) { ++ /* 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 intoVoxel, final List intoAABB, ++ final int collisionFlags, final BiPredicate predicate) { ++ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; + boolean ret = false; + +- if (checkBorder) { +- if (CollisionUtil.isAlmostCollidingOnBorder(getter.getWorldBorder(), aabb)) { ++ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { ++ if (CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), aabb)) { + if (checkOnly) { + return true; + } else { +- CollisionUtil.addBoxesTo(getter.getWorldBorder().getCollisionShape(), into); ++ final VoxelShape borderShape = world.getWorldBorder().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 = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1; +- final int maxBlockY = Mth.floor(aabb.maxY + 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 int minSection = WorldUtil.getMinSection(getter); +- final int maxSection = WorldUtil.getMaxSection(getter); +- final int minBlock = minSection << 4; +- final int maxBlock = (maxSection << 4) | 15; +- + final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); +- CollisionContext collisionShape = null; ++ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); + + // special cases: +- if (minBlockY > maxBlock || maxBlockY < minBlock) { ++ if (minBlockY > maxBlockY) { + // no point in checking + return ret; + } + +- final int minYIterate = Math.max(minBlock, minBlockY); +- final int maxYIterate = Math.min(maxBlock, maxBlockY); +- + final int minChunkX = minBlockX >> 4; + final int maxChunkX = maxBlockX >> 4; + + final int minChunkY = minBlockY >> 4; + final int maxChunkY = maxBlockY >> 4; + +- final int minChunkYIterate = minYIterate >> 4; +- final int maxChunkYIterate = maxYIterate >> 4; +- + final int minChunkZ = minBlockZ >> 4; + final int maxChunkZ = maxBlockZ >> 4; + +- final ServerChunkCache chunkProvider; +- if (getter instanceof WorldGenRegion) { +- chunkProvider = null; +- } else if (getter instanceof ServerLevel) { +- chunkProvider = ((ServerLevel)getter).getChunkSource(); +- } else { +- chunkProvider = null; +- } ++ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; ++ final ServerChunkCache chunkSource = (ServerChunkCache)world.getChunkSource(); + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { +- final int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk +- final int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk +- + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { +- final int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk +- final int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk +- +- final int chunkXGlobalPos = currChunkX << 4; +- final int chunkZGlobalPos = currChunkZ << 4; +- final ChunkAccess chunk; +- if (chunkProvider == null) { +- chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ); +- } else { +- // Folia start - ignore chunk if we do not own the region +- if (!io.papermc.paper.util.TickThread.isTickThreadFor(chunkProvider.chunkMap.level, currChunkX, currChunkZ)) { +- chunk = null; +- } else { // Folia end - ignore chunk if we do not own the region +- chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); +- } // Folia - ignore chunk if we do not own the region +- } ++ final ChunkAccess chunk = loadChunks ? chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, true) : chunkSource.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); + + if (chunk == null) { +- if (collidesWithUnloaded) { ++ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { + if (checkOnly) { + return true; + } else { +- into.add(getBoxForChunk(currChunkX, currChunkZ)); ++ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); + ret = true; + } + } +@@ -503,303 +1665,104 @@ public final class CollisionUtil { + final LevelChunkSection[] sections = chunk.getSections(); + + // bound y +- +- for (int currChunkY = minChunkYIterate; currChunkY <= maxChunkYIterate; ++currChunkY) { +- final LevelChunkSection section = sections[currChunkY - minSection]; ++ 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 PalettedContainer blocks = section.states; + +- final int minY = currChunkY == minChunkYIterate ? minYIterate & 15 : 0; // coordinate in chunk +- final int maxY = currChunkY == maxChunkYIterate ? maxYIterate & 15 : 15; // coordinate in chunk +- final int chunkYGlobalPos = currChunkY << 4; +- +- final boolean sectionHasSpecial = section.hasSpecialCollidingBlocks(); +- +- final int minXIterate; +- final int maxXIterate; +- final int minZIterate; +- final int maxZIterate; +- final int minYIterateLocal; +- final int maxYIterateLocal; +- +- if (!sectionHasSpecial) { +- minXIterate = currChunkX == minChunkX ? minX + 1 : minX; +- maxXIterate = currChunkX == maxChunkX ? maxX - 1 : maxX; +- minZIterate = currChunkZ == minChunkZ ? minZ + 1 : minZ; +- maxZIterate = currChunkZ == maxChunkZ ? maxZ - 1 : maxZ; +- minYIterateLocal = currChunkY == minChunkY ? minY + 1 : minY; +- maxYIterateLocal = currChunkY == maxChunkY ? maxY - 1 : maxY; +- if (minXIterate > maxXIterate || minZIterate > maxZIterate) { +- continue; +- } +- } else { +- minXIterate = minX; +- maxXIterate = maxX; +- minZIterate = minZ; +- maxZIterate = maxZ; +- minYIterateLocal = minY; +- maxYIterateLocal = maxY; +- } ++ final boolean hasSpecial = section.getSpecialCollidingBlocks() != 0; ++ final int sectionAdjust = !hasSpecial ? 1 : 0; + +- for (int currY = minYIterateLocal; currY <= maxYIterateLocal; ++currY) { +- long collisionForHorizontal = section.getKnownBlockInfoHorizontalRaw(currY, minZIterate & 15); +- for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ, +- collisionForHorizontal = (currZ & 1) == 0 ? section.getKnownBlockInfoHorizontalRaw(currY, currZ & 15) : collisionForHorizontal) { +- // From getKnownBlockInfoHorizontalRaw: +- // important detail: this returns 32 values, one for localZ = localZ & (~1) and one for localZ = localZ | 1 +- // the even localZ is the lower 32 bits, the odd is the upper 32 bits +- // We want to use a bitset to only iterate over non-empty blocks. +- // We need to build a bitset mask to and out the other collisions we just don't care at all about +- // First, we need to build a bitset from 0..n*2 where n is the number of blocks on the x axis +- // It's important to note that the iterate values can be outside [0, 15], but if they are, +- // then none of the x or z loops would meet their conditions. So we can assume they are never +- // out of bounds here +- final int xAxisBits = (maxXIterate - minXIterate + 1) << 1; // << 1 -> * 2 // Never > 32 +- long bitset = (1L << xAxisBits) - 1; +- // Now we need to offset it by 32 bits if current Z is odd (lower 32 bits is 16 block infos for even z, upper is for odd) +- int shift = (currZ & 1) << 5; // this will be a LEFT shift +- // Now we need to offset shift so that the bitset first position is at minXIterate +- shift += (minXIterate << 1); // 0th pos -> 0th bit, 1st pos -> 2nd bit, ... +- +- // all done +- bitset = bitset << shift; +- if ((collisionForHorizontal & bitset) == 0L) { +- // All empty +- continue; +- } +- for (int currX = minXIterate; currX <= maxXIterate; ++currX) { +- final int localBlockIndex = (currX) | (currZ << 4) | (currY << 8); ++ final PalettedContainer blocks = section.states; + +- final int blockInfo = (int) LevelChunkSection.getKnownBlockInfo(localBlockIndex, collisionForHorizontal); ++ 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); + +- switch (blockInfo) { +- case (int) CollisionUtil.KNOWN_EMPTY_BLOCK: { +- continue; +- } +- case (int) CollisionUtil.KNOWN_FULL_BLOCK: { +- double blockX = (double)(currX | chunkXGlobalPos); +- double blockY = (double)(currY | chunkYGlobalPos); +- double blockZ = (double)(currZ | chunkZGlobalPos); +- final AABB blockBox = new AABB( +- blockX, blockY, blockZ, +- blockX + 1.0, blockY + 1.0, blockZ + 1.0, +- true +- ); +- if (predicate != null) { +- if (!voxelShapeIntersect(aabb, blockBox)) { +- continue; +- } +- // fall through to get the block for the predicate +- } else { +- if (voxelShapeIntersect(aabb, blockBox)) { +- if (checkOnly) { +- return true; +- } else { +- into.add(blockBox); +- ret = true; +- } +- } +- continue; +- } +- } +- // default: fall through to standard logic ++ 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; + } + +- int blockX = currX | chunkXGlobalPos; +- int blockY = currY | chunkYGlobalPos; +- int blockZ = currZ | chunkZGlobalPos; ++ final BlockState blockData = blocks.get(localBlockIndex); + +- int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + +- ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + +- ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0); +- if (edgeCount == 3) { ++ if (blockData.emptyCollisionShape()) { + continue; + } + +- BlockState blockData = blocks.get(localBlockIndex); ++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { ++ VoxelShape blockCollision = blockData.getConstantCollisionShape(); + +- if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { +- mutablePos.set(blockX, blockY, blockZ); +- if (collisionShape == null) { +- collisionShape = new LazyEntityCollisionContext(entity); ++ if (blockCollision == null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); + } +- VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape); +- if (voxelshape2 != Shapes.empty()) { +- VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ); + +- if (predicate != null && !predicate.test(blockData, mutablePos)) { ++ AABB singleAABB = blockCollision.getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); ++ if (!voxelShapeIntersect(aabb, singleAABB)) { + continue; + } + +- if (checkOnly) { +- if (voxelshape3.intersects(aabb)) { +- return true; ++ if (predicate != null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ if (!predicate.test(blockData, mutablePos)) { ++ continue; + } ++ } ++ ++ if (checkOnly) { ++ return true; + } else { +- ret |= addBoxesToIfIntersects(voxelshape3, aabb, into); ++ ret = true; ++ intoAABB.add(singleAABB); ++ continue; + } + } +- } +- } +- } +- } +- } +- } +- } +- +- return ret; +- } +- +- public static boolean getCollisionsForBlocksOrWorldBorderReference(final CollisionGetter getter, final Entity entity, final AABB aabb, +- final List into, final boolean loadChunks, final boolean collidesWithUnloaded, +- final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) { +- boolean ret = false; +- +- if (checkBorder) { +- if (CollisionUtil.isAlmostCollidingOnBorder(getter.getWorldBorder(), aabb)) { +- if (checkOnly) { +- return true; +- } else { +- CollisionUtil.addBoxesTo(getter.getWorldBorder().getCollisionShape(), into); +- ret = true; +- } +- } +- } +- +- final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; +- final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; +- +- final int minBlockY = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1; +- final int maxBlockY = 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 int minSection = WorldUtil.getMinSection(getter); +- final int maxSection = WorldUtil.getMaxSection(getter); +- final int minBlock = minSection << 4; +- final int maxBlock = (maxSection << 4) | 15; +- +- final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); +- CollisionContext collisionShape = null; +- +- // special cases: +- if (minBlockY > maxBlock || maxBlockY < minBlock) { +- // no point in checking +- return ret; +- } +- +- final int minYIterate = Math.max(minBlock, minBlockY); +- final int maxYIterate = Math.min(maxBlock, maxBlockY); +- +- final int minChunkX = minBlockX >> 4; +- final int maxChunkX = maxBlockX >> 4; +- +- final int minChunkY = minBlockY >> 4; +- final int maxChunkY = maxBlockY >> 4; +- +- final int minChunkYIterate = minYIterate >> 4; +- final int maxChunkYIterate = maxYIterate >> 4; +- +- final int minChunkZ = minBlockZ >> 4; +- final int maxChunkZ = maxBlockZ >> 4; +- +- final ServerChunkCache chunkProvider; +- if (getter instanceof WorldGenRegion) { +- chunkProvider = null; +- } else if (getter instanceof ServerLevel) { +- chunkProvider = ((ServerLevel)getter).getChunkSource(); +- } else { +- chunkProvider = null; +- } +- +- for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { +- final int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk +- final int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk +- +- for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { +- final int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk +- final int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk +- +- final int chunkXGlobalPos = currChunkX << 4; +- final int chunkZGlobalPos = currChunkZ << 4; +- final ChunkAccess chunk; +- if (chunkProvider == null) { +- chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ); +- } else { +- chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); +- } +- +- if (chunk == null) { +- if (collidesWithUnloaded) { +- if (checkOnly) { +- return true; +- } else { +- into.add(getBoxForChunk(currChunkX, currChunkZ)); +- ret = true; +- } +- } +- continue; +- } +- +- final LevelChunkSection[] sections = chunk.getSections(); +- +- // bound y +- for (int currChunkY = minChunkYIterate; currChunkY <= maxChunkYIterate; ++currChunkY) { +- final LevelChunkSection section = sections[currChunkY - minSection]; +- if (section == null || section.hasOnlyAir()) { +- // empty +- continue; +- } +- final PalettedContainer blocks = section.states; +- +- final int minY = currChunkY == minChunkYIterate ? minYIterate & 15 : 0; // coordinate in chunk +- final int maxY = currChunkY == maxChunkYIterate ? maxYIterate & 15 : 15; // coordinate in chunk +- final int chunkYGlobalPos = currChunkY << 4; + +- for (int currY = minY; currY <= maxY; ++currY) { +- for (int currZ = minZ; currZ <= maxZ; ++currZ) { +- for (int currX = minX; currX <= maxX; ++currX) { +- int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); +- int blockX = currX | chunkXGlobalPos; +- int blockY = currY | chunkYGlobalPos; +- int blockZ = currZ | chunkZGlobalPos; +- +- int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + +- ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + +- ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0); +- if (edgeCount == 3) { +- continue; +- } ++ if (blockCollision.isEmpty()) { ++ continue; ++ } + +- BlockState blockData = blocks.get(localBlockIndex); +- if (blockData.getBlockCollisionBehavior() == CollisionUtil.KNOWN_EMPTY_BLOCK) { +- continue; +- } ++ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); + +- if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { +- mutablePos.set(blockX, blockY, blockZ); +- if (collisionShape == null) { +- collisionShape = new LazyEntityCollisionContext(entity); ++ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { ++ continue; + } +- VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape); +- if (voxelshape2 != Shapes.empty()) { +- VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ); + +- if (predicate != null && !predicate.test(blockData, mutablePos)) { ++ if (predicate != null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ if (!predicate.test(blockData, mutablePos)) { + continue; + } ++ } + +- if (checkOnly) { +- if (voxelshape3.intersects(aabb)) { +- return true; +- } +- } else { +- ret |= addBoxesToIfIntersects(voxelshape3, aabb, into); +- } ++ if (checkOnly) { ++ return true; ++ } else { ++ ret = true; ++ intoVoxel.add(blockCollisionOffset); ++ continue; + } + } + } +@@ -813,66 +1776,75 @@ public final class CollisionUtil { + } + + public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb, +- final List into, final boolean checkOnly, final Predicate predicate) { +- if (isEmpty(aabb) || !(getter instanceof EntityGetter entityGetter)) { ++ final List into, final int collisionFlags, final Predicate 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 we only get stuff we definitely collide with. ++ // 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 entities = CachedLists.getTempGetEntitiesList(); +- try { +- if (entity != null && entity.hardCollides()) { +- entityGetter.getEntities(entity, aabb, predicate, entities); +- } else { +- entityGetter.getHardCollidingEntities(entity, aabb, predicate, entities); +- } ++ final List 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); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); + +- if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { +- if (checkOnly) { +- return true; +- } else { +- into.add(otherEntity.getBoundingBox()); +- ret = true; +- } ++ 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; + } + } +- } finally { +- CachedLists.returnTempGetEntitiesList(entities); + } + + return ret; + } + +- public static boolean getCollisions(final CollisionGetter view, final Entity entity, final AABB aabb, +- final List into, final boolean loadChunks, final boolean collidesWithUnloadedChunks, +- final boolean checkBorder, final boolean checkOnly, final BiPredicate blockPredicate, ++ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, ++ final List intoVoxel, final List intoAABB, final int collisionFlags, ++ final BiPredicate blockPredicate, + final Predicate entityPredicate) { +- if (checkOnly) { +- return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) +- || getEntityHardCollisions(view, entity, aabb, into, checkOnly, 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(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) +- | getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate); ++ 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; + } +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 aabbs, ++ boolean isOffset, ++ double offX, double offY, double offZ ++) { ++ ++ public CachedToAABBs removeOffset() { ++ final List toOffset = this.aabbs; ++ final double offX = this.offX; ++ final double offY = this.offY; ++ final double offZ = this.offZ; ++ ++ final List 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/io/papermc/paper/voxel/AABBVoxelShape.java b/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java +deleted file mode 100644 +index 3d2fa2466fe40e0b9d7749498684587a11dfa80a..0000000000000000000000000000000000000000 +--- a/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java ++++ /dev/null +@@ -1,200 +0,0 @@ +-package io.papermc.paper.voxel; +- +-import io.papermc.paper.util.CollisionUtil; +-import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +-import it.unimi.dsi.fastutil.doubles.DoubleList; +-import net.minecraft.core.Direction; +-import net.minecraft.world.phys.AABB; +-import net.minecraft.world.phys.shapes.Shapes; +-import net.minecraft.world.phys.shapes.VoxelShape; +-import java.util.ArrayList; +-import java.util.List; +- +-public final class AABBVoxelShape extends VoxelShape { +- +- public final AABB aabb; +- +- public AABBVoxelShape(AABB aabb) { +- super(Shapes.getFullUnoptimisedCube().shape); +- this.aabb = aabb; +- } +- +- @Override +- public boolean isEmpty() { +- return CollisionUtil.isEmpty(this.aabb); +- } +- +- @Override +- public double min(Direction.Axis enumdirection_enumaxis) { +- switch (enumdirection_enumaxis.ordinal()) { +- case 0: +- return this.aabb.minX; +- case 1: +- return this.aabb.minY; +- case 2: +- return this.aabb.minZ; +- default: +- throw new IllegalStateException("Unknown axis requested"); +- } +- } +- +- @Override +- public double max(Direction.Axis enumdirection_enumaxis) { +- switch (enumdirection_enumaxis.ordinal()) { +- case 0: +- return this.aabb.maxX; +- case 1: +- return this.aabb.maxY; +- case 2: +- return this.aabb.maxZ; +- default: +- throw new IllegalStateException("Unknown axis requested"); +- } +- } +- +- @Override +- public AABB bounds() { +- return this.aabb; +- } +- +- // enum direction axis is from 0 -> 2, so we keep the lower bits for direction axis. +- @Override +- protected double get(Direction.Axis enumdirection_enumaxis, int i) { +- switch (enumdirection_enumaxis.ordinal() | (i << 2)) { +- case (0 | (0 << 2)): +- return this.aabb.minX; +- case (1 | (0 << 2)): +- return this.aabb.minY; +- case (2 | (0 << 2)): +- return this.aabb.minZ; +- case (0 | (1 << 2)): +- return this.aabb.maxX; +- case (1 | (1 << 2)): +- return this.aabb.maxY; +- case (2 | (1 << 2)): +- return this.aabb.maxZ; +- default: +- throw new IllegalStateException("Unknown axis requested"); +- } +- } +- +- private DoubleList cachedListX; +- private DoubleList cachedListY; +- private DoubleList cachedListZ; +- +- @Override +- protected DoubleList getCoords(Direction.Axis enumdirection_enumaxis) { +- switch (enumdirection_enumaxis.ordinal()) { +- case 0: +- return this.cachedListX == null ? this.cachedListX = DoubleArrayList.wrap(new double[] { this.aabb.minX, this.aabb.maxX }) : this.cachedListX; +- case 1: +- return this.cachedListY == null ? this.cachedListY = DoubleArrayList.wrap(new double[] { this.aabb.minY, this.aabb.maxY }) : this.cachedListY; +- case 2: +- return this.cachedListZ == null ? this.cachedListZ = DoubleArrayList.wrap(new double[] { this.aabb.minZ, this.aabb.maxZ }) : this.cachedListZ; +- default: +- throw new IllegalStateException("Unknown axis requested"); +- } +- } +- +- @Override +- public VoxelShape move(double d0, double d1, double d2) { +- return new AABBVoxelShape(this.aabb.move(d0, d1, d2)); +- } +- +- @Override +- public VoxelShape optimize() { +- if (this.isEmpty()) { +- return Shapes.empty(); +- } else if (this == Shapes.BLOCK_OPTIMISED || this.aabb.equals(Shapes.BLOCK_OPTIMISED.aabb)) { +- return Shapes.BLOCK_OPTIMISED; +- } +- return this; +- } +- +- @Override +- public void forAllBoxes(Shapes.DoubleLineConsumer voxelshapes_a) { +- voxelshapes_a.consume(this.aabb.minX, this.aabb.minY, this.aabb.minZ, this.aabb.maxX, this.aabb.maxY, this.aabb.maxZ); +- } +- +- @Override +- public List toAabbs() { // getAABBs +- List ret = new ArrayList<>(1); +- ret.add(this.aabb); +- return ret; +- } +- +- @Override +- protected int findIndex(Direction.Axis enumdirection_enumaxis, double d0) { // findPointIndexAfterOffset +- switch (enumdirection_enumaxis.ordinal()) { +- case 0: +- return d0 < this.aabb.maxX ? (d0 < this.aabb.minX ? -1 : 0) : 1; +- case 1: +- return d0 < this.aabb.maxY ? (d0 < this.aabb.minY ? -1 : 0) : 1; +- case 2: +- return d0 < this.aabb.maxZ ? (d0 < this.aabb.minZ ? -1 : 0) : 1; +- default: +- throw new IllegalStateException("Unknown axis requested"); +- } +- } +- +- @Override +- protected VoxelShape calculateFace(Direction direction) { +- if (this.isEmpty()) { +- return Shapes.empty(); +- } +- if (this == Shapes.BLOCK_OPTIMISED) { +- return this; +- } +- switch (direction) { +- case EAST: // +X +- case WEST: { // -X +- final double from = direction == Direction.EAST ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; +- if (from > this.aabb.maxX || this.aabb.minX > from) { +- return Shapes.empty(); +- } +- return new AABBVoxelShape(new AABB(0.0, this.aabb.minY, this.aabb.minZ, 1.0, this.aabb.maxY, this.aabb.maxZ)).optimize(); +- } +- case UP: // +Y +- case DOWN: { // -Y +- final double from = direction == Direction.UP ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; +- if (from > this.aabb.maxY || this.aabb.minY > from) { +- return Shapes.empty(); +- } +- return new AABBVoxelShape(new AABB(this.aabb.minX, 0.0, this.aabb.minZ, this.aabb.maxX, 1.0, this.aabb.maxZ)).optimize(); +- } +- case SOUTH: // +Z +- case NORTH: { // -Z +- final double from = direction == Direction.SOUTH ? 1.0 - CollisionUtil.COLLISION_EPSILON : CollisionUtil.COLLISION_EPSILON; +- if (from > this.aabb.maxZ || this.aabb.minZ > from) { +- return Shapes.empty(); +- } +- return new AABBVoxelShape(new AABB(this.aabb.minX, this.aabb.minY, 0.0, this.aabb.maxX, this.aabb.maxY, 1.0)).optimize(); +- } +- default: { +- throw new IllegalStateException("Unknown axis requested"); +- } +- } +- } +- +- @Override +- public double collide(Direction.Axis enumdirection_enumaxis, AABB axisalignedbb, double d0) { +- if (CollisionUtil.isEmpty(this.aabb)) { +- return d0; +- } +- switch (enumdirection_enumaxis.ordinal()) { +- case 0: +- return CollisionUtil.collideX(this.aabb, axisalignedbb, d0); +- case 1: +- return CollisionUtil.collideY(this.aabb, axisalignedbb, d0); +- case 2: +- return CollisionUtil.collideZ(this.aabb, axisalignedbb, d0); +- default: +- throw new IllegalStateException("Unknown axis requested"); +- } +- } +- +- @Override +- public boolean intersects(AABB axisalingedbb) { +- return CollisionUtil.voxelShapeIntersect(this.aabb, axisalingedbb); +- } +-} +diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java +index d0a8092bf57a29ab7c00ec0ddf52a9fdb2a33267..392406722b0a040c1d41fdc1154d75d39f6e9c86 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 ++ // 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/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index fa35d2c1c8de225acd68e08f15976c92f7ab82aa..91d96f89f33668a38c158c1bc1f76577d71514db 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -1450,8 +1450,10 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + + private Vec3 collide(Vec3 movement) { + // Paper start - optimise collisions +- // This is a copy of vanilla's except that it uses strictly AABB math +- if (movement.x == 0.0 && movement.y == 0.0 && movement.z == 0.0) { ++ 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; + } + +@@ -1462,63 +1464,69 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + return movement; + } + +- final List potentialCollisions = io.papermc.paper.util.CachedLists.getTempCollisionList(); +- try { +- final double stepHeight = (double)this.maxUpStep(); +- final AABB collisionBox; ++ final List potentialCollisionsBB = new java.util.ArrayList<>(); ++ final List potentialCollisionsVoxel = new java.util.ArrayList<>(); ++ final double stepHeight = (double)this.maxUpStep(); ++ final AABB collisionBox; ++ final boolean onGround = this.onGround; + +- if (movement.x == 0.0 && movement.z == 0.0 && movement.y != 0.0) { +- 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); +- } ++ if (xZero & zZero) { ++ if (movement.y > 0.0) { ++ collisionBox = io.papermc.paper.util.CollisionUtil.cutUpwards(currBoundingBox, movement.y); + } else { +- if (stepHeight > 0.0 && (this.onGround || (movement.y < 0.0)) && (movement.x != 0.0 || movement.z != 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); +- } ++ 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, movement.y, movement.z); ++ 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, ++ (0), ++ null, null ++ ); + +- io.papermc.paper.util.CollisionUtil.getCollisions(world, this, collisionBox, potentialCollisions, false, this.level.paperConfig().chunks.preventMovingIntoUnloadedChunks, +- false, false, null, null); ++ if (io.papermc.paper.util.CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { ++ potentialCollisionsVoxel.add(world.getWorldBorder().getCollisionShape()); ++ } + +- if (collidingWithWorldBorder = io.papermc.paper.util.CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { // Paper - this line *is* correct, ignore the IDE warning about assignments being used as a condition +- io.papermc.paper.util.CollisionUtil.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions); +- } ++ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) { ++ return movement; ++ } + +- final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisions); ++ final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); + +- if (stepHeight > 0.0 +- && (this.onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0)) ++ 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, potentialCollisions); +- final Vec3 vec3d3 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisions); ++ 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), potentialCollisions).add(vec3d3); ++ 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() > limitedMoveVector.horizontalDistanceSqr()) { +- return vec3d2.add(io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisions)); ++ if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { ++ vec3d2 = vec3d4; + } ++ } + +- return limitedMoveVector; +- } else { +- return limitedMoveVector; ++ 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)); + } +- } finally { +- io.papermc.paper.util.CachedLists.returnTempCollisionList(potentialCollisions); ++ ++ return limitedMoveVector; ++ } else { ++ return limitedMoveVector; + } + // Paper end - optimise collisions + } +@@ -2728,30 +2736,69 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { + float f = this.dimensions.width * 0.8F; + AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); + ++ // Paper start - optimise collisions ++ if (io.papermc.paper.util.CollisionUtil.isEmpty(axisalignedbb)) { ++ return false; ++ } ++ ++ 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(); + +- BlockPos.MutableBlockPos blockposition = new BlockPos.MutableBlockPos(); +- int minX = Mth.floor(axisalignedbb.minX); +- int minY = Mth.floor(axisalignedbb.minY); +- int minZ = Mth.floor(axisalignedbb.minZ); +- int maxX = Mth.floor(axisalignedbb.maxX); +- int maxY = Mth.floor(axisalignedbb.maxY); +- int maxZ = Mth.floor(axisalignedbb.maxZ); ++ 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) { +- net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); +- if (chunk == null) { ++ 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; + } + +- BlockState iblockdata = chunk.getBlockStateFinal(fx, fy, fz); +- blockposition.set(fx, fy, fz); +- if (!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)) { ++ 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 5c6e060a54ffb13c37ff5711992e964bdd59643d..d07e140593f0df52314121dad5a58fb3206fde59 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -353,7 +353,7 @@ public class ArmorStand extends LivingEntity { + @Override + protected void pushEntities() { + if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper +- List list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); ++ List list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); // Paper - optimise collisions + + for (int i = 0; i < list.size(); ++i) { + Entity entity = (Entity) list.get(i); +diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java +index ad2c533e9a0f0e2d97620b0e16200d7eeaedeefb..6cda651b7a44867947053538cb40e1126b4ba550 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 c0dd933c8e64484c4ae0d4fa0f6e19969cf09b37..2619e26544af14b473bae240c959071b8ecb7d6c 100644 +--- a/src/main/java/net/minecraft/world/level/CollisionGetter.java ++++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java +@@ -37,31 +37,35 @@ public interface CollisionGetter extends BlockGetter { + + // Paper start - optimise collisions + default boolean noCollision(Entity entity, AABB box, boolean loadChunks) { +- return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, loadChunks, false, entity != null, true, null) +- && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); ++ return this.noCollision(entity, box); + } + // Paper end - optimise collisions + + default boolean noCollision(AABB box) { +- // Paper start - optimise collisions +- return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, null, box, null, false, false, false, true, null) +- && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, null, box, null, true, null); +- // Paper end - optimise collisions ++ return this.noCollision((Entity)null, box); + } + + default boolean noCollision(Entity entity) { +- // Paper start - optimise collisions +- AABB box = entity.getBoundingBox(); +- return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) +- && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); +- // Paper end - optimise collisions ++ return this.noCollision(entity, entity.getBoundingBox()); + } + + default boolean noCollision(@Nullable Entity entity, AABB box) { +- // Paper start - optimise collisions +- return !io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, false, false, entity != null, true, null) +- && !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, true, null); +- // Paper end - optimise collisions ++ try { if (entity != null) entity.collisionLoadChunks = true; // Paper ++ for(VoxelShape voxelShape : this.getBlockCollisions(entity, box)) { ++ if (!voxelShape.isEmpty()) { ++ return false; ++ } ++ } ++ } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper ++ ++ if (!this.getEntityCollisions(entity, box).isEmpty()) { ++ return false; ++ } else if (entity == null) { ++ return true; ++ } else { ++ VoxelShape voxelShape2 = this.borderCollision(entity, box); ++ return voxelShape2 == null || !Shapes.joinIsNotEmpty(voxelShape2, Shapes.create(box), BooleanOp.AND); ++ } + } + + List getEntityCollisions(@Nullable Entity entity, AABB box); +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index b1a6a66ed02706c1adc36dcedfa415f5a24a25a0..d125daebf852683b66eca400e1dffd1951b90913 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -51,17 +51,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)) && shape.intersects(entity.getBoundingBox())) { // Paper +- return false; ++ return false; ++ } ++ ++ final AABB singleAABB = shape.getSingleAABBRepresentation(); ++ final List 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 List getEntitiesOfClass(Class entityClass, AABB box) { +@@ -69,23 +88,41 @@ public interface EntityGetter { + } + + default List 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 entities; ++ if (entity != null && entity.hardCollides()) { ++ entities = this.getEntities(entity, box, null); + } else { +- Predicate predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); +- List list = this.getEntities(entity, box.inflate(-1.0E-7D), predicate); // Paper - needs to be negated, or else we get things we don't collide with +- if (list.isEmpty()) { +- return List.of(); +- } else { +- ImmutableList.Builder 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 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 +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 916df0c8d263f90e04564c5f512fd5ed5eaaa6d5..2af7c9b316aee1589b6f3f1cc44819a6fdf51c26 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -377,6 +377,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.entityLimiter = new org.spigotmc.TickLimiter(spigotConfig.entityMaxTickTime); + this.tileLimiter = new org.spigotmc.TickLimiter(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 +@@ -418,6 +422,370 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return true; + } + // Paper end ++ // 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 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 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; ++ } ++ } ++ } ++ ++ /** ++ * @reason Route to optimized call ++ * @author Spottedleaf ++ */ ++ @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 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 aabbs = new java.util.ArrayList<>(); ++ final List 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 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; +@@ -1058,7 +1426,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 + +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 5a216545781ac5929d497834a74ec0b0b635a6b2..4ddc3484311f9c83a6f8aecb89188195ab281742 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -277,7 +277,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 de4c1e4701236e7d5ec77339c51ad6a9d8288bb6..2e25c867bec9c7734456833660ede098ffcfcd1c 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 +@@ -815,6 +815,10 @@ public abstract class BlockBehaviour implements FeatureElement { + this.instrument = blockbase_info.instrument; + this.replaceable = blockbase_info.replaceable; + this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // 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 - impl cached craft block data, lazy load to fix issue with loading at the wrong time + private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; +@@ -863,12 +867,52 @@ public abstract class BlockBehaviour implements FeatureElement { + return this.conditionallyFullOpaque; + } + // Paper end - starlight +- private long blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK; ++ // 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 long getBlockCollisionBehavior() { +- return this.blockCollisionBehavior; ++ public final boolean hasCache() { ++ return this.cache != null; + } +- // Paper end ++ ++ 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()); +@@ -880,35 +924,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 +- if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(this)) { +- this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK; +- } else { ++ // Paper start - optimise collisions ++ if (this.cache != null) { ++ final VoxelShape collisionShape = this.cache.collisionShape; + try { +- // There is NOTHING HACKY ABOUT THIS AT ALLLLLLLLLLLLLLL +- VoxelShape constantShape = this.getCollisionShape(null, null, null); +- if (constantShape == null) { +- this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_UNKNOWN_BLOCK; +- } else { +- constantShape = constantShape.optimize(); +- if (constantShape.isEmpty()) { +- this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_EMPTY_BLOCK; +- } else { +- final List boxes = constantShape.toAabbs(); +- if (constantShape == net.minecraft.world.phys.shapes.Shapes.getFullUnoptimisedCube() || (boxes.size() == 1 && boxes.get(0).equals(net.minecraft.world.phys.shapes.Shapes.BLOCK_OPTIMISED.aabb))) { +- this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_FULL_BLOCK; +- } else { +- this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_UNKNOWN_BLOCK; +- } +- } +- } +- } catch (final Error error) { +- throw error; ++ this.constantCollisionShape = this.getCollisionShape(null, null, null); ++ this.constantAABBCollision = this.constantCollisionShape == null ? null : this.constantCollisionShape.getSingleAABBRepresentation(); + } catch (final Throwable throwable) { +- this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_UNKNOWN_BLOCK; ++ 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 ++ // 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 d4477b0dda6a1ef7bd8323c0d11b636bd071d18e..f0de72afad4bb571153436399386a6a8a70582a6 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> 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 datapaletteblock, PalettedContainer> palettedcontainerro) { + // CraftBukkit end +@@ -42,110 +58,6 @@ public class LevelChunkSection { + this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes + } + +- // Paper start +- protected int specialCollidingBlocks; +- // blockIndex = x | (z << 4) | (y << 8) +- private long[] knownBlockCollisionData; +- +- private long[] initKnownDataField() { +- return this.knownBlockCollisionData = new long[16 * 16 * 16 * 2 / Long.SIZE]; +- } +- +- public final boolean hasSpecialCollidingBlocks() { +- return this.specialCollidingBlocks != 0; +- } +- +- public static long getKnownBlockInfo(final int blockIndex, final long value) { +- final int valueShift = (blockIndex & (Long.SIZE / 2 - 1)); +- +- return (value >>> (valueShift << 1)) & 0b11L; +- } +- +- public final long getKnownBlockInfo(final int blockIndex) { +- if (this.knownBlockCollisionData == null) { +- return 0L; +- } +- +- final int arrayIndex = (blockIndex >>> (6 - 1)); // blockIndex / (64/2) +- final int valueShift = (blockIndex & (Long.SIZE / 2 - 1)); +- +- final long value = this.knownBlockCollisionData[arrayIndex]; +- +- return (value >>> (valueShift << 1)) & 0b11L; +- } +- +- // important detail: this returns 32 values, one for localZ = localZ & (~1) and one for localZ = localZ | 1 +- // the even localZ is the lower 32 bits, the odd is the upper 32 bits +- public final long getKnownBlockInfoHorizontalRaw(final int localY, final int localZ) { +- if (this.knownBlockCollisionData == null) { +- return 0L; +- } +- +- final int horizontalIndex = (localZ << 4) | (localY << 8); +- return this.knownBlockCollisionData[horizontalIndex >>> (6 - 1)]; +- } +- +- private void initBlockCollisionData() { +- this.specialCollidingBlocks = 0; +- // In 1.18 all sections will be initialised, whether or not they have blocks (fucking stupid btw) +- // This means we can't aggressively initialise the backing long[], or else memory usage will just skyrocket. +- // So only init if we contain non-empty blocks. +- if (this.nonEmptyBlockCount == 0) { +- this.knownBlockCollisionData = null; +- return; +- } +- this.initKnownDataField(); +- for (int index = 0; index < (16 * 16 * 16); ++index) { +- final BlockState state = this.states.get(index); +- this.setKnownBlockInfo(index, state); +- if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(state)) { +- ++this.specialCollidingBlocks; +- } +- } +- } +- +- // only use for initBlockCollisionData +- private void setKnownBlockInfo(final int blockIndex, final BlockState blockState) { +- final int arrayIndex = (blockIndex >>> (6 - 1)); // blockIndex / (64/2) +- final int valueShift = (blockIndex & (Long.SIZE / 2 - 1)) << 1; +- +- long value = this.knownBlockCollisionData[arrayIndex]; +- +- value &= ~(0b11L << valueShift); +- value |= blockState.getBlockCollisionBehavior() << valueShift; +- +- this.knownBlockCollisionData[arrayIndex] = value; +- } +- +- public void updateKnownBlockInfo(final int blockIndex, final BlockState from, final BlockState to) { +- if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(from)) { +- --this.specialCollidingBlocks; +- } +- if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(to)) { +- ++this.specialCollidingBlocks; +- } +- +- if (this.nonEmptyBlockCount == 0) { +- this.knownBlockCollisionData = null; +- return; +- } +- +- if (this.knownBlockCollisionData == null) { +- this.initKnownDataField(); +- } +- +- final int arrayIndex = (blockIndex >>> (6 - 1)); // blockIndex / (64/2) +- final int valueShift = (blockIndex & (Long.SIZE / 2 - 1)) << 1; +- +- long value = this.knownBlockCollisionData[arrayIndex]; +- +- value &= ~(0b11L << valueShift); +- value |= to.getBlockCollisionBehavior() << valueShift; +- +- this.knownBlockCollisionData[arrayIndex] = value; +- } +- // Paper end +- + public BlockState getBlockState(int x, int y, int z) { + return (BlockState) this.states.get(x, y, z); + } +@@ -206,7 +118,7 @@ public class LevelChunkSection { + ++this.tickingFluidCount; + } + +- this.updateKnownBlockInfo(x | (z << 4) | (y << 8), iblockdata1, state); // Paper ++ this.updateBlockCallback(x, y, z, iblockdata1, state); // Paper - optimise collisions + return iblockdata1; + } + +@@ -251,10 +163,15 @@ public class LevelChunkSection { + } + } + ++ // Paper start - optimise collisions ++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(iblockdata)) { ++ ++this.specialCollidingBlocks; ++ } ++ // Paper end - optimise collisions ++ + }); + } // Paper - do not run forEachLocation on clearly empty sections + // Paper end +- this.initBlockCollisionData(); // Paper + } + + public PalettedContainer getStates() { +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 5502ad143fd2575f1346334b5b4fe7846628f54e..1bbe534bb3058fe804fcaa5fce62c64b48a4dd19 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 ffc76354ead6937daf366c3d87bcb51d3e4c47f5..e3dbf3066337a482460238f8a94d854cf88adfa2 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -316,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 ca5f01be5d5ccfcc56780ff93cca3824409ffc0d..a232b9396a41c11579a4d691b05717b16473513e 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +@@ -6,9 +6,6 @@ import java.util.Arrays; + import net.minecraft.Util; + import net.minecraft.core.Direction; + +-// Paper start +-import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; +-// Paper end + public class ArrayVoxelShape extends VoxelShape { + private final DoubleList xs; + private final DoubleList ys; +@@ -18,12 +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) { +- // Paper start - optimise multi-aabb shapes +- this(shape, xPoints, yPoints, zPoints, null, 0.0, 0.0, 0.0); +- } +- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints, net.minecraft.world.phys.AABB[] boundingBoxesRepresentation, double offsetX, double offsetY, double offsetZ) { +- // Paper end - optimise multi-aabb shapes ++ 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; +@@ -35,12 +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.")); + } +- // Paper start - optimise multi-aabb shapes +- this.boundingBoxesRepresentation = boundingBoxesRepresentation == null ? this.toAabbs().toArray(EMPTY) : boundingBoxesRepresentation; +- this.offsetX = offsetX; +- this.offsetY = offsetY; +- this.offsetZ = offsetZ; +- // Paper end - optimise multi-aabb shapes ++ this.initCache(); // Paper - optimise collisions + } + + @Override +@@ -57,151 +44,4 @@ public class ArrayVoxelShape extends VoxelShape { + } + } + +- // Paper start +- public static final class DoubleListOffsetExposed extends AbstractDoubleList { +- +- public final DoubleArrayList list; +- public final double offset; +- +- public DoubleListOffsetExposed(final DoubleArrayList list, final double offset) { +- this.list = list; +- this.offset = offset; +- } +- +- @Override +- public double getDouble(final int index) { +- return this.list.getDouble(index) + this.offset; +- } +- +- @Override +- public int size() { +- return this.list.size(); +- } +- } +- +- static final net.minecraft.world.phys.AABB[] EMPTY = new net.minecraft.world.phys.AABB[0]; +- final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation; +- +- final double offsetX; +- final double offsetY; +- final double offsetZ; +- +- public final net.minecraft.world.phys.AABB[] getBoundingBoxesRepresentation() { +- return this.boundingBoxesRepresentation; +- } +- +- public final double getOffsetX() { +- return this.offsetX; +- } +- +- public final double getOffsetY() { +- return this.offsetY; +- } +- +- public final double getOffsetZ() { +- return this.offsetZ; +- } +- +- @Override +- public java.util.List toAabbs() { +- if (this.boundingBoxesRepresentation == null) { +- return super.toAabbs(); +- } +- java.util.List ret = new java.util.ArrayList<>(this.boundingBoxesRepresentation.length); +- +- double offX = this.offsetX; +- double offY = this.offsetY; +- double offZ = this.offsetZ; +- +- for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { +- ret.add(boundingBox.move(offX, offY, offZ)); +- } +- +- return ret; +- } +- +- protected static DoubleArrayList getList(DoubleList from) { +- if (from instanceof DoubleArrayList) { +- return (DoubleArrayList)from; +- } else { +- return DoubleArrayList.wrap(from.toDoubleArray()); +- } +- } +- +- @Override +- public VoxelShape move(double x, double y, double z) { +- if (x == 0.0 && y == 0.0 && z == 0.0) { +- return this; +- } +- DoubleListOffsetExposed xPoints, yPoints, zPoints; +- double offsetX, offsetY, offsetZ; +- +- if (this.xs instanceof DoubleListOffsetExposed) { +- xPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.xs).list, offsetX = this.offsetX + x); +- yPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.ys).list, offsetY = this.offsetY + y); +- zPoints = new DoubleListOffsetExposed(((DoubleListOffsetExposed)this.zs).list, offsetZ = this.offsetZ + z); +- } else { +- xPoints = new DoubleListOffsetExposed(getList(this.xs), offsetX = x); +- yPoints = new DoubleListOffsetExposed(getList(this.ys), offsetY = y); +- zPoints = new DoubleListOffsetExposed(getList(this.zs), offsetZ = z); +- } +- +- return new ArrayVoxelShape(this.shape, xPoints, yPoints, zPoints, this.boundingBoxesRepresentation, offsetX, offsetY, offsetZ); +- } +- +- @Override +- public final boolean intersects(net.minecraft.world.phys.AABB axisalingedbb) { +- // this can be optimised by checking an "overall shape" first, but not needed +- double offX = this.offsetX; +- double offY = this.offsetY; +- double offZ = this.offsetZ; +- +- for (net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { +- if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(axisalingedbb, boundingBox.minX + offX, boundingBox.minY + offY, boundingBox.minZ + offZ, +- boundingBox.maxX + offX, boundingBox.maxY + offY, boundingBox.maxZ + offZ)) { +- return true; +- } +- } +- +- return false; +- } +- +- @Override +- public void forAllBoxes(Shapes.DoubleLineConsumer doubleLineConsumer) { +- if (this.boundingBoxesRepresentation == null) { +- super.forAllBoxes(doubleLineConsumer); +- return; +- } +- for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { +- doubleLineConsumer.consume(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, +- boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ); +- } +- } +- +- @Override +- public VoxelShape optimize() { +- if (this == Shapes.empty() || this.boundingBoxesRepresentation.length == 0) { +- return this; +- } +- +- VoxelShape simplified = Shapes.empty(); +- for (final net.minecraft.world.phys.AABB boundingBox : this.boundingBoxesRepresentation) { +- simplified = Shapes.joinUnoptimized(simplified, Shapes.box(boundingBox.minX + this.offsetX, boundingBox.minY + this.offsetY, boundingBox.minZ + this.offsetZ, +- boundingBox.maxX + this.offsetX, boundingBox.maxY + this.offsetY, boundingBox.maxZ + this.offsetZ), BooleanOp.OR); +- } +- +- if (!(simplified instanceof ArrayVoxelShape)) { +- return simplified; +- } +- +- final net.minecraft.world.phys.AABB[] boundingBoxesRepresentation = ((ArrayVoxelShape)simplified).getBoundingBoxesRepresentation(); +- +- if (boundingBoxesRepresentation.length == 1) { +- return new io.papermc.paper.voxel.AABBVoxelShape(boundingBoxesRepresentation[0]).optimize(); +- } +- +- return simplified; +- } +- // Paper end +- + } +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 731c7dd15f131dc124be6af8f342b122cb89491b..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,20 +16,49 @@ 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); +- }); public static VoxelShape getFullUnoptimisedCube() { return BLOCK; } // Paper - OBFHELPER ++ // 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}))); +- public static final io.papermc.paper.voxel.AABBVoxelShape BLOCK_OPTIMISED = new io.papermc.paper.voxel.AABBVoxelShape(new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)); // Paper ++ ++ // 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; + } + + public static VoxelShape block() { +- return BLOCK_OPTIMISED; // Paper ++ return BLOCK; + } + + public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { +@@ -42,14 +71,46 @@ 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)) { +- return new io.papermc.paper.voxel.AABBVoxelShape(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); // Paper ++ // Paper start - optimise collisions ++ // force ArrayVoxelShape in every case ++ final int bitsX = findBits(minX, maxX); ++ final int bitsY = findBits(minY, maxY); ++ final int bitsZ = findBits(minZ, maxZ); ++ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) { ++ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) { ++ return BLOCK; ++ } else { ++ final int sizeX = 1 << bitsX; ++ final int sizeY = 1 << bitsY; ++ final int sizeZ = 1 << bitsZ; ++ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds( ++ sizeX, sizeY, sizeZ, ++ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ), ++ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ) ++ ); ++ return new ArrayVoxelShape( ++ shape, ++ PARTS_BY_BITS[bitsX], ++ PARTS_BY_BITS[bitsY], ++ PARTS_BY_BITS[bitsZ] ++ ); ++ } ++ } else { ++ 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(); + } + } + + public static VoxelShape create(AABB box) { +- return new io.papermc.paper.voxel.AABBVoxelShape(box); // Paper ++ return create(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + } + + @VisibleForTesting +@@ -81,81 +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) { +- // Paper start - optimise voxelshape +- if (predicate == BooleanOp.AND) { +- if (shape1 instanceof io.papermc.paper.voxel.AABBVoxelShape && shape2 instanceof io.papermc.paper.voxel.AABBVoxelShape) { +- return io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(((io.papermc.paper.voxel.AABBVoxelShape)shape1).aabb, ((io.papermc.paper.voxel.AABBVoxelShape)shape2).aabb); +- } else if (shape1 instanceof io.papermc.paper.voxel.AABBVoxelShape && shape2 instanceof ArrayVoxelShape) { +- return ((ArrayVoxelShape)shape2).intersects(((io.papermc.paper.voxel.AABBVoxelShape)shape1).aabb); +- } else if (shape2 instanceof io.papermc.paper.voxel.AABBVoxelShape && shape1 instanceof ArrayVoxelShape) { +- return ((ArrayVoxelShape)shape1).intersects(((io.papermc.paper.voxel.AABBVoxelShape)shape2).aabb); +- } +- } +- return joinIsNotEmptyVanilla(shape1, shape2, predicate); +- } +- public static boolean joinIsNotEmptyVanilla(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { +- // Paper end - optimise voxelshape +- 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,153 +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()) { +- 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); + } +- } + +- public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { +- // Paper start - optimise shape creation here for lighting, as this shape is going to be used +- // for transparency checks +- if (shape == BLOCK || shape == BLOCK_OPTIMISED) { +- return BLOCK_OPTIMISED; +- } else if (shape == empty()) { +- return empty(); ++ if (shape.isEmpty() | neighbor.isEmpty()) { ++ return false; + } + +- if (shape instanceof io.papermc.paper.voxel.AABBVoxelShape) { +- final AABB box = ((io.papermc.paper.voxel.AABBVoxelShape)shape).aabb; +- switch (direction) { +- case WEST: // -X +- case EAST: { // +X +- final boolean useEmpty = direction == Direction.EAST ? !DoubleMath.fuzzyEquals(box.maxX, 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) : +- !DoubleMath.fuzzyEquals(box.minX, 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); +- return useEmpty ? empty() : new io.papermc.paper.voxel.AABBVoxelShape(new AABB(0.0, box.minY, box.minZ, 1.0, box.maxY, box.maxZ)).optimize(); +- } +- case DOWN: // -Y +- case UP: { // +Y +- final boolean useEmpty = direction == Direction.UP ? !DoubleMath.fuzzyEquals(box.maxY, 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) : +- !DoubleMath.fuzzyEquals(box.minY, 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); +- return useEmpty ? empty() : new io.papermc.paper.voxel.AABBVoxelShape(new AABB(box.minX, 0.0, box.minZ, box.maxX, 1.0, box.maxZ)).optimize(); +- } +- case NORTH: // -Z +- case SOUTH: { // +Z +- final boolean useEmpty = direction == Direction.SOUTH ? !DoubleMath.fuzzyEquals(box.maxZ, 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) : +- !DoubleMath.fuzzyEquals(box.minZ,0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); +- return useEmpty ? empty() : new io.papermc.paper.voxel.AABBVoxelShape(new AABB(box.minX, box.minY, 0.0, box.maxX, box.maxY, 1.0)).optimize(); +- } +- } ++ // 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; + } + +- // fall back to vanilla +- return getFaceShapeVanilla(shape, direction); ++ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST); ++ // Paper end - optimise collisions + } +- public static VoxelShape getFaceShapeVanilla(VoxelShape shape, Direction direction) { +- // Paper end +- 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 (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i).optimize().optimize()); // Paper - first optimize converts to ArrayVoxelShape, second optimize could convert to AABBVoxelShape +- } ++ public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { ++ return shape.getFaceShapeClamped(direction); // Paper - optimise collisions ++ } ++ ++ // 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) { +- // Paper start - try to optimise for the case where the shapes do _not_ occlude +- // which is _most_ of the time in lighting +- if (one == getFullUnoptimisedCube() || one == BLOCK_OPTIMISED +- || two == getFullUnoptimisedCube() || two == BLOCK_OPTIMISED) { ++ // Paper start - optimise collisions ++ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) { + return true; + } +- boolean v1Empty = one == empty(); +- boolean v2Empty = two == empty(); +- if (v1Empty && v2Empty) { ++ ++ final boolean s1Empty = one.isEmpty(); ++ final boolean s2Empty = two.isEmpty(); ++ if (s1Empty & s2Empty) { + return false; + } +- if ((one instanceof io.papermc.paper.voxel.AABBVoxelShape || v1Empty) +- && (two instanceof io.papermc.paper.voxel.AABBVoxelShape || v2Empty)) { +- if (!v1Empty && !v2Empty && (one != two)) { +- AABB boundingBox1 = ((io.papermc.paper.voxel.AABBVoxelShape)one).aabb; +- AABB boundingBox2 = ((io.papermc.paper.voxel.AABBVoxelShape)two).aabb; +- // can call it here in some cases +- +- // check overall bounding box +- double minY = Math.min(boundingBox1.minY, boundingBox2.minY); +- double maxY = Math.max(boundingBox1.maxY, boundingBox2.maxY); +- if (minY > io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON || maxY < (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { +- return false; +- } +- double minX = Math.min(boundingBox1.minX, boundingBox2.minX); +- double maxX = Math.max(boundingBox1.maxX, boundingBox2.maxX); +- if (minX > io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON || maxX < (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { +- return false; +- } +- double minZ = Math.min(boundingBox1.minZ, boundingBox2.minZ); +- double maxZ = Math.max(boundingBox1.maxZ, boundingBox2.maxZ); +- if (minZ > io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON || maxZ < (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { +- return false; +- } +- // fall through to full merge check +- } else { +- AABB boundingBox = v1Empty ? ((io.papermc.paper.voxel.AABBVoxelShape)two).aabb : ((io.papermc.paper.voxel.AABBVoxelShape)one).aabb; +- // check if the bounding box encloses the full cube +- return (boundingBox.minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && +- (boundingBox.minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && +- (boundingBox.minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && boundingBox.maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)); +- } ++ ++ if (s1Empty | s2Empty) { ++ return s2Empty ? one.occludesFullBlock() : two.occludesFullBlock(); + } +- return faceShapeOccludesVanilla(one, two); +- } +- public static boolean faceShapeOccludesVanilla(VoxelShape one, VoxelShape two) { +- // Paper end +- 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 { +- return true; ++ ++ 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 2182afd1b95acf14c55bddfeec17dae0a63e1f00..19330cc86c86e100f32d490aba0881eefec0c1b6 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +@@ -16,36 +16,401 @@ import net.minecraft.world.phys.BlockHitResult; + import net.minecraft.world.phys.Vec3; + + public abstract class VoxelShape { +- public final DiscreteVoxelShape shape; // Paper - public ++ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public + @Nullable + private VoxelShape[] faces; + +- // Paper start +- public boolean intersects(AABB shape) { +- return Shapes.joinIsNotEmpty(this, new io.papermc.paper.voxel.AABBVoxelShape(shape), BooleanOp.AND); ++ // 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() { ++ final 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) { ++ // note: Shapes.join(BLOCK, this, NOT_SAME) cannot be empty when voxelSize > 2 ++ 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 ++ // 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 + } + + protected double get(Direction.Axis axis, int index) { +@@ -55,19 +420,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 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) { +@@ -85,12 +537,43 @@ public abstract class VoxelShape { + }, true); + } + ++ // Paper start - optimise collisions ++ private List toAabbsUncached() { ++ final List 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 toAabbs() { +- List 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) { +@@ -117,37 +600,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 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 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) { +@@ -169,7 +700,7 @@ public abstract class VoxelShape { + } + } + +- protected VoxelShape calculateFace(Direction direction) { // Paper ++ private VoxelShape calculateFace(Direction direction) { + Direction.Axis axis = direction.getAxis(); + DoubleList doubleList = this.getCoords(axis); + if (doubleList.size() == 2 && DoubleMath.fuzzyEquals(doubleList.getDouble(0), 0.0D, 1.0E-7D) && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0D, 1.0E-7D)) { +@@ -182,7 +713,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) { diff --git a/patches/server/0028-fixup-Optimise-collision-checking-in-player-move-pac.patch b/patches/server/0028-fixup-Optimise-collision-checking-in-player-move-pac.patch new file mode 100644 index 0000000000..55df1dc71d --- /dev/null +++ b/patches/server/0028-fixup-Optimise-collision-checking-in-player-move-pac.patch @@ -0,0 +1,53 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 12 Sep 2023 05:00:36 -0700 +Subject: [PATCH] fixup! Optimise collision checking in player move packet + handling + + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 678bba9d636a0eb34270a2d26b5b3d0d6d900115..a1a931095c41126a05e806541075ccc40b9ea8f9 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -1742,22 +1742,29 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic + // Paper end + // Paper start - optimise out extra getCubes + private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { +- final List collisions = io.papermc.paper.util.CachedLists.getTempCollisionList(); +- try { +- io.papermc.paper.util.CollisionUtil.getCollisions(world, entity, newBox, collisions, false, true, +- true, false, null, null); ++ final List collisionsBB = new java.util.ArrayList<>(); ++ final List collisionsVoxel = new java.util.ArrayList<>(); ++ io.papermc.paper.util.CollisionUtil.getCollisions( ++ world, entity, newBox, collisionsVoxel, collisionsBB, ++ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null, null ++ ); + +- for (int i = 0, len = collisions.size(); i < len; ++i) { +- final AABB box = collisions.get(i); +- if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { +- return true; +- } ++ for (int i = 0, len = collisionsBB.size(); i < len; ++i) { ++ final AABB box = collisionsBB.get(i); ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { ++ return true; + } ++ } + +- return false; +- } finally { +- io.papermc.paper.util.CachedLists.returnTempCollisionList(collisions); ++ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) { ++ final VoxelShape voxel = collisionsVoxel.get(i); ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) { ++ return true; ++ } + } ++ ++ return false; + } + // Paper end - optimise out extra getCubes + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { diff --git a/patches/server/0029-Actually-optimise-explosions.patch b/patches/server/0029-Actually-optimise-explosions.patch new file mode 100644 index 0000000000..036429810b --- /dev/null +++ b/patches/server/0029-Actually-optimise-explosions.patch @@ -0,0 +1,469 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 12 Sep 2023 06:50:16 -0700 +Subject: [PATCH] Actually optimise explosions + +The vast majority of blocks an explosion of power ~4 tries +to destroy are duplicates. The core of the block destroying +part of this patch is to cache the block state, resistance, and +whether it should explode - as those will not change. + +The other part of this patch is to optimise the visibility +percentage calculation. The new visibility calculation takes +advantage of the block caching already done by the explosion logic. +It continues to update the cache as the visibility calculation +uses many rays which can overlap significantly. + +Effectively, the patch uses a lot of caching to eliminate +redundant operations. + +Performance benchmarking explosions is challenging, as it varies +depending on the power, the number of nearby entities, and the +nearby terrain. This means that no benchmark can cover all the cases. +I decided to test a giant block of TNT, as that's where the optimisations +would be needed the most. + +I tested using a 50x10x50 block of TNT above ground +and determined the following: + +Vanilla time per explosion: 2.27ms +Lithium time per explosion: 1.07ms +This patch time per explosion: 0.45ms + +The results indicate that this logic is 5 times faster than Vanilla +and 2.3 times faster than Lithium. + +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index e8c4815960ab144298d4352f393b9670e7004e62..53f708a58a2b86fb81602b028a357990a632b49b 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -96,6 +96,271 @@ public class Explosion { + this.damageCalculator = behavior == null ? this.makeDamageCalculator(entity) : behavior; + } + ++ // Paper start - optimise collisions ++ private static final double[] CACHED_RAYS; ++ static { ++ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); ++ ++ for (int x = 0; x <= 15; ++x) { ++ for (int y = 0; y <= 15; ++y) { ++ for (int z = 0; z <= 15; ++z) { ++ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { ++ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); ++ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); ++ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); ++ ++ double mag = Math.sqrt( ++ xDir * xDir + yDir * yDir + zDir * zDir ++ ); ++ ++ rayCoords.add((xDir / mag) * (double)0.3F); ++ rayCoords.add((yDir / mag) * (double)0.3F); ++ rayCoords.add((zDir / mag) * (double)0.3F); ++ } ++ } ++ } ++ } ++ ++ CACHED_RAYS = rayCoords.toDoubleArray(); ++ } ++ ++ private static final int CHUNK_CACHE_SHIFT = 2; ++ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; ++ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; ++ ++ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; ++ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; ++ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; ++ ++ // resistance = (res + 0.3F) * 0.3F; ++ // so for resistance = 0, we need res = -0.3F ++ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); ++ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; ++ ++ public static final class ExplosionBlockCache { ++ ++ public final long key; ++ public final BlockPos immutablePos; ++ public final BlockState blockState; ++ public final FluidState fluidState; ++ public final float resistance; ++ public final boolean outOfWorld; ++ public Boolean shouldExplode; // null -> not called yet ++ public net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape; ++ ++ public ExplosionBlockCache(long key, BlockPos immutablePos, BlockState blockState, FluidState fluidState, float resistance, ++ boolean outOfWorld) { ++ this.key = key; ++ this.immutablePos = immutablePos; ++ this.blockState = blockState; ++ this.fluidState = fluidState; ++ this.resistance = resistance; ++ this.outOfWorld = outOfWorld; ++ } ++ } ++ ++ private long[] chunkPosCache = null; ++ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; ++ ++ private ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, ++ final long key, final boolean calculateResistance) { ++ ExplosionBlockCache ret = this.blockCache.get(key); ++ if (ret != null) { ++ return ret; ++ } ++ ++ BlockPos pos = new BlockPos(x, y, z); ++ ++ if (!this.level.isInWorldBounds(pos)) { ++ ret = new ExplosionBlockCache(key, pos, null, null, 0.0f, true); ++ } else { ++ net.minecraft.world.level.chunk.LevelChunk chunk; ++ long chunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); ++ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); ++ if (this.chunkPosCache[chunkCacheKey] == chunkKey) { ++ chunk = this.chunkCache[chunkCacheKey]; ++ } else { ++ this.chunkPosCache[chunkCacheKey] = chunkKey; ++ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); ++ } ++ ++ BlockState blockState = chunk.getBlockStateFinal(x, y, z); ++ FluidState fluidState = blockState.getFluidState(); ++ ++ Optional resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); ++ ++ ret = new ExplosionBlockCache( ++ key, pos, blockState, fluidState, ++ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, ++ false ++ ); ++ } ++ ++ this.blockCache.put(key, ret); ++ ++ return ret; ++ } ++ ++ private boolean clipsAnything(final Vec3 from, final Vec3 to, ++ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context, ++ final ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos currPos) { ++ // assume that context.delegated = false ++ 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 false; ++ } ++ ++ 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 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)); ++ ++ for (;;) { ++ currPos.set(currX, currY, currZ); ++ ++ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape ++ // ClipContext.Fluid.NONE -> ignore fluids ++ ++ // read block from cache ++ final long key = BlockPos.asLong(currX, currY, currZ); ++ ++ final int cacheKey = ++ (currX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ ExplosionBlockCache cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); ++ } ++ ++ final BlockState blockState = cachedBlock.blockState; ++ if (blockState != null && !blockState.emptyCollisionShape()) { ++ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; ++ if (collision == null) { ++ collision = blockState.getConstantCollisionShape(); ++ if (collision == null) { ++ collision = blockState.getCollisionShape(this.level, currPos, context); ++ if (!context.isDelegated()) { ++ // if it was not delegated during this call, assume that for any future ones it will not be delegated ++ // again, and cache the result ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } else { ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } ++ ++ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { ++ return true; ++ } ++ } ++ ++ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { ++ return false; ++ } ++ ++ // 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; ++ } ++ } ++ } ++ ++ private float getSeenFraction(final Vec3 source, final Entity target, ++ final ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos blockPos) { ++ final AABB boundingBox = target.getBoundingBox(); ++ final double diffX = boundingBox.maxX - boundingBox.minX; ++ final double diffY = boundingBox.maxY - boundingBox.minY; ++ final double diffZ = boundingBox.maxZ - boundingBox.minZ; ++ ++ final double incX = 1.0 / (diffX * 2.0 + 1.0); ++ final double incY = 1.0 / (diffY * 2.0 + 1.0); ++ final double incZ = 1.0 / (diffZ * 2.0 + 1.0); ++ ++ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { ++ return 0.0f; ++ } ++ ++ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; ++ final double offY = boundingBox.minY; ++ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; ++ ++ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(target); ++ ++ int totalRays = 0; ++ int missedRays = 0; ++ ++ for (double dx = 0.0; dx <= 1.0; dx += incX) { ++ final double fromX = Math.fma(dx, diffX, offX); ++ for (double dy = 0.0; dy <= 1.0; dy += incY) { ++ final double fromY = Math.fma(dy, diffY, offY); ++ for (double dz = 0.0; dz <= 1.0; dz += incZ) { ++ ++totalRays; ++ ++ final Vec3 from = new Vec3( ++ fromX, ++ fromY, ++ Math.fma(dz, diffZ, offZ) ++ ); ++ ++ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { ++ ++missedRays; ++ } ++ } ++ } ++ } ++ ++ return (float)missedRays / (float)totalRays; ++ } ++ // Paper end - optimise collisions ++ + private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity entity) { + return (ExplosionDamageCalculator) (entity == null ? Explosion.EXPLOSION_DAMAGE_CALCULATOR : new EntityBasedExplosionDamageCalculator(entity)); + } +@@ -148,40 +413,90 @@ public class Explosion { + int i; + int j; + +- for (int k = 0; k < 16; ++k) { +- for (i = 0; i < 16; ++i) { +- for (j = 0; j < 16; ++j) { +- if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { +- double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); +- double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); +- double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); +- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); +- +- d0 /= d3; +- d1 /= d3; +- d2 /= d3; ++ // Paper start - optimise explosions ++ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); ++ ++ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); ++ ++ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ ++ final ExplosionBlockCache[] blockCache = new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; ++ // use initial cache value that is most likely to be used: the source position ++ final ExplosionBlockCache initialCache; ++ { ++ final int blockX = Mth.floor(this.x); ++ final int blockY = Mth.floor(this.y); ++ final int blockZ = Mth.floor(this.z); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of ++ // a 16x16x16 cube ++ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and ++ // calculations in one go ++ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most ++ // block retrievals are not unique ++ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { ++ { ++ { ++ { ++ ExplosionBlockCache cachedBlock = initialCache; ++ ++ double d0 = CACHED_RAYS[ray]; ++ double d1 = CACHED_RAYS[ray + 1]; ++ double d2 = CACHED_RAYS[ray + 2]; ++ ray += 3; ++ // Paper end - optimise explosions + float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); + double d4 = this.x; + double d5 = this.y; + double d6 = this.z; + + for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { +- BlockPos blockposition = BlockPos.containing(d4, d5, d6); +- BlockState iblockdata = this.level.getBlockState(blockposition); +- if (!iblockdata.isDestroyable()) continue; // Paper +- FluidState fluid = iblockdata.getFluidState(); // Paper ++ // Paper start - optimise explosions ++ final int blockX = Mth.floor(d4); ++ final int blockY = Mth.floor(d5); ++ final int blockZ = Mth.floor(d6); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ if (cachedBlock.key != key) { ++ final int cacheKey = ++ (blockX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ } + +- if (!this.level.isInWorldBounds(blockposition)) { ++ if (cachedBlock.outOfWorld) { + break; + } + +- Optional optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); ++ BlockPos blockposition = cachedBlock.immutablePos; ++ BlockState iblockdata = cachedBlock.blockState; ++ // Paper end - optimise explosions + +- if (optional.isPresent()) { +- f -= ((Float) optional.get() + 0.3F) * 0.3F; +- } ++ if (!iblockdata.isDestroyable()) continue; // Paper ++ // Paper - optimise explosions + +- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { ++ // Paper - optimise explosions ++ ++ f -= cachedBlock.resistance; // Paper - optimise explosions ++ ++ if (f > 0.0F && cachedBlock.shouldExplode == null) { // Paper - optimise explosions ++ // Paper start - optimise explosions ++ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. ++ // basically, it is unused, which allows us to cache the result ++ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode(this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, f); ++ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; ++ if (shouldExplode && (this.fire || !cachedBlock.blockState.isAir())) { ++ // Paper end - optimise explosions + set.add(blockposition); + // Paper start - prevent headless pistons from forming + if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { +@@ -192,11 +507,12 @@ public class Explosion { + } + } + // Paper end ++ } // Paper - optimise explosions + } + +- d4 += d0 * 0.30000001192092896D; +- d5 += d1 * 0.30000001192092896D; +- d6 += d2 * 0.30000001192092896D; ++ d4 += d0; // Paper - optimise explosions ++ d5 += d1; // Paper - optimise explosions ++ d6 += d2; // Paper - optimise explosions + } + } + } +@@ -215,6 +531,8 @@ public class Explosion { + List list = this.level.getEntities(this.source, new AABB((double) i, (double) l, (double) j1, (double) j, (double) i1, (double) k1), (com.google.common.base.Predicate) entity -> entity.isAlive() && !entity.isSpectator()); // Paper - Fix lag from explosions processing dead entities + Vec3 vec3d = new Vec3(this.x, this.y, this.z); + ++ final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); // Paper - optimise explosions ++ + for (int l1 = 0; l1 < list.size(); ++l1) { + Entity entity = (Entity) list.get(l1); + +@@ -231,7 +549,7 @@ public class Explosion { + d8 /= d11; + d9 /= d11; + d10 /= d11; +- double d12 = this.getBlockDensity(vec3d, entity); // Paper - Optimize explosions ++ double d12 = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - Optimize explosions // Paper - optimise explosions + double d13 = (1.0D - d7) * d12; + + // CraftBukkit start +@@ -295,6 +613,10 @@ public class Explosion { + } + } + ++ this.blockCache = null; // Paper - optimise explosions ++ this.chunkPosCache = null; // Paper - optimise explosions ++ this.chunkCache = null; // Paper - optimise explosions ++ + } + + public void finalizeExplosion(boolean particles) {