From d322ab68b363953f9a2b7bddc680498a532b3708 Mon Sep 17 00:00:00 2001 From: 2No2Name <2No2Name@web.de> Date: Thu, 9 Jul 2020 17:20:56 +0200 Subject: [PATCH] fix: add hitboxes inside some voxelshapes of cuboids (e.g. extended piston base) fixes: Lithium removes hitboxes inside some blocks. #60 squashed: fix small mistakes and improve bad comments --- ...elShapeCuboidWithCollisionBoxesInside.java | 130 ++++++++++++++++++ .../common/shapes/VoxelShapeSimpleCube.java | 22 ++- .../specialized_shapes/VoxelShapesMixin.java | 37 ++++- src/main/resources/lithium.accesswidener | 4 +- 4 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeCuboidWithCollisionBoxesInside.java diff --git a/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeCuboidWithCollisionBoxesInside.java b/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeCuboidWithCollisionBoxesInside.java new file mode 100644 index 000000000..607ec1e92 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/shapes/VoxelShapeCuboidWithCollisionBoxesInside.java @@ -0,0 +1,130 @@ +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 VoxelShapeCuboidWithCollisionBoxesInside extends VoxelShapeSimpleCube { + //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 collision box between two adjacent segments (if both are inside the shape) + private final int xSegments; + private final int ySegments; + private final int zSegments; + + public VoxelShapeCuboidWithCollisionBoxesInside(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 collision boxes in vanilla on the given axis (only one segment in total) + //We set the segment count to 1 to signal that there are no inside shape segment borders, which is the fast branch in calculatePenetration + this.xSegments = xRes <= 1 ? 1 : (1 << xRes); + this.ySegments = yRes <= 1 ? 1 : (1 << yRes); + this.zSegments = zRes <= 1 ? 1 : (1 << zRes); + } + + /** + * Constructor for use in offset() calls. + */ + public VoxelShapeCuboidWithCollisionBoxesInside(VoxelSet voxels, int xSegments, int ySegments, int zSegments, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + super(voxels, minX, minY, minZ, maxX, maxY, maxZ); + + this.xSegments = xSegments; + this.ySegments = ySegments; + this.zSegments = zSegments; + } + + @Override + public VoxelShape offset(double x, double y, double z) { + return new VoxelShapeCuboidWithCollisionBoxesInside(this.voxels, this.xSegments, this.ySegments, this.zSegments, this.minX + x, this.minY + y, this.minZ + z, this.maxX + x, this.maxY + y, this.maxZ + z); + } + + @Override + double calculatePenetration(AxisCycleDirection dir, Box box, double maxDist) { + switch (dir) { + case NONE: + return this.calculatePenetration(this.minX, this.maxX, this.xSegments, box.minX, box.maxX, maxDist); + case FORWARD: + return this.calculatePenetration(this.minZ, this.maxZ, this.zSegments, box.minZ, box.maxZ, maxDist); + case BACKWARD: + return this.calculatePenetration(this.minY, this.maxY, this.ySegments, box.minY, box.maxY, maxDist); + default: + throw new IllegalArgumentException(); + } + } + + /** + * Determine how far the movement is possible. + */ + private double calculatePenetration(double aMin, double aMax, final int segmentCount, double bMin, double bMax, double maxDist) { + double gap; + + if (maxDist > 0.0D) { + gap = aMin - bMax; + + if (gap < -EPSILON) { + //already far enough inside this shape to not collide with the surface + if (segmentCount == 1) { + //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 * segmentCount); + if (segmentsToEnd < 0) + return maxDist; + //using 1D because the BitSetVoxelSet we are mimicking is always assuming a size of 1 for the whole voxel + double distanceToSegment = distanceToEnd - ((segmentsToEnd + 1D) / segmentCount); + //apply a possible backwards movement because vanilla has it (vanilla: very slightly behind hitbox -> move back) + if (distanceToSegment > -EPSILON) { + segmentsToEnd++; + } + if (segmentsToEnd > 0) //no need to check segmentsToEnd >= segmentCount, because gap < -EPSILON already + //limit movement to the next segment-segment hitbox + maxDist = Math.min(maxDist, distanceToEnd - segmentsToEnd / (double) segmentCount); + 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 (VoxelShape.calculateMaxDistance) stops movement in this case and instead tries to adjust position to be outside the shape again + //This is usually cancelled during the next call of calculatePenetration due to abs(maxDist) < EPSILON returning 0. + //If this is the last iteration it might be used without being set to 0. + return -gap; + } + //allow moving up to the shape but not into it + return gap; + } else { + //whole code again, just negated for the other direction + gap = aMax - bMin; + + if (gap > EPSILON) { + if (segmentCount == 1) { + return maxDist; + } + double negDistanceToEnd = aMin - bMin; + int negSegmentsToEnd = (int)(negDistanceToEnd * segmentCount); + double negDistanceToSegment = negDistanceToEnd - (negSegmentsToEnd - 1D) / segmentCount; + if (negDistanceToSegment < EPSILON) { + negSegmentsToEnd--; + } + if (negSegmentsToEnd < 0) + maxDist = Math.max(maxDist, negDistanceToEnd - negSegmentsToEnd / (double) segmentCount); + 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..26ceeee44 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,21 +89,29 @@ private double calculatePenetration(double a1, double a2, double b1, double b2, penetration = a1 - b2; if ((penetration < -EPSILON) || (maxDist < penetration)) { + //already far enough inside this shape to not collide with the surface or + //outside the shape and still far enough away for no collision at all return maxDist; } - if (penetration < EPSILON) { - return 0.0D; + if (penetration < 0.0D) { //also penetration >= -EPSILON + //already slightly inside the shape but within the margin + //vanilla (VoxelShape.calculateMaxDistance) stops movement in this case and instead tries to adjust position to be outside the shape again + //This is usually cancelled during the next call of calculatePenetration due to abs(maxDist) < EPSILON returning 0. + //If this is the last iteration it might be used without being set to 0. + return -penetration; } + //allow moving up to the shape but not into it } else { + //whole code again, just negated for the other direction penetration = a2 - b1; if ((penetration > EPSILON) || (maxDist > penetration)) { 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..302972cd1 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.VoxelShapeCuboidWithCollisionBoxesInside; import net.minecraft.util.math.Box; import net.minecraft.util.shape.BitSetVoxelSet; import net.minecraft.util.shape.VoxelSet; @@ -66,11 +67,43 @@ 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, + //VoxelShapeCuboidWithCollisionBoxesInside.offset() does not cause a recalculation of these values + //If the VoxelShape cannot be represented by a bitset with 3 bit length on any axis, a shape without boxes inside will be used in vanilla + //The case of at most 1 bit per axis can also be handled by VoxelShapeSimpleCube, as those shapes never have + //hitboxes inside themselves (e.g.: 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 there is no extra hitbox inside the shape + //as there are hitboxes between segment, we test if there is at most 1 segment inside the shape on each axis + //res <= 1 only allows 2 segments max, but if both were in the shape the resolution would be lower + //(max-min)*segmentCount < 1.5D checks if there are less than 1.5 segments, so at most 1. + (xRes <= 1 || (box.maxX - box.minX) * (1 << xRes) < 1.5D) && + (yRes <= 1 || (box.maxY - box.minY) * (1 << yRes) < 1.5D) && + (zRes <= 1 || (box.maxZ - box.minZ) * (1 << 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 xRes, yRes, zRes here, we try to match its behavior + return new VoxelShapeCuboidWithCollisionBoxesInside(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