aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server')
-rw-r--r--patches/server/1049-Incremental-chunk-and-player-saving.patch139
-rw-r--r--patches/server/1050-Optimise-general-POI-access.patch1061
-rw-r--r--patches/server/1051-Fix-entity-tracker-desync-when-new-players-are-added.patch105
-rw-r--r--patches/server/1052-Lag-compensation-ticks.patch129
-rw-r--r--patches/server/1053-Optimise-collision-checking-in-player-move-packet-ha.patch168
5 files changed, 1602 insertions, 0 deletions
diff --git a/patches/server/1049-Incremental-chunk-and-player-saving.patch b/patches/server/1049-Incremental-chunk-and-player-saving.patch
new file mode 100644
index 0000000000..9a12860c6a
--- /dev/null
+++ b/patches/server/1049-Incremental-chunk-and-player-saving.patch
@@ -0,0 +1,139 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Shane Freeder <[email protected]>
+Date: Sun, 9 Jun 2019 03:53:22 +0100
+Subject: [PATCH] Incremental chunk and player saving
+
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index de80ac827c8ac3630d68b73cb425d4b56f7d2cd7..f422cbcb69d6fda2b4e229cbdbf10abd0d36d6f9 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -1009,7 +1009,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ try {
+ this.isSaving = true;
+- this.getPlayerList().saveAll();
++ this.getPlayerList().saveAll(); // Paper - Incremental chunk and player saving; diff on change
+ flag3 = this.saveAllChunks(suppressLogs, flush, force);
+ } finally {
+ this.isSaving = false;
+@@ -1676,9 +1676,29 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ }
+
+ --this.ticksUntilAutosave;
+- if (this.autosavePeriod > 0 && this.ticksUntilAutosave <= 0) { // CraftBukkit
+- this.autoSave();
++ // Paper start - Incremental chunk and player saving
++ final ProfilerFiller profiler = Profiler.get();
++ int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate;
++ if (playerSaveInterval < 0) {
++ playerSaveInterval = autosavePeriod;
++ }
++ profiler.push("save");
++ final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0;
++ try {
++ this.isSaving = true;
++ if (playerSaveInterval > 0) {
++ this.playerList.saveAll(playerSaveInterval);
++ }
++ for (final ServerLevel level : this.getAllLevels()) {
++ if (level.paperConfig().chunks.autoSaveInterval.value() > 0) {
++ level.saveIncrementally(fullSave);
++ }
++ }
++ } finally {
++ this.isSaving = false;
+ }
++ profiler.pop();
++ // Paper end - Incremental chunk and player saving
+
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index a7420e4522e0dff72ce7f8a791b9cd4bfa270106..fd07824ff6a928ca6e2f56477a63bac7aaeb8c15 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -1371,6 +1371,35 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos);
+ }
+
++ // Paper start - Incremental chunk and player saving
++ public void saveIncrementally(boolean doFull) {
++ ServerChunkCache chunkproviderserver = this.getChunkSource();
++
++ if (doFull) {
++ org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld()));
++ }
++
++ try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) {
++ if (doFull) {
++ this.saveLevelData(true);
++ }
++
++ // chunk autosave is already called by the ChunkSystem during unload processing (ChunkMap#processUnloads)
++
++ // Copied from save()
++ // CraftBukkit start - moved from MinecraftServer.saveChunks
++ if (doFull) { // Paper
++ ServerLevel worldserver1 = this;
++
++ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings());
++ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess()));
++ this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
++ }
++ // CraftBukkit end
++ }
++ }
++ // Paper end - Incremental chunk and player saving
++
+ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
+ // Paper start - add close param
+ this.save(progressListener, flush, savingDisabled, false);
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 8ceeebb561046933cba0725e15732fa074226884..8c9148426f23cbbdfaf7ae66657d1a620f8bd853 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -221,6 +221,7 @@ import org.bukkit.inventory.MainHand;
+ public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
+
+ private static final Logger LOGGER = LogUtils.getLogger();
++ public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving
+ private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
+ private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
+ private static final int FLY_STAT_RECORDING_SPEED = 25;
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 3642444d45038fd1a07768ff96bfbd8678b02e04..f8f8e8f602f416fe97fc23ef6efeee7af2749292 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -519,6 +519,7 @@ public abstract class PlayerList {
+
+ protected void save(ServerPlayer player) {
+ if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
++ player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving
+ this.playerIo.save(player);
+ ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit
+
+@@ -1153,10 +1154,22 @@ public abstract class PlayerList {
+ }
+
+ public void saveAll() {
++ // Paper start - Incremental chunk and player saving
++ this.saveAll(-1);
++ }
++
++ public void saveAll(int interval) {
+ io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
+ MinecraftTimings.savePlayers.startTiming(); // Paper
++ int numSaved = 0;
++ long now = MinecraftServer.currentTick;
+ for (int i = 0; i < this.players.size(); ++i) {
+- this.save(this.players.get(i));
++ ServerPlayer entityplayer = this.players.get(i);
++ if (interval == -1 || now - entityplayer.lastSave >= interval) {
++ this.save(entityplayer);
++ if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; }
++ }
++ // Paper end - Incremental chunk and player saving
+ }
+ MinecraftTimings.savePlayers.stopTiming(); // Paper
+ return null; }); // Paper - ensure main
diff --git a/patches/server/1050-Optimise-general-POI-access.patch b/patches/server/1050-Optimise-general-POI-access.patch
new file mode 100644
index 0000000000..0e3d18cfa4
--- /dev/null
+++ b/patches/server/1050-Optimise-general-POI-access.patch
@@ -0,0 +1,1061 @@
+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 e8aa27547e3fa1a42720889c7038d4fb0273e7b5..e1b6fe9ecda25f86431baf414f1bfd3a26a8b2bd 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
+@@ -71,11 +71,11 @@ public class AcquirePoi {
+ return true;
+ }
+ };
+- Set<Pair<Holder<PoiType>, BlockPos>> set = poiManager.findAllClosestFirstWithType(
+- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE
+- )
+- .limit(5L)
+- .collect(Collectors.toSet());
++ // Paper start - optimise POI access
++ 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);
++ Set<Pair<Holder<PoiType>, BlockPos>> set = new java.util.HashSet<>(poiposes);
++ // 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) {
diff --git a/patches/server/1051-Fix-entity-tracker-desync-when-new-players-are-added.patch b/patches/server/1051-Fix-entity-tracker-desync-when-new-players-are-added.patch
new file mode 100644
index 0000000000..6aecc462a4
--- /dev/null
+++ b/patches/server/1051-Fix-entity-tracker-desync-when-new-players-are-added.patch
@@ -0,0 +1,105 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <[email protected]>
+Date: Tue, 20 Feb 2024 18:24:16 -0800
+Subject: [PATCH] Fix entity tracker desync when new players are added to the
+ tracker
+
+The delta position packet instructs the client to update
+the entity position by a position difference. However, this position
+difference is relative to the last position in the entity tracker
+state, not the last position which has been sent to the player. As
+a result, if the last position the player has recorded is different
+than the one stored in the entity tracker (which occurs when a new
+player is added to an existing entity tracker state) then the sent
+position difference will cause a position desync for the client.
+
+We can resolve this problem by either tracking the last position
+sent per-player, or by simply resetting the last sent position
+in the entity tracker state every time a new player is added.
+Resetting the last sent position every time a new player is
+added to the tracker is just easier to do, so that is what
+this patch does.
+
+This patch also fixes entities appearing to disappear when
+teleporting to players by changing the initial position
+in the spawn packet to the entities current tracking position.
+When teleporting, the spawn packet will contain the old position
+which is most likely in an unloaded chunk - which means that the
+client will not tick the entity and thus not lerp the entity
+from its old position to its new position.
+
+diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
+index f6e1deb2f849d8b01b15cfa69e2f6cd5f2b1512b..f66e40326c510aa3267542b1a24ed75d1ed6d3f1 100644
+--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundAddEntityPacket.java
+@@ -42,9 +42,11 @@ public class ClientboundAddEntityPacket implements Packet<ClientGamePacketListen
+ this(
+ entity.getId(),
+ entity.getUUID(),
+- entityTrackerEntry.getPositionBase().x(),
+- entityTrackerEntry.getPositionBase().y(),
+- entityTrackerEntry.getPositionBase().z(),
++ // Paper start - fix entity tracker desync
++ entity.trackingPosition().x(),
++ entity.trackingPosition().y(),
++ entity.trackingPosition().z(),
++ // Paper end - fix entity tracker desync
+ entityTrackerEntry.getLastSentXRot(),
+ entityTrackerEntry.getLastSentYRot(),
+ entity.getType(),
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index d692af061ded8cd5bcf1d268e6bd521d84f99c39..bf43bdb43c5301c0e0954729bc531fb6a5045075 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -1277,6 +1277,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.serverEntity.addPairing(player);
+ }
+ // Paper end - entity tracking events
++ this.serverEntity.onPlayerAdd(); // Paper - fix desync when a player is added to the tracker
+ }
+ } else if (this.seenBy.remove(player.connection)) {
+ this.serverEntity.removePairing(player);
+diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
+index 5bbc7ceaafc163f12344e5d5d355ad2ff30ddca2..90eb4927fa51ce3df86aa7b6c71f49150a03e337 100644
+--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
+@@ -100,6 +100,13 @@ public class ServerEntity {
+ this.trackedDataValues = entity.getEntityData().getNonDefaultValues();
+ }
+
++ // Paper start - fix desync when a player is added to the tracker
++ private boolean forceStateResync;
++ public void onPlayerAdd() {
++ this.forceStateResync = true;
++ }
++ // Paper end - fix desync when a player is added to the tracker
++
+ public void sendChanges() {
+ // Paper start - optimise collisions
+ if (((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)this.entity).moonrise$isHardColliding()) {
+@@ -149,7 +156,7 @@ public class ServerEntity {
+ }
+ }
+
+- if (this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isDirty()) {
++ if (this.forceStateResync || this.tickCount % this.updateInterval == 0 || this.entity.hasImpulse || this.entity.getEntityData().isDirty()) { // Paper - fix desync when a player is added to the tracker
+ byte b0 = Mth.packDegrees(this.entity.getYRot());
+ byte b1 = Mth.packDegrees(this.entity.getXRot());
+ boolean flag = Math.abs(b0 - this.lastSentYRot) >= 1 || Math.abs(b1 - this.lastSentXRot) >= 1;
+@@ -199,7 +206,7 @@ public class ServerEntity {
+ long k = this.positionCodec.encodeZ(vec3d);
+ boolean flag5 = i < -32768L || i > 32767L || j < -32768L || j > 32767L || k < -32768L || k > 32767L;
+
+- if (!flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) {
++ if (!this.forceStateResync && !flag5 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { // Paper - fix desync when a player is added to the tracker
+ if ((!flag2 || !flag) && !(this.entity instanceof AbstractArrow)) {
+ if (flag2) {
+ packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) i), (short) ((int) j), (short) ((int) k), this.entity.onGround());
+@@ -265,6 +272,7 @@ public class ServerEntity {
+ }
+
+ this.entity.hasImpulse = false;
++ this.forceStateResync = false; // Paper - fix desync when a player is added to the tracker
+ }
+
+ ++this.tickCount;
diff --git a/patches/server/1052-Lag-compensation-ticks.patch b/patches/server/1052-Lag-compensation-ticks.patch
new file mode 100644
index 0000000000..2643c943aa
--- /dev/null
+++ b/patches/server/1052-Lag-compensation-ticks.patch
@@ -0,0 +1,129 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <[email protected]>
+Date: Sat, 23 Sep 2023 22:05:35 -0700
+Subject: [PATCH] Lag compensation ticks
+
+Areas affected by lag comepnsation:
+ - Block breaking and destroying
+ - Eating food items
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index f422cbcb69d6fda2b4e229cbdbf10abd0d36d6f9..73855f4555f781741f70267be65dec1ebb98b46a 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -332,6 +332,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+
+ public volatile Thread shutdownThread; // Paper
+ public volatile boolean abnormalExit = false; // Paper
++ public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation
+
+ public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
+ AtomicReference<S> atomicreference = new AtomicReference();
+@@ -1874,6 +1875,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+ worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
+ worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
+ net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers
++ worldserver.updateLagCompensationTick(); // Paper - lag compensation
+
+ gameprofilerfiller.push(() -> {
+ String s = String.valueOf(worldserver);
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index fd07824ff6a928ca6e2f56477a63bac7aaeb8c15..b2bbc9f3efbb7c949cc862eeee5d5f47be5d804f 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -577,6 +577,17 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ );
+ }
+ // Paper end - chunk tick iteration
++ // Paper start - lag compensation
++ private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT;
++
++ public long getLagCompensationTick() {
++ return this.lagCompensationTick;
++ }
++
++ public void updateLagCompensationTick() {
++ this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L));
++ }
++ // Paper end - lag compensation
+
+ // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
+ public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
+index 504c996220b278c194c93e001a3b326d549868ec..a96f859a5d0c6ec692d4627a69f3c9ee49199dbc 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
+@@ -127,7 +127,7 @@ public class ServerPlayerGameMode {
+ }
+
+ public void tick() {
+- this.gameTicks = MinecraftServer.currentTick; // CraftBukkit;
++ this.gameTicks = (int)this.level.getLagCompensationTick(); // CraftBukkit; // Paper - lag compensation
+ BlockState iblockdata;
+
+ if (this.hasDelayedDestroy) {
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index 22b3d3d945cbddae25abfca7d900324c79d32293..a68ca22d5f8909d2ad37feded448f777736bf7db 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -4052,6 +4052,10 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer);
+ }
+ // Paper end - Properly cancel usable items
++ // Paper start - lag compensate eating
++ protected long eatStartTime;
++ protected int totalEatTimeTicks;
++ // Paper end - lag compensate eating
+ private void updatingUsingItem() {
+ if (this.isUsingItem()) {
+ if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) {
+@@ -4066,7 +4070,12 @@ public abstract class LivingEntity extends Entity implements Attackable {
+
+ protected void updateUsingItem(ItemStack stack) {
+ stack.onUseTick(this.level(), this, this.getUseItemRemainingTicks());
+- if (--this.useItemRemaining == 0 && !this.level().isClientSide && !stack.useOnRelease()) {
++ // Paper start - lag compensate eating
++ // we add 1 to the expected time to avoid lag compensating when we should not
++ final boolean shouldLagCompensate = this.useItem.has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1L + this.totalEatTimeTicks) * 50L * (1000L * 1000L));
++ if ((--this.useItemRemaining == 0 || shouldLagCompensate) && !this.level().isClientSide && !stack.useOnRelease()) {
++ this.useItemRemaining = 0;
++ // Paper end - lag compensate eating
+ this.completeUsingItem();
+ }
+
+@@ -4104,7 +4113,10 @@ public abstract class LivingEntity extends Entity implements Attackable {
+
+ if (!itemstack.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack
+ this.useItem = itemstack;
+- this.useItemRemaining = itemstack.getUseDuration(this);
++ // Paper start - lag compensate eating
++ this.useItemRemaining = this.totalEatTimeTicks = itemstack.getUseDuration(this);
++ this.eatStartTime = System.nanoTime();
++ // Paper end - lag compensate eating
+ if (!this.level().isClientSide) {
+ this.setLivingEntityFlag(1, true);
+ this.setLivingEntityFlag(2, hand == InteractionHand.OFF_HAND);
+@@ -4129,7 +4141,10 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+ } else if (!this.isUsingItem() && !this.useItem.isEmpty()) {
+ this.useItem = ItemStack.EMPTY;
+- this.useItemRemaining = 0;
++ // Paper start - lag compensate eating
++ this.useItemRemaining = this.totalEatTimeTicks = 0;
++ this.eatStartTime = -1L;
++ // Paper end - lag compensate eating
+ }
+ }
+
+@@ -4260,7 +4275,10 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+
+ this.useItem = ItemStack.EMPTY;
+- this.useItemRemaining = 0;
++ // Paper start - lag compensate eating
++ this.useItemRemaining = this.totalEatTimeTicks = 0;
++ this.eatStartTime = -1L;
++ // Paper end - lag compensate eating
+ }
+
+ public boolean isBlocking() {
diff --git a/patches/server/1053-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/server/1053-Optimise-collision-checking-in-player-move-packet-ha.patch
new file mode 100644
index 0000000000..a75038dc8a
--- /dev/null
+++ b/patches/server/1053-Optimise-collision-checking-in-player-move-packet-ha.patch
@@ -0,0 +1,168 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <[email protected]>
+Date: Thu, 2 Jul 2020 12:02:43 -0700
+Subject: [PATCH] Optimise collision checking in player move packet handling
+
+Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision
+
+diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+index eef96e946b80064fe211039a65db4192ea7a52d3..c4b016a2fb5c79fb3f191e243712bee7cbe5cd2c 100644
+--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+@@ -577,7 +577,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ return;
+ }
+
+- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
++ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet
+
+ d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above
+ d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above
+@@ -593,6 +593,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+
+ entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8));
++ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be...
+ double d11 = d7;
+
+ d6 = d3 - entity.getX();
+@@ -606,15 +607,23 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ boolean flag2 = false;
+
+ if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
+- flag2 = true;
++ flag2 = true; // Paper - diff on change, this should be moved wrongly
+ ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)});
+ }
+
+ entity.absMoveTo(d3, d4, d5, f, f1);
+ this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+- boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
+
+- if (flag && (flag2 || !flag3)) {
++ // Paper start - optimise out extra getCubes
++ boolean teleportBack = flag2; // violating this is always a fail
++ if (!teleportBack) {
++ // note: only call after setLocation, or else getBoundingBox is wrong
++ AABB newBox = entity.getBoundingBox();
++ if (didCollide || !oldBox.equals(newBox)) {
++ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox);
++ } // else: no collision at all detected, why do we care?
++ }
++ if (teleportBack) { // Paper end - optimise out extra getCubes
+ entity.absMoveTo(d0, d1, d2, f, f1);
+ this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+ this.send(new ClientboundMoveVehiclePacket(entity));
+@@ -697,7 +706,32 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+
+ private boolean noBlocksAround(Entity entity) {
+- return entity.level().getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir);
++ // Paper start - stop using streams, this is already a known fixed problem in Entity#move
++ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D);
++ int minX = Mth.floor(box.minX);
++ int minY = Mth.floor(box.minY);
++ int minZ = Mth.floor(box.minZ);
++ int maxX = Mth.floor(box.maxX);
++ int maxY = Mth.floor(box.maxY);
++ int maxZ = Mth.floor(box.maxZ);
++
++ Level world = entity.level();
++ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
++
++ for (int y = minY; y <= maxY; ++y) {
++ for (int z = minZ; z <= maxZ; ++z) {
++ for (int x = minX; x <= maxX; ++x) {
++ pos.set(x, y, z);
++ BlockState type = world.getBlockStateIfLoaded(pos);
++ if (type != null && !type.isAir()) {
++ return false;
++ }
++ }
++ }
++ }
++
++ return true;
++ // Paper end - stop using streams, this is already a known fixed problem in Entity#move
+ }
+
+ @Override
+@@ -1398,7 +1432,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+ }
+
+- AABB axisalignedbb = this.player.getBoundingBox();
++ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB
+
+ d6 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above
+ d7 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above
+@@ -1440,6 +1474,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+
+ this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8));
+ this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move
++ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be...
+ // Paper start - prevent position desync
+ if (this.awaitingPositionFromClient != null) {
+ return; // ... thanks Mojang for letting move calls teleport across dimensions.
+@@ -1470,7 +1505,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+
+ // Paper start - Add fail move event
+- boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2));
++ // Paper start - optimise out extra getCubes
++ boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && movedWrongly;
++ this.player.absMoveTo(d0, d1, d2, f, f1); // prevent desync by tping to the set position, dropped for unknown reasons by mojang
++ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) {
++ AABB newBox = this.player.getBoundingBox();
++ if (didCollide || !axisalignedbb.equals(newBox)) {
++ // note: only call after setLocation, or else getBoundingBox is wrong
++ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox);
++ } // else: no collision at all detected, why do we care?
++ }
++ // Paper end - optimise out extra getCubes
+ if (teleportBack) {
+ io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK,
+ toX, toY, toZ, toYaw, toPitch, false);
+@@ -1594,7 +1639,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+
+ private boolean updateAwaitingTeleport() {
+ if (this.awaitingPositionFromClient != null) {
+- if (this.tickCount - this.awaitingTeleportTime > 20) {
++ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT
+ this.awaitingTeleportTime = this.tickCount;
+ this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot());
+ }
+@@ -1607,6 +1652,33 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
+ }
+ }
+
++ // Paper start - optimise out extra getCubes
++ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) {
++ final List<AABB> collisionsBB = new java.util.ArrayList<>();
++ final List<VoxelShape> collisionsVoxel = new java.util.ArrayList<>();
++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions(
++ world, entity, newBox, collisionsVoxel, collisionsBB,
++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
++ null, null
++ );
++
++ for (int i = 0, len = collisionsBB.size(); i < len; ++i) {
++ final AABB box = collisionsBB.get(i);
++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersect(box, oldBox)) {
++ return true;
++ }
++ }
++
++ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) {
++ final VoxelShape voxel = collisionsVoxel.get(i);
++ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) {
++ return true;
++ }
++ }
++
++ return false;
++ }
++ // Paper end - optimise out extra getCubes
+ private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) {
+ AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ());
+ Iterable<VoxelShape> iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D));