diff options
Diffstat (limited to 'patches/server')
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)); |