From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Mon, 4 May 2020 10:06:24 -0700 Subject: [PATCH] Highly optimise single and multi-AABB VoxelShapes and collisions diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java index be668387f65a633c6ac497fca632a4767a1bf3a2..e08f4e39db4ee3fed62e37364d17dcc5c5683504 100644 --- a/src/main/java/io/papermc/paper/util/CachedLists.java +++ b/src/main/java/io/papermc/paper/util/CachedLists.java @@ -1,8 +1,57 @@ package io.papermc.paper.util; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.util.UnsafeList; +import java.util.List; + public final class CachedLists { - public static void reset() { + // Paper start - optimise collisions + static final UnsafeList TEMP_COLLISION_LIST = new UnsafeList<>(1024); + static boolean tempCollisionListInUse; + + public static UnsafeList getTempCollisionList() { + if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) { + return new UnsafeList<>(16); + } + tempCollisionListInUse = true; + return TEMP_COLLISION_LIST; + } + + public static void returnTempCollisionList(List list) { + if (list != TEMP_COLLISION_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempCollisionListInUse = false; + } + static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024); + static boolean tempGetEntitiesListInUse; + + public static UnsafeList getTempGetEntitiesList() { + if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) { + return new UnsafeList<>(16); + } + tempGetEntitiesListInUse = true; + return TEMP_GET_ENTITIES_LIST; + } + + public static void returnTempGetEntitiesList(List list) { + if (list != TEMP_GET_ENTITIES_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempGetEntitiesListInUse = false; + } + // Paper end - optimise collisions + + public static void reset() { + // Paper start - optimise collisions + TEMP_COLLISION_LIST.completeReset(); + TEMP_GET_ENTITIES_LIST.completeReset(); + // Paper end - optimise collisions } } diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..a87f6380b2c387fb0cdd40d5087b5c93492e3c88 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java @@ -0,0 +1,899 @@ +package io.papermc.paper.util; + +import io.papermc.paper.voxel.AABBVoxelShape; +import net.minecraft.core.BlockPos; +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.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.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.CollisionContext; +import net.minecraft.world.phys.shapes.EntityCollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.List; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +public final class CollisionUtil { + + public static final double COLLISION_EPSILON = 1.0E-7; + + public static final 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 boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { + return block.shapeExceedsCube() || block.getBlock() == Blocks.MOVING_PISTON; + } + + public static boolean isEmpty(final AABB aabb) { + return (aabb.maxX - aabb.minX) < COLLISION_EPSILON && (aabb.maxY - aabb.minY) < COLLISION_EPSILON && (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; + } + + public static boolean isEmpty(final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (maxX - minX) < COLLISION_EPSILON && (maxY - minY) < COLLISION_EPSILON && (maxZ - minZ) < COLLISION_EPSILON; + } + + public static AABB getBoxForChunk(final int chunkX, final int chunkZ) { + double x = (double)(chunkX << 4); + double z = (double)(chunkZ << 4); + // use a bounding box bigger than the chunk to prevent entities from entering it on move + return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, + x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON), false); + } + + /* + A couple of rules for VoxelShape collisions: + Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement + checks. + If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite + direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code + will automatically round it to 0. + */ + + public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1, + final double maxY1, final double maxZ1, final double minX2, final double minY2, + final double minZ2, final double maxX2, final double maxY2, final double maxZ2) { + return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON && + (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON && + (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ, + final double maxX, final double maxY, final double maxZ) { + return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && + (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && + (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) { + return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && + (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && + (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; + } + + 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) { + final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + 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) { + final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision + if (max_move < -COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision + if (max_move > COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + public static double collideZ(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.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; + } + + public static AABB offsetX(final AABB box, final double dx) { + return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); + } + + public static AABB offsetY(final AABB box, final double dy) { + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); + } + + public static AABB offsetZ(final AABB box, final double dz) { + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false); + } + + public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); + } + + public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, false); + } + + public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); + } + + public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false); + } + + public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false); + } + + public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false); + } + + public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 + return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); + } + + public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 + return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false); + } + + public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 + return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); + } + + public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 + return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false); + } + + public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 + return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false); + } + + public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 + return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false); + } + + public static double performCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideX(target, currentBoundingBox, value); + } + + return value; + } + + public static double performCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideY(target, currentBoundingBox, value); + } + + return value; + } + + public static double performCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + final AABB target = potentialCollisions.get(i); + value = collideZ(target, currentBoundingBox, value); + } + + return value; + } + + public static Vec3 performCollisions(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); + if (y != 0.0) { + axisalignedbb = offsetY(axisalignedbb, y); + } + } + + final boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = performCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = offsetZ(axisalignedbb, z); + } + } + + if (x != 0.0) { + x = performCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = offsetX(axisalignedbb, x); + } + } + + if (!xSmaller && z != 0.0) { + z = performCollisionsZ(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; + } + } + + return ret; + } else { + final List boxes = shape.toAabbs(); + + boolean ret = false; + + 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; + } + } + + return ret; + } + } + + 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); + } + } 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(); + + for (final AABB boundingBox : shapeCasted.getBoundingBoxesRepresentation()) { + final AABB box = boundingBox.move(offX, offY, offZ); + if (!isEmpty(box)) { + list.add(box); + } + } + } 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); + } + } + } + } + + public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final AABB boundingBox) { + return isAlmostCollidingOnBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public static boolean isAlmostCollidingOnBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = worldborder.getMinX(); // -X + final double borderMaxX = worldborder.getMaxX(); // +X + + final double borderMinZ = worldborder.getMinZ(); // -Z + final double borderMaxZ = worldborder.getMaxZ(); // +Z + + return + // Not intersecting if we're smaller + !voxelShapeIntersect( + boxMinX + COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ + COLLISION_EPSILON, + boxMaxX - COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ - COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ) + && + + // Are intersecting if we're larger + voxelShapeIntersect( + boxMinX - COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ - COLLISION_EPSILON, + boxMaxX + COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ + COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ); + } + + public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final AABB boundingBox) { + return isCollidingWithBorderEdge(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public static boolean isCollidingWithBorderEdge(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, + final double boxMinZ, final double boxMaxZ) { + final double borderMinX = worldborder.getMinX() + COLLISION_EPSILON; // -X + final double borderMaxX = worldborder.getMaxX() - COLLISION_EPSILON; // +X + + final double borderMinZ = worldborder.getMinZ() + COLLISION_EPSILON; // -Z + final double borderMaxZ = worldborder.getMaxZ() - COLLISION_EPSILON; // +Z + + 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) { + 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; + + 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; + } + + 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 int blockInfo = (int) LevelChunkSection.getKnownBlockInfo(localBlockIndex, collisionForHorizontal); + + 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 + } + + 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; + } + + BlockState blockData = blocks.get(localBlockIndex); + + if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { + mutablePos.set(blockX, blockY, blockZ); + if (collisionShape == null) { + collisionShape = new LazyEntityCollisionContext(entity); + } + 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)) { + continue; + } + + if (checkOnly) { + if (voxelshape3.intersects(aabb)) { + return true; + } + } else { + ret |= addBoxesToIfIntersects(voxelshape3, aabb, into); + } + } + } + } + } + } + } + } + } + + 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; + } + + BlockState blockData = blocks.get(localBlockIndex); + if (blockData.getBlockCollisionBehavior() == CollisionUtil.KNOWN_EMPTY_BLOCK) { + continue; + } + + if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { + mutablePos.set(blockX, blockY, blockZ); + if (collisionShape == null) { + collisionShape = new LazyEntityCollisionContext(entity); + } + 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)) { + continue; + } + + if (checkOnly) { + if (voxelshape3.intersects(aabb)) { + return true; + } + } else { + ret |= addBoxesToIfIntersects(voxelshape3, aabb, into); + } + } + } + } + } + } + } + } + } + + return ret; + } + + 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)) { + return false; + } + + boolean ret = false; + + // to comply with vanilla intersection rules, expand by -epsilon so 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); + } + + 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; + } + } + } + } 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, + final Predicate entityPredicate) { + if (checkOnly) { + return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) + || getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate); + } else { + return getCollisionsForBlocksOrWorldBorder(view, entity, aabb, into, loadChunks, collidesWithUnloadedChunks, checkBorder, checkOnly, blockPredicate) + | getEntityHardCollisions(view, entity, aabb, into, checkOnly, entityPredicate); + } + } + + public static final class LazyEntityCollisionContext extends EntityCollisionContext { + + private CollisionContext delegate; + + public LazyEntityCollisionContext(final Entity entity) { + super(false, 0.0, null, null, entity); + } + + public CollisionContext getDelegate() { + final Entity entity = this.getEntity(); + return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; + } + + @Override + public boolean isDescending() { + return this.getDelegate().isDescending(); + } + + @Override + public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { + return this.getDelegate().isAbove(shape, pos, defaultValue); + } + + @Override + public boolean isHoldingItem(final Item item) { + return this.getDelegate().isHoldingItem(item); + } + + @Override + public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) { + return this.getDelegate().canStandOnFluid(state, fluidState); + } + } + + private CollisionUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java b/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java new file mode 100644 index 0000000000000000000000000000000000000000..d67a40e7be030142443680c89e1763fc9ecdfe0a --- /dev/null +++ b/src/main/java/io/papermc/paper/voxel/AABBVoxelShape.java @@ -0,0 +1,200 @@ +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) || CollisionUtil.isEmpty(axisalignedbb)) { + 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/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index 6b9622c41966843411184493f42b3d4005be533e..3c13862e7ec96e7ee57c942bb2a7084f4553ae20 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -414,7 +414,7 @@ public class ServerPlayer extends Player { if (blockposition1 != null) { this.moveTo(blockposition1, 0.0F, 0.0F); - if (world.noCollision((Entity) this)) { + if (world.noCollision(this, this.getBoundingBox(), true)) { // Paper - make sure this loads chunks, we default to NOT loading now break; } } @@ -422,7 +422,7 @@ public class ServerPlayer extends Player { } else { this.moveTo(blockposition, 0.0F, 0.0F); - while (!world.noCollision((Entity) this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { + while (!world.noCollision(this, this.getBoundingBox(), true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); } } diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 2aa31d8ad15100b3a25dd7da41bf17b126ba937e..bb4e844c77c76eb893c55844c5a814ccd337b7f4 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -936,7 +936,7 @@ public abstract class PlayerList { // CraftBukkit end worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper - while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { + while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Paper - make sure this loads chunks, we default to NOT loading now entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); } // CraftBukkit start diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 53588a621fb922112da7c585a09ba331fa03e858..396ee5a8aa57e7c2e9a14606d37cae3c02dc4283 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -1095,9 +1095,44 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { float f2 = this.getBlockSpeedFactor(); this.setDeltaMovement(this.getDeltaMovement().multiply((double) f2, 1.0D, (double) f2)); - if (this.level.getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata1) -> { - return iblockdata1.is(BlockTags.FIRE) || iblockdata1.is(Blocks.LAVA); - })) { + // Paper start - remove expensive streams from here + boolean noneMatch = true; + AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D); + { + int minX = Mth.floor(fireSearchBox.minX); + int minY = Mth.floor(fireSearchBox.minY); + int minZ = Mth.floor(fireSearchBox.minZ); + int maxX = Mth.floor(fireSearchBox.maxX); + int maxY = Mth.floor(fireSearchBox.maxY); + int maxZ = Mth.floor(fireSearchBox.maxZ); + fire_search_loop: + for (int fz = minZ; fz <= maxZ; ++fz) { + for (int fx = minX; fx <= maxX; ++fx) { + for (int fy = minY; fy <= maxY; ++fy) { + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); + if (chunk == null) { + // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true + // even if we're in lava/fire + noneMatch = true; + break fire_search_loop; + } + if (!noneMatch) { + // don't do get type, we already know we're in fire - we just need to check the chunks + // loaded state + continue; + } + + BlockState type = chunk.getBlockStateFinal(fx, fy, fz); + if (type.is(BlockTags.FIRE) || type.is(Blocks.LAVA)) { + noneMatch = false; + // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded + } + } + } + } + } + if (noneMatch) { + // Paper end - remove expensive streams from here if (this.remainingFireTicks <= 0) { this.setRemainingFireTicks(-this.getFireImmuneTicks()); } @@ -1231,32 +1266,78 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource { } private Vec3 collide(Vec3 movement) { - AABB axisalignedbb = this.getBoundingBox(); - List list = this.level.getEntityCollisions(this, axisalignedbb.expandTowards(movement)); - Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level, list); - boolean flag = movement.x != vec3d1.x; - boolean flag1 = movement.y != vec3d1.y; - boolean flag2 = movement.z != vec3d1.z; - boolean flag3 = this.onGround || flag1 && movement.y < 0.0D; - - if (this.maxUpStep > 0.0F && flag3 && (flag || flag2)) { - Vec3 vec3d2 = Entity.collideBoundingBox(this, new Vec3(movement.x, (double) this.maxUpStep, movement.z), axisalignedbb, this.level, list); - Vec3 vec3d3 = Entity.collideBoundingBox(this, new Vec3(0.0D, (double) this.maxUpStep, 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level, list); - - if (vec3d3.y < (double) this.maxUpStep) { - Vec3 vec3d4 = Entity.collideBoundingBox(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level, list).add(vec3d3); - - if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { - vec3d2 = vec3d4; + // 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) { + return movement; + } + + final Level world = this.level; + final AABB currBoundingBox = this.getBoundingBox(); + + if (io.papermc.paper.util.CollisionUtil.isEmpty(currBoundingBox)) { + return movement; + } + + final List potentialCollisions = io.papermc.paper.util.CachedLists.getTempCollisionList(); + try { + final double stepHeight = (double)this.maxUpStep; + final AABB collisionBox; + + 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); + } + } 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); + } + } else { + collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z); } } - if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { - return vec3d2.add(Entity.collideBoundingBox(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level, list)); + io.papermc.paper.util.CollisionUtil.getCollisions(world, this, collisionBox, potentialCollisions, false, true, + false, false, null, null); + + if (io.papermc.paper.util.CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { + io.papermc.paper.util.CollisionUtil.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions); } - } - return vec3d1; + final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisions); + + if (stepHeight > 0.0 + && (this.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); + + 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 (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)); + } + + return limitedMoveVector; + } else { + return limitedMoveVector; + } + } finally { + io.papermc.paper.util.CachedLists.returnTempCollisionList(potentialCollisions); + } + // Paper end - optimise collisions } public static Vec3 collideBoundingBox(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, List collisions) { @@ -2354,11 +2435,30 @@ 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); - return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> { - BlockState iblockdata = this.level.getBlockState(blockposition); + 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); + for (int fz = minZ; fz <= maxZ; ++fz) { + for (int fx = minX; fx <= maxX; ++fx) { + for (int fy = minY; fy <= maxY; ++fy) { + net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); + if (chunk == null) { + continue; + } - return !iblockdata.isAir() && iblockdata.isSuffocating(this.level, blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level, blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND); - }); + 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)) { + return true; + } + } + } + } + return false; } } diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java index a733c91700a38634806e9155c693b227e6aa16b6..120e1778f2bdd64ca19ee21dc5c5f2f382895470 100644 --- a/src/main/java/net/minecraft/world/level/BlockCollisions.java +++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java @@ -106,7 +106,7 @@ public class BlockCollisions extends AbstractIterator { VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); if (voxelShape == Shapes.block()) { - if (!this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { + if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(this.box, (double)i, (double)j, (double)k, (double)i + 1.0D, (double)j + 1.0D, (double)k + 1.0D)) { // Paper - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil continue; } diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java index 56d94c94fb0d4dc468bb5d69be655ddd5c6b5360..d7d396ad73866a97cd9f63b34ad8c587f522e713 100644 --- a/src/main/java/net/minecraft/world/level/CollisionGetter.java +++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java @@ -35,31 +35,33 @@ public interface CollisionGetter extends BlockGetter { return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox())); } + // Paper start - optimise collisions + default boolean noCollision(Entity entity, AABB box, boolean loadChunks) { + return !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); + } + // Paper end - optimise collisions + default boolean noCollision(AABB box) { - return this.noCollision((Entity)null, 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 } default boolean noCollision(Entity entity) { - return this.noCollision(entity, entity.getBoundingBox()); + // 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 } default boolean noCollision(@Nullable Entity entity, AABB box) { - 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); - } + // 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 } 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 c0817ef8927f00e2fd3fbf3289f8041fcb494049..3f458ddd4dc04ed28510a212be76bb19e7f6a61e 100644 --- a/src/main/java/net/minecraft/world/level/EntityGetter.java +++ b/src/main/java/net/minecraft/world/level/EntityGetter.java @@ -49,7 +49,7 @@ public interface EntityGetter { return true; } else { for(Entity entity : this.getEntities(except, shape.bounds())) { - if (!entity.isRemoved() && entity.blocksBuilding && (except == null || !entity.isPassengerOfSameVehicle(except)) && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) { + if (!entity.isRemoved() && entity.blocksBuilding && (except == null || !entity.isPassengerOfSameVehicle(except)) && shape.intersects(entity.getBoundingBox())) { // Paper return false; } } @@ -67,7 +67,7 @@ public interface EntityGetter { return List.of(); } 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); + 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 { 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 d545b05cfbb14e5a15b26efe372509e498605016..61590f2f04a797235299f1bd6b78a08f5bfe4a33 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 @@ -719,6 +719,13 @@ public abstract class BlockBehaviour { return this.conditionallyFullOpaque; } // Paper end + // Paper start + private long blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK; + + public final long getBlockCollisionBehavior() { + return this.blockCollisionBehavior; + } + // Paper end public void initCache() { this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid() @@ -728,7 +735,35 @@ public abstract class BlockBehaviour { } this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - cache opacity for light - + // Paper start + if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(this)) { + this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK; + } else { + 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; + } catch (final Throwable throwable) { + this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_UNKNOWN_BLOCK; + } + } + // Paper end } 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 6afad987f6dd1fd7243dfa6c50549c2a88768962..b11ad90c0956ac1b8ee069fa3f4553a2b4ce88e9 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java @@ -44,6 +44,110 @@ 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 static int getBottomBlockY(int chunkPos) { return chunkPos << 4; } @@ -68,8 +172,8 @@ public class LevelChunkSection { return this.setBlockState(x, y, z, state, true); } - public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { - BlockState iblockdata1; + public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { // Paper - state -> new state + BlockState iblockdata1; // Paper - iblockdata1 -> oldState if (lock) { iblockdata1 = (BlockState) this.states.getAndSet(x, y, z, state); @@ -108,6 +212,7 @@ public class LevelChunkSection { ++this.tickingFluidCount; } + this.updateKnownBlockInfo(x | (z << 4) | (y << 8), iblockdata1, state); // Paper return iblockdata1; } @@ -157,6 +262,7 @@ public class LevelChunkSection { }); // Paper end + this.initBlockCollisionData(); // Paper } public PalettedContainer getStates() { diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java index 120498a39b7ca7aee9763084507508d4a1c425aa..68cc6f2a78a06293a29317fda72ab3ee79b3533a 100644 --- a/src/main/java/net/minecraft/world/phys/AABB.java +++ b/src/main/java/net/minecraft/world/phys/AABB.java @@ -25,6 +25,17 @@ public class AABB { this.maxZ = Math.max(z1, z2); } + // Paper start + public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) { + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; + } + // Paper end + public AABB(BlockPos pos) { this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1)); } diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java index 9d627b8e6bf3140b894d38b9a720896e2d776369..ca5f01be5d5ccfcc56780ff93cca3824409ffc0d 100644 --- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java @@ -6,6 +6,9 @@ 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; @@ -16,6 +19,11 @@ public class ArrayVoxelShape extends VoxelShape { } 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 super(shape); int i = shape.getXSize() + 1; int j = shape.getYSize() + 1; @@ -27,6 +35,12 @@ 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 } @Override @@ -42,4 +56,152 @@ public class ArrayVoxelShape extends VoxelShape { throw new IllegalArgumentException(); } } + + // 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/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java index 9176735c08a75854209f24113b0e78332249dc4d..731c7dd15f131dc124be6af8f342b122cb89491b 100644 --- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java @@ -19,16 +19,17 @@ public final class Shapes { DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1); discreteVoxelShape.fill(0, 0, 0); return new CubeVoxelShape(discreteVoxelShape); - }); + }); public static VoxelShape getFullUnoptimisedCube() { return BLOCK; } // Paper - OBFHELPER 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 public static VoxelShape empty() { return EMPTY; } public static VoxelShape block() { - return BLOCK; + return BLOCK_OPTIMISED; // Paper } public static VoxelShape box(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { @@ -41,29 +42,14 @@ public final class Shapes { public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { if (!(maxX - minX < 1.0E-7D) && !(maxY - minY < 1.0E-7D) && !(maxZ - minZ < 1.0E-7D)) { - int i = findBits(minX, maxX); - int j = findBits(minY, maxY); - int k = findBits(minZ, maxZ); - if (i >= 0 && j >= 0 && k >= 0) { - if (i == 0 && j == 0 && k == 0) { - return block(); - } else { - int l = 1 << i; - int m = 1 << j; - int n = 1 << k; - BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(l, m, n, (int)Math.round(minX * (double)l), (int)Math.round(minY * (double)m), (int)Math.round(minZ * (double)n), (int)Math.round(maxX * (double)l), (int)Math.round(maxY * (double)m), (int)Math.round(maxZ * (double)n)); - return new CubeVoxelShape(bitSetDiscreteVoxelShape); - } - } else { - return new ArrayVoxelShape(BLOCK.shape, (DoubleList)DoubleArrayList.wrap(new double[]{minX, maxX}), (DoubleList)DoubleArrayList.wrap(new double[]{minY, maxY}), (DoubleList)DoubleArrayList.wrap(new double[]{minZ, maxZ})); - } + return new io.papermc.paper.voxel.AABBVoxelShape(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); // Paper } else { return empty(); } } public static VoxelShape create(AABB box) { - return create(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + return new io.papermc.paper.voxel.AABBVoxelShape(box); // Paper } @VisibleForTesting @@ -125,6 +111,20 @@ public final class Shapes { } 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 { @@ -196,6 +196,43 @@ public final class Shapes { } 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 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(); + } + } + } + + // fall back to vanilla + return getFaceShapeVanilla(shape, direction); + } + public static VoxelShape getFaceShapeVanilla(VoxelShape shape, Direction direction) { + // Paper end if (shape == block()) { return block(); } else { @@ -210,7 +247,7 @@ public final class Shapes { i = 0; } - return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); + return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i).optimize().optimize()); // Paper - first optimize converts to ArrayVoxelShape, second optimize could convert to AABBVoxelShape } } @@ -235,6 +272,53 @@ public final class Shapes { } 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) { + return true; + } + boolean v1Empty = one == empty(); + boolean v2Empty = two == empty(); + if (v1Empty && v2Empty) { + 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)); + } + } + 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; 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 c4ca051720f790f5b8eb860b14e268de8557454d..2182afd1b95acf14c55bddfeec17dae0a63e1f00 100644 --- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java @@ -16,11 +16,17 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; public abstract class VoxelShape { - protected final DiscreteVoxelShape shape; + public final DiscreteVoxelShape shape; // Paper - public @Nullable private VoxelShape[] faces; - VoxelShape(DiscreteVoxelShape voxels) { + // Paper start + public boolean intersects(AABB shape) { + return Shapes.joinIsNotEmpty(this, new io.papermc.paper.voxel.AABBVoxelShape(shape), BooleanOp.AND); + } + // Paper end + + protected VoxelShape(DiscreteVoxelShape voxels) { // Paper - protected this.shape = voxels; } @@ -163,7 +169,7 @@ public abstract class VoxelShape { } } - private VoxelShape calculateFace(Direction direction) { + protected VoxelShape calculateFace(Direction direction) { // Paper 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)) {