diff --git a/patches/api/Add-view-distance-API.patch b/patches/api/Add-view-distance-API.patch
index 35fbf40515..02b12e6cb2 100644
--- a/patches/api/Add-view-distance-API.patch
+++ b/patches/api/Add-view-distance-API.patch
@@ -23,13 +23,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ void setViewDistance(int viewDistance);
+
+ /**
++ * Sets the simulation distance for this world.
++ * @param simulationDistance simulation distance in [2, 32]
++ */
++ void setSimulationDistance(int simulationDistance);
++
++ /**
+ * Returns the no-tick view distance for this world.
+ *
+ * No-tick view distance is the view distance where chunks will load, however the chunks and their entities will not
+ * be set to tick.
+ *
+ * @return The no-tick view distance for this world.
++ * @deprecated Use {@link #getViewDistance()}
+ */
++ @Deprecated
+ int getNoTickViewDistance();
+
+ /**
@@ -39,7 +47,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ * be set to tick.
+ *
+ * @param viewDistance view distance in [2, 32]
++ * @deprecated Use {@link #setViewDistance(int)}
+ */
++ @Deprecated
+ void setNoTickViewDistance(int viewDistance);
+
+ /**
@@ -49,7 +59,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ *
+ * @return The sending view distance for this world.
+ */
-+ public int getSendViewDistance();
++ int getSendViewDistance();
+
+ /**
+ * Sets the sending view distance for this world.
@@ -58,7 +68,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ *
+ * @param viewDistance view distance in [2, 32] or -1
+ */
-+ public void setSendViewDistance(int viewDistance);
++ void setSendViewDistance(int viewDistance);
+ // Paper end - view distance api
+
// Spigot start
@@ -78,7 +88,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ *
+ * @return the player's view distance
+ * @see org.bukkit.World#getViewDistance()
-+ * @see org.bukkit.World#getNoTickViewDistance()
+ */
+ public int getViewDistance();
+
@@ -87,9 +96,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ *
+ * @param viewDistance the player's view distance
+ * @see org.bukkit.World#setViewDistance(int)
-+ * @see org.bukkit.World#setNoTickViewDistance(int)
+ */
+ public void setViewDistance(int viewDistance);
++
++ /**
++ * Gets the simulation distance for this player
++ *
++ * @return the player's simulation distance
++ */
++ public int getSimulationDistance();
++
++ /**
++ * Sets the simulation distance for this player
++ *
++ * @param simulationDistance the player's new simulation distance
++ */
++ public void setSimulationDistance(int simulationDistance);
+
+ /**
+ * Gets the no-ticking view distance for this player.
@@ -98,7 +120,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ * be set to tick.
+ *
+ * @return The no-tick view distance for this player.
++ * @deprecated Use {@link #getViewDistance()}
+ */
++ @Deprecated
+ public int getNoTickViewDistance();
+
+ /**
@@ -108,7 +132,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ * be set to tick.
+ *
+ * @param viewDistance view distance in [2, 32] or -1
++ * @deprecated Use {@link #setViewDistance(int)}
+ */
++ @Deprecated
+ public void setNoTickViewDistance(int viewDistance);
+
+ /**
diff --git a/patches/server/Add-tick-times-API-and-mspt-command.patch b/patches/server/Add-tick-times-API-and-mspt-command.patch
index d8122d0d36..229555dc78 100644
--- a/patches/server/Add-tick-times-API-and-mspt-command.patch
+++ b/patches/server/Add-tick-times-API-and-mspt-command.patch
@@ -84,8 +84,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
commands.put("paper", new PaperCommand("paper"));
+ commands.put("mspt", new MSPTCommand("mspt"));
- version = getInt("config-version", 24);
- set("config-version", 24);
+ version = getInt("config-version", 25);
+ set("config-version", 25);
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
diff --git a/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch b/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch
index 1948c9649a..f326b3dcdc 100644
--- a/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch
+++ b/patches/server/Consolidate-flush-calls-for-entity-tracker-packets.patch
@@ -26,9 +26,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
-
- // Paper - no, iterating just ONCE is expensive enough! Don't do it TWICE! Code moved up
+ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
gameprofilerfiller.pop();
+ // Paper end - use set of chunks requiring updates, rather than iterating every single one loaded
+ // Paper start - controlled flush for entity tracker packets
+ List disabledFlushes = new java.util.ArrayList<>(this.level.players.size());
+ for (ServerPlayer player : this.level.players) {
diff --git a/patches/server/Do-not-copy-visible-chunks.patch b/patches/server/Do-not-copy-visible-chunks.patch
index 493affd3b6..aab2372150 100644
--- a/patches/server/Do-not-copy-visible-chunks.patch
+++ b/patches/server/Do-not-copy-visible-chunks.patch
@@ -116,7 +116,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
if (playerchunk != null) {
- this.pendingUnloads.put(j, playerchunk);
+ playerchunk.onChunkRemove(); // Paper
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
if (!this.modified) {
return false;
diff --git a/patches/server/Execute-chunk-tasks-fairly-for-worlds-while-waiting-.patch b/patches/server/Execute-chunk-tasks-fairly-for-worlds-while-waiting-.patch
new file mode 100644
index 0000000000..b20a028e48
--- /dev/null
+++ b/patches/server/Execute-chunk-tasks-fairly-for-worlds-while-waiting-.patch
@@ -0,0 +1,37 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf
+Date: Tue, 28 Dec 2021 07:19:01 -0800
+Subject: [PATCH] Execute chunk tasks fairly for worlds while waiting for next
+ tick
+
+Currently, only the first world would have had tasks executed.
+This might result in chunks loading far slower in the nether,
+for example.
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop into, final boolean loadChunks, final boolean collidesWithUnloaded,
-+ final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) {
++ final List into, final boolean loadChunks, final boolean collidesWithUnloaded,
++ final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) {
+ boolean ret = false;
+
+ if (checkBorder) {
@@ -486,21 +495,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+ }
+
-+ int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
-+ int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
++ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
++ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
+
-+ int minBlockY = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1;
-+ int maxBlockY = Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1;
++ final int minBlockY = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1;
++ final int maxBlockY = Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1;
+
-+ int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
-+ int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
++ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
++ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
+
+ final int minSection = WorldUtil.getMinSection(getter);
+ final int maxSection = WorldUtil.getMaxSection(getter);
+ final int minBlock = minSection << 4;
+ final int maxBlock = (maxSection << 4) | 15;
+
-+ BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
+ CollisionContext collisionShape = null;
+
+ // special cases:
@@ -509,16 +518,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return ret;
+ }
+
-+ int minYIterate = Math.max(minBlock, minBlockY);
-+ int maxYIterate = Math.min(maxBlock, maxBlockY);
++ final int minYIterate = Math.max(minBlock, minBlockY);
++ final int maxYIterate = Math.min(maxBlock, maxBlockY);
+
-+ int minChunkX = minBlockX >> 4;
-+ int maxChunkX = maxBlockX >> 4;
++ final int minChunkX = minBlockX >> 4;
++ final int maxChunkX = maxBlockX >> 4;
+
-+ int minChunkZ = minBlockZ >> 4;
-+ int maxChunkZ = maxBlockZ >> 4;
++ final int minChunkY = minBlockY >> 4;
++ final int maxChunkY = maxBlockY >> 4;
+
-+ ServerChunkCache chunkProvider;
++ final int minChunkYIterate = minYIterate >> 4;
++ final int maxChunkYIterate = maxYIterate >> 4;
++
++ final int minChunkZ = minBlockZ >> 4;
++ final int maxChunkZ = maxBlockZ >> 4;
++
++ final ServerChunkCache chunkProvider;
+ if (getter instanceof WorldGenRegion) {
+ chunkProvider = null;
+ } else if (getter instanceof ServerLevel) {
@@ -526,26 +541,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ } else {
+ chunkProvider = null;
+ }
-+ // TODO special case single chunk?
+
+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
-+ int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk
-+ int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk
++ final int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk
++ final int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk
+
+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk
-+ int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk
++ final int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk
++ final int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk
+
-+ int chunkXGlobalPos = currChunkX << 4;
-+ int chunkZGlobalPos = currChunkZ << 4;
-+ ChunkAccess chunk;
++ final int chunkXGlobalPos = currChunkX << 4;
++ final int chunkZGlobalPos = currChunkZ << 4;
++ final ChunkAccess chunk;
+ if (chunkProvider == null) {
+ chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ);
+ } else {
+ chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
+ }
+
-+
+ if (chunk == null) {
+ if (collidesWithUnloaded) {
+ if (checkOnly) {
@@ -558,59 +571,306 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ continue;
+ }
+
-+ LevelChunkSection[] sections = chunk.getSections();
++ final LevelChunkSection[] sections = chunk.getSections();
+
+ // bound y
+
-+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
-+ LevelChunkSection section = sections[(currY >> 4) - minSection];
-+ if (section.hasOnlyAir()) {
++ for (int currChunkY = minChunkYIterate; currChunkY <= maxChunkYIterate; ++currChunkY) {
++ final LevelChunkSection section = sections[currChunkY - minSection];
++ if (section == null || section.hasOnlyAir()) {
+ // empty
-+ // skip to next section
-+ currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one
+ continue;
+ }
++ final PalettedContainer blocks = section.states;
+
-+ net.minecraft.world.level.chunk.PalettedContainer blocks = section.states;
++ final int minY = currChunkY == minChunkYIterate ? minYIterate & 15 : 0; // coordinate in chunk
++ final int maxY = currChunkY == maxChunkYIterate ? maxYIterate & 15 : 15; // coordinate in chunk
++ final int chunkYGlobalPos = currChunkY << 4;
+
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8);
-+ int blockX = currX | chunkXGlobalPos;
-+ int blockY = currY;
-+ int blockZ = currZ | chunkZGlobalPos;
++ final boolean sectionHasSpecial = section.hasSpecialCollidingBlocks();
+
-+ int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
-+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
-+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0);
-+ if (edgeCount == 3) {
++ final int minXIterate;
++ final int maxXIterate;
++ final int minZIterate;
++ final int maxZIterate;
++ final int minYIterateLocal;
++ final int maxYIterateLocal;
++
++ if (!sectionHasSpecial) {
++ minXIterate = currChunkX == minChunkX ? minX + 1 : minX;
++ maxXIterate = currChunkX == maxChunkX ? maxX - 1 : maxX;
++ minZIterate = currChunkZ == minChunkZ ? minZ + 1 : minZ;
++ maxZIterate = currChunkZ == maxChunkZ ? maxZ - 1 : maxZ;
++ minYIterateLocal = currChunkY == minChunkY ? minY + 1 : minY;
++ maxYIterateLocal = currChunkY == maxChunkY ? maxY - 1 : maxY;
++ if (minXIterate > maxXIterate || minZIterate > maxZIterate) {
++ continue;
++ }
++ } else {
++ minXIterate = minX;
++ maxXIterate = maxX;
++ minZIterate = minZ;
++ maxZIterate = maxZ;
++ minYIterateLocal = minY;
++ maxYIterateLocal = maxY;
++ }
++
++ for (int currY = minYIterateLocal; currY <= maxYIterateLocal; ++currY) {
++ long collisionForHorizontal = section.getKnownBlockInfoHorizontalRaw(currY, minZIterate & 15);
++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ,
++ collisionForHorizontal = (currZ & 1) == 0 ? section.getKnownBlockInfoHorizontalRaw(currY, currZ & 15) : collisionForHorizontal) {
++ // From getKnownBlockInfoHorizontalRaw:
++ // important detail: this returns 32 values, one for localZ = localZ & (~1) and one for localZ = localZ | 1
++ // the even localZ is the lower 32 bits, the odd is the upper 32 bits
++ // We want to use a bitset to only iterate over non-empty blocks.
++ // We need to build a bitset mask to and out the other collisions we just don't care at all about
++ // First, we need to build a bitset from 0..n*2 where n is the number of blocks on the x axis
++ // It's important to note that the iterate values can be outside [0, 15], but if they are,
++ // then none of the x or z loops would meet their conditions. So we can assume they are never
++ // out of bounds here
++ final int xAxisBits = (maxXIterate - minXIterate + 1) << 1; // << 1 -> * 2 // Never > 32
++ long bitset = (1L << xAxisBits) - 1;
++ // Now we need to offset it by 32 bits if current Z is odd (lower 32 bits is 16 block infos for even z, upper is for odd)
++ int shift = (currZ & 1) << 5; // this will be a LEFT shift
++ // Now we need to offset shift so that the bitset first position is at minXIterate
++ shift += (minXIterate << 1); // 0th pos -> 0th bit, 1st pos -> 2nd bit, ...
++
++ // all done
++ bitset = bitset << shift;
++ if ((collisionForHorizontal & bitset) == 0L) {
++ // All empty
+ continue;
+ }
++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
++ final int localBlockIndex = (currX) | (currZ << 4) | (currY << 8);
+
-+ BlockState blockData = blocks.get(localBlockIndex);
-+ if (blockData.isAir()) {
-+ continue;
-+ }
++ final int blockInfo = (int) LevelChunkSection.getKnownBlockInfo(localBlockIndex, collisionForHorizontal);
+
-+ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) {
-+ mutablePos.set(blockX, blockY, blockZ);
-+ if (collisionShape == null) {
-+ collisionShape = new LazyEntityCollisionContext(entity);
-+ }
-+ VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape);
-+ if (voxelshape2 != Shapes.empty()) {
-+ VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ);
-+
-+ if (predicate != null && !predicate.test(blockData, mutablePos)) {
++ switch (blockInfo) {
++ case (int) CollisionUtil.KNOWN_EMPTY_BLOCK: {
+ continue;
+ }
-+
-+ if (checkOnly) {
-+ if (voxelshape3.intersects(aabb)) {
-+ return true;
++ case (int) CollisionUtil.KNOWN_FULL_BLOCK: {
++ double blockX = (double)(currX | chunkXGlobalPos);
++ double blockY = (double)(currY | chunkYGlobalPos);
++ double blockZ = (double)(currZ | chunkZGlobalPos);
++ final AABB blockBox = new AABB(
++ blockX, blockY, blockZ,
++ blockX + 1.0, blockY + 1.0, blockZ + 1.0,
++ true
++ );
++ if (predicate != null) {
++ if (!voxelShapeIntersect(aabb, blockBox)) {
++ continue;
++ }
++ // fall through to get the block for the predicate
++ } else {
++ if (voxelShapeIntersect(aabb, blockBox)) {
++ if (checkOnly) {
++ return true;
++ } else {
++ into.add(blockBox);
++ ret = true;
++ }
++ }
++ continue;
++ }
++ }
++ // default: fall through to standard logic
++ }
++
++ int blockX = currX | chunkXGlobalPos;
++ int blockY = currY | chunkYGlobalPos;
++ int blockZ = currZ | chunkZGlobalPos;
++
++ int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
++ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
++ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0);
++ if (edgeCount == 3) {
++ continue;
++ }
++
++ BlockState blockData = blocks.get(localBlockIndex);
++
++ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) {
++ mutablePos.set(blockX, blockY, blockZ);
++ if (collisionShape == null) {
++ collisionShape = new LazyEntityCollisionContext(entity);
++ }
++ VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape);
++ if (voxelshape2 != Shapes.empty()) {
++ VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ);
++
++ if (predicate != null && !predicate.test(blockData, mutablePos)) {
++ continue;
++ }
++
++ if (checkOnly) {
++ if (voxelshape3.intersects(aabb)) {
++ return true;
++ }
++ } else {
++ ret |= addBoxesToIfIntersects(voxelshape3, aabb, into);
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++
++ return ret;
++ }
++
++ public static boolean getCollisionsForBlocksOrWorldBorderReference(final CollisionGetter getter, final Entity entity, final AABB aabb,
++ final List into, final boolean loadChunks, final boolean collidesWithUnloaded,
++ final boolean checkBorder, final boolean checkOnly, final BiPredicate predicate) {
++ boolean ret = false;
++
++ if (checkBorder) {
++ if (CollisionUtil.isAlmostCollidingOnBorder(getter.getWorldBorder(), aabb)) {
++ if (checkOnly) {
++ return true;
++ } else {
++ CollisionUtil.addBoxesTo(getter.getWorldBorder().getCollisionShape(), into);
++ ret = true;
++ }
++ }
++ }
++
++ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
++ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
++
++ final int minBlockY = Mth.floor(aabb.minY - COLLISION_EPSILON) - 1;
++ final int maxBlockY = Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1;
++
++ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
++ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
++
++ final int minSection = WorldUtil.getMinSection(getter);
++ final int maxSection = WorldUtil.getMaxSection(getter);
++ final int minBlock = minSection << 4;
++ final int maxBlock = (maxSection << 4) | 15;
++
++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
++ CollisionContext collisionShape = null;
++
++ // special cases:
++ if (minBlockY > maxBlock || maxBlockY < minBlock) {
++ // no point in checking
++ return ret;
++ }
++
++ final int minYIterate = Math.max(minBlock, minBlockY);
++ final int maxYIterate = Math.min(maxBlock, maxBlockY);
++
++ final int minChunkX = minBlockX >> 4;
++ final int maxChunkX = maxBlockX >> 4;
++
++ final int minChunkY = minBlockY >> 4;
++ final int maxChunkY = maxBlockY >> 4;
++
++ final int minChunkYIterate = minYIterate >> 4;
++ final int maxChunkYIterate = maxYIterate >> 4;
++
++ final int minChunkZ = minBlockZ >> 4;
++ final int maxChunkZ = maxBlockZ >> 4;
++
++ final ServerChunkCache chunkProvider;
++ if (getter instanceof WorldGenRegion) {
++ chunkProvider = null;
++ } else if (getter instanceof ServerLevel) {
++ chunkProvider = ((ServerLevel)getter).getChunkSource();
++ } else {
++ chunkProvider = null;
++ }
++
++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
++ final int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk
++ final int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk
++
++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
++ final int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk
++ final int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk
++
++ final int chunkXGlobalPos = currChunkX << 4;
++ final int chunkZGlobalPos = currChunkZ << 4;
++ final ChunkAccess chunk;
++ if (chunkProvider == null) {
++ chunk = (ChunkAccess)getter.getChunkForCollisions(currChunkX, currChunkZ);
++ } else {
++ chunk = loadChunks ? chunkProvider.getChunk(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
++ }
++
++ if (chunk == null) {
++ if (collidesWithUnloaded) {
++ if (checkOnly) {
++ return true;
++ } else {
++ into.add(getBoxForChunk(currChunkX, currChunkZ));
++ ret = true;
++ }
++ }
++ continue;
++ }
++
++ final LevelChunkSection[] sections = chunk.getSections();
++
++ // bound y
++ for (int currChunkY = minChunkYIterate; currChunkY <= maxChunkYIterate; ++currChunkY) {
++ final LevelChunkSection section = sections[currChunkY - minSection];
++ if (section == null || section.hasOnlyAir()) {
++ // empty
++ continue;
++ }
++ final PalettedContainer blocks = section.states;
++
++ final int minY = currChunkY == minChunkYIterate ? minYIterate & 15 : 0; // coordinate in chunk
++ final int maxY = currChunkY == maxChunkYIterate ? maxYIterate & 15 : 15; // coordinate in chunk
++ final int chunkYGlobalPos = currChunkY << 4;
++
++ for (int currY = minY; currY <= maxY; ++currY) {
++ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
++ for (int currX = minX; currX <= maxX; ++currX) {
++ int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8);
++ int blockX = currX | chunkXGlobalPos;
++ int blockY = currY | chunkYGlobalPos;
++ int blockZ = currZ | chunkZGlobalPos;
++
++ int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
++ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
++ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0);
++ if (edgeCount == 3) {
++ continue;
++ }
++
++ BlockState blockData = blocks.get(localBlockIndex);
++ if (blockData.getBlockCollisionBehavior() == CollisionUtil.KNOWN_EMPTY_BLOCK) {
++ continue;
++ }
++
++ if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) {
++ mutablePos.set(blockX, blockY, blockZ);
++ if (collisionShape == null) {
++ collisionShape = new LazyEntityCollisionContext(entity);
++ }
++ VoxelShape voxelshape2 = blockData.getCollisionShape(getter, mutablePos, collisionShape);
++ if (voxelshape2 != Shapes.empty()) {
++ VoxelShape voxelshape3 = voxelshape2.move((double)blockX, (double)blockY, (double)blockZ);
++
++ if (predicate != null && !predicate.test(blockData, mutablePos)) {
++ continue;
++ }
++
++ if (checkOnly) {
++ if (voxelshape3.intersects(aabb)) {
++ return true;
++ }
++ } else {
++ ret |= addBoxesToIfIntersects(voxelshape3, aabb, into);
+ }
-+ } else {
-+ ret |= addBoxesToIfIntersects(voxelshape3, aabb, into);
+ }
+ }
+ }
@@ -1219,15 +1479,199 @@ diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+@@ -0,0 +0,0 @@ public abstract class BlockBehaviour {
+ return this.conditionallyFullOpaque;
+ }
+ // Paper end
++ // Paper start
++ private long blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK;
++
++ public final long getBlockCollisionBehavior() {
++ return this.blockCollisionBehavior;
++ }
++ // Paper end
+
+ public void initCache() {
+ this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid()
@@ -0,0 +0,0 @@ public abstract class BlockBehaviour {
}
this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - cache opacity for light
-
-+ // TODO optimise light
++ // Paper start
++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(this)) {
++ this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_SPECIAL_BLOCK;
++ } else {
++ try {
++ // There is NOTHING HACKY ABOUT THIS AT ALLLLLLLLLLLLLLL
++ VoxelShape constantShape = this.getCollisionShape(null, null, null);
++ if (constantShape == null) {
++ this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_UNKNOWN_BLOCK;
++ } else {
++ constantShape = constantShape.optimize();
++ if (constantShape.isEmpty()) {
++ this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_EMPTY_BLOCK;
++ } else {
++ final List boxes = constantShape.toAabbs();
++ if (constantShape == net.minecraft.world.phys.shapes.Shapes.getFullUnoptimisedCube() || (boxes.size() == 1 && boxes.get(0).equals(net.minecraft.world.phys.shapes.Shapes.BLOCK_OPTIMISED.aabb))) {
++ this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_FULL_BLOCK;
++ } else {
++ this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_UNKNOWN_BLOCK;
++ }
++ }
++ }
++ } catch (final Error error) {
++ throw error;
++ } catch (final Throwable throwable) {
++ this.blockCollisionBehavior = io.papermc.paper.util.CollisionUtil.KNOWN_UNKNOWN_BLOCK;
++ }
++ }
++ // Paper end
}
public Block getBlock() {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+@@ -0,0 +0,0 @@ public class LevelChunkSection {
+ this.biomes = new PalettedContainer<>(biomeRegistry, (Biome) biomeRegistry.getOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes
+ }
+
++ // Paper start
++ protected int specialCollidingBlocks;
++ // blockIndex = x | (z << 4) | (y << 8)
++ private long[] knownBlockCollisionData;
++
++ private long[] initKnownDataField() {
++ return this.knownBlockCollisionData = new long[16 * 16 * 16 * 2 / Long.SIZE];
++ }
++
++ public final boolean hasSpecialCollidingBlocks() {
++ return this.specialCollidingBlocks != 0;
++ }
++
++ public static long getKnownBlockInfo(final int blockIndex, final long value) {
++ final int valueShift = (blockIndex & (Long.SIZE / 2 - 1));
++
++ return (value >>> (valueShift << 1)) & 0b11L;
++ }
++
++ public final long getKnownBlockInfo(final int blockIndex) {
++ if (this.knownBlockCollisionData == null) {
++ return 0L;
++ }
++
++ final int arrayIndex = (blockIndex >>> (6 - 1)); // blockIndex / (64/2)
++ final int valueShift = (blockIndex & (Long.SIZE / 2 - 1));
++
++ final long value = this.knownBlockCollisionData[arrayIndex];
++
++ return (value >>> (valueShift << 1)) & 0b11L;
++ }
++
++ // important detail: this returns 32 values, one for localZ = localZ & (~1) and one for localZ = localZ | 1
++ // the even localZ is the lower 32 bits, the odd is the upper 32 bits
++ public final long getKnownBlockInfoHorizontalRaw(final int localY, final int localZ) {
++ if (this.knownBlockCollisionData == null) {
++ return 0L;
++ }
++
++ final int horizontalIndex = (localZ << 4) | (localY << 8);
++ return this.knownBlockCollisionData[horizontalIndex >>> (6 - 1)];
++ }
++
++ private void initBlockCollisionData() {
++ this.specialCollidingBlocks = 0;
++ // In 1.18 all sections will be initialised, whether or not they have blocks (fucking stupid btw)
++ // This means we can't aggressively initialise the backing long[], or else memory usage will just skyrocket.
++ // So only init if we contain non-empty blocks.
++ if (this.nonEmptyBlockCount == 0) {
++ this.knownBlockCollisionData = null;
++ return;
++ }
++ this.initKnownDataField();
++ for (int index = 0; index < (16 * 16 * 16); ++index) {
++ final BlockState state = this.states.get(index);
++ this.setKnownBlockInfo(index, state);
++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(state)) {
++ ++this.specialCollidingBlocks;
++ }
++ }
++ }
++
++ // only use for initBlockCollisionData
++ private void setKnownBlockInfo(final int blockIndex, final BlockState blockState) {
++ final int arrayIndex = (blockIndex >>> (6 - 1)); // blockIndex / (64/2)
++ final int valueShift = (blockIndex & (Long.SIZE / 2 - 1)) << 1;
++
++ long value = this.knownBlockCollisionData[arrayIndex];
++
++ value &= ~(0b11L << valueShift);
++ value |= blockState.getBlockCollisionBehavior() << valueShift;
++
++ this.knownBlockCollisionData[arrayIndex] = value;
++ }
++
++ public void updateKnownBlockInfo(final int blockIndex, final BlockState from, final BlockState to) {
++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(from)) {
++ --this.specialCollidingBlocks;
++ }
++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(to)) {
++ ++this.specialCollidingBlocks;
++ }
++
++ if (this.nonEmptyBlockCount == 0) {
++ this.knownBlockCollisionData = null;
++ return;
++ }
++
++ if (this.knownBlockCollisionData == null) {
++ this.initKnownDataField();
++ }
++
++ final int arrayIndex = (blockIndex >>> (6 - 1)); // blockIndex / (64/2)
++ final int valueShift = (blockIndex & (Long.SIZE / 2 - 1)) << 1;
++
++ long value = this.knownBlockCollisionData[arrayIndex];
++
++ value &= ~(0b11L << valueShift);
++ value |= to.getBlockCollisionBehavior() << valueShift;
++
++ this.knownBlockCollisionData[arrayIndex] = value;
++ }
++ // Paper end
++
+ public static int getBottomBlockY(int chunkPos) {
+ return chunkPos << 4;
+ }
+@@ -0,0 +0,0 @@ public class LevelChunkSection {
+ return this.setBlockState(x, y, z, state, true);
+ }
+
+- public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) {
+- BlockState iblockdata1;
++ public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { // Paper - state -> new state
++ BlockState iblockdata1; // Paper - iblockdata1 -> oldState
+
+ if (lock) {
+ iblockdata1 = (BlockState) this.states.getAndSet(x, y, z, state);
+@@ -0,0 +0,0 @@ public class LevelChunkSection {
+ ++this.tickingFluidCount;
+ }
+
++ this.updateKnownBlockInfo(x | (z << 4) | (y << 8), iblockdata1, state); // Paper
+ return iblockdata1;
+ }
+
+@@ -0,0 +0,0 @@ public class LevelChunkSection {
+ }
+
+ });
++ this.initBlockCollisionData(); // Paper
+ }
+
+ public PalettedContainer getStates() {
diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/phys/AABB.java
diff --git a/patches/server/Optimise-ArraySetSorted-removeIf.patch b/patches/server/Optimise-ArraySetSorted-removeIf.patch
index 1fbcab69d8..f54572ca32 100644
--- a/patches/server/Optimise-ArraySetSorted-removeIf.patch
+++ b/patches/server/Optimise-ArraySetSorted-removeIf.patch
@@ -5,6 +5,41 @@ Subject: [PATCH] Optimise ArraySetSorted#removeIf
Remove iterator allocation and ensure the call is always O(n)
+diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ protected void purgeStaleTickets() {
+ ++this.ticketTickCounter;
+ ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
++ // Paper start - use optimised removeIf
++ long[] currChunk = new long[1];
++ long ticketCounter = DistanceManager.this.ticketTickCounter;
++ java.util.function.Predicate> removeIf = (ticket) -> {
++ final boolean ret = ticket.timedOut(ticketCounter);
++ if (ret) {
++ this.tickingTicketsTracker.removeTicket(currChunk[0], ticket);
++ }
++ return ret;
++ };
++ // Paper end - use optimised removeIf
+
+ while (objectiterator.hasNext()) {
+ Entry>> entry = (Entry) objectiterator.next();
+- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator();
+- boolean flag = false;
++ // Paper start - use optimised removeIf
++ Iterator> iterator = null;
++ currChunk[0] = entry.getLongKey();
++ boolean flag = entry.getValue().removeIf(removeIf);
+
+- while (iterator.hasNext()) {
++ while (false && iterator.hasNext()) {
++ // Paper end - use optimised removeIf
+ Ticket> ticket = (Ticket) iterator.next();
+
+ if (ticket.timedOut(this.ticketTickCounter)) {
diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/util/SortedArraySet.java
diff --git a/patches/server/Optimise-chunk-tick-iteration.patch b/patches/server/Optimise-chunk-tick-iteration.patch
index cfeb64101c..bc840e669f 100644
--- a/patches/server/Optimise-chunk-tick-iteration.patch
+++ b/patches/server/Optimise-chunk-tick-iteration.patch
@@ -5,10 +5,94 @@ Subject: [PATCH] Optimise chunk tick iteration
Use a dedicated list of entity ticking chunks to reduce the cost
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+ long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos);
+ this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
+ this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
++ // Paper start - optimise chunk tick iteration
++ if (this.needsBroadcastChanges()) {
++ this.chunkMap.needsChangeBroadcasting.add(this);
++ }
++ // Paper end - optimise chunk tick iteration
+ }
+
+ void onChunkRemove() {
+ this.playersInMobSpawnRange = null;
+ this.playersInChunkTickRange = null;
++ // Paper start - optimise chunk tick iteration
++ if (this.needsBroadcastChanges()) {
++ this.chunkMap.needsChangeBroadcasting.remove(this);
++ }
++ // Paper end - optimise chunk tick iteration
+ }
+ // Paper end - optimise anyPlayerCloseEnoughForSpawning
+ long lastAutoSaveTime; // Paper - incremental autosave
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+
+ if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
+ if (this.changedBlocksPerSection[i] == null) {
+- this.hasChangedSections = true;
++ this.hasChangedSections = true; this.addToBroadcastMap(); // Paper - optimise chunk tick iteration
+ this.changedBlocksPerSection[i] = new ShortOpenHashSet();
+ }
+
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+ int k = this.lightEngine.getMaxLightSection();
+
+ if (y >= j && y <= k) {
++ this.addToBroadcastMap(); // Paper - optimise chunk tick iteration
+ int l = y - j;
+
+ if (lightType == LightLayer.SKY) {
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+ }
+ }
+
++ // Paper start - optimise chunk tick iteration
++ public final boolean needsBroadcastChanges() {
++ return this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty();
++ }
++
++ private void addToBroadcastMap() {
++ org.spigotmc.AsyncCatcher.catchOp("ChunkHolder update");
++ this.chunkMap.needsChangeBroadcasting.add(this);
++ }
++ // Paper end - optimise chunk tick iteration
++
+ public void broadcastChanges(LevelChunk chunk) {
+- if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
++ if (this.needsBroadcastChanges()) { // Paper - moved into above, other logic needs to call
+ Level world = chunk.getLevel();
+ int i = 0;
+
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ private final Queue unloadQueue;
+ int viewDistance;
+ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper
++ public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>();
+
+ // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
+ public final CallbackExecutor callbackExecutor = new CallbackExecutor();
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -0,0 +0,0 @@ import org.apache.logging.log4j.LogManager;
+ import org.apache.logging.log4j.Logger;
+ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper
+ import java.util.function.Function; // Paper
++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper
+
+ public class ServerChunkCache extends ChunkSource {
+
@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
this.lastSpawnState = spawnercreature_d;
@@ -56,16 +140,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ LevelChunk chunk1 = iterator1.next();
+ ChunkHolder holder = chunk1.playerChunk;
+ if (holder != null) {
-+ gameprofilerfiller.popPush("broadcast");
-+ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
-+ holder.broadcastChanges(chunk1);
-+ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
-+ gameprofilerfiller.pop();
++ // Paper - move down
+ // Paper end - optimise chunk tick iteration
ChunkPos chunkcoordintpair = chunk1.getPos();
- if (this.level.isPositionEntityTicking(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkproviderserver_a.holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning
-+ if ((true || this.level.isPositionEntityTicking(chunkcoordintpair)) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration
++ if (this.level.isPositionEntityTicking(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning
chunk1.incrementInhabitedTime(j);
- if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkproviderserver_a.holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning
+ if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration
@@ -90,17 +170,34 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
gameprofilerfiller.popPush("customSpawners");
if (flag2) {
@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+ this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies);
} // Paper - timings
}
-
+-
- gameprofilerfiller.popPush("broadcast");
- list.forEach((chunkproviderserver_a1) -> {
- this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
- chunkproviderserver_a1.holder.broadcastChanges(chunkproviderserver_a1.chunk);
- this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
- });
-- gameprofilerfiller.pop();
-+ // Paper - no, iterating just ONCE is expensive enough! Don't do it TWICE! Code moved up
gameprofilerfiller.pop();
++ // Paper start - use set of chunks requiring updates, rather than iterating every single one loaded
++ gameprofilerfiller.popPush("broadcast");
++ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
++ if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) {
++ ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone();
++ this.chunkMap.needsChangeBroadcasting.clear();
++ for (ChunkHolder holder : copy) {
++ holder.broadcastChanges(holder.getFullChunkUnchecked()); // LevelChunks are NEVER unloaded
++ if (holder.needsBroadcastChanges()) {
++ // I DON'T want to KNOW what DUMB plugins might be doing.
++ this.chunkMap.needsChangeBroadcasting.add(holder);
++ }
++ }
++ }
++ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
+ gameprofilerfiller.pop();
++ // Paper end - use set of chunks requiring updates, rather than iterating every single one loaded
this.chunkMap.tick();
}
+ }
diff --git a/patches/server/Optimise-nearby-player-lookups.patch b/patches/server/Optimise-nearby-player-lookups.patch
index 3b45b0dc19..9c9fc76e42 100644
--- a/patches/server/Optimise-nearby-player-lookups.patch
+++ b/patches/server/Optimise-nearby-player-lookups.patch
@@ -13,14 +13,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -0,0 +0,0 @@ public class ChunkHolder {
- long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos);
- this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
- this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+ this.chunkMap.needsChangeBroadcasting.add(this);
+ }
+ // Paper end - optimise chunk tick iteration
+ // Paper start - optimise checkDespawn
+ LevelChunk chunk = this.getFullChunkUnchecked();
+ if (chunk != null) {
+ chunk.updateGeneralAreaCache();
+ }
++ // Paper end - optimise checkDespawn
+ }
+
+ void onChunkRemove() {
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+ this.chunkMap.needsChangeBroadcasting.remove(this);
+ }
+ // Paper end - optimise chunk tick iteration
++ // Paper start - optimise checkDespawn
++ LevelChunk chunk = this.getFullChunkUnchecked();
++ if (chunk != null) {
++ chunk.removeGeneralAreaCache();
++ }
+ // Paper end - optimise checkDespawn
}
// Paper end - optimise anyPlayerCloseEnoughForSpawning
@@ -30,8 +43,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- int viewDistance;
public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper
+ public final ReferenceOpenHashSet needsChangeBroadcasting = new ReferenceOpenHashSet<>();
+ // Paper start - optimise checkDespawn
+ public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40;
@@ -331,6 +344,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey));
+ }
+
++ public void removeGeneralAreaCache() {
++ this.playerGeneralAreaCacheSet = false;
++ this.playerGeneralAreaCache = null;
++ }
++
+ public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet value) {
+ this.playerGeneralAreaCacheSet = true;
+ this.playerGeneralAreaCache = value;
diff --git a/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch b/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch
index 75827174ee..8df66c48cd 100644
--- a/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch
+++ b/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch
@@ -18,11 +18,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInMobSpawnRange;
+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInChunkTickRange;
+
-+ void updateRanges() {
++ void onChunkAdd() {
+ long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos);
+ this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
+ this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+ }
++
++ void onChunkRemove() {
++ this.playersInMobSpawnRange = null;
++ this.playersInChunkTickRange = null;
++ }
+ // Paper end - optimise anyPlayerCloseEnoughForSpawning
+
public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
@@ -32,7 +37,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
this.setTicketLevel(level);
this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper
-+ this.updateRanges(); // Paper - optimise anyPlayerCloseEnoughForSpawning
++ this.onChunkAdd(); // Paper - optimise anyPlayerCloseEnoughForSpawning
}
// CraftBukkit start
@@ -123,13 +128,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
protected ChunkGenerator generator() {
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- } else {
- if (holder != null) {
- holder.setTicketLevel(level);
-+ holder.updateRanges(); // Paper - optimise anyPlayerCloseEnoughForSpawning
- }
+ holder = (ChunkHolder) this.pendingUnloads.remove(pos);
+ if (holder != null) {
+ holder.setTicketLevel(level);
++ holder.onChunkAdd(); // Paper - optimise anyPlayerCloseEnoughForSpawning - PUT HERE AFTER RE-ADDING ONLY
+ } else {
+ holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
+ // Paper start
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
- if (holder != null) {
+ if (playerchunk != null) {
++ playerchunk.onChunkRemove(); // Paper
+ this.pendingUnloads.put(j, playerchunk);
+ this.modified = true;
+ this.scheduleUnload(j, playerchunk); // Paper - Move up - don't leak chunks
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
return this.anyPlayerCloseEnoughForSpawning(pos, false);
}
diff --git a/patches/server/Paper-config-files.patch b/patches/server/Paper-config-files.patch
index 0541466b96..d6abc3866b 100644
--- a/patches/server/Paper-config-files.patch
+++ b/patches/server/Paper-config-files.patch
@@ -364,8 +364,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ commands = new HashMap();
+ commands.put("paper", new PaperCommand("paper"));
+
-+ version = getInt("config-version", 24);
-+ set("config-version", 24);
++ version = getInt("config-version", 25);
++ set("config-version", 25);
+ readConfig(PaperConfig.class, null);
+ }
+
diff --git a/patches/server/Replace-player-chunk-loader-system.patch b/patches/server/Replace-player-chunk-loader-system.patch
new file mode 100644
index 0000000000..632940f030
--- /dev/null
+++ b/patches/server/Replace-player-chunk-loader-system.patch
@@ -0,0 +1,2242 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf
+Date: Sun, 24 Jan 2021 20:27:32 -0800
+Subject: [PATCH] Replace player chunk loader system
+
+The old one has undebuggable problems. Rewriting seems
+the most sensible option.
+
+This new player chunk manager will also strictly rate limit
+chunk sends so that netty threads do not get overloaded, whether
+it be from the anti-xray logic or the compression itself.
+
+Chunk loading is also rate limited in the same manner, so this
+will result in a maximum responsiveness for change.
+
+Config:
+```
+chunk-loading:
+ min-load-radius: 2
+ max-concurrent-sends: 2
+ autoconfig-send-distance: true
+ target-player-chunk-send-rate: 100.0
+ global-max-chunk-send-rate: -1
+ enable-frustum-priority: false
+ global-max-chunk-load-rate: -1.0
+ player-max-concurrent-loads: 25.0
+ global-max-concurrent-loads: 500.0
+```
+
+min-load-radius - The radius of chunks around a player that
+are not throttled for loading. The number of chunks
+affected is actually the configured value plus one as this
+config controls the chunks the client will be able to render.
+
+max-concurrent-sends - The maximum number of chunks that
+can be queued to send at any given time. Low values
+are generally going to solve server-sided networking
+bottlenecks like anti-xray and chunk compression. Client
+side networking is unlikely to be helped (i.e this wont help
+people running off McDonald's wifi).
+
+autoconfig-send-distance - Whether to try to use the client's
+view distance for the send view distance in the server. In the
+case that no plugin has explicitly set the send distance and
+the client view distance is less-than the server's send distance,
+the client's view distance will be used. This will not affect
+tick view distance or no-tick view distance.
+
+target-player-chunk-send-rate - The maximum chunk send rate
+an individual player will have. -1 means no limit
+
+global-max-chunk-send-rate - The maximum chunk send rate for
+the whole server. -1 means no limit
+
+enable-frustum-priority - Whether chunks in front of a player
+are prioritised to load/send first. Disabled by default
+because the client can bug out due to the out of order
+chunk sending.
+
+global-max-chunk-load-rate - The maximum chunk load rate
+for the whole server. -1 means no limit
+
+player-max-concurrent-loads and global-max-concurrent-loads
+The maximum number of concurrent loads for the server is
+determined by the number of players on the server multiplied by the
+`player-max-concurrent-loads`. It is then limited to
+whatever `global-max-concurrent-loads` is configured to.
+
+diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/co/aikar/timings/TimingsExport.java
++++ b/src/main/java/co/aikar/timings/TimingsExport.java
+@@ -0,0 +0,0 @@ public class TimingsExport extends Thread {
+ pair("gamerules", toObjectMapper(world.getWorld().getGameRules(), rule -> {
+ return pair(rule, world.getWorld().getGameRuleValue(rule));
+ })),
+- pair("ticking-distance", world.getChunkSource().chunkMap.getEffectiveViewDistance())
++ // Paper start - replace chunk loader system
++ pair("ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()),
++ pair("no-ticking-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()),
++ pair("sending-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())
++ // Paper end - replace chunk loader system
+ ));
+ }));
+
+diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
+@@ -0,0 +0,0 @@ public class PaperConfig {
+ private static void timeCommandAffectsAllWorlds() {
+ timeCommandAffectsAllWorlds = getBoolean("settings.time-command-affects-all-worlds", timeCommandAffectsAllWorlds);
+ }
++
++
++ public static int playerMinChunkLoadRadius;
++ public static boolean playerAutoConfigureSendViewDistance;
++ public static int playerMaxConcurrentChunkSends;
++ public static double playerTargetChunkSendRate;
++ public static double globalMaxChunkSendRate;
++ public static boolean playerFrustumPrioritisation;
++ public static double globalMaxChunkLoadRate;
++ public static double playerMaxConcurrentChunkLoads;
++ public static double globalMaxConcurrentChunkLoads;
++
++ private static void newPlayerChunkManagement() {
++ playerMinChunkLoadRadius = getInt("settings.chunk-loading.min-load-radius", 2);
++ playerMaxConcurrentChunkSends = getInt("settings.chunk-loading.max-concurrent-sends", 2);
++ playerAutoConfigureSendViewDistance = getBoolean("settings.chunk-loading.autoconfig-send-distance", true);
++ playerTargetChunkSendRate = getDouble("settings.chunk-loading.target-player-chunk-send-rate", 100.0);
++ globalMaxChunkSendRate = getDouble("settings.chunk-loading.global-max-chunk-send-rate", -1.0);
++ playerFrustumPrioritisation = getBoolean("settings.chunk-loading.enable-frustum-priority", false);
++ globalMaxChunkLoadRate = getDouble("settings.chunk-loading.global-max-chunk-load-rate", -1.0);
++ if (version < 23 && globalMaxChunkLoadRate == 300.0) {
++ set("settings.chunk-loading.global-max-chunk-load-rate", globalMaxChunkLoadRate = -1.0);
++ }
++ playerMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.player-max-concurrent-loads", 20.0);
++ if (version < 25 && playerMaxConcurrentChunkLoads == 4.0) {
++ set("settings.chunk-loading.player-max-concurrent-loads", playerMaxConcurrentChunkLoads = 20.0);
++ }
++ globalMaxConcurrentChunkLoads = getDouble("settings.chunk-loading.global-max-concurrent-loads", 500.0);
++ }
+ }
+diff --git a/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/chunk/PlayerChunkLoader.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.chunk;
++
++import com.destroystokyo.paper.PaperConfig;
++import com.destroystokyo.paper.util.misc.PlayerAreaMap;
++import com.destroystokyo.paper.util.misc.PooledLinkedHashSets;
++import io.papermc.paper.util.CoordinateUtils;
++import io.papermc.paper.util.IntervalledCounter;
++import io.papermc.paper.util.TickThread;
++import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket;
++import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket;
++import net.minecraft.server.MCUtil;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.*;
++import net.minecraft.util.Mth;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.chunk.LevelChunk;
++import org.apache.commons.lang3.mutable.MutableObject;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++import org.bukkit.entity.Player;
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.TreeSet;
++import java.util.concurrent.atomic.AtomicInteger;
++
++public final class PlayerChunkLoader {
++
++ public static final int MIN_VIEW_DISTANCE = 2;
++ public static final int MAX_VIEW_DISTANCE = 32;
++
++ public static final int TICK_TICKET_LEVEL = 31;
++ public static final int LOADED_TICKET_LEVEL = 33;
++
++ public static int getTickViewDistance(final Player player) {
++ return getTickViewDistance(((CraftPlayer)player).getHandle());
++ }
++
++ public static int getTickViewDistance(final ServerPlayer player) {
++ final ServerLevel level = (ServerLevel)player.level;
++ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player);
++ if (data == null) {
++ return level.chunkSource.chunkMap.playerChunkManager.getTargetTickViewDistance();
++ }
++ return data.getTargetTickViewDistance();
++ }
++
++ public static int getLoadViewDistance(final Player player) {
++ return getLoadViewDistance(((CraftPlayer)player).getHandle());
++ }
++
++ public static int getLoadViewDistance(final ServerPlayer player) {
++ final ServerLevel level = (ServerLevel)player.level;
++ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player);
++ if (data == null) {
++ return level.chunkSource.chunkMap.playerChunkManager.getLoadDistance();
++ }
++ return data.getLoadDistance();
++ }
++
++ public static int getSendViewDistance(final Player player) {
++ return getSendViewDistance(((CraftPlayer)player).getHandle());
++ }
++
++ public static int getSendViewDistance(final ServerPlayer player) {
++ final ServerLevel level = (ServerLevel)player.level;
++ final PlayerLoaderData data = level.chunkSource.chunkMap.playerChunkManager.getData(player);
++ if (data == null) {
++ return level.chunkSource.chunkMap.playerChunkManager.getTargetSendDistance();
++ }
++ return data.getTargetSendViewDistance();
++ }
++
++ protected final ChunkMap chunkMap;
++ protected final Reference2ObjectLinkedOpenHashMap playerMap = new Reference2ObjectLinkedOpenHashMap<>(512, 0.7f);
++ protected final ReferenceLinkedOpenHashSet chunkSendQueue = new ReferenceLinkedOpenHashSet<>(512, 0.7f);
++
++ protected final TreeSet chunkLoadQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> {
++ if (p1 == p2) {
++ return 0;
++ }
++
++ final ChunkPriorityHolder holder1 = p1.loadQueue.peekFirst();
++ final ChunkPriorityHolder holder2 = p2.loadQueue.peekFirst();
++
++ final int priorityCompare = Double.compare(holder1 == null ? Double.MAX_VALUE : holder1.priority, holder2 == null ? Double.MAX_VALUE : holder2.priority);
++
++ if (priorityCompare != 0) {
++ return priorityCompare;
++ }
++
++ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId());
++
++ if (idCompare != 0) {
++ return idCompare;
++ }
++
++ // last resort
++ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2));
++ });
++
++ protected final TreeSet chunkSendWaitQueue = new TreeSet<>((final PlayerLoaderData p1, final PlayerLoaderData p2) -> {
++ if (p1 == p2) {
++ return 0;
++ }
++
++ final int timeCompare = Long.compare(p1.nextChunkSendTarget, p2.nextChunkSendTarget);
++ if (timeCompare != 0) {
++ return timeCompare;
++ }
++
++ final int idCompare = Integer.compare(p1.player.getId(), p2.player.getId());
++
++ if (idCompare != 0) {
++ return idCompare;
++ }
++
++ // last resort
++ return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2));
++ });
++
++
++ // no throttling is applied below this VD for loading
++
++ /**
++ * The chunks to be sent to players, provided they're send-ready. Send-ready means the chunk and its 1 radius neighbours are loaded.
++ */
++ public final PlayerAreaMap broadcastMap;
++
++ /**
++ * The chunks to be brought up to send-ready status. Send-ready means the chunk and its 1 radius neighbours are loaded.
++ */
++ public final PlayerAreaMap loadMap;
++
++ /**
++ * Areamap used only to remove tickets for send-ready chunks. View distance is always + 1 of load view distance. Thus,
++ * this map is always representing the chunks we are actually going to load.
++ */
++ public final PlayerAreaMap loadTicketCleanup;
++
++ /**
++ * The chunks to brought to ticking level. Each chunk must have 2 radius neighbours loaded before this can happen.
++ */
++ public final PlayerAreaMap tickMap;
++
++ /**
++ * -1 if defaulting to [load distance], else always in [2, load distance]
++ */
++ protected int rawSendDistance = -1;
++
++ /**
++ * -1 if defaulting to [tick view distance + 1], else always in [tick view distance + 1, 32 + 1]
++ */
++ protected int rawLoadDistance = -1;
++
++ /**
++ * Never -1, always in [2, 32]
++ */
++ protected int rawTickDistance = -1;
++
++ // methods to bridge for API
++
++ public int getTargetTickViewDistance() {
++ return this.getTickDistance();
++ }
++
++ public void setTargetTickViewDistance(final int distance) {
++ this.setTickDistance(distance);
++ }
++
++ public int getTargetNoTickViewDistance() {
++ return this.getLoadDistance() - 1;
++ }
++
++ public void setTargetNoTickViewDistance(final int distance) {
++ this.setLoadDistance(distance == -1 ? -1 : distance + 1);
++ }
++
++ public int getTargetSendDistance() {
++ return this.rawSendDistance == -1 ? this.getLoadDistance() : this.rawSendDistance;
++ }
++
++ public void setTargetSendDistance(final int distance) {
++ this.setSendDistance(distance);
++ }
++
++ // internal methods
++
++ public int getSendDistance() {
++ final int loadDistance = this.getLoadDistance();
++ return this.rawSendDistance == -1 ? loadDistance : Math.min(this.rawSendDistance, loadDistance);
++ }
++
++ public void setSendDistance(final int distance) {
++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ this.rawSendDistance = distance;
++ }
++
++ public int getLoadDistance() {
++ final int tickDistance = this.getTickDistance();
++ return this.rawLoadDistance == -1 ? tickDistance + 1 : Math.max(tickDistance + 1, this.rawLoadDistance);
++ }
++
++ public void setLoadDistance(final int distance) {
++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ this.rawLoadDistance = distance;
++ }
++
++ public int getTickDistance() {
++ return this.rawTickDistance;
++ }
++
++ public void setTickDistance(final int distance) {
++ if (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ this.rawTickDistance = distance;
++ }
++
++ /*
++ Players have 3 different types of view distance:
++ 1. Sending view distance
++ 2. Loading view distance
++ 3. Ticking view distance
++
++ But for configuration purposes (and API) there are:
++ 1. No-tick view distance
++ 2. Tick view distance
++ 3. Broadcast view distance
++
++ These aren't always the same as the types we represent internally.
++
++ Loading view distance is always max(no-tick + 1, tick + 1)
++ - no-tick has 1 added because clients need an extra radius to render chunks
++ - tick has 1 added because it needs an extra radius of chunks to load before they can be marked ticking
++
++ Loading view distance is defined as the radius of chunks that will be brought to send-ready status, which means
++ it loads chunks in radius load-view-distance + 1.
++
++ The maximum value for send view distance is the load view distance. API can set it lower.
++ */
++
++ public PlayerChunkLoader(final ChunkMap chunkMap, final PooledLinkedHashSets pooledHashSets) {
++ this.chunkMap = chunkMap;
++ this.broadcastMap = new PlayerAreaMap(pooledHashSets,
++ null,
++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
++ PlayerChunkLoader.this.onChunkLeave(player, rangeX, rangeZ);
++ });
++ this.loadMap = new PlayerAreaMap(pooledHashSets,
++ null,
++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
++ if (newState != null) {
++ return;
++ }
++ PlayerChunkLoader.this.isTargetedForPlayerLoad.remove(CoordinateUtils.getChunkKey(rangeX, rangeZ));
++ });
++ this.loadTicketCleanup = new PlayerAreaMap(pooledHashSets,
++ null,
++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
++ if (newState != null) {
++ return;
++ }
++ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ);
++ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos);
++ if (PlayerChunkLoader.this.chunkTicketTracker.remove(chunkPos.toLong())) {
++ --PlayerChunkLoader.this.concurrentChunkLoads;
++ }
++ });
++ this.tickMap = new PlayerAreaMap(pooledHashSets,
++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
++ if (newState.size() != 1) {
++ return;
++ }
++ LevelChunk chunk = PlayerChunkLoader.this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ);
++ if (chunk == null || !chunk.areNeighboursLoaded(2)) {
++ return;
++ }
++
++ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ);
++ PlayerChunkLoader.this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
++ },
++ (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> {
++ if (newState != null) {
++ return;
++ }
++ ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ);
++ PlayerChunkLoader.this.chunkMap.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
++ });
++ }
++
++ protected final LongOpenHashSet isTargetedForPlayerLoad = new LongOpenHashSet();
++ protected final LongOpenHashSet chunkTicketTracker = new LongOpenHashSet();
++
++ public boolean isChunkNearPlayers(final int chunkX, final int chunkZ) {
++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ);
++
++ return playersInSendRange != null;
++ }
++
++ public void onChunkPostProcessing(final int chunkX, final int chunkZ) {
++ this.onChunkSendReady(chunkX, chunkZ);
++ }
++
++ private boolean chunkNeedsPostProcessing(final int chunkX, final int chunkZ) {
++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key);
++
++ if (chunk == null) {
++ return false;
++ }
++
++ final LevelChunk levelChunk = chunk.getSendingChunk();
++
++ return levelChunk != null && !levelChunk.isPostProcessingDone;
++ }
++
++ // rets whether the chunk is at a loaded stage that is ready to be sent to players
++ public boolean isChunkPlayerLoaded(final int chunkX, final int chunkZ) {
++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
++ final ChunkHolder chunk = this.chunkMap.getVisibleChunkIfPresent(key);
++
++ if (chunk == null) {
++ return false;
++ }
++
++ final LevelChunk levelChunk = chunk.getSendingChunk();
++
++ return levelChunk != null && levelChunk.isPostProcessingDone && this.isTargetedForPlayerLoad.contains(key);
++ }
++
++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) {
++ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ);
++ }
++
++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) {
++ final PlayerLoaderData data = this.playerMap.get(player);
++ if (data == null) {
++ return false;
++ }
++
++ return data.hasSentChunk(chunkX, chunkZ);
++ }
++
++ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) {
++ final PlayerLoaderData data = this.playerMap.get(player);
++ if (data == null) {
++ return false;
++ }
++
++ final boolean center = data.hasSentChunk(chunkX, chunkZ);
++ if (!center) {
++ return false;
++ }
++
++ return !(data.hasSentChunk(chunkX - 1, chunkZ) && data.hasSentChunk(chunkX + 1, chunkZ) &&
++ data.hasSentChunk(chunkX, chunkZ - 1) && data.hasSentChunk(chunkX, chunkZ + 1));
++ }
++
++ protected int getMaxConcurrentChunkSends() {
++ return PaperConfig.playerMaxConcurrentChunkSends;
++ }
++
++ protected int getMaxChunkLoads() {
++ double config = PaperConfig.playerMaxConcurrentChunkLoads;
++ double max = PaperConfig.globalMaxConcurrentChunkLoads;
++ return (int)Math.ceil(Math.min(config * MinecraftServer.getServer().getPlayerCount(), max <= 1.0 ? Double.MAX_VALUE : max));
++ }
++
++ protected long getTargetSendPerPlayerAddend() {
++ return PaperConfig.playerTargetChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / PaperConfig.playerTargetChunkSendRate);
++ }
++
++ protected long getMaxSendAddend() {
++ return PaperConfig.globalMaxChunkSendRate <= 1.0 ? 0L : (long)Math.round(1.0e9 / PaperConfig.globalMaxChunkSendRate);
++ }
++
++ public void onChunkPlayerTickReady(final int chunkX, final int chunkZ) {
++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, TICK_TICKET_LEVEL, chunkPos);
++ }
++
++ public void onChunkSendReady(final int chunkX, final int chunkZ) {
++ final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInSendRange = this.broadcastMap.getObjectsInRange(chunkX, chunkZ);
++
++ if (playersInSendRange == null) {
++ return;
++ }
++
++ final Object[] rawData = playersInSendRange.getBackingSet();
++ for (int i = 0, len = rawData.length; i < len; ++i) {
++ final Object raw = rawData[i];
++
++ if (!(raw instanceof ServerPlayer)) {
++ continue;
++ }
++ this.onChunkSendReady((ServerPlayer)raw, chunkX, chunkZ);
++ }
++ }
++
++ public void onChunkSendReady(final ServerPlayer player, final int chunkX, final int chunkZ) {
++ final PlayerLoaderData data = this.playerMap.get(player);
++
++ if (data == null) {
++ return;
++ }
++
++ if (data.hasSentChunk(chunkX, chunkZ) || !this.isChunkPlayerLoaded(chunkX, chunkZ)) {
++ // if we don't have player tickets, then the load logic will pick this up and queue to send
++ return;
++ }
++
++ if (!data.chunksToBeSent.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
++ // don't queue to send, we don't want the chunk
++ return;
++ }
++
++ final long playerPos = this.broadcastMap.getLastCoordinate(player);
++ final int playerChunkX = CoordinateUtils.getChunkX(playerPos);
++ final int playerChunkZ = CoordinateUtils.getChunkZ(playerPos);
++ final int manhattanDistance = Math.abs(playerChunkX - chunkX) + Math.abs(playerChunkZ - chunkZ);
++
++ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, 0.0);
++ data.sendQueue.add(holder);
++ }
++
++ public void onChunkLoad(final int chunkX, final int chunkZ) {
++ if (this.chunkTicketTracker.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
++ --this.concurrentChunkLoads;
++ }
++ }
++
++ public void onChunkLeave(final ServerPlayer player, final int chunkX, final int chunkZ) {
++ final PlayerLoaderData data = this.playerMap.get(player);
++
++ if (data == null) {
++ return;
++ }
++
++ data.unloadChunk(chunkX, chunkZ);
++ }
++
++ public void addPlayer(final ServerPlayer player) {
++ TickThread.ensureTickThread("Cannot add player async");
++ if (!player.isRealPlayer) {
++ return;
++ }
++ final PlayerLoaderData data = new PlayerLoaderData(player, this);
++ if (this.playerMap.putIfAbsent(player, data) == null) {
++ data.update();
++ }
++ }
++
++ public void removePlayer(final ServerPlayer player) {
++ TickThread.ensureTickThread("Cannot remove player async");
++ if (!player.isRealPlayer) {
++ return;
++ }
++
++ final PlayerLoaderData loaderData = this.playerMap.remove(player);
++ if (loaderData == null) {
++ return;
++ }
++ loaderData.remove();
++ this.chunkLoadQueue.remove(loaderData);
++ this.chunkSendQueue.remove(loaderData);
++ this.chunkSendWaitQueue.remove(loaderData);
++ synchronized (this.sendingChunkCounts) {
++ final int count = this.sendingChunkCounts.removeInt(loaderData);
++ if (count != 0) {
++ concurrentChunkSends.getAndAdd(-count);
++ }
++ }
++ }
++
++ public void updatePlayer(final ServerPlayer player) {
++ TickThread.ensureTickThread("Cannot update player async");
++ if (!player.isRealPlayer) {
++ return;
++ }
++ final PlayerLoaderData loaderData = this.playerMap.get(player);
++ if (loaderData != null) {
++ loaderData.update();
++ }
++ }
++
++ public PlayerLoaderData getData(final ServerPlayer player) {
++ return this.playerMap.get(player);
++ }
++
++ public void tick() {
++ TickThread.ensureTickThread("Cannot tick async");
++ for (final PlayerLoaderData data : this.playerMap.values()) {
++ data.update();
++ }
++ this.tickMidTick();
++ }
++
++ protected static final AtomicInteger concurrentChunkSends = new AtomicInteger();
++ protected final Reference2IntOpenHashMap sendingChunkCounts = new Reference2IntOpenHashMap<>();
++ private static long nextChunkSend;
++ private void trySendChunks() {
++ final long time = System.nanoTime();
++ if (time < nextChunkSend) {
++ return;
++ }
++ // drain entries from wait queue
++ while (!this.chunkSendWaitQueue.isEmpty()) {
++ final PlayerLoaderData data = this.chunkSendWaitQueue.first();
++
++ if (data.nextChunkSendTarget > time) {
++ break;
++ }
++
++ this.chunkSendWaitQueue.pollFirst();
++
++ this.chunkSendQueue.add(data);
++ }
++
++ if (this.chunkSendQueue.isEmpty()) {
++ return;
++ }
++
++ final int maxSends = this.getMaxConcurrentChunkSends();
++ final long nextPlayerDeadline = this.getTargetSendPerPlayerAddend() + time;
++ for (;;) {
++ if (this.chunkSendQueue.isEmpty()) {
++ break;
++ }
++ final int currSends = concurrentChunkSends.get();
++ if (currSends >= maxSends) {
++ break;
++ }
++
++ if (!concurrentChunkSends.compareAndSet(currSends, currSends + 1)) {
++ continue;
++ }
++
++ // send chunk
++
++ final PlayerLoaderData data = this.chunkSendQueue.removeFirst();
++
++ final ChunkPriorityHolder queuedSend = data.sendQueue.pollFirst();
++ if (queuedSend == null) {
++ concurrentChunkSends.getAndDecrement(); // we never sent, so decrease
++ // stop iterating over players who have nothing to send
++ if (this.chunkSendQueue.isEmpty()) {
++ // nothing left
++ break;
++ }
++ continue;
++ }
++
++ if (!this.isChunkPlayerLoaded(queuedSend.chunkX, queuedSend.chunkZ)) {
++ throw new IllegalStateException();
++ }
++
++ data.nextChunkSendTarget = nextPlayerDeadline;
++ this.chunkSendWaitQueue.add(data);
++
++ synchronized (this.sendingChunkCounts) {
++ this.sendingChunkCounts.addTo(data, 1);
++ }
++
++ data.sendChunk(queuedSend.chunkX, queuedSend.chunkZ, () -> {
++ synchronized (this.sendingChunkCounts) {
++ final int count = this.sendingChunkCounts.getInt(data);
++ if (count == 0) {
++ // disconnected, so we don't need to decrement: it will be decremented for us
++ return;
++ }
++ if (count == 1) {
++ this.sendingChunkCounts.removeInt(data);
++ } else {
++ this.sendingChunkCounts.put(data, count - 1);
++ }
++ }
++
++ concurrentChunkSends.getAndDecrement();
++ });
++
++ nextChunkSend = this.getMaxSendAddend() + time;
++ if (time < nextChunkSend) {
++ break;
++ }
++ }
++ }
++
++ protected int concurrentChunkLoads;
++ // this interval prevents bursting a lot of chunk loads
++ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_SHORT = new IntervalledCounter((long)(1.0e6 * 50.0)); // 50ms
++ // this interval ensures the rate is kept between ticks correctly
++ protected static final IntervalledCounter TICKET_ADDITION_COUNTER_LONG = new IntervalledCounter((long)(1.0e6 * 1000.0)); // 1000ms
++ private void tryLoadChunks() {
++ if (this.chunkLoadQueue.isEmpty()) {
++ return;
++ }
++
++ final int maxLoads = this.getMaxChunkLoads();
++ final long time = System.nanoTime();
++ boolean updatedCounters = false;
++ for (;;) {
++ final PlayerLoaderData data = this.chunkLoadQueue.pollFirst();
++
++ final ChunkPriorityHolder queuedLoad = data.loadQueue.peekFirst();
++ if (queuedLoad == null) {
++ if (this.chunkLoadQueue.isEmpty()) {
++ break;
++ }
++ continue;
++ }
++
++ if (!updatedCounters) {
++ updatedCounters = true;
++ TICKET_ADDITION_COUNTER_SHORT.updateCurrentTime(time);
++ TICKET_ADDITION_COUNTER_LONG.updateCurrentTime(time);
++ }
++
++ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) {
++ // already loaded!
++ data.loadQueue.pollFirst(); // already loaded so we just skip
++ this.chunkLoadQueue.add(data);
++
++ // ensure the chunk is queued to send
++ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ);
++ continue;
++ }
++
++ final long chunkKey = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ);
++
++ final double priority = queuedLoad.priority;
++ // while we do need to rate limit chunk loads, the logic for sending chunks requires that tickets are present.
++ // when chunks are loaded (i.e spawn) but do not have this player's tickets, they have to wait behind the
++ // load queue. To avoid this problem, we check early here if tickets are required to load the chunk - if they
++ // aren't required, it bypasses the limiter system.
++ boolean unloadedTargetChunk = false;
++ unloaded_check:
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ final int offX = queuedLoad.chunkX + dx;
++ final int offZ = queuedLoad.chunkZ + dz;
++ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) == null) {
++ unloadedTargetChunk = true;
++ break unloaded_check;
++ }
++ }
++ }
++ if (unloadedTargetChunk && priority >= 0.0) {
++ // priority >= 0.0 implies rate limited chunks
++
++ final int currentChunkLoads = this.concurrentChunkLoads;
++ if (currentChunkLoads >= maxLoads || (PaperConfig.globalMaxChunkLoadRate > 0 && (TICKET_ADDITION_COUNTER_SHORT.getRate() >= PaperConfig.globalMaxChunkLoadRate || TICKET_ADDITION_COUNTER_LONG.getRate() >= PaperConfig.globalMaxChunkLoadRate))) {
++ // don't poll, we didn't load it
++ this.chunkLoadQueue.add(data);
++ break;
++ }
++ }
++
++ // can only poll after we decide to load
++ data.loadQueue.pollFirst();
++
++ // now that we've polled we can re-add to load queue
++ this.chunkLoadQueue.add(data);
++
++ // add necessary tickets to load chunk up to send-ready
++ for (int dz = -1; dz <= 1; ++dz) {
++ for (int dx = -1; dx <= 1; ++dx) {
++ final int offX = queuedLoad.chunkX + dx;
++ final int offZ = queuedLoad.chunkZ + dz;
++ final ChunkPos chunkPos = new ChunkPos(offX, offZ);
++
++ this.chunkMap.level.getChunkSource().addTicketAtLevel(TicketType.PLAYER, chunkPos, LOADED_TICKET_LEVEL, chunkPos);
++ if (this.chunkMap.level.getChunkSource().getChunkAtIfLoadedMainThreadNoCache(offX, offZ) != null) {
++ continue;
++ }
++
++ if (priority > 0.0 && this.chunkTicketTracker.add(CoordinateUtils.getChunkKey(offX, offZ))) {
++ // won't reach here if unloadedTargetChunk is false
++ ++this.concurrentChunkLoads;
++ TICKET_ADDITION_COUNTER_SHORT.addTime(time);
++ TICKET_ADDITION_COUNTER_LONG.addTime(time);
++ }
++ }
++ }
++
++ // mark that we've added tickets here
++ this.isTargetedForPlayerLoad.add(chunkKey);
++
++ // it's possible all we needed was the player tickets to queue up the send.
++ if (this.isChunkPlayerLoaded(queuedLoad.chunkX, queuedLoad.chunkZ)) {
++ // yup, all we needed.
++ this.onChunkSendReady(queuedLoad.chunkX, queuedLoad.chunkZ);
++ } else if (this.chunkNeedsPostProcessing(queuedLoad.chunkX, queuedLoad.chunkZ)) {
++ // requires post processing
++ this.chunkMap.mainThreadExecutor.execute(() -> {
++ final long key = CoordinateUtils.getChunkKey(queuedLoad.chunkX, queuedLoad.chunkZ);
++ final ChunkHolder holder = PlayerChunkLoader.this.chunkMap.getVisibleChunkIfPresent(key);
++
++ if (holder == null) {
++ return;
++ }
++
++ final LevelChunk chunk = holder.getSendingChunk();
++
++ if (chunk != null && !chunk.isPostProcessingDone) {
++ chunk.postProcessGeneration();
++ }
++ });
++ }
++ }
++ }
++
++ public void tickMidTick() {
++ // try to send more chunks
++ this.trySendChunks();
++
++ // try to queue more chunks to load
++ this.tryLoadChunks();
++ }
++
++ static final class ChunkPriorityHolder {
++ public final int chunkX;
++ public final int chunkZ;
++ public final int manhattanDistanceToPlayer;
++ public final double priority;
++
++ public ChunkPriorityHolder(final int chunkX, final int chunkZ, final int manhattanDistanceToPlayer, final double priority) {
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.manhattanDistanceToPlayer = manhattanDistanceToPlayer;
++ this.priority = priority;
++ }
++ }
++
++ public static final class PlayerLoaderData {
++
++ protected static final float FOV = 110.0f;
++ protected static final double PRIORITISED_DISTANCE = 12.0 * 16.0;
++
++ // Player max sprint speed is approximately 8m/s
++ protected static final double LOOK_PRIORITY_SPEED_THRESHOLD = (10.0/20.0) * (10.0/20.0);
++ protected static final double LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD = 3.0f;
++
++ protected double lastLocX = Double.NEGATIVE_INFINITY;
++ protected double lastLocZ = Double.NEGATIVE_INFINITY;
++
++ protected int lastChunkX = Integer.MIN_VALUE;
++ protected int lastChunkZ = Integer.MIN_VALUE;
++
++ // this is corrected so that 0 is along the positive x-axis
++ protected float lastYaw = Float.NEGATIVE_INFINITY;
++
++ protected int lastSendDistance = Integer.MIN_VALUE;
++ protected int lastLoadDistance = Integer.MIN_VALUE;
++ protected int lastTickDistance = Integer.MIN_VALUE;
++ protected boolean usingLookingPriority;
++
++ protected final ServerPlayer player;
++ protected final PlayerChunkLoader loader;
++
++ // warning: modifications of this field must be aware that the loadQueue inside PlayerChunkLoader uses this field
++ // in a comparator!
++ protected final ArrayDeque loadQueue = new ArrayDeque<>();
++ protected final LongOpenHashSet sentChunks = new LongOpenHashSet();
++ protected final LongOpenHashSet chunksToBeSent = new LongOpenHashSet();
++
++ protected final TreeSet sendQueue = new TreeSet<>((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> {
++ final int distanceCompare = Integer.compare(p1.manhattanDistanceToPlayer, p2.manhattanDistanceToPlayer);
++ if (distanceCompare != 0) {
++ return distanceCompare;
++ }
++
++ final int coordinateXCompare = Integer.compare(p1.chunkX, p2.chunkX);
++ if (coordinateXCompare != 0) {
++ return coordinateXCompare;
++ }
++
++ return Integer.compare(p1.chunkZ, p2.chunkZ);
++ });
++
++ protected int sendViewDistance = -1;
++ protected int loadViewDistance = -1;
++ protected int tickViewDistance = -1;
++
++ protected long nextChunkSendTarget;
++
++ public PlayerLoaderData(final ServerPlayer player, final PlayerChunkLoader loader) {
++ this.player = player;
++ this.loader = loader;
++ }
++
++ // these view distance methods are for api
++ public int getTargetSendViewDistance() {
++ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
++ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance);
++ final int clientViewDistance = this.getClientViewDistance();
++ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!PaperConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance);
++ return sendViewDistance;
++ }
++
++ public void setTargetSendViewDistance(final int distance) {
++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE + 1)) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ this.sendViewDistance = distance;
++ }
++
++ public int getTargetNoTickViewDistance() {
++ return (this.loadViewDistance == -1 ? this.getLoadDistance() : this.loadViewDistance) - 1;
++ }
++
++ public void setTargetNoTickViewDistance(final int distance) {
++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ this.loadViewDistance = distance == -1 ? -1 : distance + 1;
++ }
++
++ public int getTargetTickViewDistance() {
++ return this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
++ }
++
++ public void setTargetTickViewDistance(final int distance) {
++ if (distance != -1 && (distance < MIN_VIEW_DISTANCE || distance > MAX_VIEW_DISTANCE)) {
++ throw new IllegalArgumentException(Integer.toString(distance));
++ }
++ this.tickViewDistance = distance;
++ }
++
++ protected int getLoadDistance() {
++ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
++
++ return Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance);
++ }
++
++ public boolean hasSentChunk(final int chunkX, final int chunkZ) {
++ return this.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++
++ public void sendChunk(final int chunkX, final int chunkZ, final Runnable onChunkSend) {
++ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
++ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player,
++ new ChunkPos(chunkX, chunkZ), new MutableObject<>(), false, true); // unloaded, loaded
++ this.player.connection.connection.execute(onChunkSend);
++ } else {
++ throw new IllegalStateException();
++ }
++ }
++
++ public void unloadChunk(final int chunkX, final int chunkZ) {
++ if (this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
++ this.player.getLevel().getChunkSource().chunkMap.updateChunkTracking(this.player,
++ new ChunkPos(chunkX, chunkZ), null, true, false); // unloaded, loaded
++ }
++ }
++
++ protected static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ,
++ final int sendRadius) {
++ // expect sendRadius to be = 1 + target viewable radius
++ return ChunkMap.isChunkInRange(chunkX, chunkZ, centerX, centerZ, sendRadius);
++ }
++
++ protected static boolean triangleIntersects(final double p1x, final double p1z, // triangle point
++ final double p2x, final double p2z, // triangle point
++ final double p3x, final double p3z, // triangle point
++
++ final double targetX, final double targetZ) { // point
++ // from barycentric coordinates:
++ // targetX = a*p1x + b*p2x + c*p3x
++ // targetZ = a*p1z + b*p2z + c*p3z
++ // 1.0 = a*1.0 + b*1.0 + c*1.0
++ // where a, b, c >= 0.0
++ // so, if any of a, b, c are less-than zero then there is no intersection.
++
++ // d = ((p2z - p3z)(p1x - p3x) + (p3x - p2x)(p1z - p3z))
++ // a = ((p2z - p3z)(targetX - p3x) + (p3x - p2x)(targetZ - p3z)) / d
++ // b = ((p3z - p1z)(targetX - p3x) + (p1x - p3x)(targetZ - p3z)) / d
++ // c = 1.0 - a - b
++
++ final double d = (p2z - p3z)*(p1x - p3x) + (p3x - p2x)*(p1z - p3z);
++ final double a = ((p2z - p3z)*(targetX - p3x) + (p3x - p2x)*(targetZ - p3z)) / d;
++
++ if (a < 0.0 || a > 1.0) {
++ return false;
++ }
++
++ final double b = ((p3z - p1z)*(targetX - p3x) + (p1x - p3x)*(targetZ - p3z)) / d;
++ if (b < 0.0 || b > 1.0) {
++ return false;
++ }
++
++ final double c = 1.0 - a - b;
++
++ return c >= 0.0 && c <= 1.0;
++ }
++
++ public void remove() {
++ this.loader.broadcastMap.remove(this.player);
++ this.loader.loadMap.remove(this.player);
++ this.loader.loadTicketCleanup.remove(this.player);
++ this.loader.tickMap.remove(this.player);
++ }
++
++ protected int getClientViewDistance() {
++ return this.player.clientViewDistance == null ? -1 : this.player.clientViewDistance.intValue();
++ }
++
++ public void update() {
++ final int tickViewDistance = this.tickViewDistance == -1 ? this.loader.getTickDistance() : this.tickViewDistance;
++ // load view cannot be less-than tick view + 1
++ final int loadViewDistance = Math.max(tickViewDistance + 1, this.loadViewDistance == -1 ? this.loader.getLoadDistance() : this.loadViewDistance);
++ // send view cannot be greater-than load view
++ final int clientViewDistance = this.getClientViewDistance();
++ final int sendViewDistance = Math.min(loadViewDistance, this.sendViewDistance == -1 ? (!PaperConfig.playerAutoConfigureSendViewDistance || clientViewDistance == -1 ? this.loader.getSendDistance() : clientViewDistance + 1) : this.sendViewDistance);
++
++ final double posX = this.player.getX();
++ final double posZ = this.player.getZ();
++ final float yaw = MCUtil.normalizeYaw(this.player.yRot + 90.0f); // mc yaw 0 is along the positive z axis, but obviously this is really dumb - offset so we are at positive x-axis
++
++ // in general, we really only want to prioritise chunks in front if we know we're moving pretty fast into them.
++ final boolean useLookPriority = PaperConfig.playerFrustumPrioritisation && (this.player.getDeltaMovement().horizontalDistanceSqr() > LOOK_PRIORITY_SPEED_THRESHOLD ||
++ this.player.getAbilities().flying);
++
++ // make sure we're in the send queue
++ this.loader.chunkSendWaitQueue.add(this);
++
++ if (
++ // has view distance stayed the same?
++ sendViewDistance == this.lastSendDistance
++ && loadViewDistance == this.lastLoadDistance
++ && tickViewDistance == this.lastTickDistance
++
++ && (this.usingLookingPriority ? (
++ // has our block stayed the same (this also accounts for chunk change)?
++ Mth.floor(this.lastLocX) == Mth.floor(posX)
++ && Mth.floor(this.lastLocZ) == Mth.floor(posZ)
++ ) : (
++ // has our chunk stayed the same
++ (Mth.floor(this.lastLocX) >> 4) == (Mth.floor(posX) >> 4)
++ && (Mth.floor(this.lastLocZ) >> 4) == (Mth.floor(posZ) >> 4)
++ ))
++
++ // has our decision about look priority changed?
++ && this.usingLookingPriority == useLookPriority
++
++ // if we are currently using look priority, has our yaw stayed within recalc threshold?
++ && (!this.usingLookingPriority || Math.abs(yaw - this.lastYaw) <= LOOK_PRIORITY_YAW_DELTA_RECALC_THRESHOLD)
++ ) {
++ // nothing we care about changed, so we're not re-calculating
++ return;
++ }
++
++ final int centerChunkX = Mth.floor(posX) >> 4;
++ final int centerChunkZ = Mth.floor(posZ) >> 4;
++
++ final boolean needsChunkCenterUpdate = (centerChunkX != this.lastChunkX) || (centerChunkZ != this.lastChunkZ);
++ this.loader.broadcastMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, sendViewDistance);
++ this.loader.loadMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance);
++ this.loader.loadTicketCleanup.addOrUpdate(this.player, centerChunkX, centerChunkZ, loadViewDistance + 1);
++ this.loader.tickMap.addOrUpdate(this.player, centerChunkX, centerChunkZ, tickViewDistance);
++
++ if (sendViewDistance != this.lastSendDistance) {
++ // update the view radius for client
++ // note that this should be after the map calls because the client wont expect unload calls not in its VD
++ // and it's possible we decreased VD here
++ this.player.connection.send(new ClientboundSetChunkCacheRadiusPacket(sendViewDistance));
++ }
++ if (tickViewDistance != this.lastTickDistance) {
++ this.player.connection.send(new ClientboundSetSimulationDistancePacket(tickViewDistance));
++ }
++
++ this.lastLocX = posX;
++ this.lastLocZ = posZ;
++ this.lastYaw = yaw;
++ this.lastSendDistance = sendViewDistance;
++ this.lastLoadDistance = loadViewDistance;
++ this.lastTickDistance = tickViewDistance;
++ this.usingLookingPriority = useLookPriority;
++
++ this.lastChunkX = centerChunkX;
++ this.lastChunkZ = centerChunkZ;
++
++ // points for player "view" triangle:
++
++ // obviously, the player pos is a vertex
++ final double p1x = posX;
++ final double p1z = posZ;
++
++ // to the left of the looking direction
++ final double p2x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector
++ + p1x; // offset vector
++ final double p2z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw + (double)(FOV / 2.0))) // calculate rotated vector
++ + p1z; // offset vector
++
++ // to the right of the looking direction
++ final double p3x = PRIORITISED_DISTANCE * Math.cos(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector
++ + p1x; // offset vector
++ final double p3z = PRIORITISED_DISTANCE * Math.sin(Math.toRadians(yaw - (double)(FOV / 2.0))) // calculate rotated vector
++ + p1z; // offset vector
++
++ // now that we have all of our points, we can recalculate the load queue
++
++ final List loadQueue = new ArrayList<>();
++
++ // clear send queue, we are re-sorting
++ this.sendQueue.clear();
++ // clear chunk want set, vd/position might have changed
++ this.chunksToBeSent.clear();
++
++ final int searchViewDistance = Math.max(loadViewDistance, sendViewDistance);
++
++ for (int dx = -searchViewDistance; dx <= searchViewDistance; ++dx) {
++ for (int dz = -searchViewDistance; dz <= searchViewDistance; ++dz) {
++ final int chunkX = dx + centerChunkX;
++ final int chunkZ = dz + centerChunkZ;
++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
++ final boolean sendChunk = squareDistance <= sendViewDistance && wantChunkLoaded(centerChunkX, centerChunkZ, chunkX, chunkZ, sendViewDistance);
++
++ if (this.hasSentChunk(chunkX, chunkZ)) {
++ // already sent (which means it is also loaded)
++ if (!sendChunk) {
++ // have sent the chunk, but don't want it anymore
++ // unload it now
++ this.unloadChunk(chunkX, chunkZ);
++ }
++ continue;
++ }
++
++ final boolean loadChunk = squareDistance <= loadViewDistance;
++
++ final boolean prioritised = useLookPriority && triangleIntersects(
++ // prioritisation triangle
++ p1x, p1z, p2x, p2z, p3x, p3z,
++
++ // center of chunk
++ (double)((chunkX << 4) | 8), (double)((chunkZ << 4) | 8)
++ );
++
++ final int manhattanDistance = Math.abs(dx) + Math.abs(dz);
++
++ final double priority;
++
++ if (squareDistance <= PaperConfig.playerMinChunkLoadRadius) {
++ // priority should be negative, and we also want to order it from center outwards
++ // so we want (0,0) to be the smallest, and (minLoadedRadius,minLoadedRadius) to be the greatest
++ priority = -((2 * PaperConfig.playerMinChunkLoadRadius + 1) - manhattanDistance);
++ } else {
++ if (prioritised) {
++ // we don't prioritise these chunks above others because we also want to make sure some chunks
++ // will be loaded if the player changes direction
++ priority = (double)manhattanDistance / 6.0;
++ } else {
++ priority = (double)manhattanDistance;
++ }
++ }
++
++ final ChunkPriorityHolder holder = new ChunkPriorityHolder(chunkX, chunkZ, manhattanDistance, priority);
++
++ if (!this.loader.isChunkPlayerLoaded(chunkX, chunkZ)) {
++ if (loadChunk) {
++ loadQueue.add(holder);
++ if (sendChunk) {
++ this.chunksToBeSent.add(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
++ }
++ } else {
++ // loaded but not sent: so queue it!
++ if (sendChunk) {
++ this.sendQueue.add(holder);
++ }
++ }
++ }
++ }
++
++ loadQueue.sort((final ChunkPriorityHolder p1, final ChunkPriorityHolder p2) -> {
++ return Double.compare(p1.priority, p2.priority);
++ });
++
++ // we're modifying loadQueue, must remove
++ this.loader.chunkLoadQueue.remove(this);
++
++ this.loadQueue.clear();
++ this.loadQueue.addAll(loadQueue);
++
++ // must re-add
++ this.loader.chunkLoadQueue.add(this);
++
++ // update the chunk center
++ // this must be done last so that the client does not ignore any of our unload chunk packets
++ if (needsChunkCenterUpdate) {
++ this.player.connection.send(new ClientboundSetChunkCacheCenterPacket(centerChunkX, centerChunkZ));
++ }
++ }
++ }
++}
+diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/network/Connection.java
++++ b/src/main/java/net/minecraft/network/Connection.java
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ public boolean queueImmunity = false;
+ public ConnectionProtocol protocol;
+ // Paper end
++ // Paper start - add pending task queue
++ private final Queue pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
++ public void execute(final Runnable run) {
++ if (this.channel == null || !this.channel.isRegistered()) {
++ run.run();
++ return;
++ }
++ final boolean queue = !this.queue.isEmpty();
++ if (!queue) {
++ this.channel.eventLoop().execute(run);
++ } else {
++ this.pendingTasks.add(run);
++ if (this.queue.isEmpty()) {
++ // something flushed async, dump tasks now
++ Runnable r;
++ while ((r = this.pendingTasks.poll()) != null) {
++ this.channel.eventLoop().execute(r);
++ }
++ }
++ }
++ }
++ // Paper end - add pending task queue
+
+ // Paper start - allow controlled flushing
+ volatile boolean canFlush = true;
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ return false;
+ }
+ private boolean processQueue() {
++ try { // Paper - add pending task queue
+ if (this.queue.isEmpty()) return true;
+ // Paper start - make only one flush call per sendPacketQueue() call
+ final boolean needsFlush = this.canFlush;
+@@ -0,0 +0,0 @@ public class Connection extends SimpleChannelInboundHandler> {
+ }
+ }
+ return true;
++ } finally { // Paper start - add pending task queue
++ Runnable r;
++ while ((r = this.pendingTasks.poll()) != null) {
++ this.channel.eventLoop().execute(r);
++ }
++ } // Paper end - add pending task queue
+ }
+ // Paper end
+
+diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/MCUtil.java
++++ b/src/main/java/net/minecraft/server/MCUtil.java
+@@ -0,0 +0,0 @@ public final class MCUtil {
+ });
+
+ worldData.addProperty("name", world.getWorld().getName());
+- worldData.addProperty("view-distance", world.spigotConfig.viewDistance);
++ worldData.addProperty("view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance()); // Paper - replace chunk loader system
++ worldData.addProperty("tick-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()); // Paper - replace chunk loader system
+ worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
+ worldData.addProperty("keep-spawn-loaded-range", world.paperConfig.keepLoadedRange);
+ worldData.addProperty("visible-chunk-count", visibleChunks.size());
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+ public ServerLevel getWorld() { return chunkMap.level; } // Paper
+ boolean isUpdateQueued = false; // Paper
+ private final ChunkMap chunkMap; // Paper
++ // Paper start - no-tick view distance
++ public final LevelChunk getSendingChunk() {
++ // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used
++ // in Chunk's neighbour callback
++ LevelChunk ret = this.chunkMap.level.getChunkSource().getChunkAtIfLoadedImmediately(this.pos.x, this.pos.z);
++ if (ret != null && ret.areNeighboursLoaded(1)) {
++ return ret;
++ }
++ return null;
++ }
++ // Paper end - no-tick view distance
+
+ // Paper start - optimise anyPlayerCloseEnoughForSpawning
+ // cached here to avoid a map lookup
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+
+ public void blockChanged(BlockPos pos) {
+ if (!pos.isInsideBuildHeightAndWorldBoundsHorizontal(levelHeightAccessor)) return; // Paper - SPIGOT-6086 for all invalid locations; avoid acquiring locks
+- LevelChunk chunk = this.getTickingChunk();
++ LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance
+
+ if (chunk != null) {
+ int i = this.levelHeightAccessor.getSectionIndex(pos.getY());
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+ }
+
+ public void sectionLightChanged(LightLayer lightType, int y) {
+- LevelChunk chunk = this.getTickingChunk();
++ LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance
+
+ if (chunk != null) {
+ chunk.setUnsaved(true);
+@@ -0,0 +0,0 @@ public class ChunkHolder {
+ }
+
+ public void broadcast(Packet> packet, boolean onlyOnWatchDistanceEdge) {
+- this.playerProvider.getPlayers(this.pos, onlyOnWatchDistanceEdge).forEach((entityplayer) -> {
+- entityplayer.connection.send(packet);
+- });
++ // Paper start - per player view distance
++ // there can be potential desync with player's last mapped section and the view distance map, so use the
++ // view distance map here.
++ com.destroystokyo.paper.util.misc.PlayerAreaMap viewDistanceMap = this.chunkMap.playerChunkManager.broadcastMap; // Paper - replace old player chunk manager
++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.pos);
++ if (players == null) {
++ return;
++ }
++
++ Object[] backingSet = players.getBackingSet();
++ for (int i = 0, len = backingSet.length; i < len; ++i) {
++ Object temp = backingSet[i];
++ if (!(temp instanceof ServerPlayer)) {
++ continue;
++ }
++ ServerPlayer player = (ServerPlayer)temp;
++ if (!this.chunkMap.playerChunkManager.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) {
++ continue;
++ }
++ player.connection.send(packet);
++ }
++ // Paper end - per player view distance
+ }
+
+ public CompletableFuture> getOrScheduleFuture(ChunkStatus targetStatus, ChunkMap chunkStorage) {
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick
+ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap;
+ // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
++ public final io.papermc.paper.chunk.PlayerChunkLoader playerChunkManager = new io.papermc.paper.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Paper - replace chunk loader
+ // Paper start - use distance map to optimise tracker
+ public static boolean isLegacyTrackingEntity(Entity entity) {
+ return entity.isLegacyTrackingEntity;
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ // Paper end - use distance map to optimise tracker
+
+ void addPlayerToDistanceMaps(ServerPlayer player) {
++ this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader
+ int chunkX = MCUtil.getChunkCoordinate(player.getX());
+ int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
+ // Paper start - use distance map to optimise entity tracker
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
+ int trackRange = this.entityTrackerTrackRanges[i];
+
+- trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance()));
++ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player))); // Paper - per player view distances
+ }
+ // Paper end - use distance map to optimise entity tracker
+ // Note: players need to be explicitly added to distance maps before they can be updated
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.playerMobDistanceMap.remove(player);
+ }
+ // Paper end - per player mob spawning
++ this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader
+ }
+
+ void updateMaps(ServerPlayer player) {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
+ int trackRange = this.entityTrackerTrackRanges[i];
+
+- trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance()));
++ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player))); // Paper - per player view distances
+ }
+ // Paper end - use distance map to optimise entity tracker
+ this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.playerMobDistanceMap.update(player, chunkX, chunkZ, this.distanceManager.getSimulationDistance());
+ }
+ // Paper end - per player mob spawning
++ this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader
+ }
+ // Paper end
+ // Paper start
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ completablefuture1.thenAcceptAsync((either) -> {
+ either.ifLeft((chunk) -> {
+ this.tickingGenerated.getAndIncrement();
+- MutableObject> mutableobject = new MutableObject<>(); // Paper - Anti-Xray - Bypass
+-
+- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> {
+- this.playerLoadedChunk(entityplayer, mutableobject, chunk);
+- });
++ // Paper - no-tick view distance - moved to Chunk neighbour update
+ });
+ }, (runnable) -> {
+ this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable));
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ int k = this.viewDistance;
+
+ this.viewDistance = j;
+- this.distanceManager.updatePlayerTickets(this.viewDistance + 1);
+- Iterator objectiterator = this.updatingChunks.getVisibleValuesCopy().iterator(); // Paper
+-
+- while (objectiterator.hasNext()) {
+- ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
+- ChunkPos chunkcoordintpair = playerchunk.getPos();
+- MutableObject> mutableobject = new MutableObject<>(); // Paper - Anti-Xray - Bypass
+-
+- this.getPlayers(chunkcoordintpair, false).forEach((entityplayer) -> {
+- SectionPos sectionposition = entityplayer.getLastSectionPos();
+- boolean flag = ChunkMap.isChunkInRange(chunkcoordintpair.x, chunkcoordintpair.z, sectionposition.x(), sectionposition.z(), k);
+- boolean flag1 = ChunkMap.isChunkInRange(chunkcoordintpair.x, chunkcoordintpair.z, sectionposition.x(), sectionposition.z(), this.viewDistance);
+-
+- this.updateChunkTracking(entityplayer, chunkcoordintpair, mutableobject, flag, flag1);
+- });
+- }
++ this.playerChunkManager.setLoadDistance(Mth.clamp(this.viewDistance, 2, 32)); // Paper - replace player loader system
+ }
+
+ }
+
+- protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - Anti-Xray - Bypass
++ // Paper start - replace player loader system
++ public void setTickViewDistance(int distance) {
++ this.playerChunkManager.setTickDistance(distance);
++ }
++ // Paper end - replace player loader system
++
++ public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - Anti-Xray - Bypass // Paper - public
+ if (player.level == this.level) {
+ if (newWithinViewDistance && !oldWithinViewDistance) {
+ ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong());
+
+ if (playerchunk != null) {
+- LevelChunk chunk = playerchunk.getTickingChunk();
++ LevelChunk chunk = playerchunk.getSendingChunk(); // Paper - replace chunk loader system
+
+ if (chunk != null) {
+ this.playerLoadedChunk(player, packet, chunk);
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+
+ void dumpChunks(Writer writer) throws IOException {
+ CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
+- TickingTracker tickingtracker = this.distanceManager.tickingTracker();
++ // Paper - replace loader system
+ ObjectBidirectionalIterator objectbidirectionaliterator = this.updatingChunks.getVisibleMap().clone().long2ObjectEntrySet().fastIterator(); // Paper
+
+ while (objectbidirectionaliterator.hasNext()) {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ // CraftBukkit - decompile error
+ csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getStatus).orElse(null), optional1.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> {
+ return chunk.getBlockEntities().size();
+- }).orElse(0), tickingtracker.getTicketDebugString(i), tickingtracker.getLevel(i), optional1.map((chunk) -> {
++ }).orElse(0), "Use ticket level", -1000, optional1.map((chunk) -> { // Paper - replace loader system
+ return chunk.getBlockTicks().count();
+ }).orElse(0), optional1.map((chunk) -> {
+ return chunk.getFluidTicks().count();
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.removePlayerFromDistanceMaps(player); // Paper - distance maps
+ }
+
+- for (int k = i - this.viewDistance - 1; k <= i + this.viewDistance + 1; ++k) {
+- for (int l = j - this.viewDistance - 1; l <= j + this.viewDistance + 1; ++l) {
+- if (ChunkMap.isChunkInRange(k, l, i, j, this.viewDistance)) {
+- ChunkPos chunkcoordintpair = new ChunkPos(k, l);
+-
+- this.updateChunkTracking(player, chunkcoordintpair, new MutableObject(), !added, added);
+- }
+- }
+- }
++ // Paper - handled by player chunk loader
+
+ }
+
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ SectionPos sectionposition = SectionPos.of((Entity) player);
+
+ player.setLastSectionPos(sectionposition);
+- player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionposition.x(), sectionposition.z()));
++ //player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionposition.x(), sectionposition.z())); // Paper - handled by player chunk loader
+ return sectionposition;
+ }
+
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ int k1;
+ int l1;
+
+- if (Math.abs(i1 - i) <= this.viewDistance * 2 && Math.abs(j1 - j) <= this.viewDistance * 2) {
+- k1 = Math.min(i, i1) - this.viewDistance - 1;
+- l1 = Math.min(j, j1) - this.viewDistance - 1;
+- int i2 = Math.max(i, i1) + this.viewDistance + 1;
+- int j2 = Math.max(j, j1) + this.viewDistance + 1;
+-
+- for (int k2 = k1; k2 <= i2; ++k2) {
+- for (int l2 = l1; l2 <= j2; ++l2) {
+- boolean flag3 = ChunkMap.isChunkInRange(k2, l2, i1, j1, this.viewDistance);
+- boolean flag4 = ChunkMap.isChunkInRange(k2, l2, i, j, this.viewDistance);
+-
+- this.updateChunkTracking(player, new ChunkPos(k2, l2), new MutableObject(), flag3, flag4);
+- }
+- }
+- } else {
+- boolean flag5;
+- boolean flag6;
+-
+- for (k1 = i1 - this.viewDistance - 1; k1 <= i1 + this.viewDistance + 1; ++k1) {
+- for (l1 = j1 - this.viewDistance - 1; l1 <= j1 + this.viewDistance + 1; ++l1) {
+- if (ChunkMap.isChunkInRange(k1, l1, i1, j1, this.viewDistance)) {
+- flag5 = true;
+- flag6 = false;
+- this.updateChunkTracking(player, new ChunkPos(k1, l1), new MutableObject(), true, false);
+- }
+- }
+- }
+-
+- for (k1 = i - this.viewDistance - 1; k1 <= i + this.viewDistance + 1; ++k1) {
+- for (l1 = j - this.viewDistance - 1; l1 <= j + this.viewDistance + 1; ++l1) {
+- if (ChunkMap.isChunkInRange(k1, l1, i, j, this.viewDistance)) {
+- flag5 = false;
+- flag6 = true;
+- this.updateChunkTracking(player, new ChunkPos(k1, l1), new MutableObject(), false, true);
+- }
+- }
+- }
+- }
++ // Paper - replaced by PlayerChunkLoader
+
+ this.updateMaps(player); // Paper - distance maps
++ this.playerChunkManager.updatePlayer(player); // Paper - respond to movement immediately
+
+ }
+
+ @Override
+ public List getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) {
+- Set set = this.playerMap.getPlayers(chunkPos.toLong());
+- Builder builder = ImmutableList.builder();
+- Iterator iterator = set.iterator();
++ // Paper start - per player view distance
++ // there can be potential desync with player's last mapped section and the view distance map, so use the
++ // view distance map here.
++ List ret = new java.util.ArrayList<>(4);
+
+- while (iterator.hasNext()) {
+- ServerPlayer entityplayer = (ServerPlayer) iterator.next();
+- SectionPos sectionposition = entityplayer.getLastSectionPos();
++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos);
++ if (players == null) {
++ return ret;
++ }
+
+- if (onlyOnWatchDistanceEdge && ChunkMap.isChunkOnRangeBorder(chunkPos.x, chunkPos.z, sectionposition.x(), sectionposition.z(), this.viewDistance) || !onlyOnWatchDistanceEdge && ChunkMap.isChunkInRange(chunkPos.x, chunkPos.z, sectionposition.x(), sectionposition.z(), this.viewDistance)) {
+- builder.add(entityplayer);
++ Object[] backingSet = players.getBackingSet();
++ for (int i = 0, len = backingSet.length; i < len; ++i) {
++ Object temp = backingSet[i];
++ if (!(temp instanceof ServerPlayer)) {
++ continue;
++ }
++ ServerPlayer player = (ServerPlayer)temp;
++ if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z, onlyOnWatchDistanceEdge)) {
++ continue;
+ }
++ ret.add(player);
+ }
+
+- return builder.build();
++ return ret;
++ // Paper end - per player view distance
+ }
+
+ public void addEntity(Entity entity) {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ double vec3d_dx = player.getX() - this.entity.getX();
+ double vec3d_dz = player.getZ() - this.entity.getZ();
+ // Paper end - remove allocation of Vec3D here
+- double d0 = (double) Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16);
++ double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance
+ double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper
+ double d2 = d0 * d0;
+ boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player);
+diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap();
+ private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
+ public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used
+- private final TickingTracker tickingTicketsTracker = new TickingTracker();
+- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33);
++ //private final TickingTracker tickingTicketsTracker = new TickingTracker(); // Paper - no longer used
++ //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Paper - no longer used
+ // Paper start use a queue, but still keep unique requirement
+ public final java.util.Queue pendingChunkUpdates = new java.util.ArrayDeque() {
+ @Override
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ java.util.function.Predicate> removeIf = (ticket) -> {
+ final boolean ret = ticket.timedOut(ticketCounter);
+ if (ret) {
+- this.tickingTicketsTracker.removeTicket(currChunk[0], ticket);
++ //this.tickingTicketsTracker.removeTicket(currChunk[0], ticket); // Paper - no longer used
+ }
+ return ret;
+ };
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ if (ticket.timedOut(this.ticketTickCounter)) {
+ iterator.remove();
+ flag = true;
+- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket);
++ //this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); // Paper - no longer used
+ }
+ }
+
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+
+ public boolean runAllUpdates(ChunkMap chunkStorage) {
+ //this.f.a(); // Paper - no longer used
+- this.tickingTicketsTracker.runAllUpdates();
++ //this.tickingTicketsTracker.runAllUpdates(); // Paper - no longer used
+ org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper
+- this.playerTicketManager.runAllUpdates();
++ //this.playerTicketManager.runAllUpdates(); // Paper - no longer used
+ int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
+ boolean flag = i != 0;
+
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ long j = pos.toLong();
+
+ this.addTicket(j, ticket);
+- this.tickingTicketsTracker.addTicket(j, ticket);
++ //this.tickingTicketsTracker.addTicket(j, ticket); // Paper - no longer used
+ }
+
+ public void removeRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) {
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ long j = pos.toLong();
+
+ this.removeTicket(j, ticket);
+- this.tickingTicketsTracker.removeTicket(j, ticket);
++ //this.tickingTicketsTracker.removeTicket(j, ticket); // Paper - no longer used
+ }
+
+ private SortedArraySet> getTickets(long position) {
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+
+ if (forced) {
+ this.addTicket(i, ticket);
+- this.tickingTicketsTracker.addTicket(i, ticket);
++ //this.tickingTicketsTracker.addTicket(i, ticket); // Paper - no longer used
+ } else {
+ this.removeTicket(i, ticket);
+- this.tickingTicketsTracker.removeTicket(i, ticket);
++ //this.tickingTicketsTracker.removeTicket(i, ticket); // Paper - no longer used
+ }
+
+ }
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ return new ObjectOpenHashSet();
+ })).add(player);
+ //this.f.update(i, 0, true); // Paper - no longer used
+- this.playerTicketManager.update(i, 0, true);
+- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
++ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used
++ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
+ }
+
+ public void removePlayer(SectionPos pos, ServerPlayer player) {
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ if (objectset == null || objectset.isEmpty()) { // Paper
+ this.playersPerChunk.remove(i);
+ //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
+- this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
+- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair);
++ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
++ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
+ }
+
+ }
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ }
+
+ public boolean inEntityTickingRange(long chunkPos) {
+- return this.tickingTicketsTracker.getLevel(chunkPos) < 32;
++ // Paper start - replace player chunk loader system
++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos);
++ return holder != null && holder.isEntityTickingReady();
++ // Paper end - replace player chunk loader system
+ }
+
+ public boolean inBlockTickingRange(long chunkPos) {
+- return this.tickingTicketsTracker.getLevel(chunkPos) < 33;
++ // Paper start - replace player chunk loader system
++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos);
++ return holder != null && holder.isTickingReady();
++ // Paper end - replace player chunk loader system
+ }
+
+ protected String getTicketDebugString(long pos) {
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ }
+
+ protected void updatePlayerTickets(int viewDistance) {
+- this.playerTicketManager.updateViewDistance(viewDistance);
++ this.chunkMap.playerChunkManager.setTargetNoTickViewDistance(viewDistance); // Paper - route to player chunk manager
+ }
+
+ public void updateSimulationDistance(int simulationDistance) {
+- if (simulationDistance != this.simulationDistance) {
+- this.simulationDistance = simulationDistance;
+- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel());
+- }
+-
++ this.chunkMap.playerChunkManager.setTargetTickViewDistance(simulationDistance); // Paper - route to player chunk manager
+ }
+
+ // Paper start
+ public int getSimulationDistance() {
+- return this.simulationDistance;
++ return this.chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - route to player chunk manager
+ }
+ // Paper end
+
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+
+ }
+
+- @VisibleForTesting
+- TickingTracker tickingTracker() {
+- return this.tickingTicketsTracker;
+- }
++ // Paper - replace player chunk loader
+
+ // CraftBukkit start
+ public void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) {
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ }
+ }
+
++ /* Paper - replace old loader system
+ private class FixedPlayerDistanceChunkTracker extends ChunkTracker {
+
+ protected final Long2ByteMap chunks = new Long2ByteOpenHashMap();
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ return distance <= this.viewDistance - 2;
+ }
+ }
++ */ // Paper - replace old loader system
+ }
+diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+ // Paper end
+
+ public boolean isPositionTicking(long pos) {
+- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
+-
+- if (playerchunk == null) {
+- return false;
+- } else if (!this.level.shouldTickBlocksAt(pos)) {
+- return false;
+- } else {
+- Either either = (Either) playerchunk.getTickingChunkFuture().getNow(null); // CraftBukkit - decompile error
+-
+- return either != null && either.left().isPresent();
+- }
++ // Paper start - replace player chunk loader system
++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(pos);
++ return holder != null && holder.isTickingReady();
++ // Paper end - replace player chunk loader system
+ }
+
+ public void save(boolean flush) {
+@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+ this.level.timings.doChunkMap.stopTiming(); // Spigot
+ this.level.getProfiler().popPush("chunks");
+ this.level.timings.chunks.startTiming(); // Paper - timings
++ this.chunkMap.playerChunkManager.tick(); // Paper - this is mostly is to account for view distance changes
+ this.tickChunks();
+ this.level.timings.chunks.stopTiming(); // Paper - timings
+ this.level.timings.doChunkUnload.startTiming(); // Spigot
+@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+ // Paper end - optimise chunk tick iteration
+ ChunkPos chunkcoordintpair = chunk1.getPos();
+
+- if (this.level.isPositionEntityTicking(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning
++ if ((true || this.level.isPositionEntityTicking(chunkcoordintpair)) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning // Paper - replace player chunk loader system
+ chunk1.incrementInhabitedTime(j);
+ if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration
+ NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1);
+ }
+
+- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
++ if ((true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong()))) { // Paper - replace player chunk loader system
+ this.level.tickChunk(chunk1, k);
+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper
+ }
+@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
+ public boolean pollTask() {
+ try {
+ boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ServerChunkCache.this.level.asyncChunkTaskManager.pollNextChunkTask(); // Paper
++ ServerChunkCache.this.chunkMap.playerChunkManager.tickMidTick(); // Paper
+ if (ServerChunkCache.this.runDistanceManagerUpdates()) {
+ return true;
+ } else {
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
+ gameprofilerfiller.push("checkDespawn");
+ entity.checkDespawn();
+ gameprofilerfiller.pop();
+- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
++ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list
+ Entity entity1 = entity.getVehicle();
+
+ if (entity1 != null) {
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
+
+ @Override
+ public boolean shouldTickBlocksAt(long chunkPos) {
+- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos);
++ // Paper start - replace player chunk loader system
++ ChunkHolder holder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos);
++ return holder != null && holder.isTickingReady();
++ // Paper end - replace player chunk loader system
+ }
+
+ protected void tickTime() {
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
+ private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) {
+ // Paper start - optimize is ticking ready type functions
+ ChunkHolder chunkHolder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos);
+- return chunkHolder != null && this.chunkSource.isPositionTicking(chunkPos) && chunkHolder.isTickingReady() && this.areEntitiesLoaded(chunkPos);
++ return chunkHolder != null && chunkHolder.isTickingReady() && this.areEntitiesLoaded(chunkPos); // Paper - no longer need to check with chunk source
+ // Paper end
+ }
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -0,0 +0,0 @@ import org.bukkit.inventory.MainHand;
+
+ public class ServerPlayer extends Player {
+
+- public final int getViewDistance() { return this.getLevel().getChunkSource().chunkMap.viewDistance - 1; } // Paper - placeholder
++ public final int getViewDistance() { throw new UnsupportedOperationException("Use PlayerChunkLoader"); } // Paper - placeholder
+
+ private static final Logger LOGGER = LogManager.getLogger();
+ public long lastSave = MinecraftServer.currentTick; // Paper
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+ boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
+
+ // Spigot - view distance
+- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat()));
++ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), player.gameMode.getGameModeForPlayer(), player.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.registryHolder, worldserver1.dimensionType(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), this.getMaxPlayers(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(), worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(), flag1, !flag, worldserver1.isDebug(), worldserver1.isFlat())); // Paper - replace old player chunk management
+ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit
+ playerconnection.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName())));
+ playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked()));
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+ // CraftBukkit start
+ LevelData worlddata = worldserver1.getLevelData();
+ entityplayer1.connection.send(new ClientboundRespawnPacket(worldserver1.dimensionType(), worldserver1.dimension(), BiomeManager.obfuscateSeed(worldserver1.getSeed()), entityplayer1.gameMode.getGameModeForPlayer(), entityplayer1.gameMode.getPreviousGameModeForPlayer(), worldserver1.isDebug(), worldserver1.isFlat(), flag));
+- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot
+- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot
++ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance())); // Spigot // Paper - replace old player chunk management
++ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance())); // Spigot // Paper - replace old player chunk management
+ entityplayer1.spawnIn(worldserver1);
+ entityplayer1.unsetRemoved();
+ entityplayer1.connection.teleport(new Location(worldserver1.getWorld(), entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot()));
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+
+ public void setViewDistance(int viewDistance) {
+ this.viewDistance = viewDistance;
+- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance));
++ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance
+ Iterator iterator = this.server.getAllLevels().iterator();
+
+ while (iterator.hasNext()) {
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+
+ public void setSimulationDistance(int simulationDistance) {
+ this.simulationDistance = simulationDistance;
+- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance));
++ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader
+ Iterator iterator = this.server.getAllLevels().iterator();
+
+ while (iterator.hasNext()) {
+diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
++++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java
+@@ -0,0 +0,0 @@ public class EnderDragon extends Mob implements Enemy {
+ // this.world.b(1028, this.getChunkCoordinates(), 0);
+ //int viewDistance = ((WorldServer) this.world).getServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API
+ for (net.minecraft.server.level.ServerPlayer player : (List) ((ServerLevel)level).players()) {
+- final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch
++ final int viewDistance = io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); // Paper - route to player chunk loader
+ double deltaX = this.getX() - player.getX();
+ double deltaZ = this.getZ() - player.getZ();
+ double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
+diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
++++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java
+@@ -0,0 +0,0 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob
+ // this.world.globalLevelEvent(1023, new BlockPosition(this), 0);
+ //int viewDistance = ((ServerLevel) this.level).getCraftServer().getViewDistance() * 16; // Paper - updated to use worlds actual view distance incase we have to uncomment this due to removal of player view distance API
+ for (ServerPlayer player : (List)this.level.players()) { // Paper
+- final int viewDistance = player.getViewDistance(); // TODO apply view distance api patch
++ final int viewDistance = io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); // Paper - route to player chunk loader
+ double deltaX = this.getX() - player.getX();
+ double deltaZ = this.getZ() - player.getZ();
+ double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
+diff --git a/src/main/java/net/minecraft/world/item/EnderEyeItem.java b/src/main/java/net/minecraft/world/item/EnderEyeItem.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/item/EnderEyeItem.java
++++ b/src/main/java/net/minecraft/world/item/EnderEyeItem.java
+@@ -0,0 +0,0 @@ public class EnderEyeItem extends Item {
+
+ // CraftBukkit start - Use relative location for far away sounds
+ // world.b(1038, blockposition1.c(1, 0, 1), 0);
+- int viewDistance = world.getCraftServer().getViewDistance() * 16;
++ //int viewDistance = world.getCraftServer().getViewDistance() * 16; // Paper - apply view distance patch
+ BlockPos soundPos = blockposition1.offset(1, 0, 1);
+ for (ServerPlayer player : world.getServer().getPlayerList().players) {
++ final int viewDistance = io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player); // Paper - apply view distance patch
+ double deltaX = soundPos.getX() - player.getX();
+ double deltaZ = soundPos.getZ() - player.getZ();
+ double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
+diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -0,0 +0,0 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+
+ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(ChunkHolder.FullChunkStatus.TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
+ this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
++ // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance
++ // if copied from above
++ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || ((ServerLevel)this).getChunkSource().chunkMap.playerChunkManager.broadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { // Paper - replace old player chunk management
++ ((ServerLevel)this).getChunkSource().blockChanged(blockposition);
++ // Paper end - per player view distance
+ }
+
+ if ((i & 1) != 0) {
+diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+
+ protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) {
+
++ // Paper start - no-tick view distance
++ ServerChunkCache chunkProviderServer = ((ServerLevel)this.level).getChunkSource();
++ net.minecraft.server.level.ChunkMap chunkMap = chunkProviderServer.chunkMap;
++ // this code handles the addition of ticking tickets - the distance map handles the removal
++ if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) {
++ if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system
++ // now we're ready for entity ticking
++ chunkProviderServer.mainThreadProcessor.execute(() -> {
++ // double check that this condition still holds.
++ if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system
++ chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk
++ chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update
++ }
++ });
++ }
++ }
++
++ // this code handles the chunk sending
++ if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) {
++ // Paper start - replace old player chunk loading system
++ if (chunkMap.playerChunkManager.isChunkNearPlayers(this.chunkPos.x, this.chunkPos.z)) {
++ // the post processing is expensive, so we don't want to run it unless we're actually near
++ // a player.
++ chunkProviderServer.mainThreadProcessor.execute(() -> {
++ if (!LevelChunk.this.areNeighboursLoaded(1)) {
++ return;
++ }
++ LevelChunk.this.postProcessGeneration();
++ if (!LevelChunk.this.areNeighboursLoaded(1)) {
++ return;
++ }
++ chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z);
++ });
++ }
++ // Paper end - replace old player chunk loading system
++ }
++ // Paper end - no-tick view distance
+ }
+
+ public final boolean isAnyNeighborsLoaded() {
+@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+ // Paper end - neighbour cache
+ org.bukkit.Server server = this.level.getCraftServer();
+ this.level.getChunkSource().addLoadedChunk(this); // Paper
++ ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management
+ if (server != null) {
+ /*
+ * If it's a new world, the first few chunks are generated inside
+@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+ });
+ }
+
++ public boolean isPostProcessingDone; // Paper - replace chunk loader system
++
+ public void postProcessGeneration() {
++ try { // Paper - replace chunk loader system
+ ChunkPos chunkcoordintpair = this.getPos();
+
+ for (int i = 0; i < this.postProcessing.length; ++i) {
+@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess {
+
+ this.pendingBlockEntities.clear();
+ this.upgradeData.upgrade(this);
++ } finally { // Paper start - replace chunk loader system
++ this.isPostProcessingDone = true;
++ this.level.getChunkSource().chunkMap.playerChunkManager.onChunkPostProcessing(this.chunkPos.x, this.chunkPos.z);
++ }
++ // Paper end - replace chunk loader system
+ }
+
+ @Nullable
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ // Spigot start
+ @Override
+ public int getViewDistance() {
+- return world.spigotConfig.viewDistance;
++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetNoTickViewDistance(); // Paper - replace old player chunk management
+ }
+
+ @Override
+ public int getSimulationDistance() {
+- return world.spigotConfig.simulationDistance;
++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance(); // Paper - replace old player chunk management
+ }
+
+ @Override
+ public void setViewDistance(int viewDistance) {
+- throw new UnsupportedOperationException(); //TODO
++ // Paper start - replace old player chunk management
++ if (viewDistance < 2 || viewDistance > 32) {
++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]");
++ }
++ net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap;
++ chunkMap.setViewDistance(viewDistance);
++ // Paper end - replace old player chunk management
++ }
++
++ // Paper start - replace old player chunk management
++ @Override
++ public void setSimulationDistance(int simulationDistance) {
++ // Paper start - replace old player chunk management
++ if (simulationDistance < 2 || simulationDistance > 32) {
++ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]");
++ }
++ net.minecraft.server.level.ChunkMap chunkMap = getHandle().getChunkSource().chunkMap;
++ chunkMap.setTickViewDistance(simulationDistance);
+ }
++ // Paper end - replace old player chunk management
+
+ @Override
+ public int getNoTickViewDistance() {
+- throw new UnsupportedOperationException(); //TODO
++ return this.getViewDistance(); // Paper - replace old player chunk management
+ }
+
+ @Override
+ public void setNoTickViewDistance(int viewDistance) {
+- throw new UnsupportedOperationException(); //TODO
++ this.setViewDistance(viewDistance); // Paper - replace old player chunk management
+ }
+
+ @Override
+ public int getSendViewDistance() {
+- throw new UnsupportedOperationException(); //TODO
++ return getHandle().getChunkSource().chunkMap.playerChunkManager.getTargetSendDistance(); // Paper - replace old player chunk management
+ }
+
+ @Override
+ public void setSendViewDistance(int viewDistance) {
+- throw new UnsupportedOperationException(); //TODO
++ getHandle().getChunkSource().chunkMap.playerChunkManager.setSendDistance(viewDistance); // Paper - replace old player chunk management
+ }
+ // Spigot end
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ }
+ }
+
++ // Paper start - implement view distances
+ @Override
+- public int getViewDistance() {
+- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
++ public int getSendViewDistance() {
++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
++ if (data == null) {
++ return chunkMap.playerChunkManager.getTargetSendDistance();
++ }
++ return data.getTargetSendViewDistance();
+ }
+
+ @Override
+- public void setViewDistance(int viewDistance) {
+- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
++ public void setSendViewDistance(int viewDistance) {
++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
++ if (data == null) {
++ throw new IllegalStateException("Player is not attached to world");
++ }
++
++ data.setTargetSendViewDistance(viewDistance);
+ }
+
+ @Override
+ public int getNoTickViewDistance() {
+- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
++ return this.getViewDistance();
+ }
+
+ @Override
+ public void setNoTickViewDistance(int viewDistance) {
+- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
++ this.setViewDistance(viewDistance);
+ }
+
+ @Override
+- public int getSendViewDistance() {
+- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
++ public int getViewDistance() {
++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
++ if (data == null) {
++ return chunkMap.playerChunkManager.getTargetNoTickViewDistance();
++ }
++ return data.getTargetNoTickViewDistance();
+ }
+
+ @Override
+- public void setSendViewDistance(int viewDistance) {
+- throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement (There are per world view distances though!)"); // TODO
++ public void setViewDistance(int viewDistance) {
++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
++ if (data == null) {
++ throw new IllegalStateException("Player is not attached to world");
++ }
++
++ data.setTargetNoTickViewDistance(viewDistance);
++ }
++
++ @Override
++ public int getSimulationDistance() {
++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
++ if (data == null) {
++ return chunkMap.playerChunkManager.getTargetTickViewDistance();
++ }
++ return data.getTargetTickViewDistance();
++ }
++
++ @Override
++ public void setSimulationDistance(int simulationDistance) {
++ net.minecraft.server.level.ChunkMap chunkMap = this.getHandle().getLevel().getChunkSource().chunkMap;
++ io.papermc.paper.chunk.PlayerChunkLoader.PlayerLoaderData data = chunkMap.playerChunkManager.getData(this.getHandle());
++ if (data == null) {
++ throw new IllegalStateException("Player is not attached to world");
++ }
++
++ data.setTargetTickViewDistance(simulationDistance);
+ }
++ // Paper end - implement view distances
+
+ @Override
+ public T getClientOption(com.destroystokyo.paper.ClientOption type) {
diff --git a/patches/server/Replace-ticket-level-propagator.patch b/patches/server/Replace-ticket-level-propagator.patch
new file mode 100644
index 0000000000..af61ad3274
--- /dev/null
+++ b/patches/server/Replace-ticket-level-propagator.patch
@@ -0,0 +1,249 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf
+Date: Sun, 21 Mar 2021 16:25:42 -0700
+Subject: [PATCH] Replace ticket level propagator
+
+Mojang's propagator is slow, and this isn't surprising
+given it's built on the same utilities the vanilla light engine
+is built on. The simple propagator I wrote is approximately 4x
+faster when simulating player movement. For a long time timing
+reports have shown this function take up significant tick, (
+approx 10% or more), and async sampling data shows the level
+propagation alone takes up a significant amount. So this
+should help with that. A big side effect is that mid-tick
+will be more effective, since more time will be allocated
+to actually processing chunk tasks vs the ticket level updates.
+
+diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
+@@ -0,0 +0,0 @@ import net.minecraft.world.level.chunk.LevelChunk;
+ import org.apache.logging.log4j.LogManager;
+ import org.apache.logging.log4j.Logger;
+
++import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; // Paper
+ public abstract class DistanceManager {
+
+ static final Logger LOGGER = LogManager.getLogger();
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ private static final int BLOCK_TICKING_LEVEL_THRESHOLD = 33;
+ final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap();
+ public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap();
+- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
++ //private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); // Paper - replace ticket level propagator
+ public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used
+ //private final TickingTracker tickingTicketsTracker = new TickingTracker(); // Paper - no longer used
+ //private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33); // Paper - no longer used
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ this.chunkMap = chunkMap; // Paper
+ }
+
++ // Paper start - replace ticket level propagator
++ protected final Long2IntLinkedOpenHashMap ticketLevelUpdates = new Long2IntLinkedOpenHashMap() {
++ @Override
++ protected void rehash(int newN) {
++ // no downsizing allowed
++ if (newN < this.n) {
++ return;
++ }
++ super.rehash(newN);
++ }
++ };
++ protected final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ticketLevelPropagator = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D(
++ (long coordinate, byte oldLevel, byte newLevel) -> {
++ DistanceManager.this.ticketLevelUpdates.putAndMoveToLast(coordinate, convertBetweenTicketLevels(newLevel));
++ }
++ );
++ // function for converting between ticket levels and propagator levels and vice versa
++ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects
++ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator
++ // and the levels we get out of the propagator
++
++ // this maps so that GOLDEN_TICKET + 1 will be 0 in the propagator, GOLDEN_TICKET will be 1, and so on
++ // we need GOLDEN_TICKET+1 as 0 because anything >= GOLDEN_TICKET+1 should be unloaded
++ public static int convertBetweenTicketLevels(final int level) {
++ return ChunkMap.MAX_CHUNK_DISTANCE - level + 1;
++ }
++
++ protected final int getPropagatedTicketLevel(final long coordinate) {
++ return convertBetweenTicketLevels(this.ticketLevelPropagator.getLevel(coordinate));
++ }
++
++ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) {
++ if (ticketLevel > ChunkMap.MAX_CHUNK_DISTANCE) {
++ this.ticketLevelPropagator.removeSource(coordinate);
++ } else {
++ this.ticketLevelPropagator.setSource(coordinate, convertBetweenTicketLevels(ticketLevel));
++ }
++ }
++ // Paper end - replace ticket level propagator
++
+ protected void purgeStaleTickets() {
+ ++this.ticketTickCounter;
+ ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ }
+
+ if (flag) {
+- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false);
++ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(entry.getValue())); // Paper - replace ticket level propagator
+ }
+
+ if (((SortedArraySet) entry.getValue()).isEmpty()) {
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ @Nullable
+ protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k);
+
++ protected long ticketLevelUpdateCount; // Paper - replace ticket level propagator
+ public boolean runAllUpdates(ChunkMap chunkStorage) {
+ //this.f.a(); // Paper - no longer used
+ //this.tickingTicketsTracker.runAllUpdates(); // Paper - no longer used
+ org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper
+ //this.playerTicketManager.runAllUpdates(); // Paper - no longer used
+- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
+- boolean flag = i != 0;
++ boolean flag = this.ticketLevelPropagator.propagateUpdates(); // Paper - replace ticket level propagator
+
+ if (flag) {
+ ;
+ }
+
+- // Paper start
+- if (!this.pendingChunkUpdates.isEmpty()) {
+- this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority
+- while(!this.pendingChunkUpdates.isEmpty()) {
+- ChunkHolder remove = this.pendingChunkUpdates.remove();
+- remove.isUpdateQueued = false;
+- remove.updateFutures(chunkStorage, this.mainThreadExecutor);
+- }
+- } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority
+- // Paper end
+- return true;
+- } else {
+- if (!this.ticketsToRelease.isEmpty()) {
+- LongIterator longiterator = this.ticketsToRelease.iterator();
++ // Paper start - replace level propagator
++ ticket_update_loop:
++ while (!this.ticketLevelUpdates.isEmpty()) {
++ flag = true;
+
+- while (longiterator.hasNext()) {
+- long j = longiterator.nextLong();
++ boolean oldPolling = this.pollingPendingChunkUpdates;
++ this.pollingPendingChunkUpdates = true;
++ try {
++ for (java.util.Iterator iterator = this.ticketLevelUpdates.long2IntEntrySet().fastIterator(); iterator.hasNext();) {
++ Long2IntMap.Entry entry = iterator.next();
++ long key = entry.getLongKey();
++ int newLevel = entry.getIntValue();
++ ChunkHolder chunk = this.getChunk(key);
++
++ if (chunk == null && newLevel > ChunkMap.MAX_CHUNK_DISTANCE) {
++ // not loaded and it shouldn't be loaded!
++ continue;
++ }
+
+- if (this.getTickets(j).stream().anyMatch((ticket) -> {
+- return ticket.getType() == TicketType.PLAYER;
+- })) {
+- ChunkHolder playerchunk = chunkStorage.getUpdatingChunkIfPresent(j);
++ int currentLevel = chunk == null ? ChunkMap.MAX_CHUNK_DISTANCE + 1 : chunk.getTicketLevel();
+
+- if (playerchunk == null) {
+- throw new IllegalStateException();
++ if (currentLevel == newLevel) {
++ // nothing to do
++ continue;
++ }
++
++ this.updateChunkScheduling(key, newLevel, chunk, currentLevel);
++ }
++
++ long recursiveCheck = ++this.ticketLevelUpdateCount;
++ while (!this.ticketLevelUpdates.isEmpty()) {
++ long key = this.ticketLevelUpdates.firstLongKey();
++ int newLevel = this.ticketLevelUpdates.removeFirstInt();
++ ChunkHolder chunk = this.getChunk(key);
++
++ if (chunk == null) {
++ if (newLevel <= ChunkMap.MAX_CHUNK_DISTANCE) {
++ throw new IllegalStateException("Expected chunk holder to be created");
+ }
++ // not loaded and it shouldn't be loaded!
++ continue;
++ }
+
+- CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture();
++ int currentLevel = chunk.oldTicketLevel;
+
+- completablefuture.thenAccept((either) -> {
+- this.mainThreadExecutor.execute(() -> {
+- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {
+- }, j, false));
+- });
+- });
++ if (currentLevel == newLevel) {
++ // nothing to do
++ continue;
++ }
++
++ chunk.updateFutures(chunkStorage, this.mainThreadExecutor);
++ if (recursiveCheck != this.ticketLevelUpdateCount) {
++ // back to the start, we must create player chunks and update the ticket level fields before
++ // processing the actual level updates
++ continue ticket_update_loop;
+ }
+ }
+
+- this.ticketsToRelease.clear();
+- }
++ for (;;) {
++ if (recursiveCheck != this.ticketLevelUpdateCount) {
++ continue ticket_update_loop;
++ }
++ ChunkHolder pendingUpdate = this.pendingChunkUpdates.poll();
++ if (pendingUpdate == null) {
++ break;
++ }
+
+- return flag;
++ pendingUpdate.updateFutures(chunkStorage, this.mainThreadExecutor);
++ }
++ } finally {
++ this.pollingPendingChunkUpdates = oldPolling;
++ }
+ }
++
++ return flag;
++ // Paper end - replace level propagator
+ }
+ boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority
+
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+
+ ticket1.setCreatedTick(this.ticketTickCounter);
+ if (ticket.getTicketLevel() < j) {
+- this.ticketTracker.update(i, ticket.getTicketLevel(), true);
++ this.updateTicketLevel(i, ticket.getTicketLevel()); // Paper - replace ticket level propagator
+ }
+
+ return ticket == ticket1; // CraftBukkit
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ // Paper start - Chunk priority
+ int newLevel = getTicketLevelAt(arraysetsorted);
+ if (newLevel > oldLevel) {
+- this.ticketTracker.update(i, newLevel, false);
++ this.updateTicketLevel(i, newLevel); // Paper // Paper - replace ticket level propagator
+ }
+ // Paper end
+ return removed; // CraftBukkit
+@@ -0,0 +0,0 @@ public abstract class DistanceManager {
+ SortedArraySet> tickets = entry.getValue();
+ if (tickets.remove(target)) {
+ // copied from removeTicket
+- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
++ this.updateTicketLevel(entry.getLongKey(), getTicketLevelAt(tickets)); // Paper - replace ticket level propagator
+
+ // can't use entry after it's removed
+ if (tickets.isEmpty()) {
diff --git a/patches/server/incremental-chunk-and-player-saving.patch b/patches/server/incremental-chunk-and-player-saving.patch
index 0a97f601b8..aa3efad176 100644
--- a/patches/server/incremental-chunk-and-player-saving.patch
+++ b/patches/server/incremental-chunk-and-player-saving.patch
@@ -102,7 +102,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -0,0 +0,0 @@ public class ChunkHolder {
- this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+ this.playersInChunkTickRange = null;
}
// Paper end - optimise anyPlayerCloseEnoughForSpawning
+ long lastAutoSaveTime; // Paper - incremental autosave