aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0747-Optimise-general-POI-access.patch
diff options
context:
space:
mode:
authorJake Potrebic <[email protected]>2021-12-22 10:02:31 -0800
committerGitHub <[email protected]>2021-12-22 10:02:31 -0800
commit82eaf4ee15d77303cdd352cce9b33c2f3e5d14d7 (patch)
tree53e4a42978675fc17acc76b85dcb0c3569ab3e3b /patches/server/0747-Optimise-general-POI-access.patch
parent6e5ceb34eb2ef4d6c0a35c46d0eded9099bb2fee (diff)
downloadPaper-82eaf4ee15d77303cdd352cce9b33c2f3e5d14d7.tar.gz
Paper-82eaf4ee15d77303cdd352cce9b33c2f3e5d14d7.zip
Fix duplicated BlockPistonRetractEvent call (#7111)
Diffstat (limited to 'patches/server/0747-Optimise-general-POI-access.patch')
-rw-r--r--patches/server/0747-Optimise-general-POI-access.patch997
1 files changed, 997 insertions, 0 deletions
diff --git a/patches/server/0747-Optimise-general-POI-access.patch b/patches/server/0747-Optimise-general-POI-access.patch
new file mode 100644
index 0000000000..c920f51774
--- /dev/null
+++ b/patches/server/0747-Optimise-general-POI-access.patch
@@ -0,0 +1,997 @@
+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..0a88c60161b04a733151c15046358f4b3b8b3280
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/util/PoiAccess.java
+@@ -0,0 +1,748 @@
++package io.papermc.paper.util;
++
++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 net.minecraft.core.BlockPos;
++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<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 maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findClosestPoiDataRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
++ );
++
++ return ret == null ? null : 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<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 maxDistance,
++ 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, maxDistance, 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<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 maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List<PoiRecord> ret = new ArrayList<>();
++ findClosestPoiDataRecords(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, 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<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 maxDistance,
++ 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 = maxDistance * maxDistance;
++
++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4;
++ final int lowerY = WorldUtil.getMinSection(poiStorage.world);
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
++ 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 LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
++ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
++ final LongOpenHashSet seen = new LongOpenHashSet();
++
++ 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.orElse(null);
++
++ final Map<PoiType, Set<PoiRecord>> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry<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 (positionPredicate != null && !positionPredicate.test(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<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 maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final PoiRecord ret = findNearestPoiRecord(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, occupancy, load
++ );
++ return ret == null ? null : ret.getPos();
++ }
++
++ // finds the closest `max` poi entry positions.
++ public static void findNearestPoiPositions(final PoiManager poiStorage,
++ final Predicate<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 maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List<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, maxDistance, occupancy, load, max, toConvert
++ );
++
++ for (final PoiRecord record : toConvert) {
++ ret.add(record.getPos());
++ }
++ }
++
++ // finds the closest poi entry.
++ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage,
++ final Predicate<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 maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load) {
++ final List<PoiRecord> ret = new ArrayList<>();
++ findNearestPoiRecords(
++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistance, 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<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 maxDistance,
++ final PoiManager.Occupancy occupancy,
++ final boolean load,
++ final int max,
++ final List<PoiRecord> ret) {
++ final Predicate<? super PoiRecord> occupancyFilter = occupancy.getTest();
++
++ final double maxDistanceSquared = maxDistance * maxDistance;
++ 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.world);
++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4;
++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4;
++ final int upperY = WorldUtil.getMaxSection(poiStorage.world);
++ 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 LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
++ queue.enqueue(CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ));
++ final LongOpenHashSet seen = new LongOpenHashSet();
++
++ 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.orElse(null);
++
++ final Map<PoiType, Set<PoiRecord>> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry<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<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<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<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(record.getPos());
++ }
++ }
++
++ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage,
++ final Predicate<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<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.world), 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.world), 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<PoiType, Set<PoiRecord>> sectionData = poiSection.getData();
++ if (sectionData.isEmpty()) {
++ continue;
++ }
++
++ // now we search the section data
++ for (final Map.Entry<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 84a0ee595bebcc1947c602c4c06e7437706ce37c..afbb2acd27416c801af3d718850b82a170734cd3 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
+@@ -83,7 +83,11 @@ public class AcquirePoi extends Behavior<PathfinderMob> {
+ return true;
+ }
+ };
+- Set<BlockPos> set = poiManager.findAllClosestFirst(this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE).limit(5L).collect(Collectors.toSet());
++ // Paper start - optimise POI access
++ java.util.List<BlockPos> poiposes = new java.util.ArrayList<>();
++ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, this.poiType.getPredicate(), predicate, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes);
++ Set<BlockPos> set = new java.util.HashSet<>(poiposes);
++ // Paper end - optimise POI access
+ Path path = entity.getNavigation().createPath(set, this.poiType.getValidRange());
+ 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 0eea3e39616e40e15d1662b973c097cda3b2cee7..3ccc1421f4a5a08dadb9fe3c9fa3ac3131e6ba1e 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
+@@ -49,8 +49,12 @@ public class NearestBedSensor extends Sensor<Mob> {
+ return true;
+ }
+ };
+- Stream<BlockPos> stream = poiManager.findAll(PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY);
+- Path path = entity.getNavigation().createPath(stream, PoiType.HOME.getValidRange());
++ // Paper start - optimise POI access
++ java.util.List<BlockPos> poiposes = new java.util.ArrayList<>();
++ // don't ask me why it's unbounded. ask mojang.
++ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, PoiType.HOME.getPredicate(), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes);
++ Path path = entity.getNavigation().createPath(new java.util.HashSet<>(poiposes), PoiType.HOME.getValidRange());
++ // Paper end - optimise POI access
+ if (path != null && path.canReach()) {
+ BlockPos blockPos = path.getTarget();
+ Optional<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 4a972b26242cf4c9d7e8f655cb1264cddad5f143..8a569e3300543cb171c3befae59969628adc424c 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
+@@ -37,7 +37,7 @@ public class PoiManager extends SectionStorage<PoiSection> {
+ public static final int VILLAGE_SECTION_SIZE = 1;
+ private final PoiManager.DistanceTracker distanceTracker;
+ private final LongSet loadedChunks = new LongOpenHashSet();
+- private final net.minecraft.server.level.ServerLevel world; // Paper
++ public final net.minecraft.server.level.ServerLevel world; // Paper // Paper public
+
+ public PoiManager(Path path, DataFixer dataFixer, boolean dsync, LevelHeightAccessor world) {
+ super(path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, world);
+@@ -100,36 +100,55 @@ public class PoiManager extends SectionStorage<PoiSection> {
+ }
+
+ public Optional<BlockPos> find(Predicate<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 - re-route to faster logic
+ }
+
+ public Optional<BlockPos> findClosest(Predicate<PoiType> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) {
+- return this.getInRange(typePredicate, pos, radius, occupationStatus).map(PoiRecord::getPos).min(Comparator.comparingDouble((blockPos2) -> {
+- return blockPos2.distSqr(pos);
+- }));
++ // Paper start - re-route to faster logic
++ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius*radius, occupationStatus, false);
++ return Optional.ofNullable(ret);
++ // Paper end - re-route to faster logic
+ }
+
+ public Optional<BlockPos> findClosest(Predicate<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((blockPos2) -> {
+- return blockPos2.distSqr(pos);
+- }));
++ // Paper start - re-route to faster logic
++ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false);
++ return Optional.ofNullable(ret);
++ // Paper end - re-route to faster logic
+ }
+
+ public Optional<BlockPos> take(Predicate<PoiType> typePredicate, Predicate<BlockPos> positionPredicate, BlockPos pos, int radius) {
+- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE).filter((poi) -> {
+- return positionPredicate.test(poi.getPos());
+- }).findFirst().map((poi) -> {
+- poi.acquireTicket();
+- return poi.getPos();
+- });
++ // Paper start - re-route to faster logic
++ PoiRecord ret = io.papermc.paper.util.PoiAccess.findAnyPoiRecord(
++ this, typePredicate, positionPredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE, false
++ );
++ if (ret == null) {
++ return Optional.empty();
++ }
++ ret.acquireTicket();
++ return Optional.of(ret.getPos());
++ // Paper end - re-route to faster logic
+ }
+
+ public Optional<BlockPos> getRandom(Predicate<PoiType> typePredicate, Predicate<BlockPos> positionPredicate, PoiManager.Occupancy occupationStatus, BlockPos pos, int radius, Random random) {
+- List<PoiRecord> list = this.getInRange(typePredicate, pos, radius, occupationStatus).collect(Collectors.toList());
+- Collections.shuffle(list, random);
+- return list.stream().filter((poi) -> {
+- return 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 63f283f32bdad02299d4a16c305a28c3bfbce9a8..de94f25792261c6c89986ad3dee3255c2a89357b 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
+@@ -25,7 +25,7 @@ import org.apache.logging.log4j.Logger;
+ public class PoiSection {
+ private static final Logger LOGGER = LogManager.getLogger();
+ private final Short2ObjectMap<PoiRecord> records = new Short2ObjectOpenHashMap<>();
+- private final Map<PoiType, Set<PoiRecord>> byType = Maps.newHashMap();
++ private final Map<PoiType, Set<PoiRecord>> byType = Maps.newHashMap(); public final Map<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 ff6cadec530dedf9efc5d6226e48a096a1073ad6..d73b99d7fde724da4503b5176c3ad7b013197c6a 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
+@@ -61,11 +61,11 @@ public class SectionStorage<R> extends RegionFileStorage implements AutoCloseabl
+ }
+
+ @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 ed79058696eb26a89b9d4116821840dbad9ea449..d990d1652b71205816d678618bf360a60f309ad2 100644
+--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java
+@@ -51,18 +51,40 @@ public class PortalForcer {
+ // int i = flag ? 16 : 128;
+ // CraftBukkit end
+
+- villageplace.ensureLoadedAndValid(this.level, blockposition, i);
+- Optional<PoiRecord> optional = villageplace.getInSquare((villageplacetype) -> {
+- return villageplacetype == PoiType.NETHER_PORTAL;
+- }, blockposition, i, PoiManager.Occupancy.ANY).filter((villageplacerecord) -> {
+- return worldborder.isWithinBounds(villageplacerecord.getPos());
+- }).sorted(Comparator.comparingDouble((PoiRecord villageplacerecord) -> { // CraftBukkit - decompile error
+- return villageplacerecord.getPos().distSqr(blockposition);
+- }).thenComparingInt((villageplacerecord) -> {
+- return villageplacerecord.getPos().getY();
+- })).filter((villageplacerecord) -> {
+- return this.level.getBlockState(villageplacerecord.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS);
+- }).findFirst();
++ // Paper start - optimise portals
++ Optional<PoiRecord> optional;
++ java.util.List<PoiRecord> records = new java.util.ArrayList<>();
++ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords(
++ villageplace,
++ (PoiType type) -> {
++ return type == PoiType.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.ChunkStatus.EMPTY);
++ if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.FULL)) {
++ // why would we generate the chunk?
++ return false;
++ }
++ if (!worldborder.isWithinBounds(pos)) {
++ 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.
++ PoiRecord lowestYRecord = null;
++ for (PoiRecord record : records) {
++ if (lowestYRecord == null) {
++ lowestYRecord = record;
++ } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) {
++ lowestYRecord = record;
++ }
++ }
++ // now we're done
++ optional = Optional.ofNullable(lowestYRecord);
++ // Paper end - optimise portals
+
+ return optional.map((villageplacerecord) -> {
+ BlockPos blockposition1 = villageplacerecord.getPos();