From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Thu, 17 Jun 2021 19:55:02 -0700 Subject: [PATCH] Rewrite entity bounding box lookup calls For whatever reason, Mojang thought it was OK to make this system scale logn relative to the number of entity sections loaded. On top of that, they do a hashtable lookup per section - before this was just a basic array access. This patch brings back entity slices for lookup only. diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java new file mode 100644 index 0000000000000000000000000000000000000000..47b5f75d9f27cf3ab947fd1f69cbd609fb9f2749 --- /dev/null +++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java @@ -0,0 +1,500 @@ +package io.papermc.paper.world; + +import com.destroystokyo.paper.util.maplist.EntityList; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.phys.AABB; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +public final class ChunkEntitySlices { + + protected final int minSection; + protected final int maxSection; + protected final int chunkX; + protected final int chunkZ; + protected final ServerLevel world; + + protected final EntityCollectionBySection allEntities; + protected final EntityCollectionBySection hardCollidingEntities; + protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; + protected final EntityList entities = new EntityList(); + + public ChunkHolder.FullChunkStatus status; + + // TODO implement container search optimisations + + public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkHolder.FullChunkStatus status, + final int minSection, final int maxSection) { // inclusive, inclusive + this.minSection = minSection; + this.maxSection = maxSection; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.world = world; + + this.allEntities = new EntityCollectionBySection(this); + this.hardCollidingEntities = new EntityCollectionBySection(this); + this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); + + this.status = status; + } + + // Paper start - optimise CraftChunk#getEntities + public org.bukkit.entity.Entity[] getChunkEntities() { + List ret = new java.util.ArrayList<>(); + final Entity[] entities = this.entities.getRawData(); + for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { + final Entity entity = entities[i]; + if (entity == null) { + continue; + } + final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); + if (bukkit != null && bukkit.isValid()) { + ret.add(bukkit); + } + } + + return ret.toArray(new org.bukkit.entity.Entity[0]); + } + // Paper end - optimise CraftChunk#getEntities + + public boolean isEmpty() { + return this.entities.size() == 0; + } + + private void updateTicketLevels() { + final Entity[] entities = this.entities.getRawData(); + for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { + final Entity entity = entities[i]; + entity.chunkStatus = this.status; + } + } + + public synchronized void updateStatus(final ChunkHolder.FullChunkStatus status) { + this.status = status; + this.updateTicketLevels(); + } + + public synchronized void addEntity(final Entity entity, final int chunkSection) { + if (!this.entities.add(entity)) { + return; + } + entity.chunkStatus = this.status; + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.addEntity(entity, sectionIndex); + + if (entity.hardCollides()) { + this.hardCollidingEntities.addEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().addEntity(entity, sectionIndex); + } + } + } + + public synchronized void removeEntity(final Entity entity, final int chunkSection) { + if (!this.entities.remove(entity)) { + return; + } + entity.chunkStatus = ChunkHolder.FullChunkStatus.INACCESSIBLE; + final int sectionIndex = chunkSection - this.minSection; + + this.allEntities.removeEntity(entity, sectionIndex); + + if (entity.hardCollides()) { + this.hardCollidingEntities.removeEntity(entity, sectionIndex); + } + + for (final Iterator, EntityCollectionBySection>> iterator = + this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); + + if (entry.getKey().isInstance(entity)) { + entry.getValue().removeEntity(entity, sectionIndex); + } + } + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.hardCollidingEntities.getEntities(except, box, into, predicate); + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate); + } + + protected EntityCollectionBySection initClass(final Class clazz) { + final EntityCollectionBySection ret = new EntityCollectionBySection(this); + + for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { + final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; + if (sectionEntities == null) { + continue; + } + + final Entity[] storage = sectionEntities.storage; + + for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (clazz.isInstance(entity)) { + ret.addEntity(entity, sectionIndex); + } + } + } + + return ret; + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + EntityCollectionBySection collection = this.entitiesByClass.get(clazz); + if (collection != null) { + collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); + } else { + synchronized (this) { + this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz)); + } + collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); + } + } + + public synchronized void updateEntity(final Entity entity) { + /*// TODO + if (prev aabb != entity.getBoundingBox()) { + this.entityMap.delete(entity, prev aabb); + this.entityMap.insert(entity, prev aabb = entity.getBoundingBox()); + }*/ + } + + protected static final class BasicEntityList { + + protected static final Entity[] EMPTY = new Entity[0]; + protected static final int DEFAULT_CAPACITY = 4; + + protected E[] storage; + protected int size; + + public BasicEntityList() { + this(0); + } + + public BasicEntityList(final int cap) { + this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); + } + + public boolean isEmpty() { + return this.size == 0; + } + + public int size() { + return this.size; + } + + private void resize() { + if (this.storage == EMPTY) { + this.storage = (E[])new Entity[DEFAULT_CAPACITY]; + } else { + this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); + } + } + + public void add(final E entity) { + final int idx = this.size++; + if (idx >= this.storage.length) { + this.resize(); + this.storage[idx] = entity; + } else { + this.storage[idx] = entity; + } + } + + public int indexOf(final E entity) { + final E[] storage = this.storage; + + for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { + if (storage[i] == entity) { + return i; + } + } + + return -1; + } + + public boolean remove(final E entity) { + final int idx = this.indexOf(entity); + if (idx == -1) { + return false; + } + + final int size = --this.size; + final E[] storage = this.storage; + if (idx != size) { + System.arraycopy(storage, idx + 1, storage, idx, size - idx); + } + + storage[size] = null; + + return true; + } + + public boolean has(final E entity) { + return this.indexOf(entity) != -1; + } + } + + protected static final class EntityCollectionBySection { + + protected final ChunkEntitySlices manager; + protected final long[] nonEmptyBitset; + protected final BasicEntityList[] entitiesBySection; + protected int count; + + public EntityCollectionBySection(final ChunkEntitySlices manager) { + this.manager = manager; + + final int sectionCount = manager.maxSection - manager.minSection + 1; + + this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE + this.entitiesBySection = new BasicEntityList[sectionCount]; + } + + public void addEntity(final Entity entity, final int sectionIndex) { + BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list != null && list.has(entity)) { + return; + } + + if (list == null) { + this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); + this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); + } + + list.add(entity); + ++this.count; + } + + public void removeEntity(final Entity entity, final int sectionIndex) { + final BasicEntityList list = this.entitiesBySection[sectionIndex]; + + if (list == null || !list.remove(entity)) { + return; + } + + --this.count; + + if (list.isEmpty()) { + this.entitiesBySection[sectionIndex] = null; + this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); + } + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(entity)) { + continue; + } + + into.add(entity); + } + } + } + + public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { + if (part == except || !part.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + } + } + } + } + } + + public void getEntitiesWithEnderDragonParts(final Entity except, final Class clazz, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate == null || predicate.test(entity)) { + into.add(entity); + } // else: continue to test the ender dragon parts + + if (entity instanceof EnderDragon) { + for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { + if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { + continue; + } + + if (predicate != null && !predicate.test(part)) { + continue; + } + + into.add(part); + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + if (this.count == 0) { + return; + } + + final int minSection = this.manager.minSection; + final int maxSection = this.manager.maxSection; + + final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); + final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); + + // TODO use the bitset + + final BasicEntityList[] entitiesBySection = this.entitiesBySection; + + for (int section = min; section <= max; ++section) { + final BasicEntityList list = entitiesBySection[section - minSection]; + + if (list == null) { + continue; + } + + final Entity[] storage = list.storage; + + for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { + final Entity entity = storage[i]; + + if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) { + continue; + } + + if (predicate != null && !predicate.test((T)entity)) { + continue; + } + + into.add((T)entity); + } + } + } + } +} diff --git a/src/main/java/io/papermc/paper/world/EntitySliceManager.java b/src/main/java/io/papermc/paper/world/EntitySliceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3ba094e640d7fe7803e2bbdab8ff3beb6f50e8a0 --- /dev/null +++ b/src/main/java/io/papermc/paper/world/EntitySliceManager.java @@ -0,0 +1,391 @@ +package io.papermc.paper.world; + +import io.papermc.paper.util.CoordinateUtils; +import io.papermc.paper.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.phys.AABB; +import java.util.List; +import java.util.concurrent.locks.StampedLock; +import java.util.function.Predicate; + +public final class EntitySliceManager { + + protected static final int REGION_SHIFT = 5; + protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; + protected static final int REGION_SIZE = 1 << REGION_SHIFT; + + public final ServerLevel world; + + private final StampedLock stateLock = new StampedLock(); + protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(64, 0.7f); + + private final int minSection; // inclusive + private final int maxSection; // inclusive + + protected final Long2ObjectOpenHashMap statusMap = new Long2ObjectOpenHashMap<>(); + { + this.statusMap.defaultReturnValue(ChunkHolder.FullChunkStatus.INACCESSIBLE); + } + + public EntitySliceManager(final ServerLevel world) { + this.world = world; + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + } + + public void chunkStatusChange(final int x, final int z, final ChunkHolder.FullChunkStatus newStatus) { + if (newStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) { + this.statusMap.remove(CoordinateUtils.getChunkKey(x, z)); + } else { + this.statusMap.put(CoordinateUtils.getChunkKey(x, z), newStatus); + final ChunkEntitySlices slices = this.getChunk(x, z); + if (slices != null) { + slices.updateStatus(newStatus); + } + } + } + + public synchronized void addEntity(final Entity entity) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; + final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); + final int sectionZ = pos.getZ() >> 4; + final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); + slices.addEntity(entity, sectionY); + + entity.sectionX = sectionX; + entity.sectionY = sectionY; + entity.sectionZ = sectionZ; + } + + public synchronized void removeEntity(final Entity entity) { + final ChunkEntitySlices slices = this.getChunk(entity.sectionX, entity.sectionZ); + slices.removeEntity(entity, entity.sectionY); + if (slices.isEmpty()) { + this.removeChunk(entity.sectionX, entity.sectionZ); + } + } + + public void moveEntity(final Entity entity) { + final BlockPos newPos = entity.blockPosition(); + final int newSectionX = newPos.getX() >> 4; + final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); + final int newSectionZ = newPos.getZ() >> 4; + + if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) { + return; + } + + synchronized (this) { + // are we changing chunks? + if (newSectionX != entity.sectionX || newSectionZ != entity.sectionZ) { + final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); + final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ); + synchronized (old) { + old.removeEntity(entity, entity.sectionY); + if (old.isEmpty()) { + this.removeChunk(entity.sectionX, entity.sectionZ); + } + } + + synchronized (slices) { + slices.addEntity(entity, newSectionY); + + entity.sectionX = newSectionX; + entity.sectionY = newSectionY; + entity.sectionZ = newSectionZ; + } + } else { + final ChunkEntitySlices slices = this.getChunk(newSectionX, newSectionZ); + // same chunk + synchronized (slices) { + slices.removeEntity(entity, entity.sectionY); + slices.addEntity(entity, newSectionY); + } + entity.sectionY = newSectionY; + } + } + + } + + public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getEntities(except, box, into, predicate); + } + } + } + } + } + + public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getHardCollidingEntities(except, box, into, predicate); + } + } + } + } + } + + public void getEntities(final EntityType type, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getEntities(type, box, (List)into, (Predicate)predicate); + } + } + } + } + } + + public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, + final Predicate predicate) { + final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; + final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; + final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; + final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; + + final int minRegionX = minChunkX >> REGION_SHIFT; + final int minRegionZ = minChunkZ >> REGION_SHIFT; + final int maxRegionX = maxChunkX >> REGION_SHIFT; + final int maxRegionZ = maxChunkZ >> REGION_SHIFT; + + for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { + final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; + final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; + + for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { + final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); + + if (region == null) { + continue; + } + + final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; + final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); + if (chunk == null || !chunk.status.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { + continue; + } + + chunk.getEntities(clazz, except, box, into, predicate); + } + } + } + } + } + + public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + if (region == null) { + return null; + } + + return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); + } + + public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { + final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + ChunkEntitySlices ret; + if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { + ret = new ChunkEntitySlices(this.world, chunkX, chunkZ, this.statusMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)), + WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + + this.addChunk(chunkX, chunkZ, ret); + + return ret; + } + + return ret; + } + + public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { + final long key = CoordinateUtils.getChunkKey(regionX, regionZ); + final long attempt = this.stateLock.tryOptimisticRead(); + if (attempt != 0L) { + try { + final ChunkSlicesRegion ret = this.regions.get(key); + + if (this.stateLock.validate(attempt)) { + return ret; + } + } catch (final Error error) { + throw error; + } catch (final Throwable thr) { + // ignore + } + } + + this.stateLock.readLock(); + try { + return this.regions.get(key); + } finally { + this.stateLock.tryUnlockRead(); + } + } + + public synchronized void removeChunk(final int chunkX, final int chunkZ) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + final ChunkSlicesRegion region = this.regions.get(key); + final int remaining = region.remove(relIndex); + + if (remaining == 0) { + this.stateLock.writeLock(); + try { + this.regions.remove(key); + } finally { + this.stateLock.tryUnlockWrite(); + } + } + } + + public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { + final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); + + ChunkSlicesRegion region = this.regions.get(key); + if (region != null) { + region.add(relIndex, slices); + } else { + region = new ChunkSlicesRegion(); + region.add(relIndex, slices); + this.stateLock.writeLock(); + try { + this.regions.put(key, region); + } finally { + this.stateLock.tryUnlockWrite(); + } + } + } + + public static final class ChunkSlicesRegion { + + protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; + protected int sliceCount; + + public ChunkEntitySlices get(final int index) { + return this.slices[index]; + } + + public int remove(final int index) { + final ChunkEntitySlices slices = this.slices[index]; + if (slices == null) { + throw new IllegalStateException(); + } + + this.slices[index] = null; + + return --this.sliceCount; + } + + public void add(final int index, final ChunkEntitySlices slices) { + final ChunkEntitySlices curr = this.slices[index]; + if (curr != null) { + throw new IllegalStateException(); + } + + this.slices[index] = slices; + + ++this.sliceCount; + } + } +} diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index ac3ef76f0db5a2d0f02b5e48a8bf511d9b0104d4..5c588c39de11bbabdc2f50ef4204007c622fdc6a 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -437,7 +437,7 @@ public class ServerLevel extends Level implements WorldGenLevel { DataFixer datafixer = minecraftserver.getFixerUpper(); EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, new File(convertable_conversionsession.getDimensionPath(resourcekey), "entities"), datafixer, flag2, minecraftserver); - this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); + this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Paper StructureManager definedstructuremanager = minecraftserver.getStructureManager(); int j = this.spigotConfig.viewDistance; // Spigot PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java index 0f6b534a4c789a2f09f6c4624e5d58b99c7ed0e6..21d1e0c9c471e9e556b5bd70166a769b46105c7a 100644 --- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java +++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java @@ -77,6 +77,23 @@ public class WorldGenRegion implements WorldGenLevel { @Nullable private Supplier currentlyGenerating; + // Paper start + // No-op, this class doesn't provide entity access + @Override + public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { + return Collections.emptyList(); + } + + @Override + public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} + // Paper end + public WorldGenRegion(ServerLevel world, List list, ChunkStatus chunkstatus, int i) { this.generatingStatus = chunkstatus; this.writeRadiusCutoff = i; diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index e9eacba3816ff3b76c51e9d3011c739b24864355..b7b802053c48c740161747c89cc55ade80094cb7 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -417,6 +417,56 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n } // Paper end - make end portalling safe + // Paper start + /** + * Overriding this field will cause memory leaks. + */ + private final boolean hardCollides; + + private static final java.util.Map, Boolean> cachedOverrides = java.util.Collections.synchronizedMap(new java.util.WeakHashMap<>()); + { + /* // Goodbye, broken on reobf... + Boolean hardCollides = cachedOverrides.get(this.getClass()); + if (hardCollides == null) { + try { + java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class); + java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith"); + if (!this.getClass().getMethod(hasHardCollisionBoxMethod.getName(), hasHardCollisionBoxMethod.getParameterTypes()).equals(hasHardCollisionBoxMethod) + || !this.getClass().getMethod(getHardCollisionBoxEntityMethod.getName(), getHardCollisionBoxEntityMethod.getParameterTypes()).equals(getHardCollisionBoxEntityMethod)) { + hardCollides = Boolean.TRUE; + } else { + hardCollides = Boolean.FALSE; + } + cachedOverrides.put(this.getClass(), hardCollides); + } + catch (ThreadDeath thr) { throw thr; } + catch (Throwable thr) { + // shouldn't happen, just explode + throw new RuntimeException(thr); + } + } */ + this.hardCollides = this instanceof Boat + || this instanceof net.minecraft.world.entity.monster.Shulker + || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart + || this.shouldHardCollide(); + } + + // plugins can override + protected boolean shouldHardCollide() { + return false; + } + + public final boolean hardCollides() { + return this.hardCollides; + } + + public net.minecraft.server.level.ChunkHolder.FullChunkStatus chunkStatus; + + public int sectionX = Integer.MIN_VALUE; + public int sectionY = Integer.MIN_VALUE; + public int sectionZ = Integer.MIN_VALUE; + // Paper end + public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); this.passengers = ImmutableList.of(); @@ -2280,11 +2330,11 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n return InteractionResult.PASS; } - public boolean canCollideWith(Entity other) { + public boolean canCollideWith(Entity other) { // Paper - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other); } - public boolean canBeCollidedWith() { + public boolean canBeCollidedWith() { // Paper - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override return false; } diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java index 325e244c46ec208a2e7e18d71ccbbfcc25fc1bce..94130509e3a7980c378cc95c46821cf0fc753ce6 100644 --- a/src/main/java/net/minecraft/world/level/EntityGetter.java +++ b/src/main/java/net/minecraft/world/level/EntityGetter.java @@ -18,6 +18,18 @@ import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; public interface EntityGetter { + + // Paper start + List getHardCollidingEntities(Entity except, AABB box, Predicate predicate); + + void getEntities(Entity except, AABB box, Predicate predicate, List into); + + void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into); + + void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, + Predicate predicate); + // Paper end + List getEntities(@Nullable Entity except, AABB box, Predicate predicate); List getEntities(EntityTypeTest filter, AABB box, Predicate predicate); @@ -55,8 +67,8 @@ public interface EntityGetter { return Stream.empty(); } else { AABB aABB = box.inflate(1.0E-7D); - return this.getEntities(entity, aABB, predicate.and((entityx) -> { - if (entityx.getBoundingBox().intersects(aABB)) { + Predicate hardCollides = (entityx) -> { // Paper - optimise entity hard collisions + if (true || entityx.getBoundingBox().intersects(aABB)) { // Paper - always true if (entity == null) { if (entityx.canBeCollidedWith()) { return true; @@ -67,7 +79,11 @@ public interface EntityGetter { } return false; - })).stream().map(Entity::getBoundingBox).map(Shapes::create); + }; // Paper start - optimise entity hard collisions + predicate = predicate == null ? hardCollides : hardCollides.and(predicate); + return (entity != null && entity.hardCollides() ? this.getEntities(entity, aABB, predicate) : this.getHardCollidingEntities(entity, aABB, predicate)) + .stream().map(Entity::getBoundingBox).map(Shapes::create); + // Paper end - optimise entity hard collisions } } diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index d0cfa782498eca8a13ce7d7f28d5e55ae4e402fa..1dd2e968bde16da2d2da63ca3c30515e1fd5b620 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -204,6 +204,50 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return this.typeKey; } + // Paper start + protected final io.papermc.paper.world.EntitySliceManager entitySliceManager; + + // Paper start - optimise CraftChunk#getEntities + public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { + io.papermc.paper.world.ChunkEntitySlices slices = this.entitySliceManager.getChunk(chunkX, chunkZ); + if (slices == null) { + return new org.bukkit.entity.Entity[0]; + } + return slices.getChunkEntities(); + } + // Paper end - optimise CraftChunk#getEntities + + @Override + public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { + List ret = new java.util.ArrayList<>(); + this.entitySliceManager.getEntities(except, box, ret, predicate); + return ret; + } + + @Override + public void getEntities(Entity except, AABB box, Predicate predicate, List into) { + this.entitySliceManager.getEntities(except, box, into, predicate); + } + + @Override + public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) { + this.entitySliceManager.getHardCollidingEntities(except, box, into, predicate); + } + + @Override + public void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, + Predicate predicate) { + this.entitySliceManager.getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate); + } + + @Override + public List getEntitiesOfClass(Class entityClass, AABB box, Predicate predicate) { + List ret = new java.util.ArrayList<>(); + this.entitySliceManager.getEntities(entityClass, null, box, ret, predicate); + return ret; + } + // Paper end + protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, final DimensionType dimensionmanager, Supplier supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper - Anti-Xray - Pass executor this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), this.spigotConfig); // Paper @@ -281,6 +325,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.chunkPacketBlockController = this.paperConfig.antiXray ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray + this.entitySliceManager = new io.papermc.paper.world.EntitySliceManager((ServerLevel)this); // Paper } // Paper start @@ -987,26 +1032,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { this.getProfiler().incrementCounter("getEntities"); List list = Lists.newArrayList(); - - this.getEntities().get(box, (entity1) -> { - if (entity1 != except && predicate.test(entity1)) { - list.add(entity1); - } - - if (entity1 instanceof EnderDragon) { - EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); - int i = aentitycomplexpart.length; - - for (int j = 0; j < i; ++j) { - EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; - - if (entity1 != except && predicate.test(entitycomplexpart)) { - list.add(entitycomplexpart); - } - } - } - - }, predicate == net.minecraft.world.entity.EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper + this.entitySliceManager.getEntities(except, box, list, predicate); // Paper - optimise this call return list; } @@ -1015,26 +1041,22 @@ public abstract class Level implements LevelAccessor, AutoCloseable { this.getProfiler().incrementCounter("getEntities"); List list = Lists.newArrayList(); - this.getEntities().get(filter, box, (entity) -> { - if (predicate.test(entity)) { - list.add(entity); - } - - if (entity instanceof EnderDragon) { - EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity).getSubEntities(); - int i = aentitycomplexpart.length; - - for (int j = 0; j < i; ++j) { - EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; - T t0 = filter.tryCast(entitycomplexpart); - - if (t0 != null && predicate.test(t0)) { - list.add(t0); - } - } + // Paper start - optimise this call + if (filter instanceof net.minecraft.world.entity.EntityType) { + this.entitySliceManager.getEntities((net.minecraft.world.entity.EntityType)filter, box, list, predicate); + } else { + Predicate test = (obj) -> { + return filter.tryCast(obj) != null; + }; + predicate = predicate == null ? test : test.and((Predicate)predicate); + Class base; + if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) { + this.entitySliceManager.getEntities((Entity) null, box, (List)list, (Predicate)predicate); + } else { + this.entitySliceManager.getEntities(base, null, box, (List)list, (Predicate)predicate); // Paper - optimise this call } - - }); + } + // Paper end - optimise this call return list; } 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 e89396ea1d06f9e4a0a58d86cb7f9d857d50dc0a..976d206a17add01a31ae38b966913368cf386cb1 100644 --- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java +++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java @@ -49,8 +49,10 @@ public class PersistentEntitySectionManager implements A private final Long2ObjectMap chunkLoadStatuses = new Long2ObjectOpenHashMap(); private final LongSet chunksToUnload = new LongOpenHashSet(); private final Queue> loadingInbox = Queues.newConcurrentLinkedQueue(); + public final io.papermc.paper.world.EntitySliceManager entitySliceManager; // Paper - public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess) { + public PersistentEntitySectionManager(Class entityClass, LevelCallback handler, EntityPersistentStorage dataAccess, io.papermc.paper.world.EntitySliceManager entitySliceManager) { // Paper + this.entitySliceManager = entitySliceManager; // Paper this.sectionStorage = new EntitySectionStorage<>(entityClass, this.chunkVisibility); this.chunkVisibility.defaultReturnValue(Visibility.HIDDEN); this.chunkLoadStatuses.defaultReturnValue(PersistentEntitySectionManager.ChunkLoadStatus.FRESH); @@ -112,6 +114,7 @@ public class PersistentEntitySectionManager implements A EntitySection entitysection = this.sectionStorage.getOrCreateSection(i); entitysection.add(entity); // CraftBukkit - decompile error + this.entitySliceManager.addEntity((Entity)entity); // Paper entity.setLevelCallback(new PersistentEntitySectionManager.Callback(entity, i, entitysection)); if (!existing) { this.callbacks.onCreated(entity); @@ -169,6 +172,7 @@ public class PersistentEntitySectionManager implements A io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous chunk ticking status update"); // Paper Visibility visibility = Visibility.fromFullChunkStatus(levelType); + this.entitySliceManager.chunkStatusChange(chunkPos.x, chunkPos.z, levelType); // Paper this.updateChunkStatus(chunkPos, visibility); } @@ -461,6 +465,7 @@ public class PersistentEntitySectionManager implements A long i = SectionPos.asLong(blockposition); if (i != this.currentSectionKey) { + PersistentEntitySectionManager.this.entitySliceManager.moveEntity((Entity)this.entity); // Paper Visibility visibility = this.currentSection.getStatus(); if (!this.currentSection.remove(this.entity)) { @@ -509,6 +514,7 @@ public class PersistentEntitySectionManager implements A if (!this.currentSection.remove(this.entity)) { PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (destroying due to {})", this.entity, SectionPos.of(this.currentSectionKey), reason); } + PersistentEntitySectionManager.this.entitySliceManager.removeEntity((Entity)this.entity); // Paper Visibility visibility = PersistentEntitySectionManager.getEffectiveStatus(this.entity, this.currentSection.getStatus()); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java index 5088c84f6518cb686241b1db54faa8d813cb3eaa..07dfe575dea0f41a75bb9dad0ee2d541e983f6d7 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java @@ -131,9 +131,7 @@ public class CraftChunk implements Chunk { long pair = ChunkPos.asLong(x, z); if (entityManager.areEntitiesLoaded(pair)) { // PAIL rename isEntitiesLoaded - return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() - .map(net.minecraft.world.entity.Entity::getBukkitEntity) - .filter(Objects::nonNull).toArray(Entity[]::new); + return getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - optimise this } entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading @@ -169,9 +167,7 @@ public class CraftChunk implements Chunk { } } - return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() - .map(net.minecraft.world.entity.Entity::getBukkitEntity) - .filter(Objects::nonNull).toArray(Entity[]::new); + return getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - optimise this } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java index c5d132dcf70a55de041c0187ff8fb889248bdfee..950d4381459d31d02acf55c4aef4f5e33367748b 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java @@ -254,4 +254,20 @@ public class DummyGeneratorAccess implements WorldGenLevel { public Stream> startsForFeature(SectionPos pos, StructureFeature feature) { throw new UnsupportedOperationException("Not supported yet."); } + + // Paper start + @Override + public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { + return java.util.Collections.emptyList(); + } + + @Override + public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} + + @Override + public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} + // Paper end } diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java index 85449fc6d19974622588e7f13e1dc78c8dffbeee..9c456cce42ef9d1654df9047d6fc1e0da13dc1c9 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -205,7 +205,13 @@ public class ActivationRange ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, 256, villagerActivationRange ); // Paper end - world.getEntities().get(maxBB, ActivationRange::activateEntity); + // Paper start + java.util.List entities = world.getEntities((Entity)null, maxBB, null); + for (int i = 0; i < entities.size(); i++) { + Entity entity = entities.get(i); + ActivationRange.activateEntity(entity); + } + // Paper end } MinecraftTimings.entityActivationCheckTimer.stopTiming(); }