diff --git a/src/main/java/me/jellysquid/mods/lithium/common/entity/LithiumEntityCollisions.java b/src/main/java/me/jellysquid/mods/lithium/common/entity/LithiumEntityCollisions.java index 79611dee9..4400ae484 100644 --- a/src/main/java/me/jellysquid/mods/lithium/common/entity/LithiumEntityCollisions.java +++ b/src/main/java/me/jellysquid/mods/lithium/common/entity/LithiumEntityCollisions.java @@ -1,6 +1,6 @@ package me.jellysquid.mods.lithium.common.entity; -import me.jellysquid.mods.lithium.common.entity.movement.BlockCollisionSweeper; +import me.jellysquid.mods.lithium.common.entity.movement.ChunkAwareBlockCollisionSweeper; import me.jellysquid.mods.lithium.common.util.Producer; import net.minecraft.entity.Entity; import net.minecraft.util.math.Box; @@ -32,7 +32,7 @@ public static Stream getBlockCollisions(CollisionView world, Entity return Stream.empty(); } - final BlockCollisionSweeper sweeper = new BlockCollisionSweeper(world, entity, box); + final ChunkAwareBlockCollisionSweeper sweeper = new ChunkAwareBlockCollisionSweeper(world, entity, box); return StreamSupport.stream(new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.NONNULL | Spliterator.IMMUTABLE) { private boolean skipWorldBorderCheck = entity == null; @@ -49,14 +49,10 @@ public boolean tryAdvance(Consumer consumer) { } } - while (sweeper.step()) { - VoxelShape shape = sweeper.getCollidedShape(); - - if (shape != null) { - consumer.accept(shape); - - return true; - } + VoxelShape shape = sweeper.step(); + if (shape != null) { + consumer.accept(shape); + return true; } return false; @@ -74,15 +70,10 @@ public static boolean doesBoxCollideWithBlocks(CollisionView world, Entity entit return false; } - final BlockCollisionSweeper sweeper = new BlockCollisionSweeper(world, entity, box); - - while (sweeper.step()) { - if (sweeper.getCollidedShape() != null) { - return true; - } - } + final ChunkAwareBlockCollisionSweeper sweeper = new ChunkAwareBlockCollisionSweeper(world, entity, box); - return false; + VoxelShape shape = sweeper.step(); + return shape != null; } /** diff --git a/src/main/java/me/jellysquid/mods/lithium/common/entity/movement/ChunkAwareBlockCollisionSweeper.java b/src/main/java/me/jellysquid/mods/lithium/common/entity/movement/ChunkAwareBlockCollisionSweeper.java new file mode 100644 index 000000000..ea083a564 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/entity/movement/ChunkAwareBlockCollisionSweeper.java @@ -0,0 +1,248 @@ +package me.jellysquid.mods.lithium.common.entity.movement; + +import me.jellysquid.mods.lithium.common.shapes.VoxelShapeCaster; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.ShapeContext; +import net.minecraft.entity.Entity; +import net.minecraft.util.function.BooleanBiFunction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.CollisionView; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkSection; + +import static me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions.EPSILON; + +/** + * ChunkAwareBlockCollisionSweeper iterates over blocks in one chunk section at a time. Together with the chunk + * section keeping track of the amount of oversized blocks inside the number of iterations can often be reduced. + */ +public class ChunkAwareBlockCollisionSweeper { + private static final boolean OVERSIZED_BLOCK_COUNTING_ENABLED = OversizedBlocksCounter.class.isAssignableFrom(ChunkSection.class); + + private final BlockPos.Mutable pos = new BlockPos.Mutable(); + + /** + * The collision box being swept through the world. + */ + private final Box box; + + /** + * The VoxelShape of the collision box being swept through the world. + */ + private final VoxelShape shape; + + private final CollisionView view; + private final ShapeContext context; + + //limits of the area without extension for oversized blocks + private final int minX, minY, minZ, maxX, maxY, maxZ; + + //variables prefixed with c refer to the iteration of the currently cached chunk section + private int chunkX, chunkY, chunkZ; + private int cStartX, cStartZ; + private int cEndX, cEndZ; + private int cX, cY, cZ; + + private int cTotalSize; + private int cIterated; + + private boolean sectionOversizedBlocks; + private Chunk cachedChunk; + private ChunkSection cachedChunkSection; + + public ChunkAwareBlockCollisionSweeper(CollisionView view, Entity entity, Box box) { + this.box = box; + this.shape = VoxelShapes.cuboid(box); + this.context = entity == null ? ShapeContext.absent() : ShapeContext.of(entity); + this.view = view; + + this.minX = MathHelper.floor(box.minX - EPSILON); + this.maxX = MathHelper.floor(box.maxX + EPSILON); + this.minY = MathHelper.clamp((int)(box.minY - EPSILON),0,255); + this.maxY = MathHelper.clamp((int)(box.maxY + EPSILON),0,255); + this.minZ = MathHelper.floor(box.minZ - EPSILON); + this.maxZ = MathHelper.floor(box.maxZ + EPSILON); + + this.chunkX = (this.minX - 1) >> 4; + this.chunkZ = (this.minZ - 1) >> 4; + + this.cIterated = 0; + this.cTotalSize = 0; + + //decrement as first nextSection call will increment it again + this.chunkX--; + } + + private boolean nextSection() { + do { + do { + //find the coordinates of the next section inside the area expanded by 1 block on all sides + //note: this.minX, maxX etc are not expanded, so there are lots of +1 and -1 around. + if (this.cachedChunk != null && this.chunkY < 15 && this.chunkY < ((this.maxY + 1) >> 4)) { + this.chunkY++; + this.cachedChunkSection = this.cachedChunk.getSectionArray()[this.chunkY]; + } else { + this.chunkY = MathHelper.clamp((this.minY - 1) >> 4, 0, 15); + + if ((this.chunkX < ((this.maxX + 1) >> 4))) { + //first initialization takes this branch + this.chunkX++; + } else { + this.chunkX = (this.minX - 1) >> 4; + + if (this.chunkZ < ((this.maxZ + 1) >> 4)) { + this.chunkZ++; + } else { + return false; //no more sections to iterate + } + } + //Casting to Chunk is not checked, together with other mods this could cause a ClassCastException + this.cachedChunk = (Chunk) this.view.getExistingChunk(this.chunkX, this.chunkZ); + if (this.cachedChunk != null) { + this.cachedChunkSection = this.cachedChunk.getSectionArray()[this.chunkY]; + } + } + //skip empty chunks and empty chunk sections + } while (this.cachedChunk == null || ChunkSection.isEmpty(this.cachedChunkSection)); + + this.sectionOversizedBlocks = hasChunkSectionOversizedBlocks(this.cachedChunk, this.chunkY); + + int sizeExtension = this.sectionOversizedBlocks ? 1 : 0; + + this.cEndX = Math.min(this.maxX + sizeExtension, 15 + (this.chunkX << 4)); + int cEndY = Math.min(this.maxY + sizeExtension, 15 + (this.chunkY << 4)); + this.cEndZ = Math.min(this.maxZ + sizeExtension, 15 + (this.chunkZ << 4)); + + this.cStartX = Math.max(this.minX - sizeExtension, this.chunkX << 4); + int cStartY = Math.max(this.minY - sizeExtension, this.chunkY << 4); + this.cStartZ = Math.max(this.minZ - sizeExtension, this.chunkZ << 4); + this.cX = this.cStartX; + this.cY = cStartY; + this.cZ = this.cStartZ; + + this.cTotalSize = (this.cEndX - this.cStartX + 1) * (cEndY - cStartY + 1) * (this.cEndZ - this.cStartZ + 1); + //skip completely empty section iterations + } while(this.cTotalSize == 0); + this.cIterated = 0; + + return true; + } + + + /** + * Advances the sweep forward until finding a block with a box-colliding VoxelShape + * + * @return null if no VoxelShape is left in the area, otherwise the next VoxelShape + */ + public VoxelShape step() { + while(true) { + if (this.cIterated >= this.cTotalSize) { + if (!this.nextSection()) { + return null; + } + } + this.cIterated++; + + + final int x = this.cX; + final int y = this.cY; + final int z = this.cZ; + + //The iteration order within a chunk section is chosen so that it causes a mostly linear array access in the storage. + //In net.minecraft.world.chunk.PalettedContainer.toIndex x gets the 4 least significant bits, z the 4 above, and y the 4 even higher ones. + //Linearly accessing arrays might be slightly faster than other access patterns. + //This code hasn't been benchmarked in comparison to another access order. + if (this.cX < this.cEndX) { + this.cX++; + } else if (this.cZ < this.cEndZ) { + this.cX = this.cStartX; + this.cZ++; + } else { + this.cX = this.cStartX; + this.cZ = this.cStartZ; + this.cY++; + //stop condition was already checked using this.cIterated at the start of the method + } + + final int edgesHit = this.sectionOversizedBlocks ? 0 : + //using < minX and > maxX instead of <= and >= in vanilla, because minX, maxX are the coordinates + //that were of box that wasn't extended for oversized blocks yet. + (x < this.minX || x > this.maxX ? 1 : 0) + + (y < this.minY || y > this.maxY ? 1 : 0) + + (z < this.minZ || z > this.maxZ ? 1 : 0); + + if (edgesHit == 3) { + continue; + } + + final BlockState state = this.cachedChunkSection.getBlockState(x & 15, y & 15, z & 15); + + if (canInteractWithBlock(state, edgesHit)) { + this.pos.set(x, y, z); + VoxelShape collisionShape = state.getCollisionShape(this.view, this.pos, this.context); + + if (collisionShape != VoxelShapes.empty()) { + VoxelShape collidedShape = getCollidedShape(this.box, this.shape, collisionShape, x, y, z); + if (collidedShape != null) { + return collidedShape; + } + } + } + } + } + + /** + * This is an artifact from vanilla which is used to avoid testing shapes in the extended portion of a volume + * unless they are a shape which exceeds their voxel. Pistons must be special-cased here. + * + * @return True if the shape can be interacted with at the given edge boundary + */ + private static boolean canInteractWithBlock(BlockState state, int edgesHit) { + return (edgesHit != 1 || state.exceedsCube()) && (edgesHit != 2 || state.getBlock() == Blocks.MOVING_PISTON); + } + + /** + * Checks if the {@param entityShape} or {@param entityBox} intersects the given {@param shape} which is translated + * to the given position. This is a very specialized implementation which tries to avoid going through VoxelShape + * for full-cube shapes. + * + * @return A {@link VoxelShape} which contains the shape representing that which was collided with, otherwise null + */ + private static VoxelShape getCollidedShape(Box entityBox, VoxelShape entityShape, VoxelShape shape, int x, int y, int z) { + if (shape instanceof VoxelShapeCaster) { + if (((VoxelShapeCaster) shape).intersects(entityBox, x, y, z)) { + return shape.offset(x, y, z); + } else { + return null; + } + } + + shape = shape.offset(x, y, z); + + if (VoxelShapes.matchesAnywhere(shape, entityShape, BooleanBiFunction.AND)) { + return shape; + } + + return null; + } + + /** + * Checks the cached information whether the {@param chunkY} section of the {@param chunk} has oversized blocks. + * @return Whether there are any oversized blocks in the chunk section. + */ + private static boolean hasChunkSectionOversizedBlocks(Chunk chunk, int chunkY) { + if (OVERSIZED_BLOCK_COUNTING_ENABLED) { + ChunkSection section = chunk.getSectionArray()[chunkY]; + return section != null && ((OversizedBlocksCounter)section).hasOversizedBlocks(); + } + return true; //like vanilla, assume that a chunk section has oversized blocks, when the section mixin isn't loaded + } + public interface OversizedBlocksCounter { + boolean hasOversizedBlocks(); + } +} diff --git a/src/main/java/me/jellysquid/mods/lithium/mixin/chunk/oversized_blocks/MixinChunkSection.java b/src/main/java/me/jellysquid/mods/lithium/mixin/chunk/oversized_blocks/MixinChunkSection.java new file mode 100644 index 000000000..cad61d5c3 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/mixin/chunk/oversized_blocks/MixinChunkSection.java @@ -0,0 +1,59 @@ +package me.jellysquid.mods.lithium.mixin.chunk.oversized_blocks; + +import me.jellysquid.mods.lithium.common.entity.movement.ChunkAwareBlockCollisionSweeper; +import net.minecraft.block.BlockState; +import net.minecraft.world.chunk.ChunkSection; +import net.minecraft.world.chunk.PalettedContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +/** + * Keep track of how many oversized blocks are in this chunk section. If none are there, collision code can skip a few blocks. + * Oversized blocks are fences, walls, extended piston heads and blocks with dynamic bounds (scaffolding, shulker box, moving blocks) + * @author 2No2Name + */ +@Mixin(ChunkSection.class) +public class MixinChunkSection implements ChunkAwareBlockCollisionSweeper.OversizedBlocksCounter { + @Unique + private short oversizedBlockCount; + + @Redirect(method = "calculateCounts", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/PalettedContainer;count(Lnet/minecraft/world/chunk/PalettedContainer$CountConsumer;)V")) + private void addToOversizedBlockCount(PalettedContainer palettedContainer, PalettedContainer.CountConsumer consumer) { + palettedContainer.count((state, count) -> { + consumer.accept(state, count); + if (state.exceedsCube()) { + this.oversizedBlockCount += count; + } + }); + } + + @Inject(method = "calculateCounts", at = @At("HEAD")) + private void resetOversizedBlockCount(CallbackInfo ci) { + this.oversizedBlockCount = 0; + } + + @Inject(method = "setBlockState(IIILnet/minecraft/block/BlockState;Z)Lnet/minecraft/block/BlockState;", at = @At(ordinal = 0, value = "INVOKE", target = "Lnet/minecraft/block/BlockState;hasRandomTicks()Z", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD) + private void decrOversizedBlockCount(int x, int y, int z, BlockState state, boolean lock, CallbackInfoReturnable cir) { + if (state.exceedsCube()) { + --this.oversizedBlockCount; + } + } + + @Inject(method = "setBlockState(IIILnet/minecraft/block/BlockState;Z)Lnet/minecraft/block/BlockState;", at = @At(ordinal = 1, value = "INVOKE", target = "Lnet/minecraft/block/BlockState;hasRandomTicks()Z", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD) + private void incrOversizedBlockCount(int x, int y, int z, BlockState state, boolean lock, CallbackInfoReturnable cir) { + if (state.exceedsCube()) { + ++this.oversizedBlockCount; + } + } + + @Override + public boolean hasOversizedBlocks() { + return this.oversizedBlockCount > 0; + } +} diff --git a/src/main/resources/lithium.mixins.json b/src/main/resources/lithium.mixins.json index d8c5cc21c..d27ce7bd9 100644 --- a/src/main/resources/lithium.mixins.json +++ b/src/main/resources/lithium.mixins.json @@ -44,6 +44,7 @@ "block.piston_shapes.PistonHeadBlockMixin", "cached_hashcode.BlockNeighborGroupMixin", "chunk.no_locking.PalettedContainerMixin", + "chunk.oversized_blocks.MixinChunkSection", "chunk.palette.PalettedContainerMixin", "chunk.serialization.PackedIntegerArrayMixin", "chunk.serialization.PalettedContainerMixin",