aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/1066-Optimise-general-POI-access.patch
diff options
context:
space:
mode:
authorNassim Jahnke <[email protected]>2024-12-11 12:13:51 +0100
committerNassim Jahnke <[email protected]>2024-12-11 12:13:51 +0100
commit8dc76e715261f97bbf4a3f254e123aae08588ad5 (patch)
tree8022cecd70c4fcb899dbc49eca0d59390cc9b809 /patches/server/1066-Optimise-general-POI-access.patch
parentc17ef64339ab27e5e50f331e53b00f6de45f7444 (diff)
downloadPaper-8dc76e715261f97bbf4a3f254e123aae08588ad5.tar.gz
Paper-8dc76e715261f97bbf4a3f254e123aae08588ad5.zip
[ci skip] Move back more
Diffstat (limited to 'patches/server/1066-Optimise-general-POI-access.patch')
-rw-r--r--patches/server/1066-Optimise-general-POI-access.patch1067
1 files changed, 1067 insertions, 0 deletions
diff --git a/patches/server/1066-Optimise-general-POI-access.patch b/patches/server/1066-Optimise-general-POI-access.patch
new file mode 100644
index 0000000000..79a34afd5a
--- /dev/null
+++ b/patches/server/1066-Optimise-general-POI-access.patch
@@ -0,0 +1,1067 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <[email protected]>
+Date: Sun, 31 Jan 2021 02:29:24 -0800
+Subject: [PATCH] Optimise general POI access
+
+There are a couple of problems with mojang's POI code.
+Firstly, it's all streams. Unsurprisingly, stacking
+streams on top of each other is horrible for performance
+and ultimately took up half of a villager's tick!
+
+Secondly, sometime's the search radius is large and there are
+a significant number of poi entries per chunk section. Even
+removing streams at this point doesn't help much. The only solution
+is to start at the search point and iterate outwards. This
+type of approach shows massive gains for portals, simply because
+we can avoid sync loading a large area of chunks. I also tested
+a massive farm I found in JellySquid's discord, which showed
+to benefit significantly simply because the farm had so many
+portal blocks that searching through them all was very slow.
+
+Great care has been taken so that behavior remains identical to
+vanilla, however I cannot account for oddball Stream API
+implementations, if they even exist (streams can technically
+be loose with iteration order in a sorted stream given its
+source stream is not tagged with ordered, and mojang does not
+tag the source stream as ordered). However in my testing on openjdk
+there showed no difference, as expected.
+
+This patch also specifically optimises other areas of code to
+use PoiAccess. For example, some villager AI and portaling code
+had to be specifically modified.
+
+diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f39294b1f83c4022be5ced4da781103a1eee2daf
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/PoiAccess.java
+@@ -0,0 +1,806 @@
++package io.papermc.paper.util;
++
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import com.mojang.datafixers.util.Pair;
++import it.unimi.dsi.fastutil.doubles.Double2ObjectMap;
++import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap;
++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
++import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
++import java.util.function.BiPredicate;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.Holder;
++import net.minecraft.util.Mth;
++import net.minecraft.world.entity.ai.village.poi.PoiManager;
++import net.minecraft.world.entity.ai.village.poi.PoiRecord;
++import net.minecraft.world.entity.ai.village.poi.PoiSection;
++import net.minecraft.world.entity.ai.village.poi.PoiType;
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++import java.util.Optional;
++import java.util.Set;
++import java.util.function.Predicate;
++
++/**
++ * Provides optimised access to POI data. All returned values will be identical to vanilla.
++ */
++public final class PoiAccess {
++
++ protected static double clamp(final double val, final double min, final double max) {
++ return (val < min ? min : (val > max ? max : val));
++ }
++
++ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ,
++ final double boxMaxX, final double boxMaxY, final double boxMaxZ,
++
++ final double circleX, final double circleY, final double circleZ) {
++ // is the circle center inside the box?
++ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) {
++ return 0.0;
++ }
++
++ final double boxWidthX = (boxMaxX - boxMinX) / 2.0;
++ final double boxWidthY = (boxMaxY - boxMinY) / 2.0;
++ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0;
++
++ final double boxCenterX = (boxMinX + boxMaxX) / 2.0;
++ final double boxCenterY = (boxMinY + boxMaxY) / 2.0;
++ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0;
++
++ double centerDiffX = circleX - boxCenterX;
++ double centerDiffY = circleY - boxCenterY;
++ double centerDiffZ = circleZ - boxCenterZ;
++
++ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX);
++ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY);
++ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ);
++
++ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ);
++ }
++
++
++ // key is:
++ // upper 32 bits:
++ // upper 16 bits: max y section
++ // lower 16 bits: min y section
++ // lower 32 bits:
++ // upper 16 bits: section
++ // lower 16 bits: radius
++ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) {
++ return (
++ (maxSection & 0xFFFFL) << (64 - 16)
++ | (minSection & 0xFFFFL) << (64 - 32)
++ | (section & 0xFFFFL) << (64 - 48)
++ | (radius & 0xFFFFL) << (64 - 64)
++ );
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance.
++ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findClosestPoiDataRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load
++ );
++
++ return ret == null ? null : ret.getPos();
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance.
++ public static Pair<Holder<PoiType>, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findClosestPoiDataRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load
++ );
++
++ return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos());
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
++ public static void findClosestPoiDataPositions(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final Set<BlockPos> ret) {
++ final Set<BlockPos> positions = new HashSet<>();
++ // pos predicate is last thing that runs before adding to ret.
++ final Predicate<BlockPos> newPredicate = (final BlockPos pos) -> {
++ if (positionPredicate != null && !positionPredicate.test(pos)) {
++ return false;
++ }
++ return positions.add(pos.immutable());
++ };
++
++ final List<PoiRecord> toConvert = new ArrayList<>();
++ findClosestPoiDataRecords(
++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert
++ );
++
++ for (final PoiRecord record : toConvert) {
++ ret.add(record.getPos());
++ }
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance.
++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List<PoiRecord> ret = new ArrayList<>();
++ findClosestPoiDataRecords(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret
++ );
++ return ret.isEmpty() ? null : ret.get(0);
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance.
++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final BiPredicate<Holder<PoiType>, BlockPos> predicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List<PoiRecord> ret = new ArrayList<>();
++ findClosestPoiDataRecords(
++ poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret
++ );
++ return ret.isEmpty() ? null : ret.get(0);
++ }
++
++ // only includes x/z axis
++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned.
++ public static void findClosestPoiDataRecords(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final List<PoiRecord> ret) {
++ final BiPredicate<Holder<PoiType>, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null;
++ findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret);
++ }
++
++ public static void findClosestPoiDataRecords(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final BiPredicate<Holder<PoiType>, BlockPos> predicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final List<PoiRecord> ret) {
++ final Predicate<? super PoiRecord> occupancyFilter = occupancy.getTest();
++
++ final List<PoiRecord> closestRecords = new ArrayList<>();
++ double closestDistanceSquared = maxDistanceSquared;
++
++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
++ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld());
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld());
++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
++
++ final int centerX = sourcePosition.getX() >> 4;
++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY);
++ final int centerZ = sourcePosition.getZ() >> 4;
++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ);
++
++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
++ final LongOpenHashSet seen = new LongOpenHashSet();
++ seen.add(centerKey);
++ queue.enqueue(centerKey);
++
++ while (!queue.isEmpty()) {
++ final long key = queue.dequeueLong();
++ final int sectionX = CoordinateUtils.getChunkSectionX(key);
++ final int sectionY = CoordinateUtils.getChunkSectionY(key);
++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
++
++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
++ // out of bound chunk
++ continue;
++ }
++
++ final double sectionDistanceSquared = getSmallestDistanceSquared(
++ (sectionX << 4) + 0.5,
++ (sectionY << 4) + 0.5,
++ (sectionZ << 4) + 0.5,
++ (sectionX << 4) + 15.5,
++ (sectionY << 4) + 15.5,
++ (sectionZ << 4) + 15.5,
++ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ()
++ );
++ if (sectionDistanceSquared > closestDistanceSquared) {
++ continue;
++ }
++
++ // queue all neighbours
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dy = -1; dy <= 1; ++dy) {
++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
++ continue;
++ }
++
++ final int neighbourX = sectionX + dx;
++ final int neighbourY = sectionY + dy;
++ final int neighbourZ = sectionZ + dz;
++
++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
++ if (seen.add(neighbourKey)) {
++ queue.enqueue(neighbourKey);
++ }
++ }
++ }
++ }
++
++ final Optional<PoiSection> poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
++
++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
++ continue;
++ }
++
++ final PoiSection poiSection = poiSectionOptional.get();
++
++ final Map<Holder<PoiType>, Set<PoiRecord>> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry<Holder<PoiType>, Set<PoiRecord>> entry : sectionData.entrySet()) {
++ if (!villagePlaceType.test(entry.getKey())) {
++ // filter out by poi type
++ continue;
++ }
++
++ // now we can look at the poi data
++ for (final PoiRecord poiData : entry.getValue()) {
++ if (!occupancyFilter.test(poiData)) {
++ // filter by occupancy
++ continue;
++ }
++
++ final BlockPos poiPosition = poiData.getPos();
++
++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
++ // out of range for square radius
++ continue;
++ }
++
++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
++ final double dataRange = poiPosition.distSqr(sourcePosition);
++
++ if (dataRange > closestDistanceSquared) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) {
++ // filter by position
++ continue;
++ }
++
++ if (dataRange < closestDistanceSquared) {
++ closestRecords.clear();
++ closestDistanceSquared = dataRange;
++ }
++ closestRecords.add(poiData);
++ }
++ }
++ }
++
++ // uh oh! we might have multiple records that match the distance sorting!
++ // we need to re-order our results by the way vanilla would have iterated over them.
++ closestRecords.sort((record1, record2) -> {
++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
++ // is fine and should be preserved (this sort is stable so we're good there)
++ // but they iterate sections by x then by z (like the following)
++ // for (int x = -dx; x <= dx; ++x)
++ // for (int z = -dz; z <= dz; ++z)
++ // ....
++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
++ final BlockPos pos1 = record1.getPos();
++ final BlockPos pos2 = record2.getPos();
++
++ final int cx1 = pos1.getX() >> 4;
++ final int cz1 = pos1.getZ() >> 4;
++
++ final int cx2 = pos2.getX() >> 4;
++ final int cz2 = pos2.getZ() >> 4;
++
++ if (cz2 != cz1) {
++ // want smaller z
++ return Integer.compare(cz1, cz2);
++ }
++
++ if (cx2 != cx1) {
++ // want smaller x
++ return Integer.compare(cx1, cx2);
++ }
++
++ // same chunk
++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
++ // so now we just compare section y, wanting smaller y
++
++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
++ });
++
++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
++ ret.addAll(closestRecords);
++ }
++
++ // finds the closest poi entry pos.
++ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findNearestPoiRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load
++ );
++ return ret == null ? null : ret.getPos();
++ }
++
++ // finds the closest `max` poi entry positions.
++ public static void findNearestPoiPositions(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List<Pair<Holder<PoiType>, BlockPos>> ret) {
++ final Set<BlockPos> positions = new HashSet<>();
++ // pos predicate is last thing that runs before adding to ret.
++ final Predicate<BlockPos> newPredicate = (final BlockPos pos) -> {
++ if (positionPredicate != null && !positionPredicate.test(pos)) {
++ return false;
++ }
++ return positions.add(pos.immutable());
++ };
++
++ final List<PoiRecord> toConvert = new ArrayList<>();
++ findNearestPoiRecords(
++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert
++ );
++
++ for (final PoiRecord record : toConvert) {
++ ret.add(Pair.of(record.getPoiType(), record.getPos()));
++ }
++ }
++
++ // finds the closest poi entry.
++ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List<PoiRecord> ret = new ArrayList<>();
++ findNearestPoiRecords(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load,
++ 1, ret
++ );
++ return ret.isEmpty() ? null : ret.get(0);
++ }
++
++ // finds the closest `max` poi entries.
++ public static void findNearestPoiRecords(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ // position predicate must not modify chunk POI
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final double maxDistanceSquared,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List<PoiRecord> ret) {
++ final Predicate<? super PoiRecord> occupancyFilter = occupancy.getTest();
++
++ final Double2ObjectRBTreeMap<List<PoiRecord>> closestRecords = new Double2ObjectRBTreeMap<>();
++ int totalRecords = 0;
++ double furthestDistanceSquared = maxDistanceSquared;
++
++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
++ final int lowerY = WorldUtil.getMinSection(poiStorage.moonrise$getWorld());
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = WorldUtil.getMaxSection(poiStorage.moonrise$getWorld());
++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
++
++ final int centerX = sourcePosition.getX() >> 4;
++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY);
++ final int centerZ = sourcePosition.getZ() >> 4;
++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ);
++
++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
++ final LongOpenHashSet seen = new LongOpenHashSet();
++ seen.add(centerKey);
++ queue.enqueue(centerKey);
++
++ while (!queue.isEmpty()) {
++ final long key = queue.dequeueLong();
++ final int sectionX = CoordinateUtils.getChunkSectionX(key);
++ final int sectionY = CoordinateUtils.getChunkSectionY(key);
++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key);
++
++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) {
++ // out of bound chunk
++ continue;
++ }
++
++ final double sectionDistanceSquared = getSmallestDistanceSquared(
++ (sectionX << 4) + 0.5,
++ (sectionY << 4) + 0.5,
++ (sectionZ << 4) + 0.5,
++ (sectionX << 4) + 15.5,
++ (sectionY << 4) + 15.5,
++ (sectionZ << 4) + 15.5,
++ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ()
++ );
++
++ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) {
++ continue;
++ }
++
++ // queue all neighbours
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ for (int dy = -1; dy <= 1; ++dy) {
++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many
++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set
++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) {
++ continue;
++ }
++
++ final int neighbourX = sectionX + dx;
++ final int neighbourY = sectionY + dy;
++ final int neighbourZ = sectionZ + dz;
++
++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ);
++ if (seen.add(neighbourKey)) {
++ queue.enqueue(neighbourKey);
++ }
++ }
++ }
++ }
++
++ final Optional<PoiSection> poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key);
++
++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) {
++ continue;
++ }
++
++ final PoiSection poiSection = poiSectionOptional.get();
++
++ final Map<Holder<PoiType>, Set<PoiRecord>> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry<Holder<PoiType>, Set<PoiRecord>> entry : sectionData.entrySet()) {
++ if (!villagePlaceType.test(entry.getKey())) {
++ // filter out by poi type
++ continue;
++ }
++
++ // now we can look at the poi data
++ for (final PoiRecord poiData : entry.getValue()) {
++ if (!occupancyFilter.test(poiData)) {
++ // filter by occupancy
++ continue;
++ }
++
++ final BlockPos poiPosition = poiData.getPos();
++
++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
++ // out of range for square radius
++ continue;
++ }
++
++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped!
++ final double dataRange = poiPosition.distSqr(sourcePosition);
++
++ if (dataRange > maxDistanceSquared) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (dataRange > furthestDistanceSquared && totalRecords >= max) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
++ // filter by position
++ continue;
++ }
++
++ if (dataRange > furthestDistanceSquared) {
++ // we know totalRecords < max, so this entry is now our furthest
++ furthestDistanceSquared = dataRange;
++ }
++
++ closestRecords.computeIfAbsent(dataRange, (final double unused) -> {
++ return new ArrayList<>();
++ }).add(poiData);
++
++ if (++totalRecords >= max) {
++ if (closestRecords.size() >= 2) {
++ int entriesInClosest = 0;
++ final Iterator<Double2ObjectMap.Entry<List<PoiRecord>>> iterator = closestRecords.double2ObjectEntrySet().iterator();
++ double nextFurthestDistanceSquared = 0.0;
++
++ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) {
++ final Double2ObjectMap.Entry<List<PoiRecord>> recordEntry = iterator.next();
++ entriesInClosest += recordEntry.getValue().size();
++ nextFurthestDistanceSquared = recordEntry.getDoubleKey();
++ }
++
++ if (entriesInClosest >= max) {
++ // the last set of entries at range wont even be considered for sure... nuke em
++ final Double2ObjectMap.Entry<List<PoiRecord>> recordEntry = iterator.next();
++ totalRecords -= recordEntry.getValue().size();
++ iterator.remove();
++
++ furthestDistanceSquared = nextFurthestDistanceSquared;
++ }
++ }
++ }
++ }
++ }
++ }
++
++ final List<PoiRecord> closestRecordsUnsorted = new ArrayList<>();
++
++ // we're done here, so now just flatten the map and sort it.
++
++ for (final List<PoiRecord> records : closestRecords.values()) {
++ closestRecordsUnsorted.addAll(records);
++ }
++
++ // uh oh! we might have multiple records that match the distance sorting!
++ // we need to re-order our results by the way vanilla would have iterated over them.
++ closestRecordsUnsorted.sort((record1, record2) -> {
++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section
++ // is fine and should be preserved (this sort is stable so we're good there)
++ // but they iterate sections by x then by z (like the following)
++ // for (int x = -dx; x <= dx; ++x)
++ // for (int z = -dz; z <= dz; ++z)
++ // ....
++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first
++ final BlockPos pos1 = record1.getPos();
++ final BlockPos pos2 = record2.getPos();
++
++ final int cx1 = pos1.getX() >> 4;
++ final int cz1 = pos1.getZ() >> 4;
++
++ final int cx2 = pos2.getX() >> 4;
++ final int cz2 = pos2.getZ() >> 4;
++
++ if (cz2 != cz1) {
++ // want smaller z
++ return Integer.compare(cz1, cz2);
++ }
++
++ if (cx2 != cx1) {
++ // want smaller x
++ return Integer.compare(cx1, cx2);
++ }
++
++ // same chunk
++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y
++ // so now we just compare section y, wanting smaller section y
++
++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4);
++ });
++
++ // trim out any entries exceeding our maximum
++ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) {
++ closestRecordsUnsorted.remove(i);
++ }
++
++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully).
++ ret.addAll(closestRecordsUnsorted);
++ }
++
++ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findAnyPoiRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load
++ );
++
++ return ret == null ? null : ret.getPos();
++ }
++
++ public static void findAnyPoiPositions(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List<Pair<Holder<PoiType>, BlockPos>> ret) {
++ final Set<BlockPos> positions = new HashSet<>();
++ // pos predicate is last thing that runs before adding to ret.
++ final Predicate<BlockPos> newPredicate = (final BlockPos pos) -> {
++ if (positionPredicate != null && !positionPredicate.test(pos)) {
++ return false;
++ }
++ return positions.add(pos.immutable());
++ };
++
++ final List<PoiRecord> toConvert = new ArrayList<>();
++ findAnyPoiRecords(
++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert
++ );
++
++ for (final PoiRecord record : toConvert) {
++ ret.add(Pair.of(record.getPoiType(), record.getPos()));
++ }
++ }
++
++ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List<PoiRecord> ret = new ArrayList<>();
++ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret);
++ return ret.isEmpty() ? null : ret.get(0);
++ }
++
++ public static void findAnyPoiRecords(final PoiManager poiStorage,
++ final Predicate<Holder<PoiType>> villagePlaceType,
++ final Predicate<BlockPos> positionPredicate,
++ final BlockPos sourcePosition,
++ final int range, // distance on x y z axis
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List<PoiRecord> ret) {
++ // the biggest issue with the original mojang implementation is that they chain so many streams together
++ // the amount of streams chained just rolls performance, even if nothing is iterated over
++ final Predicate<? super PoiRecord> occupancyFilter = occupancy.getTest();
++ final double rangeSquared = range * range;
++
++ int added = 0;
++
++ // First up, we need to iterate the chunks
++ // all the values here are in chunk sections
++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
++ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() - range) >> 4);
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.moonrise$getWorld()), Mth.floor(sourcePosition.getY() + range) >> 4);
++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4;
++
++ // Vanilla iterates by x until max is reached then increases z
++ // vanilla also searches by increasing Y section value
++ for (int currZ = lowerZ; currZ <= upperZ; ++currZ) {
++ for (int currX = lowerX; currX <= upperX; ++currX) {
++ for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need
++ final Optional<PoiSection> poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) :
++ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ));
++ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null);
++ if (poiSection == null) {
++ continue;
++ }
++
++ final Map<Holder<PoiType>, Set<PoiRecord>> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry<Holder<PoiType>, Set<PoiRecord>> entry : sectionData.entrySet()) {
++ if (!villagePlaceType.test(entry.getKey())) {
++ // filter out by poi type
++ continue;
++ }
++
++ // now we can look at the poi data
++ for (final PoiRecord poiData : entry.getValue()) {
++ if (!occupancyFilter.test(poiData)) {
++ // filter by occupancy
++ continue;
++ }
++
++ final BlockPos poiPosition = poiData.getPos();
++
++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range
++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) {
++ // out of range for square radius
++ continue;
++ }
++
++ if (poiPosition.distSqr(sourcePosition) > rangeSquared) {
++ // out of range for distance check
++ continue;
++ }
++
++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) {
++ // filter by position
++ continue;
++ }
++
++ // found one!
++ ret.add(poiData);
++ if (++added >= max) {
++ return;
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++
++ private PoiAccess() {
++ throw new RuntimeException();
++ }
++}
+diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
+index 45974895c464b4c0186ab9add5eacb98abe90e09..0d177e828c2b338ce93c58aaef04df326e1eb0b2 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java
+@@ -84,12 +84,16 @@ public class AcquirePoi {
+ return true;
+ }
+ };
+- Set<Pair<Holder<PoiType>, BlockPos>> set = poiManager.findAllClosestFirstWithType(
+- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
+- )
+- .limit(5L)
+- .filter(pairx -> worldPosBiPredicate.test(world, (BlockPos)pairx.getSecond()))
+- .collect(Collectors.toSet());
++ // Paper start - optimise POI access
++ final java.util.List<Pair<Holder<PoiType>, BlockPos>> poiposes = new java.util.ArrayList<>();
++ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes);
++ final Set<Pair<Holder<PoiType>, BlockPos>> set = new java.util.HashSet<>(poiposes.size());
++ for (final Pair<Holder<PoiType>, BlockPos> poiPose : poiposes) {
++ if (worldPosBiPredicate.test(world, poiPose.getSecond())) {
++ set.add(poiPose);
++ }
++ }
++ // Paper end - optimise POI access
+ Path path = findPathToPois(entity, set);
+ if (path != null && path.canReach()) {
+ BlockPos blockPos = path.getTarget();
+diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
+index d5a549f08b98c80a5cf0eef02cb8a389c32dfecb..92731b6b593289e9f583c9b705b219e81fcd8e73 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java
+@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor<Mob> {
+ return true;
+ }
+ };
+- Set<Pair<Holder<PoiType>, BlockPos>> set = poiManager.findAllWithType(
+- holder -> holder.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY
+- )
+- .collect(Collectors.toSet());
+- Path path = AcquirePoi.findPathToPois(entity, set);
++ // Paper start - optimise POI access
++ java.util.List<Pair<Holder<PoiType>, BlockPos>> poiposes = new java.util.ArrayList<>();
++ // don't ask me why it's unbounded. ask mojang.
++ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes);
++ Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes));
++ // Paper end - optimise POI access
+ if (path != null && path.canReach()) {
+ BlockPos blockPos = path.getTarget();
+ Optional<Holder<PoiType>> optional = poiManager.getType(blockPos);
+diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+index 5930a430983061afddf20e3208ff2462ca1b78cd..63a94b6068fdaef8bb26675c2927cb729ced1dac 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+@@ -254,36 +254,45 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
+ public Optional<BlockPos> find(
+ Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus
+ ) {
+- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst();
++ // Paper start - re-route to faster logic
++ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false);
++ return Optional.ofNullable(ret);
++ // Paper end
+ }
+
+ public Optional<BlockPos> findClosest(Predicate<Holder<PoiType>> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) {
+- return this.getInRange(typePredicate, pos, radius, occupationStatus)
+- .map(PoiRecord::getPos)
+- .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos)));
++ // Paper start - re-route to faster logic
++ BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false);
++ return Optional.ofNullable(closestPos);
++ // Paper end - re-route to faster logic
+ }
+
+ public Optional<Pair<Holder<PoiType>, BlockPos>> findClosestWithType(
+ Predicate<Holder<PoiType>> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus
+ ) {
+- return this.getInRange(typePredicate, pos, radius, occupationStatus)
+- .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos)))
+- .map(poi -> Pair.of(poi.getPoiType(), poi.getPos()));
++ // Paper start - re-route to faster logic
++ return Optional.ofNullable(io.papermc.paper.util.PoiAccess.findClosestPoiDataTypeAndPosition(
++ this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false
++ ));
++ // Paper end - re-route to faster logic
+ }
+
+ public Optional<BlockPos> findClosest(
+ Predicate<Holder<PoiType>> typePredicate, Predicate<BlockPos> posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus
+ ) {
+- return this.getInRange(typePredicate, pos, radius, occupationStatus)
+- .map(PoiRecord::getPos)
+- .filter(posPredicate)
+- .min(Comparator.comparingDouble(poiPos -> poiPos.distSqr(pos)));
++ // Paper start - re-route to faster logic
++ BlockPos closestPos = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false);
++ return Optional.ofNullable(closestPos);
++ // Paper end - re-route to faster logic
+ }
+
+ public Optional<BlockPos> take(Predicate<Holder<PoiType>> typePredicate, BiPredicate<Holder<PoiType>, BlockPos> posPredicate, BlockPos pos, int radius) {
+- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE)
+- .filter(poi -> posPredicate.test(poi.getPoiType(), poi.getPos()))
+- .findFirst()
++ // Paper start - re-route to faster logic
++ final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord(
++ this, typePredicate, posPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false
++ );
++ return Optional.ofNullable(closest)
++ // Paper end - re-route to faster logic
+ .map(poi -> {
+ poi.acquireTicket();
+ return poi.getPos();
+@@ -298,8 +307,21 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
+ int radius,
+ RandomSource random
+ ) {
+- List<PoiRecord> list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random);
+- return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos);
++ // Paper start - re-route to faster logic
++ List<PoiRecord> list = new java.util.ArrayList<>();
++ io.papermc.paper.util.PoiAccess.findAnyPoiRecords(
++ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list
++ );
++
++ // the old method shuffled the list and then tried to find the first element in it that
++ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a
++ // shuffle entirely, and just pick a random element from list
++ if (list.isEmpty()) {
++ return Optional.empty();
++ }
++
++ return Optional.of(list.get(random.nextInt(list.size())).getPos());
++ // Paper end - re-route to faster logic
+ }
+
+ public boolean release(BlockPos pos) {
+diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+index 712cbfc100e8aaf612d1d651dae64f57f892a768..827991ee61406bcda3f4794dcc735c0e2e0e09af 100644
+--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+@@ -26,7 +26,7 @@ import org.slf4j.Logger;
+ public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.level.poi.ChunkSystemPoiSection { // Paper - rewrite chunk system
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final Short2ObjectMap<PoiRecord> records = new Short2ObjectOpenHashMap<>();
+- private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap();
++ private final Map<Holder<PoiType>, Set<PoiRecord>> byType = Maps.newHashMap(); public final Map<Holder<PoiType>, Set<PoiRecord>> getData() { return this.byType; } // Paper - public accessor
+ private final Runnable setDirty;
+ private boolean isValid;
+
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
+index c3beb7fcad46a917d2b61bd0a0e98e5106056728..9b97fb2d125df4df715599aab27e074707731466 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java
+@@ -131,11 +131,11 @@ public class SectionStorage<R, P> implements AutoCloseable, ca.spottedleaf.moonr
+ }
+
+ @Nullable
+- protected Optional<R> get(long pos) {
++ public Optional<R> get(long pos) { // Paper - public
+ return this.storage.get(pos);
+ }
+
+- protected Optional<R> getOrLoad(long pos) {
++ public Optional<R> getOrLoad(long pos) { // Paper - public
+ if (this.outsideStoredRange(pos)) {
+ return Optional.empty();
+ } else {
+diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+index 83d294f6f48b867d09ea0d339c779011bf4138a5..9204bb0538297f233442a86733a33e6d0eea8114 100644
+--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+@@ -53,17 +53,39 @@ public class PortalForcer {
+ // int i = flag ? 16 : 128;
+ // CraftBukkit end
+
+- villageplace.ensureLoadedAndValid(this.level, blockposition, i);
+- Stream<BlockPos> stream = villageplace.getInSquare((holder) -> { // CraftBukkit - decompile error
+- return holder.is(PoiTypes.NETHER_PORTAL);
+- }, blockposition, i, PoiManager.Occupancy.ANY).map(PoiRecord::getPos);
+-
+- Objects.requireNonNull(worldborder);
+- return stream.filter(worldborder::isWithinBounds).filter(pos -> !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))).filter((blockposition1) -> { // Paper - Configurable nether ceiling damage
+- return this.level.getBlockState(blockposition1).hasProperty(BlockStateProperties.HORIZONTAL_AXIS);
+- }).min(Comparator.comparingDouble((BlockPos blockposition1) -> { // CraftBukkit - decompile error
+- return blockposition1.distSqr(blockposition);
+- }).thenComparingInt(Vec3i::getY));
++ // Paper start - optimise portals
++ Optional<PoiRecord> optional;
++ java.util.List<PoiRecord> records = new java.util.ArrayList<>();
++ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords(
++ villageplace,
++ type -> type.is(PoiTypes.NETHER_PORTAL),
++ (BlockPos pos) -> {
++ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY);
++ if (!lowest.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL)
++ && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) {
++ // why would we generate the chunk?
++ return false;
++ }
++ if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage
++ return false;
++ }
++ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS);
++ },
++ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records
++ );
++
++ // this gets us most of the way there, but we bias towards lower y values.
++ BlockPos lowestPos = null;
++ for (PoiRecord record : records) {
++ if (lowestPos == null) {
++ lowestPos = record.getPos();
++ } else if (lowestPos.getY() > record.getPos().getY()) {
++ lowestPos = record.getPos();
++ }
++ }
++ // now we're done
++ return Optional.ofNullable(lowestPos);
++ // Paper end - optimise portals
+ }
+
+ public Optional<BlockUtil.FoundRectangle> createPortal(BlockPos pos, Direction.Axis axis) {