From f2ddf33312008171f23dd4da2b093da5672afbc5 Mon Sep 17 00:00:00 2001 From: Etil <81570777+etil2jz@users.noreply.github.com> Date: Tue, 21 Sep 2021 18:13:30 +0200 Subject: [PATCH] (Airplane-temporary) Collision cache To drop when merged into Airplane's main branch --- ...0-Airplane-temporary-Collision-cache.patch | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 patches/server/0040-Airplane-temporary-Collision-cache.patch diff --git a/patches/server/0040-Airplane-temporary-Collision-cache.patch b/patches/server/0040-Airplane-temporary-Collision-cache.patch new file mode 100644 index 0000000..00f4e85 --- /dev/null +++ b/patches/server/0040-Airplane-temporary-Collision-cache.patch @@ -0,0 +1,522 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Etil <81570777+etil2jz@users.noreply.github.com> +Date: Tue, 21 Sep 2021 18:11:59 +0200 +Subject: [PATCH] (Airplane-temporary) Collision cache + + +diff --git a/src/main/java/gg/airplane/entity/CollisionCache.java b/src/main/java/gg/airplane/entity/CollisionCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..db38d58016965daee8003b98202c32080c369688 +--- /dev/null ++++ b/src/main/java/gg/airplane/entity/CollisionCache.java +@@ -0,0 +1,244 @@ ++package gg.airplane.entity; ++ ++import io.papermc.paper.util.CollisionUtil; ++import io.papermc.paper.util.WorldUtil; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.CollisionGetter; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.shapes.CollisionContext; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import org.bukkit.craftbukkit.util.UnsafeList; ++import org.jetbrains.annotations.NotNull; ++import org.simpleyaml.utils.Validate; ++ ++import java.util.HashSet; ++import java.util.List; ++import java.util.Set; ++import java.util.function.BiPredicate; ++ ++public class CollisionCache { ++ ++ private static record BlockEntry(int x, int y, int z, BlockState state, VoxelShape shape){} ++ ++ @NotNull ++ private final Entity entity; ++ private final UnsafeList blocks = new UnsafeList<>(); ++ private Set chunkToList = new HashSet<>(); ++ ++ private boolean dirty = true; ++ ++ private int previousMinBlockX; ++ private int previousMaxBlockX; ++ private int previousMinBlockY; ++ private int previousMaxBlockY; ++ private int previousMinBlockZ; ++ private int previousMaxBlockZ; ++ ++ public CollisionCache(@NotNull Entity entity) { ++ this.entity = entity; ++ } ++ ++ public int getId() { ++ return this.entity.getId(); ++ } ++ ++ public void dirtySection(@NotNull SectionPos sectionPos) { ++ this.dirty = true; ++ } ++ ++ public void onRemove() { ++ this.blocks.setSize(0); ++ ++ for (CollisionCacheList collisionCaches : this.chunkToList) { ++ collisionCaches.remove(this); ++ } ++ this.chunkToList.clear(); ++ this.dirty = false; ++ } ++ ++ public boolean getCollisions(final CollisionGetter view, AABB aabb, List into, boolean collidesWithUnloaded, boolean checkOnly, BiPredicate predicate) { ++ boolean ret = false; ++ ++ int minBlockX = Mth.floor(aabb.minX - CollisionUtil.COLLISION_EPSILON) - 1; ++ int maxBlockX = Mth.floor(aabb.maxX + CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ int minBlockY = Mth.floor(aabb.minY - CollisionUtil.COLLISION_EPSILON) - 1; ++ int maxBlockY = Mth.floor(aabb.maxY + CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ int minBlockZ = Mth.floor(aabb.minZ - CollisionUtil.COLLISION_EPSILON) - 1; ++ int maxBlockZ = Mth.floor(aabb.maxZ + CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ // if nothing changed and the location didn't move out of our area, use previous set ++ if (!this.dirty && minBlockX >= this.previousMinBlockX && maxBlockX <= this.previousMaxBlockX && ++ minBlockY >= this.previousMinBlockY && maxBlockY <= this.previousMaxBlockY && ++ minBlockZ >= this.previousMinBlockZ && maxBlockZ <= this.previousMaxBlockZ) { ++ if (checkOnly) { ++ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ for (int i = 0, length = this.blocks.size(); i < length; i++) { ++ BlockEntry entry = this.blocks.unsafeGet(i); ++ if (entry.shape.intersects(aabb) && predicate.test(entry.state, pos.set(entry.x, entry.y, entry.z))) { ++ return true; ++ } ++ } ++ } else { ++ for (int i = 0, length = this.blocks.size(); i < length; i++) { ++ ret |= CollisionUtil.addBoxesToIfIntersects(this.blocks.unsafeGet(i).shape, aabb, into); ++ } ++ } ++ ++ return ret; ++ } else if (checkOnly) { ++ // tl;dr this is only used by inWall right now, and we don't want to generate a cache for inWall because it'll always be smaller than a move cache anyways ++ return CollisionUtil.getCollisionsForBlocksOrWorldBorder(view, this.entity, aabb, into, false, collidesWithUnloaded, false, checkOnly, predicate); ++ } ++ ++ Validate.isTrue(predicate == null, "predicate cannot be used without checkOnly"); ++ ++ this.previousMinBlockX = minBlockX; ++ this.previousMaxBlockX = maxBlockX; ++ ++ this.previousMinBlockY = minBlockY; ++ this.previousMaxBlockY = maxBlockY; ++ ++ this.previousMinBlockZ = minBlockZ; ++ this.previousMaxBlockZ = maxBlockZ; ++ ++ // remove old shapes, since we missed cache ++ this.blocks.setSize(0); ++ this.dirty = false; ++ ++ final int minSection = WorldUtil.getMinSection(this.entity.level); ++ final int maxSection = WorldUtil.getMaxSection(this.entity.level); ++ final int minBlock = minSection << 4; ++ final int maxBlock = (maxSection << 4) | 15; ++ ++ BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ CollisionContext collisionShape = null; ++ ++ // special cases: ++ if (minBlockY > maxBlock || maxBlockY < minBlock) { ++ // no point in checking ++ if (!this.chunkToList.isEmpty()) { ++ for (CollisionCacheList collisionCaches : this.chunkToList) { ++ collisionCaches.remove(this); ++ } ++ this.chunkToList.clear(); ++ } ++ return ret; ++ } ++ ++ int minYIterate = Math.max(minBlock, minBlockY); ++ int maxYIterate = Math.min(maxBlock, maxBlockY); ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ ServerChunkCache chunkProvider = (ServerChunkCache) this.entity.level.getChunkSource(); ++ ++ Set cacheLists = new HashSet<>(this.blocks.size()); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk ++ int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk ++ ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk ++ int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk ++ ++ int chunkXGlobalPos = currChunkX << 4; ++ int chunkZGlobalPos = currChunkZ << 4; ++ ++ LevelChunk chunk = chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); ++ ++ if (chunk == null) { ++ if (collidesWithUnloaded) { ++ into.add(CollisionUtil.getBoxForChunk(currChunkX, currChunkZ)); ++ ret = true; ++ } ++ continue; ++ } ++ ++ LevelChunkSection[] sections = chunk.getSections(); ++ ++ // bound y ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ int sectionIndex = SectionPos.blockToSectionCoord(currY) - minSection; ++ ++ CollisionCacheList cacheList = chunk.collisionCaches[sectionIndex]; ++ if (cacheLists.add(cacheList)) { ++ cacheList.add(this); ++ } ++ ++ LevelChunkSection section = sections[sectionIndex]; ++ if (section == null || section.isEmpty()) { ++ // empty ++ // skip to next section ++ currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one ++ continue; ++ } ++ ++ net.minecraft.world.level.chunk.PalettedContainer blocks = section.states; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8); ++ int blockX = currX | chunkXGlobalPos; ++ int blockY = currY; ++ int blockZ = currZ | chunkZGlobalPos; ++ ++ int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + ++ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + ++ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0); ++ if (edgeCount == 3) { ++ continue; ++ } ++ ++ BlockState blockData = blocks.get(localBlockIndex); ++ if (blockData.isAir()) { ++ continue; ++ } ++ ++ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { ++ mutablePos.set(blockX, blockY, blockZ); ++ if (collisionShape == null) { ++ collisionShape = new CollisionUtil.LazyEntityCollisionContext(entity); ++ } ++ VoxelShape voxelshape2 = blockData.getCollisionShape(this.entity.level, mutablePos, collisionShape); ++ if (voxelshape2 != Shapes.empty()) { ++ VoxelShape voxelshape3 = voxelshape2.move((double) blockX, (double) blockY, (double) blockZ); ++ ++ this.blocks.add(new BlockEntry(blockX, blockY, blockZ, blockData, voxelshape3)); ++ ++ ret |= CollisionUtil.addBoxesToIfIntersects(voxelshape3, aabb, into); ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ for (CollisionCacheList cache : this.chunkToList) { ++ if (!cacheLists.contains(cache)) { ++ cache.remove(this); ++ } ++ } ++ this.chunkToList = cacheLists; ++ ++ return ret; ++ } ++ ++} +\ No newline at end of file +diff --git a/src/main/java/gg/airplane/entity/CollisionCacheList.java b/src/main/java/gg/airplane/entity/CollisionCacheList.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3e22d2fc190995fd536338bcacda1b054a555801 +--- /dev/null ++++ b/src/main/java/gg/airplane/entity/CollisionCacheList.java +@@ -0,0 +1,128 @@ ++package gg.airplane.entity; ++ ++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; ++ ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.NoSuchElementException; ++ ++/** ++ * @see com.destroystokyo.paper.util.maplist.EntityList ++ */ ++public class CollisionCacheList implements Iterable { ++ ++ protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f); ++ ++ { ++ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE); ++ } ++ ++ protected static final CollisionCache[] EMPTY_LIST = new CollisionCache[0]; ++ ++ protected CollisionCache[] entities = EMPTY_LIST; ++ protected int count; ++ ++ public int size() { ++ return this.count; ++ } ++ ++ public boolean contains(final CollisionCache entity) { ++ return this.entityToIndex.containsKey(entity.getId()); ++ } ++ ++ public boolean remove(final CollisionCache entity) { ++ final int index = this.entityToIndex.remove(entity.getId()); ++ if (index == Integer.MIN_VALUE) { ++ return false; ++ } ++ ++ // move the entity at the end to this index ++ final int endIndex = --this.count; ++ final CollisionCache end = this.entities[endIndex]; ++ if (index != endIndex) { ++ // not empty after this call ++ this.entityToIndex.put(end.getId(), index); // update index ++ } ++ this.entities[index] = end; ++ this.entities[endIndex] = null; ++ ++ return true; ++ } ++ ++ public boolean add(final CollisionCache entity) { ++ final int count = this.count; ++ final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count); ++ ++ if (currIndex != Integer.MIN_VALUE) { ++ return false; // already in this list ++ } ++ ++ CollisionCache[] list = this.entities; ++ ++ if (list.length == count) { ++ // resize required ++ list = this.entities = Arrays.copyOf(list, (int) Math.max(4L, count * 2L)); // overflow results in negative ++ } ++ ++ list[count] = entity; ++ this.count = count + 1; ++ ++ return true; ++ } ++ ++ public CollisionCache getChecked(final int index) { ++ if (index < 0 || index >= this.count) { ++ throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count); ++ } ++ return this.entities[index]; ++ } ++ ++ public CollisionCache getUnchecked(final int index) { ++ return this.entities[index]; ++ } ++ ++ public CollisionCache[] getRawData() { ++ return this.entities; ++ } ++ ++ public void clear() { ++ this.entityToIndex.clear(); ++ Arrays.fill(this.entities, 0, this.count, null); ++ this.count = 0; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ return new Iterator() { ++ ++ CollisionCache lastRet; ++ int current; ++ ++ @Override ++ public boolean hasNext() { ++ return this.current < CollisionCacheList.this.count; ++ } ++ ++ @Override ++ public CollisionCache next() { ++ if (this.current >= CollisionCacheList.this.count) { ++ throw new NoSuchElementException(); ++ } ++ return this.lastRet = CollisionCacheList.this.entities[this.current++]; ++ } ++ ++ @Override ++ public void remove() { ++ final CollisionCache lastRet = this.lastRet; ++ ++ if (lastRet == null) { ++ throw new IllegalStateException(); ++ } ++ this.lastRet = null; ++ ++ CollisionCacheList.this.remove(lastRet); ++ --this.current; ++ } ++ }; ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java +index 98ca1199a823cdf55b913396ce0a24554e85f116..16952b7303f02373b4c9bd0ffe613d517598dab4 100644 +--- a/src/main/java/io/papermc/paper/util/CollisionUtil.java ++++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java +@@ -546,6 +546,13 @@ public final class CollisionUtil { + + return ret; + } ++ ++ public static boolean getEntityCollisionsWithCache(final net.minecraft.world.level.Level getter, Entity entity, AABB aabb, List into, ++ final boolean loadChunks, final boolean collidesWithUnloaded, ++ final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) { ++ return entity.collisionCache.getCollisions(getter, aabb, into, collidesWithUnloaded, checkOnly, predicate) || ++ getEntityHardCollisions(getter, entity, aabb, into, false, null); ++ } + + public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb, + final List into, final boolean checkOnly, final Predicate predicate) { +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 3c272e6c7552cb5bc875cdaefc91aaac3751ec3a..c6d5451b326a34b4df638156b1cb5069ebeb7b59 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -341,6 +341,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + // Airplane start + public int activatedPriority = gg.airplane.AirplaneConfig.maximumActivationPrio; // golf score + public final BlockPos.MutableBlockPos cachedBlockPos = new BlockPos.MutableBlockPos(); // used where needed ++ public final gg.airplane.entity.CollisionCache collisionCache = new gg.airplane.entity.CollisionCache(this); + // Airplane end + + public float getBukkitYaw() { +@@ -1290,8 +1291,10 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + } + } + +- io.papermc.paper.util.CollisionUtil.getCollisions(world, this, collisionBox, potentialCollisions, false, true, +- false, false, null, null); ++ // Airplane start - use collision cache ++ io.papermc.paper.util.CollisionUtil.getEntityCollisionsWithCache(world, this, collisionBox, potentialCollisions, false, true, ++ false, false, null); ++ // Airplane end + + if (io.papermc.paper.util.CollisionUtil.isCollidingWithBorderEdge(world.getWorldBorder(), collisionBox)) { + io.papermc.paper.util.CollisionUtil.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions); +@@ -2462,7 +2465,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); + + // Paper start +- return io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this.level, this, axisalignedbb, null, ++ return io.papermc.paper.util.CollisionUtil.getEntityCollisionsWithCache(this.level, this, axisalignedbb, null, // Airplane - use cache + false, false, false, true, this.level.isAlmostSuffocating); // Airplane - don't allocate lambda here + // Paper end + } +@@ -3831,7 +3834,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + } + + public boolean updateFluidHeightAndDoFluidPushing(Tag tag, double d0) { +- if (this.touchingUnloadedChunk()) { ++ if (false && this.touchingUnloadedChunk()) { // Airplane - cost of a lookup here is the same cost as below, so skip + return false; + } else { + AABB axisalignedbb = this.getBoundingBox().deflate(0.001D); +@@ -3881,7 +3884,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n + + net.minecraft.world.level.chunk.ChunkAccess chunk = this.level.getChunkIfLoadedImmediately(currChunkX, currChunkZ); + if (chunk == null) { +- continue; ++ return false; // if we're touching an unloaded chunk then it's false + } + + net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index e265980f07d95a7912bf8873819033e51ef04c98..1174d94a032c1158e5d9a8be3beff1ba6f0d196b 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -185,6 +185,7 @@ public class LevelChunk implements ChunkAccess { + // Airplane end + + private final int levelHeight; private final int minBuildHeight; // Airplane ++ public final gg.airplane.entity.CollisionCacheList[] collisionCaches; // Airplane + public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes) { + this(world, pos, biomes, UpgradeData.EMPTY, EmptyTickList.empty(), EmptyTickList.empty(), 0L, (LevelChunkSection[]) null, (Consumer) null); + } +@@ -223,6 +224,12 @@ public class LevelChunk implements ChunkAccess { + this.inhabitedTime = inhabitedTime; + this.postLoad = loadToWorldConsumer; + this.sections = new LevelChunkSection[world.getSectionsCount()]; ++ // Airplane start ++ this.collisionCaches = new gg.airplane.entity.CollisionCacheList[world.getSectionsCount()]; ++ for (int i = 0; i < this.collisionCaches.length; i++) { ++ this.collisionCaches[i] = new gg.airplane.entity.CollisionCacheList(); ++ } ++ // Airplane end + if (sections != null) { + if (this.sections.length == sections.length) { + System.arraycopy(sections, 0, this.sections, 0, this.sections.length); +@@ -669,6 +676,17 @@ public class LevelChunk implements ChunkAccess { + int l = i & 15; + int i1 = blockposition.getZ() & 15; + BlockState iblockdata1 = chunksection.setBlockState(k, l, i1, iblockdata); ++ ++ // Airplane start - notify dirty ++ SectionPos pos = SectionPos.of(this.chunkPos, j); ++ gg.airplane.entity.CollisionCache[] caches = this.collisionCaches[j].getRawData(); ++ for (int index = 0; index < caches.length; index++) { ++ gg.airplane.entity.CollisionCache cache = caches[index]; ++ if (cache != null) { ++ cache.dirtySection(pos); ++ } ++ } ++ // Airplane end + + if (iblockdata1 == iblockdata) { + return null; +diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +index 1ed6573e0ca6b353d1de3b4486e199a5db9aa447..840ec9dabb787cea2bcd9b46f08443512d931db8 100644 +--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java ++++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +@@ -606,6 +606,12 @@ public class PersistentEntitySectionManager implements A + PersistentEntitySectionManager.this.knownUuids.remove(this.entity.getUUID()); + this.entity.setLevelCallback(PersistentEntitySectionManager.Callback.NULL); + PersistentEntitySectionManager.this.removeSectionIfEmpty(this.currentSectionKey, this.currentSection); ++ ++ // Airplane start ++ if (this.entity instanceof Entity realEntity) { ++ realEntity.collisionCache.onRemove(); ++ } ++ // Airplane end + } + } + }