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