From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: JellySquid Date: Thu, 20 Aug 2020 15:26:53 +0300 Subject: [PATCH] lithium collision optimizations Original code by JellySquid, licensed under LGPLv3 you can find the original code on https://github.com/jellysquid3/lithium-fabric/ (Yarn mappings) Co-authored-by: Ivan Pekov diff --git a/src/main/java/me/jellysquid/mods/lithium/common/entity/EntityClassGroup.java b/src/main/java/me/jellysquid/mods/lithium/common/entity/EntityClassGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..b400a5870b64b9eb92b6a8311793f5c8ea8df9af --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/entity/EntityClassGroup.java @@ -0,0 +1,101 @@ +package me.jellysquid.mods.lithium.common.entity; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import net.minecraft.server.Entity; + +/** + * Class for grouping Entity classes that meet some requirement for use in TypeFilterableList + * Designed to allow create groups of entity classes that are updated when mods add new entities that fit into the group. + * + * @author 2No2Name + */ +public class EntityClassGroup { + //Keep a set of classes that were already added to matching class groups, so we only analyse them once. + private static final Map, Object> knownEntityClasses = new ConcurrentHashMap<>(); //value unused, no set variant available + //Keep track of available class groups for updating them in case an entity class is instantiated for the first time + private static final List entityClassGroups = new ArrayList<>(); + + public static final EntityClassGroup COLLISION_BOX_OVERRIDE = new EntityClassGroup( + (entityClass) -> { + boolean overwritten; + while (entityClass != null && entityClass != Entity.class) { + try { + overwritten = true; + entityClass.getDeclaredMethod("aY"); + } catch (NoSuchMethodException e) { + overwritten = false; + entityClass = entityClass.getSuperclass(); + } + if (overwritten) { + return true; + } + } + return false; + } + ); + public static final EntityClassGroup HARD_COLLISION_BOX_OVERRIDE = new EntityClassGroup( + (entityClass) -> { + boolean overwritten; + while (entityClass != null && entityClass != Entity.class) { + try { + overwritten = true; + entityClass.getDeclaredMethod("j", Entity.class); + } catch (NoSuchMethodException e) { + overwritten = false; + entityClass = entityClass.getSuperclass(); + } + if (overwritten) + return true; + } + return false; + } + ); + + private final Map, Object> classGroup; //value unused, no set variant available + private final Function, Boolean> classFitEvaluator; + + public EntityClassGroup(Function, Boolean> classFitEvaluator) { + this.classGroup = new ConcurrentHashMap<>(); + EntityClassGroup.entityClassGroups.add(this); + this.classFitEvaluator = classFitEvaluator; + } + + public EntityClassGroup add(Class entityClass) { + this.classGroup.put(entityClass, entityClass); + return this; + } + + public boolean contains(Class entityClass) { + EntityClassGroup.analyseEntityClass(entityClass); + return this.classGroup.containsKey(entityClass); + } + + public Collection> getCollection() { + return this.classGroup.keySet(); + } + + public void addClassIfFitting(Class discoveredEntityClass) { + if (this.classGroup.containsKey(discoveredEntityClass)) { + return; + } + if (this.classFitEvaluator != null && this.classFitEvaluator.apply(discoveredEntityClass)) { + this.classGroup.put(discoveredEntityClass, discoveredEntityClass); + } + } + + public static void analyseEntityClass(Class entityClass) { + if (EntityClassGroup.knownEntityClasses.containsKey(entityClass)) { + return; + } + EntityClassGroup.knownEntityClasses.put(entityClass, entityClass); + + for (EntityClassGroup entityClassGroup : EntityClassGroup.entityClassGroups) { + entityClassGroup.addClassIfFitting(entityClass); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/lithium/common/entity/EntityClassGroupHelper.java b/src/main/java/me/jellysquid/mods/lithium/common/entity/EntityClassGroupHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..20f80ae80de91615ea02f0771f7c020c3e4c6d1e --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/entity/EntityClassGroupHelper.java @@ -0,0 +1,32 @@ +package me.jellysquid.mods.lithium.common.entity; + +import java.util.List; +import me.jellysquid.mods.lithium.common.world.WorldHelper; +import net.minecraft.server.AxisAlignedBB; +import net.minecraft.server.Entity; +import net.minecraft.server.IEntityAccess; +import net.minecraft.server.World; + +public class EntityClassGroupHelper { + + /** + * Partial [VanillaCopy] Classes overriding Entity.getHardCollisionBox(Entity other) or Entity.getCollisionBox() + * The returned entity list is only used to call getCollisionBox and getHardCollisionBox. As most entities return null + * for both of these methods, getting those is not necessary. This is why we only get entities when they overwrite + * getCollisionBox + * + * @param entityView the world + * @param selection the box the entities have to collide with + * @param entity the entity that is searching for the colliding entities + * @return list of entities with collision boxes + */ + public static List getEntitiesWithCollisionBoxForEntity(IEntityAccess entityView, AxisAlignedBB selection, Entity entity, boolean loadChunks) { + if (entity != null && EntityClassGroup.HARD_COLLISION_BOX_OVERRIDE.contains(entity.getClass()) || !(entityView instanceof World)) { + //use vanilla code when method_30949 (previously getHardCollisionBox(Entity other)) is overwritten, as every entity could be relevant as argument of getHardCollisionBox + return entityView.getEntities(entity, selection); + } else { + //only get entities that overwrite method_30948 (previously getCollisionBox) + return WorldHelper.getEntitiesOfClassGroup((World) entityView, entity, EntityClassGroup.COLLISION_BOX_OVERRIDE, selection, loadChunks); + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..f2e08aafd31b3adbbbcc96df6aea7a647fb8bdf8 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/entity/LithiumEntityCollisions.java @@ -0,0 +1,188 @@ +package me.jellysquid.mods.lithium.common.entity; + +import java.util.Iterator; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; +import me.jellysquid.mods.lithium.common.entity.movement.ChunkAwareBlockCollisionSweeper; +import me.jellysquid.mods.lithium.common.util.Producer; +import net.minecraft.server.AxisAlignedBB; +import net.minecraft.server.Entity; +import net.minecraft.server.ICollisionAccess; +import net.minecraft.server.IEntityAccess; +import net.minecraft.server.VoxelShape; +import net.minecraft.server.VoxelShapes; +import net.minecraft.server.WorldBorder; +import net.yatopia.server.EntityFilter; + +public class LithiumEntityCollisions { + public static final double EPSILON = 1.0E-7D; + + /** + * [VanillaCopy] CollisionView#getBlockCollisions(Entity, Box) + * This is a much, much faster implementation which uses simple collision testing against full-cube block shapes. + * Checks against the world border are replaced with our own optimized functions which do not go through the + * VoxelShape system. + */ + public static Stream getBlockCollisions(ICollisionAccess world, Entity entity, AxisAlignedBB box) { + if (isBoxEmpty(box)) { + return Stream.empty(); + } + + return Producer.asStream(getBlockCollisionProducer(world, entity, box)); + } + + public static void fillBlockCollisionsList(ICollisionAccess world, List filled, Entity entity, AxisAlignedBB box) { + if (isBoxEmpty(box)) { + return; + } + + Producer.fillList( + getBlockCollisionProducer(world, entity, box), + filled, + (voxelShape, axisAlignedBBS) -> VoxelShapes.addBoxesToIfIntersects(voxelShape, box, axisAlignedBBS) + ); + } + + public static Producer getBlockCollisionProducer(ICollisionAccess world, Entity entity, AxisAlignedBB box) { + final ChunkAwareBlockCollisionSweeper sweeper = new ChunkAwareBlockCollisionSweeper(world, entity, box); + + return consumer -> { + VoxelShape shape = sweeper.getNextCollidedShape(); + if (shape != null) { + consumer.accept(shape); + return true; + } + return false; + }; + } + + /** + * See {@link LithiumEntityCollisions#getBlockCollisions(ICollisionAccess, Entity, AxisAlignedBB)} + * + * @return True if the box (possibly that of an entity's) collided with any blocks + */ + public static boolean doesBoxCollideWithBlocks(ICollisionAccess world, Entity entity, AxisAlignedBB box) { + if (isBoxEmpty(box)) { + return false; + } + + final ChunkAwareBlockCollisionSweeper sweeper = new ChunkAwareBlockCollisionSweeper(world, entity, box); + + VoxelShape shape = sweeper.getNextCollidedShape(); + return shape != null; + } + + /** + * See {@link LithiumEntityCollisions#getEntityCollisions(IEntityAccess, Entity, AxisAlignedBB, Predicate)} + * + * @return True if the box (possibly that of an entity's) collided with any other entities + */ + public static boolean doesBoxCollideWithEntities(IEntityAccess view, Entity entity, AxisAlignedBB box, Predicate predicate) { + if (isBoxEmpty(box)) { + return false; + } + + return getEntityCollisionProducer(view, entity, box.grow(EPSILON), predicate, false).computeNext(null); + } + + /** + * Returns a stream of entity collision boxes. + */ + public static Stream getEntityCollisions(IEntityAccess view, Entity entity, AxisAlignedBB box, Predicate predicate) { + if (isBoxEmpty(box)) { + return Stream.empty(); + } + + return Producer.asStream(getEntityCollisionProducer(view, entity, box.grow(EPSILON), predicate, false)); + } + + public static void fillEntityCollisionsList(IEntityAccess view, Entity entity, List filled, AxisAlignedBB box, boolean loadChunks) { + if (isBoxEmpty(box)) { + return; + } + + Producer.fillList( + getEntityCollisionProducer(view, entity, box.grow(EPSILON), EntityFilter.getFilter(entity), loadChunks), + filled, + (voxelShape, axisAlignedBBS) -> VoxelShapes.addBoxesToIfIntersects(voxelShape, box, axisAlignedBBS) + ); + } + + /** + * [VanillaCopy] EntityView#getEntityCollisions + * Re-implements the function named above without stream code or unnecessary allocations. This can provide a small + * boost in some situations (such as heavy entity crowding) and reduces the allocation rate significantly. + */ + public static Producer getEntityCollisionProducer(IEntityAccess view, Entity entity, AxisAlignedBB box, Predicate predicate, boolean loadChunks) { + return new Producer() { + private Iterator it; + + @Override + public boolean computeNext(Consumer consumer) { + if (it == null) { + /* + * In case entity's class is overriding method_30949, all types of entities may be (=> are assumed to be) required. + * Otherwise only get entities that override method_30948 are required, as other entities cannot collide. + */ + this.it = EntityClassGroupHelper.getEntitiesWithCollisionBoxForEntity(view, box, entity, loadChunks).iterator(); + } + + while (this.it.hasNext()) { + Entity otherEntity = this.it.next(); + + if (!predicate.test(otherEntity)) { + continue; + } + + if (entity == null) { + if (!otherEntity.collisionBoxIsHard()) { + continue; + } + } else if (!entity.hardCollidesWith(otherEntity)) { + continue; + } + + if (consumer != null) { + consumer.accept(VoxelShapes.of(otherEntity.getBoundingBox())); + } + return true; + } + + return false; + } + }; + } + + /** + * This provides a faster check for seeing if an entity is within the world border as it avoids going through + * the slower shape system. + * + * @return True if the {@param box} is fully within the {@param border}, otherwise false. + */ + public static boolean isWithinWorldBorder(WorldBorder border, AxisAlignedBB box) { + double wboxMinX = Math.floor(border.getMinX()); + double wboxMinZ = Math.floor(border.getMinZ()); + + double wboxMaxX = Math.ceil(border.getMaxX()); + double wboxMaxZ = Math.ceil(border.getMaxZ()); + + return box.minX >= wboxMinX && box.minX < wboxMaxX && box.minZ >= wboxMinZ && box.minZ < wboxMaxZ && + box.maxX >= wboxMinX && box.maxX < wboxMaxX && box.maxZ >= wboxMinZ && box.maxZ < wboxMaxZ; + } + + public static boolean canEntityCollideWithWorldBorder(ICollisionAccess world, Entity entity) { + WorldBorder border = world.getWorldBorder(); + + boolean isInsideBorder = isWithinWorldBorder(border, entity.getBoundingBox().shrink(EPSILON)); + boolean isCrossingBorder = isWithinWorldBorder(border, entity.getBoundingBox().grow(EPSILON)); + + return !isInsideBorder && isCrossingBorder; + } + + private static boolean isBoxEmpty(AxisAlignedBB box) { + return box.getAverageSideLength() <= EPSILON; + } +} 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 0000000000000000000000000000000000000000..7ed343cfb3130446c85dab2ca04d60f91e2c94fb --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/entity/movement/ChunkAwareBlockCollisionSweeper.java @@ -0,0 +1,278 @@ +package me.jellysquid.mods.lithium.common.entity.movement; + +import me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions; +import net.minecraft.server.AxisAlignedBB; +import net.minecraft.server.BlockPosition; +import net.minecraft.server.Blocks; +import net.minecraft.server.Chunk; +import net.minecraft.server.ChunkSection; +import net.minecraft.server.Entity; +import net.minecraft.server.IBlockData; +import net.minecraft.server.IChunkAccess; +import net.minecraft.server.ICollisionAccess; +import net.minecraft.server.MathHelper; +import net.minecraft.server.OperatorBoolean; +import net.minecraft.server.VoxelShape; +import net.minecraft.server.VoxelShapeCollision; +import net.minecraft.server.VoxelShapes; + +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 BlockPosition.MutableBlockPosition pos = new BlockPosition.MutableBlockPosition(); + + /** + * The collision box being swept through the world. + */ + private final AxisAlignedBB box; + + /** + * The VoxelShape of the collision box being swept through the world. + */ + private final VoxelShape shape; + + private final ICollisionAccess view; + private final VoxelShapeCollision context; + + private final Entity entity; + + //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 IChunkAccess cachedChunk; + private ChunkSection cachedChunkSection; + private boolean needEntityCollisionCheck; + + public ChunkAwareBlockCollisionSweeper(ICollisionAccess view, Entity entity, AxisAlignedBB box) { + this.box = box; + this.shape = VoxelShapes.of(box); + this.context = entity == null ? VoxelShapeCollision.a() : VoxelShapeCollision.a(entity); + this.view = view; + this.entity = entity; + this.needEntityCollisionCheck = entity != null; + + 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.getSections()[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 = (IChunkAccess) this.view.c(this.chunkX, this.chunkZ); + if (this.cachedChunk != null) { + this.cachedChunkSection = this.cachedChunk.getSections()[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; + } + + public VoxelShape getNextCollidedShape() { + VoxelShape shape = null; + + if (this.needEntityCollisionCheck) { + shape = this.getNextEntityCollision(); + + this.needEntityCollisionCheck = false; + } + + if (shape == null) { + shape = this.getNextBlockCollision(); + } + + return shape; + } + + + /** + * 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 + */ + private VoxelShape getNextBlockCollision() { + while (true) { + if (this.cIterated >= this.cTotalSize) { + if (!this.nextSection()) { + break; + } + } + 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 + } + + //using < minX and > maxX instead of <= and >= in vanilla, because minX, maxX are the coordinates + //of the box that wasn't extended for oversized blocks yet. + final int edgesHit = this.sectionOversizedBlocks ? + (x < this.minX || x > this.maxX ? 1 : 0) + + (y < this.minY || y > this.maxY ? 1 : 0) + + (z < this.minZ || z > this.maxZ ? 1 : 0) : 0; + + if (edgesHit == 3) { + continue; + } + + final IBlockData state = this.cachedChunkSection.getType(x & 15, y & 15, z & 15); + + if (canInteractWithBlock(state, edgesHit)) { + this.pos.setValues(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; + } + } + } + } + + return null; + } + + private VoxelShape getNextEntityCollision() { + if (LithiumEntityCollisions.canEntityCollideWithWorldBorder(this.view, this.entity)) { + return this.view.getWorldBorder().asVoxelShape(); + } + + return null; + } + + /** + * 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(IBlockData state, int edgesHit) { + return (edgesHit != 1 || state.shapeExceedsCube()) && (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(AxisAlignedBB entityBox, VoxelShape entityShape, VoxelShape shape, int x, int y, int z) { + if (shape.intersects(entityBox)) { + return shape.offset(x, y, z); + } else { + shape = shape.offset(x, y, z); + + if (VoxelShapes.applyOperation(shape, entityShape, OperatorBoolean.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(IChunkAccess chunk, int chunkY) { + if (OVERSIZED_BLOCK_COUNTING_ENABLED) { + ChunkSection section = chunk.getSections()[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/common/util/Producer.java b/src/main/java/me/jellysquid/mods/lithium/common/util/Producer.java new file mode 100644 index 0000000000000000000000000000000000000000..f3224ea636fef95d368b934ed4b3e9060c4b10a2 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/util/Producer.java @@ -0,0 +1,55 @@ +package me.jellysquid.mods.lithium.common.util; + +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import net.yatopia.server.HoldingConsumer; + +public interface Producer { + /** + * Computes the next sequence of values in a collection. If a null value is passed for {@param consumer}, then + * the producer will only return whether or not elements existed. + * + * @param consumer The (nullable) consumer which will accept the computed values during this run. + * @return True if the producer produced any values, otherwise false + */ + boolean computeNext(Consumer consumer); + + default Producer map(Function mapper) { + return consumer -> { + Consumer con = (t) -> consumer.accept(mapper.apply(t)); + return Producer.this.computeNext(con); + }; + } + + static Stream asStream(Producer producer) { + return StreamSupport.stream(new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.ORDERED | Spliterator.NONNULL) { + @Override + public boolean tryAdvance(Consumer action) { + return producer.computeNext(action); + } + }, false); + } + + // WARNING: does not check contains, you have to do that in the add function + static void fillList(Producer producer, List list, BiConsumer> addFunction) { + HoldingConsumer consumer = new HoldingConsumer<>(); + while (producer.computeNext(consumer)) { + T value = consumer.getValue(); + if (value == null) { continue; } + addFunction.accept(value, list); + } + } + + Producer EMPTY_PRODUCER = consumer -> false; + + @SuppressWarnings("unchecked") + static Producer empty() { + return (Producer) EMPTY_PRODUCER; + } +} diff --git a/src/main/java/me/jellysquid/mods/lithium/common/world/WorldHelper.java b/src/main/java/me/jellysquid/mods/lithium/common/world/WorldHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..f858e5dcda60391fe869264f0c4a20bb14159e82 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/world/WorldHelper.java @@ -0,0 +1,59 @@ +package me.jellysquid.mods.lithium.common.world; + +import com.google.common.collect.Lists; +import java.util.List; +import me.jellysquid.mods.lithium.common.entity.EntityClassGroup; +import me.jellysquid.mods.lithium.common.world.chunk.ClassGroupFilterableList; +import net.minecraft.server.AxisAlignedBB; +import net.minecraft.server.Chunk; +import net.minecraft.server.Entity; +import net.minecraft.server.EntitySlice; +import net.minecraft.server.MathHelper; +import net.minecraft.server.World; + +public class WorldHelper { + + /** + * Method that allows getting entities of a class group. + * [VanillaCopy] but custom combination of: get class filtered entities together with excluding one entity + */ + public static List getEntitiesOfClassGroup(World world, Entity excluded, EntityClassGroup type, AxisAlignedBB box_1, boolean loadChunks) { + int int_1 = MathHelper.floor((box_1.minX - 2.0D) / 16.0D); + int int_2 = MathHelper.f((box_1.maxX + 2.0D) / 16.0D); + int int_3 = MathHelper.floor((box_1.minZ - 2.0D) / 16.0D); + int int_4 = MathHelper.f((box_1.maxZ + 2.0D) / 16.0D); + List list_1 = Lists.newArrayList(); + + for (int int_5 = int_1; int_5 < int_2; ++int_5) { + for (int int_6 = int_3; int_6 < int_4; ++int_6) { + Chunk worldChunk_1 = loadChunks ? world.getChunkAt(int_5, int_6) : world.getChunkIfLoaded(int_5, int_6); + if (worldChunk_1 != null) { + WorldHelper.getEntitiesOfClassGroup(worldChunk_1, excluded, type, box_1, list_1); + } + } + } + + return list_1; + } + + /** + * Method that allows getting entities of a class group. + * [VanillaCopy] but custom combination of: get class filtered entities together with excluding one entity + */ + public static void getEntitiesOfClassGroup(Chunk worldChunk, Entity excluded, EntityClassGroup type, AxisAlignedBB box_1, List list_1) { + EntitySlice[] entitySections = worldChunk.getAsSlices(); + int int_1 = MathHelper.floor((box_1.minY - 2.0D) / 16.0D); + int int_2 = MathHelper.floor((box_1.maxY + 2.0D) / 16.0D); + int_1 = MathHelper.clamp(int_1, 0, entitySections.length - 1); + int_2 = MathHelper.clamp(int_2, 0, entitySections.length - 1); + + for (int int_3 = int_1; int_3 <= int_2; ++int_3) { + //noinspection rawtypes + for (Object entity_1 : ((ClassGroupFilterableList) entitySections[int_3]).getAllOfGroupType(type)) { + if (entity_1 != excluded && ((Entity) entity_1).getBoundingBox().intersects(box_1)) { + list_1.add((Entity) entity_1); + } + } + } + } +} diff --git a/src/main/java/me/jellysquid/mods/lithium/common/world/chunk/ClassGroupFilterableList.java b/src/main/java/me/jellysquid/mods/lithium/common/world/chunk/ClassGroupFilterableList.java new file mode 100644 index 0000000000000000000000000000000000000000..493103d755c222f2c6031ffb8f5c0b7a2e3fde77 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/lithium/common/world/chunk/ClassGroupFilterableList.java @@ -0,0 +1,8 @@ +package me.jellysquid.mods.lithium.common.world.chunk; + +import java.util.Collection; +import me.jellysquid.mods.lithium.common.entity.EntityClassGroup; + +public interface ClassGroupFilterableList { + Collection getAllOfGroupType(EntityClassGroup type); +} diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java index ac6e5e3309affc830d4db07fd9b8d809c3085033..b7fcbef8d38c9406a891d64f8016bc16572f00d9 100644 --- a/src/main/java/net/minecraft/server/Chunk.java +++ b/src/main/java/net/minecraft/server/Chunk.java @@ -36,7 +36,7 @@ public class Chunk implements IChunkAccess { public final Map heightMap; private final ChunkConverter i; public final Map tileEntities; - public final List[] entitySlices; // Spigot + public final EntitySlice[] entitySlices; // Spigot // Yatopia - md_5 is dumb private final Map, StructureStart> l; private final Map, LongSet> m; private final ShortList[] n; @@ -149,7 +149,7 @@ public class Chunk implements IChunkAccess { this.l = Maps.newHashMap(); this.m = Maps.newHashMap(); this.n = new ShortList[16]; - this.entitySlices = (List[]) (new List[16]); // Spigot + this.entitySlices = new EntitySlice[16]; // Spigot // Yatopia - md_5 is stupid this.world = (WorldServer) world; // CraftBukkit - type this.loc = chunkcoordintpair; this.coordinateKey = MCUtil.getCoordinateKey(chunkcoordintpair); // Paper - cache coordinate key this.i = chunkconverter; @@ -165,7 +165,7 @@ public class Chunk implements IChunkAccess { } for (int l = 0; l < this.entitySlices.length; ++l) { - this.entitySlices[l] = new org.bukkit.craftbukkit.util.UnsafeList(); // Spigot + this.entitySlices[l] = new EntitySlice<>(Entity.class); // Spigot // Yatopia - md_5 is stupid } this.d = biomestorage; @@ -615,8 +615,8 @@ public class Chunk implements IChunkAccess { k = this.entitySlices.length - 1; } // Paper - remove from any old list if its in one - List nextSlice = this.entitySlices[k]; // the next list to be added to - List currentSlice = entity.entitySlice; + EntitySlice nextSlice = this.entitySlices[k]; // the next list to be added to // Yatopia - paper not stupid, md_5 is + EntitySlice currentSlice = entity.entitySlice; // Yatopia - paper not stupid, md_5 is if (nextSlice == currentSlice) { if (World.DEBUG_ENTITIES) MinecraftServer.LOGGER.warn("Entity was already in this chunk!" + entity, new Throwable()); return; // ??? silly plugins @@ -930,12 +930,12 @@ public class Chunk implements IChunkAccess { j = MathHelper.clamp(j, 0, this.entitySlices.length - 1); for (int k = i; k <= j; ++k) { - List entityslice = this.entitySlices[k]; // Spigot - List list1 = entityslice; // Spigot + EntitySlice entityslice = this.entitySlices[k]; // Spigot // Yatopia - md_5 is stupid + EntitySlice list1 = entityslice; // Spigot // Yatopia - md_5 is stupid int l = list1.size(); - for (int i1 = 0; i1 < l; ++i1) { - Entity entity1 = (Entity) list1.get(i1); + for (Entity entity1 : list1) { // Yatopia + //Entity entity1 = (Entity) list1.get(i1); // Yatopia if (entity1.shouldBeRemoved) continue; // Paper if (entity1.getBoundingBox().c(axisalignedbb) && entity1 != entity) { @@ -1067,8 +1067,16 @@ public class Chunk implements IChunkAccess { } public List[] getEntitySlices() { // Spigot - return this.entitySlices; + // Yatopia start + List[] ret = new List[entitySlices.length]; + for (int i = 0, len = entitySlices.length; i < len; i++) { + EntitySlice slice = entitySlices[i]; + ret[i] = slice.toList(); + } + return ret; } + public EntitySlice[] getAsSlices() { return entitySlices; } + // Yatopia end @Override public NBTTagCompound i(BlockPosition blockposition) { diff --git a/src/main/java/net/minecraft/server/Entity.java b/src/main/java/net/minecraft/server/Entity.java index 798ce0029c980d340d74dff53d702ef3cd00e33e..eec6dbda1b5a34122a75f11dfa82506f9763779c 100644 --- a/src/main/java/net/minecraft/server/Entity.java +++ b/src/main/java/net/minecraft/server/Entity.java @@ -73,7 +73,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke } } }; - List entitySlice = null; + EntitySlice entitySlice = null; // Yatopia - change to EntitySlice public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper end diff --git a/src/main/java/net/minecraft/server/EntitySlice.java b/src/main/java/net/minecraft/server/EntitySlice.java index 1250c3cbe915815939627701c153ba6254fc05f0..e0cbcc0a15f8089c29957badc20b0a786831c64b 100644 --- a/src/main/java/net/minecraft/server/EntitySlice.java +++ b/src/main/java/net/minecraft/server/EntitySlice.java @@ -14,18 +14,35 @@ import java.util.Map.Entry; import java.util.stream.Collectors; import java.util.stream.Stream; -public class EntitySlice extends AbstractCollection { +// Yatopia start +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import me.jellysquid.mods.lithium.common.entity.EntityClassGroup; +// Yatopia end + +public class EntitySlice extends AbstractCollection implements me.jellysquid.mods.lithium.common.world.chunk.ClassGroupFilterableList { // Yatopia private final Map, List> a = Maps.newHashMap(); private final Class b; private final List c = Lists.newArrayList(); + private Reference2ReferenceOpenHashMap> entitiesByGroup; // Yatopia + public EntitySlice(Class oclass) { this.b = oclass; this.a.put(oclass, this.c); + this.entitiesByGroup = new Reference2ReferenceOpenHashMap<>(); // Yatopia } public boolean add(T t0) { + // Yatopia start + for (Map.Entry> entityGroupAndSet : this.entitiesByGroup.entrySet()) { + EntityClassGroup entityGroup = entityGroupAndSet.getKey(); + if (entityGroup.contains(((Entity)t0).getClass())) { + entityGroupAndSet.getValue().add((t0)); + } + } + // Yatopia end boolean flag = false; Iterator iterator = this.a.entrySet().iterator(); @@ -41,6 +58,11 @@ public class EntitySlice extends AbstractCollection { } public boolean remove(Object object) { + // Yatopia start + for (Map.Entry> entityGroupAndSet : this.entitiesByGroup.entrySet()) { + entityGroupAndSet.getValue().remove(object); + } + // Yatopia end boolean flag = false; Iterator iterator = this.a.entrySet().iterator(); @@ -65,11 +87,16 @@ public class EntitySlice extends AbstractCollection { if (!this.b.isAssignableFrom(oclass)) { throw new IllegalArgumentException("Don't know how to search for " + oclass); } else { - List list = (List) this.a.computeIfAbsent(oclass, (oclass1) -> { - Stream stream = this.c.stream(); - - oclass1.getClass(); - return (List) stream.filter(oclass1::isInstance).collect(Collectors.toList()); + List list = (List) this.a.computeIfAbsent(oclass, (oclass1) -> { // Yatopia - decompile fix + // Yatopia start - how about we nuke stream? + List ret = Lists.newArrayList(); + for (T t : c) { + if (oclass1.isInstance(t)) { + ret.add(t); + } + } + return ret; + // Yatopia end }); return Collections.unmodifiableCollection(list); @@ -87,4 +114,38 @@ public class EntitySlice extends AbstractCollection { public int size() { return this.c.size(); } + + // Yatopia start + @Override + public Collection getAllOfGroupType(EntityClassGroup type) { + Collection collection = this.entitiesByGroup.get(type); + + if (collection == null) { + collection = this.createAllOfGroupType(type); + } + + return Collections.unmodifiableCollection(collection); + } + + private Collection createAllOfGroupType(EntityClassGroup type) { + ReferenceLinkedOpenHashSet allOfType = new ReferenceLinkedOpenHashSet<>(); + + for (T entity : this.c) { + if (type.contains(entity.getClass())) { + allOfType.add(entity); + } + } + this.entitiesByGroup.put(type, allOfType); + return allOfType; + } + + /** + * A workaround about md_5's dumb changes + * + * @return list representation of EntitySlice + */ + public List toList() { + return c; + } + // Yatopia end } diff --git a/src/main/java/net/minecraft/server/ICollisionAccess.java b/src/main/java/net/minecraft/server/ICollisionAccess.java index b66c802d5e27518069bf42e577bcc9a26c4d873e..22728e5ba8b2dd6efc3164d06ea791693de50936 100644 --- a/src/main/java/net/minecraft/server/ICollisionAccess.java +++ b/src/main/java/net/minecraft/server/ICollisionAccess.java @@ -52,7 +52,14 @@ public interface ICollisionAccess extends IBlockAccess { default boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { // Tuinity end - allow overriding in WorldServer try { if (entity != null) entity.collisionLoadChunks = true; // Paper - return this.d(entity, axisalignedbb, predicate).allMatch(VoxelShape::isEmpty); + // Yatopia start + //return this.d(entity, axisalignedbb, predicate).allMatch(VoxelShape::isEmpty); + boolean ret = !me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions.doesBoxCollideWithBlocks(this, entity, axisalignedbb); + if (ret && this instanceof IEntityAccess) { + ret = !me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions.doesBoxCollideWithEntities((IEntityAccess) this, entity, axisalignedbb, predicate); + } + return ret; + // Yatopia end } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper } @@ -63,7 +70,7 @@ public interface ICollisionAccess extends IBlockAccess { } default Stream b(@Nullable Entity entity, AxisAlignedBB axisalignedbb) { - return StreamSupport.stream(new VoxelShapeSpliterator(this, entity, axisalignedbb), false); + return me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions.getBlockCollisions(this, entity, axisalignedbb); // Yatopia } default Stream b(@Nullable Entity entity, AxisAlignedBB axisalignedbb, BiPredicate bipredicate) { diff --git a/src/main/java/net/minecraft/server/IEntityAccess.java b/src/main/java/net/minecraft/server/IEntityAccess.java index 882b82d8952d34f6e3c639404d1a1521dedf1bb0..ccf1416000354b78ccef78b072062ce081826e1a 100644 --- a/src/main/java/net/minecraft/server/IEntityAccess.java +++ b/src/main/java/net/minecraft/server/IEntityAccess.java @@ -62,6 +62,10 @@ public interface IEntityAccess { // Tuinity end - optimise hard collision default Stream c(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { + // Yatopia start - replace this + if (predicate == null) predicate = (e) -> true; // Tuinity - allow nullable + return me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions.getEntityCollisions(this, entity, axisalignedbb, predicate); + /* if (axisalignedbb.a() < 1.0E-7D) { return Stream.empty(); } else { @@ -91,6 +95,7 @@ public interface IEntityAccess { return flag; }); return ((entity != null && entity.hardCollides()) ? this.getEntities(entity, axisalignedbb1, predicate) : this.getHardCollidingEntities(entity, axisalignedbb1, predicate)).stream().map(Entity::getBoundingBox).map(VoxelShapes::a); // Tuinity - optimise entity hard collisions } + */ // Yatopia end } default EntityHuman findNearbyPlayer(Entity entity, double d0, @Nullable Predicate predicate) { return this.findNearbyPlayer(entity.locX(), entity.locY(), entity.locZ(), d0, predicate); } // Paper diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java index c3eb85c71539ebdb1b6c9a386e4de9ba717f030e..9f32a26fdbfaf024cfe5c0996c2253f2dd581d5e 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -1343,11 +1343,11 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { chunk.setLoaded(true); this.world.a(chunk.getTileEntities().values()); List list = null; - List[] aentityslice = chunk.getEntitySlices(); // Spigot + EntitySlice[] aentityslice = chunk.getAsSlices(); // Spigot // Yatopia - md_5 is stupid int i = aentityslice.length; for (int j = 0; j < i; ++j) { - List entityslice = aentityslice[j]; // Spigot + EntitySlice entityslice = aentityslice[j]; // Spigot // Yatopia - md_5 is stupid Iterator iterator = entityslice.iterator(); while (iterator.hasNext()) { @@ -1640,7 +1640,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { // CraftBukkit - decompile error csvwriter.a(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(IChunkAccess::getChunkStatus).orElse(null), optional1.map(Chunk::getState).orElse(null), a(playerchunk.c()), a(playerchunk.a()), a(playerchunk.b()), this.chunkDistanceManager.c(entry.getLongKey()), !this.isOutsideOfRange(chunkcoordintpair), optional1.map((chunk) -> { int sum = 0; - for (List entities : chunk.getEntitySlices()) { + for (EntitySlice entities : chunk.getAsSlices()) { // Yatopia - this was caused due to md_5 being stupid int size = entities.size(); sum += size; } diff --git a/src/main/java/net/minecraft/server/VoxelShapes.java b/src/main/java/net/minecraft/server/VoxelShapes.java index f9a1f5e92a7559a50dfb72d7455a8fc03dbad25f..3094ce00b3fa5b266f5d0ad0875f160e80c62e18 100644 --- a/src/main/java/net/minecraft/server/VoxelShapes.java +++ b/src/main/java/net/minecraft/server/VoxelShapes.java @@ -40,7 +40,9 @@ public final class VoxelShapes { if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { com.tuinity.tuinity.voxel.AABBVoxelShape shapeCasted = (com.tuinity.tuinity.voxel.AABBVoxelShape)shape; if (shapeCasted.aabb.voxelShapeIntersect(aabb)) { + if (!list.contains(shapeCasted.aabb)) { // Yatopia start - make sure it doesn't contain already list.add(shapeCasted.aabb); + } // Yatopia end } } else if (shape instanceof VoxelShapeArray) { VoxelShapeArray shapeCasted = (VoxelShapeArray)shape; diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java index e86d439a7a340e31071c5b2f6531abd1537db06d..00930eeea75758645b173f4bc2fbf635e1d426ba 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java @@ -649,8 +649,14 @@ public class WorldServer extends World implements GeneratorAccessSeed { } public final void getCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List list, boolean loadChunks) { + // Yatopia start - jellysquid is better + /* this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, list, loadChunks); this.getEntityHardCollisions(entity, axisalignedbb, null, list); + */ + me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions.fillEntityCollisionsList(this, entity, list, axisalignedbb, loadChunks); + me.jellysquid.mods.lithium.common.entity.LithiumEntityCollisions.fillBlockCollisionsList(this, list, entity, axisalignedbb); + // Yatopia end } @Override @@ -1743,12 +1749,12 @@ public class WorldServer extends World implements GeneratorAccessSeed { } // Spigot End this.tileEntityListUnload.addAll(chunk.getTileEntities().values()); - List[] aentityslice = chunk.getEntitySlices(); // Spigot + EntitySlice[] aentityslice = chunk.getAsSlices(); // Spigot // Yatopia - md_5 is stupid int i = aentityslice.length; java.util.List toMoveChunks = new java.util.ArrayList<>(); // Paper for (int j = 0; j < i; ++j) { - List entityslice = aentityslice[j]; // Spigot + EntitySlice entityslice = aentityslice[j]; // Spigot // Yatopia - md_5 is stupid Iterator iterator = entityslice.iterator(); while (iterator.hasNext()) { diff --git a/src/main/java/net/yatopia/server/HoldingConsumer.java b/src/main/java/net/yatopia/server/HoldingConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..44f39064fa52b8249887f83494b71699d653c4f8 --- /dev/null +++ b/src/main/java/net/yatopia/server/HoldingConsumer.java @@ -0,0 +1,17 @@ +package net.yatopia.server; + +import java.util.function.Consumer; + +public final class HoldingConsumer implements Consumer { + + private T value; + + @Override + public void accept(T t) { + this.value = t; + } + + public T getValue() { + return value; + } +}