Avoid creating SliceShape when retrieving Voxelshape faces

SliceShape is not always the ideal shape, as sometimes the
face result is empty or full cube - but is represented as
SliceShape. Additionally, sometimes SliceShape should be
empty but isEmpty() is false.
This commit is contained in:
Spottedleaf
2024-08-11 22:08:17 -07:00
parent c0a2e488fc
commit e68d381fc5
10 changed files with 447 additions and 55 deletions

View File

@@ -9,12 +9,12 @@ import org.spongepowered.asm.mixin.Mixin;
@Mixin(ClientSuggestionProvider.class)
abstract class ClientSuggestionProviderMixin implements CommandClientCommandSource, FabricClientCommandSource {
@Override
public void moonrise$sendSuccess(final Component message) {
public final void moonrise$sendSuccess(final Component message) {
this.sendFeedback(message);
}
@Override
public void moonrise$sendFailure(final Component message) {
public final void moonrise$sendFailure(final Component message) {
this.sendError(message);
}
}

View File

@@ -2,7 +2,6 @@ package ca.spottedleaf.moonrise.neoforge;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
@@ -15,7 +14,6 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.Explosion;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;

View File

@@ -25,12 +25,12 @@ abstract class ClientCommandSourceStackMixin extends CommandSourceStack implemen
public abstract void sendSuccess(Supplier<Component> message, boolean sendToAdmins);
@Override
public void moonrise$sendFailure(final Component message) {
public final void moonrise$sendFailure(final Component message) {
this.sendFailure(message);
}
@Override
public void moonrise$sendSuccess(final Component message) {
public final void moonrise$sendSuccess(final Component message) {
this.sendSuccess(() -> message, true);
}
}

View File

@@ -11,9 +11,9 @@ import org.spongepowered.asm.mixin.Shadow;
@Mixin(ServerChunkCache.MainThreadExecutor.class)
abstract class ServerChunkCache$MainThreadExecutorMixin extends BlockableEventLoop<Runnable> {
@Shadow(aliases = "this$0")
@Shadow(aliases = "this$0") // Neoforge
@Final
ServerChunkCache field_18810;
ServerChunkCache field_18810; // Fabric
protected ServerChunkCache$MainThreadExecutorMixin(String string) {
super(string);

View File

@@ -27,7 +27,7 @@ abstract class StructureTemplate$PaletteMixin {
value = "RETURN"
)
)
private <K, V> void makeCacheCHM(final CallbackInfo ci) {
private void makeCacheCHM(final CallbackInfo ci) {
this.cache = new ConcurrentHashMap<>();
}
}

View File

@@ -65,13 +65,19 @@ abstract class BlockStateBaseMixin extends StateHolder<Block, BlockState> implem
private AABB constantAABBCollision;
@Unique
private static void initCaches(final VoxelShape shape) {
private static void initCaches(final VoxelShape shape, final boolean neighbours) {
((CollisionVoxelShape)shape).moonrise$isFullBlock();
((CollisionVoxelShape)shape).moonrise$occludesFullBlock();
shape.toAabbs();
if (!shape.isEmpty()) {
shape.bounds();
}
if (neighbours) {
for (final Direction direction : DIRECTIONS_CACHED) {
initCaches(Shapes.getFaceShape(shape, direction), false);
initCaches(shape.getFaceShape(direction), false);
}
}
}
/**
@@ -98,17 +104,13 @@ abstract class BlockStateBaseMixin extends StateHolder<Block, BlockState> implem
this.occludesFullBlock = ((CollisionVoxelShape)collisionShape).moonrise$occludesFullBlock();
this.emptyCollisionShape = collisionShape.isEmpty();
// init caches
initCaches(collisionShape);
if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) {
for (final Direction direction : DIRECTIONS_CACHED) {
// initialise the directional face shape cache as well
final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction);
initCaches(shape);
}
initCaches(collisionShape, true);
if (this.constantCollisionShape != null) {
initCaches(this.constantCollisionShape, true);
}
if (this.cache.occlusionShapes != null) {
for (final VoxelShape shape : this.cache.occlusionShapes) {
initCaches(shape);
initCaches(shape, false);
}
}
} else {

View File

@@ -60,7 +60,7 @@ abstract class DiscreteVoxelShapeMixin implements CollisionDiscreteVoxelShape {
}
}
final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0);
final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && (voxelSet[0] & 1L) != 0L;
final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X);
final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y);

View File

@@ -23,7 +23,6 @@ import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
import net.minecraft.world.phys.shapes.OffsetDoubleList;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.SliceShape;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
@@ -208,13 +207,13 @@ abstract class VoxelShapeMixin implements CollisionVoxelShape {
if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, CollisionUtil.COLLISION_EPSILON)) {
ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1));
ret = CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1);
} else {
ret = Shapes.empty();
}
} else {
if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, CollisionUtil.COLLISION_EPSILON)) {
ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0));
ret = CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, 0);
} else {
ret = Shapes.empty();
}
@@ -225,24 +224,6 @@ abstract class VoxelShapeMixin implements CollisionVoxelShape {
return ret;
}
@Unique
private static VoxelShape tryForceBlock(final VoxelShape other) {
if (other == Shapes.block()) {
return other;
}
final AABB otherAABB = ((CollisionVoxelShape)other).moonrise$getSingleAABBRepresentation();
if (otherAABB == null) {
return other;
}
if (((CollisionVoxelShape)Shapes.block()).moonrise$getSingleAABBRepresentation().equals(otherAABB)) {
return Shapes.block();
}
return other;
}
@Unique
private boolean computeOccludesFullBlock() {
if (this.isEmpty) {
@@ -353,6 +334,115 @@ abstract class VoxelShapeMixin implements CollisionVoxelShape {
return this.isEmpty;
}
/**
* @author Spottedleaf
* @reason Use cached bounds
*/
@Overwrite
public VoxelShape singleEncompassing() {
if (this.isEmpty) {
return Shapes.empty();
}
return Shapes.create(this.bounds());
}
/**
* @author Spottedleaf
* @reason Optimise implementation to avoid indirection
*/
@Overwrite
protected double get(final Direction.Axis axis, final int idx) {
switch (axis) {
case X: {
return this.rootCoordinatesX[idx] + this.offsetX;
}
case Y: {
return this.rootCoordinatesY[idx] + this.offsetY;
}
case Z: {
return this.rootCoordinatesZ[idx] + this.offsetZ;
}
default: {
throw new IllegalStateException("Unknown axis: " + axis);
}
}
}
/**
* @author Spottedleaf
* @reason Optimise implementation to avoid indirection
*/
@Overwrite
public int findIndex(final Direction.Axis axis, final double value) {
switch (axis) {
case X: {
final double[] values = this.rootCoordinatesX;
return CollisionUtil.findFloor(
values, value - this.offsetX, 0, values.length - 1
);
}
case Y: {
final double[] values = this.rootCoordinatesY;
return CollisionUtil.findFloor(
values, value - this.offsetY, 0, values.length - 1
);
}
case Z: {
final double[] values = this.rootCoordinatesZ;
return CollisionUtil.findFloor(
values, value - this.offsetZ, 0, values.length - 1
);
}
default: {
throw new IllegalStateException("Unknown axis: " + axis);
}
}
}
@Unique
private VoxelShape calculateFaceDirect(final Direction direction, final Direction.Axis axis, final double[] coords, final double offset) {
if (coords.length == 2 &&
DoubleMath.fuzzyEquals(coords[0] + offset, 0.0, CollisionUtil.COLLISION_EPSILON) &&
DoubleMath.fuzzyEquals(coords[1] + offset, 1.0, CollisionUtil.COLLISION_EPSILON)) {
return (VoxelShape)(Object)this;
}
final boolean positiveDir = direction.getAxisDirection() == Direction.AxisDirection.POSITIVE;
// see findIndex
final int index = CollisionUtil.findFloor(
coords, (positiveDir ? (1.0 - CollisionUtil.COLLISION_EPSILON) : (0.0 + CollisionUtil.COLLISION_EPSILON)) - offset,
0, coords.length - 1
);
return CollisionUtil.sliceShape(
(VoxelShape)(Object)this, axis, index
);
}
/**
* @author Spottedleaf
* @reason Avoid creating SliceShape
*/
@Overwrite
public VoxelShape calculateFace(final Direction direction) {
final Direction.Axis axis = direction.getAxis();
switch (axis) {
case X: {
return this.calculateFaceDirect(direction, axis, this.rootCoordinatesX, this.offsetX);
}
case Y: {
return this.calculateFaceDirect(direction, axis, this.rootCoordinatesY, this.offsetY);
}
case Z: {
return this.calculateFaceDirect(direction, axis, this.rootCoordinatesZ, this.offsetZ);
}
default: {
throw new IllegalStateException("Unknown axis: " + axis);
}
}
}
/**
* @author Spottedleaf
* @reason Route to optimized collision method
@@ -382,11 +472,12 @@ abstract class VoxelShapeMixin implements CollisionVoxelShape {
}
@Unique
private static DoubleList offsetList(final DoubleList src, final double by) {
if (src instanceof OffsetDoubleList offsetDoubleList) {
return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset);
private static DoubleList offsetList(final double[] src, final double by) {
final DoubleArrayList wrap = DoubleArrayList.wrap(src);
if (by == 0.0) {
return wrap;
}
return new OffsetDoubleList(src, by);
return new OffsetDoubleList(wrap, by);
}
/**
@@ -401,9 +492,9 @@ abstract class VoxelShapeMixin implements CollisionVoxelShape {
final ArrayVoxelShape ret = new ArrayVoxelShape(
this.shape,
offsetList(this.getCoords(Direction.Axis.X), x),
offsetList(this.getCoords(Direction.Axis.Y), y),
offsetList(this.getCoords(Direction.Axis.Z), z)
offsetList(this.rootCoordinatesX, this.offsetX + x),
offsetList(this.rootCoordinatesY, this.offsetY + y),
offsetList(this.rootCoordinatesZ, this.offsetZ + z)
);
final CachedToAABBs cachedToAABBs = this.cachedToAABBs;
@@ -416,10 +507,12 @@ abstract class VoxelShapeMixin implements CollisionVoxelShape {
@Unique
private List<AABB> toAabbsUncached() {
final List<AABB> ret = new ArrayList<>();
final List<AABB> ret;
if (this.singleAABBRepresentation != null) {
ret = new ArrayList<>(1);
ret.add(this.singleAABBRepresentation);
} else {
ret = new ArrayList<>();
final double[] coordsX = this.rootCoordinatesX;
final double[] coordsY = this.rootCoordinatesY;
final double[] coordsZ = this.rootCoordinatesZ;
@@ -718,6 +811,11 @@ abstract class VoxelShapeMixin implements CollisionVoxelShape {
final List<AABB> aabbs = this.toAabbs();
if (aabbs.isEmpty()) {
// We are a SliceShape, which does not properly fill isEmpty for every case
return Shapes.empty();
}
if (aabbs.size() == 1) {
final AABB singleAABB = aabbs.get(0);
final VoxelShape ret = Shapes.create(singleAABB);

View File

@@ -4,8 +4,6 @@ import net.minecraft.client.multiplayer.resolver.ServerAddressResolver;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -29,9 +27,10 @@ interface ServerAddressResolverMixin {
private static InetAddress eliminateRDNS(final String name) throws UnknownHostException {
final InetAddress ret = InetAddress.getByName(name);
if (ret instanceof Inet4Address || ret instanceof Inet6Address) {
final byte[] address = ret.getAddress();
if (address != null) {
// pass name to prevent rDNS
return InetAddress.getByAddress(name, ret.getAddress());
return InetAddress.getByAddress(name, address);
}
return ret;

View File

@@ -35,9 +35,11 @@ import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
import net.minecraft.world.phys.shapes.EntityCollisionContext;
import net.minecraft.world.phys.shapes.OffsetDoubleList;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.SliceShape;
import net.minecraft.world.phys.shapes.VoxelShape;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
@@ -162,7 +164,8 @@ public final class CollisionUtil {
// startIndex and endIndex inclusive
// assumes indices are in range of array
private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) {
public static int findFloor(final double[] values, final double value, int startIndex, int endIndex) {
Objects.checkFromToIndex(startIndex, endIndex + 1, values.length);
do {
final int middle = (startIndex + endIndex) >>> 1;
final double middleVal = values[middle];
@@ -177,6 +180,214 @@ public final class CollisionUtil {
return startIndex - 1;
}
private static VoxelShape sliceShapeVanilla(final VoxelShape src, final Direction.Axis axis,
final int index) {
return new SliceShape(src, axis, index);
}
private static DoubleList offsetList(final double[] src, final double by) {
final DoubleArrayList wrap = DoubleArrayList.wrap(src);
if (by == 0.0) {
return wrap;
}
return new OffsetDoubleList(wrap, by);
}
private static VoxelShape sliceShapeOptimised(final VoxelShape src, final Direction.Axis axis,
final int index) {
// assume index in range
final double off_x = ((CollisionVoxelShape)src).moonrise$offsetX();
final double off_y = ((CollisionVoxelShape)src).moonrise$offsetY();
final double off_z = ((CollisionVoxelShape)src).moonrise$offsetZ();
final double[] coords_x = ((CollisionVoxelShape)src).moonrise$rootCoordinatesX();
final double[] coords_y = ((CollisionVoxelShape)src).moonrise$rootCoordinatesY();
final double[] coords_z = ((CollisionVoxelShape)src).moonrise$rootCoordinatesZ();
final CachedShapeData cached_shape_data = ((CollisionVoxelShape)src).moonrise$getCachedVoxelData();
// note: size = coords.length - 1
final int size_x = cached_shape_data.sizeX();
final int size_y = cached_shape_data.sizeY();
final int size_z = cached_shape_data.sizeZ();
final long[] bitset = cached_shape_data.voxelSet();
final DoubleList list_x;
final DoubleList list_y;
final DoubleList list_z;
final int shape_sx;
final int shape_ex;
final int shape_sy;
final int shape_ey;
final int shape_sz;
final int shape_ez;
switch (axis) {
case X: {
// validate index
if (index < 0 || index >= size_x) {
return Shapes.empty();
}
// test if input is already "sliced"
if (coords_x.length == 2 && (coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0) {
return src;
}
// test if result would be full box
if (coords_y.length == 2 && coords_z.length == 2 &&
(coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0 &&
(coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) {
// note: size_y == size_z == 1
final int bitIdx = 0 + 0*size_z + index*(size_z*size_y);
return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block();
}
list_x = ZERO_ONE;
list_y = offsetList(coords_y, off_y);
list_z = offsetList(coords_z, off_z);
shape_sx = index;
shape_ex = index + 1;
shape_sy = 0;
shape_ey = size_y;
shape_sz = 0;
shape_ez = size_z;
break;
}
case Y: {
// validate index
if (index < 0 || index >= size_y) {
return Shapes.empty();
}
// test if input is already "sliced"
if (coords_y.length == 2 && (coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) {
return src;
}
// test if result would be full box
if (coords_x.length == 2 && coords_z.length == 2 &&
(coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 &&
(coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) {
// note: size_x == size_z == 1
final int bitIdx = 0 + index*size_z + 0*(size_z*size_y);
return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block();
}
list_x = offsetList(coords_x, off_x);
list_y = ZERO_ONE;
list_z = offsetList(coords_z, off_z);
shape_sx = 0;
shape_ex = size_x;
shape_sy = index;
shape_ey = index + 1;
shape_sz = 0;
shape_ez = size_z;
break;
}
case Z: {
// validate index
if (index < 0 || index >= size_z) {
return Shapes.empty();
}
// test if input is already "sliced"
if (coords_z.length == 2 && (coords_z[0] + off_z) == 0.0 && (coords_z[1] + off_z) == 1.0) {
return src;
}
// test if result would be full box
if (coords_x.length == 2 && coords_y.length == 2 &&
(coords_x[0] + off_x) == 0.0 && (coords_x[1] + off_x) == 1.0 &&
(coords_y[0] + off_y) == 0.0 && (coords_y[1] + off_y) == 1.0) {
// note: size_x == size_y == 1
final int bitIdx = index + 0*size_z + 0*(size_z*size_y);
return (bitset[bitIdx >>> 6] & (1L << bitIdx)) == 0L ? Shapes.empty() : Shapes.block();
}
list_x = offsetList(coords_x, off_x);
list_y = offsetList(coords_y, off_y);
list_z = ZERO_ONE;
shape_sx = 0;
shape_ex = size_x;
shape_sy = 0;
shape_ey = size_y;
shape_sz = index;
shape_ez = index + 1;
break;
}
default: {
throw new IllegalStateException("Unknown axis: " + axis);
}
}
final int local_len_x = shape_ex - shape_sx;
final int local_len_y = shape_ey - shape_sy;
final int local_len_z = shape_ez - shape_sz;
final BitSetDiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(local_len_x, local_len_y, local_len_z);
final int idx_off = shape_sz + shape_sy*size_z + shape_sx*(size_z*size_y);
for (int x = 0; x < local_len_x; ++x) {
boolean setX = false;
for (int y = 0; y < local_len_y; ++y) {
boolean setY = false;
for (int z = 0; z < local_len_z; ++z) {
final int unslicedIdx = idx_off + z + y*size_z + x*(size_z*size_y);
if ((bitset[unslicedIdx >>> 6] & (1L << unslicedIdx)) == 0L) {
continue;
}
setY = true;
setX = true;
shape.zMin = Math.min(shape.zMin, z);
shape.zMax = Math.max(shape.zMax, z + 1);
shape.storage.set(
z + y*local_len_z + x*(local_len_y*local_len_z)
);
}
if (setY) {
shape.yMin = Math.min(shape.yMin, y);
shape.yMax = Math.max(shape.yMax, y + 1);
}
}
if (setX) {
shape.xMin = Math.min(shape.xMin, x);
shape.xMax = Math.max(shape.xMax, x + 1);
}
}
return shape.isEmpty() ? Shapes.empty() : new ArrayVoxelShape(
shape, list_x, list_y, list_z
);
}
private static final boolean DEBUG_SLICE_SHAPE = false;
public static VoxelShape sliceShape(final VoxelShape src, final Direction.Axis axis,
final int index) {
final VoxelShape ret = sliceShapeOptimised(src, axis, index);
if (DEBUG_SLICE_SHAPE) {
final VoxelShape vanilla = sliceShapeVanilla(src, axis, index);
if (!equals(ret, vanilla)) {
// special case: SliceShape is not empty when it should be!
if (areAnyFull(ret.shape) || areAnyFull(vanilla.shape)) {
equals(ret, vanilla);
sliceShapeOptimised(src, axis, index);
throw new IllegalStateException("Slice shape mismatch");
}
}
}
return ret;
}
public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) {
if (voxel.isEmpty()) {
return false;
@@ -1306,7 +1517,7 @@ public final class CollisionUtil {
return true;
} else if (isEmpty1 ^ isEmpty2) {
return false;
}
} // else: isEmpty1 = isEmpty2 = false
if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) {
return false;
@@ -1327,6 +1538,12 @@ public final class CollisionUtil {
// useful only for testing
public static boolean equals(final VoxelShape shape1, final VoxelShape shape2) {
if (shape1.isEmpty() & shape2.isEmpty()) {
return true;
} else if (shape1.isEmpty() ^ shape2.isEmpty()) {
return false;
}
if (!equals(shape1.shape, shape2.shape)) {
return false;
}
@@ -1336,6 +1553,84 @@ public final class CollisionUtil {
shape1.getCoords(Direction.Axis.Z).equals(shape2.getCoords(Direction.Axis.Z));
}
public static boolean areAnyFull(final DiscreteVoxelShape shape) {
if (shape.isEmpty()) {
return false;
}
final int sizeX = shape.getXSize();
final int sizeY = shape.getYSize();
final int sizeZ = shape.getZSize();
for (int x = 0; x < sizeX; ++x) {
for (int y = 0; y < sizeY; ++y) {
for (int z = 0; z < sizeZ; ++z) {
if (shape.isFull(x, y, z)) {
return true;
}
}
}
}
return false;
}
public static String shapeMismatch(final DiscreteVoxelShape shape1, final DiscreteVoxelShape shape2) {
final CachedShapeData cachedShapeData1 = ((CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData();
final CachedShapeData cachedShapeData2 = ((CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData();
final boolean isEmpty1 = cachedShapeData1.isEmpty();
final boolean isEmpty2 = cachedShapeData2.isEmpty();
if (isEmpty1 & isEmpty2) {
return null;
} else if (isEmpty1 ^ isEmpty2) {
return null;
} // else: isEmpty1 = isEmpty2 = false
if (cachedShapeData1.sizeX() != cachedShapeData2.sizeX()) {
return "size x: " + cachedShapeData1.sizeX() + " != " + cachedShapeData2.sizeX();
}
if (cachedShapeData1.sizeY() != cachedShapeData2.sizeY()) {
return "size y: " + cachedShapeData1.sizeY() + " != " + cachedShapeData2.sizeY();
}
if (cachedShapeData1.sizeZ() != cachedShapeData2.sizeZ()) {
return "size z: " + cachedShapeData1.sizeZ() + " != " + cachedShapeData2.sizeZ();
}
final StringBuilder ret = new StringBuilder();
final int sizeX = cachedShapeData1.sizeX();;
final int sizeY = cachedShapeData1.sizeY();
final int sizeZ = cachedShapeData1.sizeZ();
boolean first = true;
for (int x = 0; x < sizeX; ++x) {
for (int y = 0; y < sizeY; ++y) {
for (int z = 0; z < sizeZ; ++z) {
final boolean isFull1 = shape1.isFull(x, y, z);
final boolean isFull2 = shape2.isFull(x, y, z);
if (isFull1 == isFull2) {
continue;
}
if (first) {
first = false;
} else {
ret.append(", ");
}
ret.append("(").append(x).append(",").append(y).append(",").append(z)
.append("): shape1: ").append(isFull1).append(", shape2: ").append(isFull2);
}
}
}
return ret.isEmpty() ? null : ret.toString();
}
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);
}