Skip to content

Commit

Permalink
fix: add hitboxes inside some voxelshapes of cuboids (e.g. extended p…
Browse files Browse the repository at this point in the history
…iston base)

fixes: Lithium removes hitboxes inside some blocks. #60

squashed: fix small mistakes and improve bad comments
  • Loading branch information
2No2Name committed Jul 13, 2020
1 parent a51d340 commit d322ab6
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
4 changes: 3 additions & 1 deletion src/main/resources/lithium.accesswidener
Original file line number Diff line number Diff line change
Expand Up @@ -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
accessible method net/minecraft/server/world/ChunkTicket isExpired (J)Z

accessible method net/minecraft/util/shape/VoxelShapes findRequiredBitResolution (DD)I

0 comments on commit d322ab6

Please sign in to comment.