diff --git a/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeCuboidWithSurfacesInside.java b/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeCuboidWithSurfacesInside.java new file mode 100644 index 000000000..776495344 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeCuboidWithSurfacesInside.java @@ -0,0 +1,125 @@ +package me.jellysquid.mods.lithium.common.shapes; + +import net.minecraft.util.math.AxisCycleDirection; +import net.minecraft.util.math.Box; +import net.minecraft.util.shape.VoxelSet; +import net.minecraft.util.shape.VoxelShape; + +/** + * An efficient implementation of {@link VoxelShape} for a shape with one simple cuboid. + * This is an alternative to VoxelShapeSimpleCube with extra hitboxes inside. + * Vanilla has extra hitboxes at steps of 1/8th or 1/4th of a block depending on the exact coordinates of the shape. + * We are mimicking the effect on collisions here, as otherwise some contraptions would not behave like vanilla. + * @author 2No2Name + */ +public class VoxelShapeCuboidWithSurfacesInside extends VoxelShapeSimpleCube implements VoxelShapeCaster { + //In bit aligned shapes the bitset adds segments are between minX/Y/Z and maxX/Y/Z. + //Segments all have the same size. There is an additional hitbox between two adjacent segments (if both are inside the shape) + private final double xSegmentSize, ySegmentSize, zSegmentSize; + + public VoxelShapeCuboidWithSurfacesInside(VoxelSet voxels, double minX, double minY, double minZ, double maxX, double maxY, double maxZ, int xRes, int yRes, int zRes) { + super(voxels, minX, minY, minZ, maxX, maxY, maxZ); + //If the voxelshape doesn't contain any extra hitboxes in vanilla on the given axis (only one segment in total) + //we will not have segments inside by using infinity as step size, special case in calculatePenetration + this.xSegmentSize = xRes <= 0 ? Double.POSITIVE_INFINITY : 1D / (1 << xRes); + this.ySegmentSize = yRes <= 0 ? Double.POSITIVE_INFINITY : 1D / (1 << yRes); + this.zSegmentSize = zRes <= 0 ? Double.POSITIVE_INFINITY : 1D / (1 << zRes); + } + + public VoxelShapeCuboidWithSurfacesInside(VoxelSet voxels, double minX, double minY, double minZ, double maxX, double maxY, double maxZ, double xSegmentSize, double ySegmentSize, double zSegmentSize) { + super(voxels, minX, minY, minZ, maxX, maxY, maxZ); + + this.xSegmentSize = xSegmentSize; + this.ySegmentSize = ySegmentSize; + this.zSegmentSize = zSegmentSize; + } + + + @Override + public VoxelShape offset(double x, double y, double z) { + return new VoxelShapeCuboidWithSurfacesInside(this.voxels, this.minX + x, this.minY + y, this.minZ + z, this.maxX + x, this.maxY + y, this.maxZ + z, this.xSegmentSize, this.ySegmentSize, this.zSegmentSize); + } + + @Override + double calculatePenetration(AxisCycleDirection dir, Box box, double maxDist) { + switch (dir) { + case NONE: + return this.calculatePenetration(this.minX, this.maxX, this.xSegmentSize, box.minX, box.maxX, maxDist); + case FORWARD: + return this.calculatePenetration(this.minZ, this.maxZ, this.zSegmentSize, box.minZ, box.maxZ, maxDist); + case BACKWARD: + return this.calculatePenetration(this.minY, this.maxY, this.ySegmentSize, box.minY, box.maxY, maxDist); + default: + throw new IllegalArgumentException(); + } + } + + /** + * Determine how far the movement is possible. + */ + private double calculatePenetration(double aMin, double aMax, double segmentSize, double bMin, double bMax, double maxDist) { + double gap; + + if (maxDist > 0.0D) { + gap = aMin - bMax; + + if (gap < -EPSILON) { + //already far enough inside this shape + if (segmentSize == Double.POSITIVE_INFINITY) { + //no extra segments to collide with, because only one segment in total + return maxDist; + } + //extra hitboxes inside this shape, evenly spaced out by segmentSize + double distanceToEnd = aMax - bMax; + int segmentsToEnd = (int)(distanceToEnd / segmentSize); + double distanceToSegment = distanceToEnd - ((segmentsToEnd + 1) * segmentSize); + //apply a possible backwards movement because vanilla has it (vanilla: very slightly behind hitbox -> move back) + if (distanceToSegment > -EPSILON) { + segmentsToEnd++; + } + if (segmentsToEnd > 0) + //limit movement to the next segment-segment hitbox + maxDist = Math.min(maxDist, distanceToEnd - segmentsToEnd * segmentSize); + return maxDist; + } else if (maxDist < gap) { + //outside the shape and still far enough away for no collision at all + return maxDist; + + //not really inside the shape and collision is happening + } else if (gap < 0.0D) { //also gap >= -EPSILON here + //already slightly inside the shape but within the margin + //vanilla ignores movement in this case but instead tries to adjust position to be outside the shape again + //cf. these lines in VoxelShape.calculateMaxDistance + // if (g >= -1.0E-7D) { + // maxDist = Math.min(maxDist, g); + //} + return -gap; + } + //allow moving up to the shape but not into it + return gap; + } else { + //whole code again, just negated + gap = aMax - bMin; + + if (gap > EPSILON) { + if (segmentSize == Double.POSITIVE_INFINITY) { + return maxDist; + } + double negDistanceToEnd = aMin - bMin; + int negSegmentsToEnd = (int)(negDistanceToEnd / segmentSize); + double negDistanceToSegment = negDistanceToEnd - ((negSegmentsToEnd - 1) * segmentSize); + if (negDistanceToSegment < EPSILON) { + negSegmentsToEnd--; + } + if (negSegmentsToEnd < 0) + maxDist = Math.max(maxDist, negDistanceToEnd - negSegmentsToEnd * segmentSize); + return maxDist; + } else if (maxDist > gap) { + return maxDist; + } else if (gap > 0.0D) { + return -gap; + } + return gap; + } + } +} diff --git a/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeSimpleCube.java b/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeSimpleCube.java index 87a6949dd..6c5271dac 100644 --- a/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeSimpleCube.java +++ b/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeSimpleCube.java @@ -21,9 +21,9 @@ * handling in most cases as block shapes are often nothing more than a single cuboid. */ public class VoxelShapeSimpleCube extends VoxelShape implements VoxelShapeCaster { - private static final double EPSILON = 1.0E-7D; + static final double EPSILON = 1.0E-7D; - private final double minX, minY, minZ, maxX, maxY, maxZ; + final double minX, minY, minZ, maxX, maxY, maxZ; public VoxelShapeSimpleCube(VoxelSet voxels, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { super(voxels); @@ -56,7 +56,7 @@ public double calculateMaxDistance(AxisCycleDirection cycleDirection, Box box, d return maxDist; } - private double calculatePenetration(AxisCycleDirection dir, Box box, double maxDist) { + double calculatePenetration(AxisCycleDirection dir, Box box, double maxDist) { switch (dir) { case NONE: return this.calculatePenetration(this.minX, this.maxX, box.minX, box.maxX, maxDist); @@ -89,12 +89,19 @@ private double calculatePenetration(double a1, double a2, double b1, double b2, penetration = a1 - b2; if ((penetration < -EPSILON) || (maxDist < penetration)) { + //already inside the shape or far enough away -> no collision return maxDist; } - if (penetration < EPSILON) { - return 0.0D; + if (penetration < 0.0D) { //&& penetration >= -EPSILON + //only a tiny bit inside the shape -> move backwards, like vanilla (!) + //cf. these lines in VoxelShape.calculateMaxDistance + // if (g >= -1.0E-7D) { + // maxDist = Math.min(maxDist, g); + //} + return -penetration; } + //outside the shape, move up to its boundary } else { penetration = a2 - b1; @@ -102,8 +109,8 @@ private double calculatePenetration(double a1, double a2, double b1, double b2, return maxDist; } - if (penetration > -EPSILON) { - return 0.0D; + if (penetration > 0.0D) { + return -penetration; } } diff --git a/src/main/java/me/jellysquid/mods/lithium/mixin/shapes/specialized_shapes/VoxelShapesMixin.java b/src/main/java/me/jellysquid/mods/lithium/mixin/shapes/specialized_shapes/VoxelShapesMixin.java index ace2fffcb..b3a50bc7b 100644 --- a/src/main/java/me/jellysquid/mods/lithium/mixin/shapes/specialized_shapes/VoxelShapesMixin.java +++ b/src/main/java/me/jellysquid/mods/lithium/mixin/shapes/specialized_shapes/VoxelShapesMixin.java @@ -2,6 +2,7 @@ import me.jellysquid.mods.lithium.common.shapes.VoxelShapeEmpty; import me.jellysquid.mods.lithium.common.shapes.VoxelShapeSimpleCube; +import me.jellysquid.mods.lithium.common.shapes.VoxelShapeCuboidWithSurfacesInside; import net.minecraft.util.math.Box; import net.minecraft.util.shape.BitSetVoxelSet; import net.minecraft.util.shape.VoxelSet; @@ -66,11 +67,40 @@ public abstract class VoxelShapesMixin { * represent these cases with nothing more than the two vertexes. This provides a modest speed up for entity * collision code by allowing them to also use our optimized shapes. * + * Vanilla uses different kinds of VoxelShapes depending on the size and position of the box. + * A box that isn't aligned with 1/8th of a block will become a very simple ArrayVoxelShape, while others + * will become a "SimpleVoxelShape" with a BitSetVoxelSet that possibly has a higher resolution (1-3 bits) per axis. + * + * Shapes that have a high resolution (e.g. extended piston base has 2 bits on one axis) have collision + * layers inside them. An upwards extended piston base has extra collision boxes at 0.25 and 0.5 height. + * Slabs don't have extra collision boxes, because they are only as high as the smallest height that is possible + * with their bit resolution (1, so half a block). + * * @reason Use our optimized shape types - * @author JellySquid + * @author JellySquid, 2No2Name */ @Overwrite public static VoxelShape cuboid(Box box) { - return new VoxelShapeSimpleCube(FULL_CUBE_VOXELS, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + int xRes; + int yRes; + int zRes; + //findRequiredBitResolution looks unnecessarily slow, and it seems to be bugged (unintentionally returns -1) on inputs like -1e-8, + //VoxelShapeStupidCuboid.offset() does not cause a recalculation of these values + //when the voxelshape cannot be represented by a bitset with 3 bit length on any axis, a shape without boxes inside + //the case of at most 1 bit per axis can also be handled by VoxelShapeSimpleCube, as those shapes do not have + //hitboxes inside themselves (example: slabs, top slabs) + if ((xRes = VoxelShapes.findRequiredBitResolution(box.minX, box.maxX)) == -1 || + (yRes = VoxelShapes.findRequiredBitResolution(box.minY, box.maxY)) == -1 || + (zRes = VoxelShapes.findRequiredBitResolution(box.minZ, box.maxZ)) == -1 || + ( + //use a VoxelShapeSimpleCube if this test shows that there is no extra hitbox inside the shape + (xRes <= 1 || (box.maxX - box.minX) * xRes < 1.5D) && //< 1.5D just avoids rounding and checking <= 1 + (yRes <= 1 || (box.maxY - box.minY) * yRes < 1.5D) && + (zRes <= 1 || (box.maxZ - box.minZ) * zRes < 1.5D) + )) { + return new VoxelShapeSimpleCube(FULL_CUBE_VOXELS, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); + } + //vanilla would use a SimpleVoxelShape with a BitSetVoxelSet of resolution of a,b,c here, we try to match its behavior + return new VoxelShapeCuboidWithSurfacesInside(FULL_CUBE_VOXELS, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, xRes, yRes, zRes); } } diff --git a/src/main/resources/lithium.accesswidener b/src/main/resources/lithium.accesswidener index e7b8850e4..7c16e5b39 100644 --- a/src/main/resources/lithium.accesswidener +++ b/src/main/resources/lithium.accesswidener @@ -25,4 +25,6 @@ accessible field net/minecraft/util/math/noise/SimplexNoiseSampler gradients [[I accessible class net/minecraft/server/world/ChunkTicketManager$TicketDistanceLevelPropagator accessible method net/minecraft/world/ChunkPosDistanceLevelPropagator updateLevel (JIZ)V -accessible method net/minecraft/server/world/ChunkTicket isExpired (J)Z \ No newline at end of file +accessible method net/minecraft/server/world/ChunkTicket isExpired (J)Z + +accessible method net/minecraft/util/shape/VoxelShapes findRequiredBitResolution (DD)I \ No newline at end of file