From 878c66f116d983112497cab482765531acbcfaf6 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Wed, 6 May 2020 03:49:52 -0400 Subject: [PATCH] No-Tick view distance implementation - Closes #3196 Implements world view distance getters/setters Per-Player is absent due to difficulty of maintaining the diff required to make it happen. --- .../0204-World-view-distance-api.patch | 48 ++ ...No-Tick-view-distance-implementation.patch | 662 ++++++++++++++++++ 2 files changed, 710 insertions(+) create mode 100644 Spigot-API-Patches/0204-World-view-distance-api.patch create mode 100644 Spigot-Server-Patches/0506-No-Tick-view-distance-implementation.patch diff --git a/Spigot-API-Patches/0204-World-view-distance-api.patch b/Spigot-API-Patches/0204-World-view-distance-api.patch new file mode 100644 index 000000000..ec0885e14 --- /dev/null +++ b/Spigot-API-Patches/0204-World-view-distance-api.patch @@ -0,0 +1,48 @@ +From 919aecffc9c10f8e95f114e326cae396bd48e409 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 5 May 2020 21:28:01 -0700 +Subject: [PATCH] World view distance api + + +diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java +index db18f70e..421ad6a9 100644 +--- a/src/main/java/org/bukkit/World.java ++++ b/src/main/java/org/bukkit/World.java +@@ -3166,6 +3166,34 @@ public interface World extends PluginMessageRecipient, Metadatable { + int getViewDistance(); + // Spigot end + ++ // Paper start - view distance api ++ /** ++ * Sets the view distance for this world. ++ * @param viewDistance view distance in [2, 32] ++ */ ++ void setViewDistance(int viewDistance); ++ ++ /** ++ * 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. ++ */ ++ int getNoTickViewDistance(); ++ ++ /** ++ * Sets 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. ++ *

++ * @param viewDistance view distance in [2, 32] ++ */ ++ void setNoTickViewDistance(int viewDistance); ++ // Paper end - view distance api ++ + // Spigot start + public class Spigot { + +-- +2.26.0 + diff --git a/Spigot-Server-Patches/0506-No-Tick-view-distance-implementation.patch b/Spigot-Server-Patches/0506-No-Tick-view-distance-implementation.patch new file mode 100644 index 000000000..f89a49981 --- /dev/null +++ b/Spigot-Server-Patches/0506-No-Tick-view-distance-implementation.patch @@ -0,0 +1,662 @@ +From 8e744f5b5d46417c511bce46f59fc1c329d30394 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 5 May 2020 21:23:34 -0700 +Subject: [PATCH] No-Tick view distance implementation + +Implements world view distance getters/setters + +Per-Player is absent due to difficulty of maintaining +the diff required to make it happen. + +diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +index bfb52d75c7..57baf7092d 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +@@ -691,4 +691,9 @@ public class PaperWorldConfig { + phantomIgnoreCreative = getBoolean("phantoms-do-not-spawn-on-creative-players", phantomIgnoreCreative); + phantomOnlyAttackInsomniacs = getBoolean("phantoms-only-attack-insomniacs", phantomOnlyAttackInsomniacs); + } ++ ++ public int noTickViewDistance; ++ private void viewDistance() { ++ this.noTickViewDistance = this.getInt("viewdistances.no-tick-view-distance", -1); ++ } + } +diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java +index 53d3acccd3..af8a3e186e 100644 +--- a/src/main/java/net/minecraft/server/Chunk.java ++++ b/src/main/java/net/minecraft/server/Chunk.java +@@ -245,7 +245,51 @@ public class Chunk implements IChunkAccess { + } + + protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { ++ // Paper start - no-tick view distance ++ ChunkProviderServer chunkProviderServer = ((WorldServer)this.world).getChunkProvider(); ++ PlayerChunkMap chunkMap = chunkProviderServer.playerChunkMap; ++ // this code handles the addition of ticking tickets - the distance map handles the removal ++ if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) { ++ if (chunkMap.playerViewDistanceTickMap.getObjectsInRange(this.coordinateKey) != null) { ++ // now we're ready for entity ticking ++ chunkProviderServer.serverThreadQueue.execute(() -> { ++ // double check that this condition still holds. ++ if (Chunk.this.areNeighboursLoaded(2) && chunkMap.playerViewDistanceTickMap.getObjectsInRange(Chunk.this.coordinateKey) != null) { ++ chunkProviderServer.addTicketAtLevel(TicketType.PLAYER, Chunk.this.loc, 31, Chunk.this.loc); // 31 -> entity ticking, TODO check on update ++ } ++ }); ++ } ++ } + ++ // this code handles the chunk sending ++ if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) { ++ if (chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(this.coordinateKey) != null) { ++ // now we're ready to send ++ chunkMap.mailboxMain.a(ChunkTaskQueueSorter.a(chunkMap.getUpdatingChunk(this.coordinateKey), (() -> { // Copied frm PlayerChunkMap ++ // double check that this condition still holds. ++ if (!Chunk.this.areNeighboursLoaded(1)) { ++ return; ++ } ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = chunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(Chunk.this.coordinateKey); ++ if (inRange == null) { ++ return; ++ } ++ ++ // broadcast ++ Object[] backingSet = inRange.getBackingSet(); ++ Packet[] chunkPackets = new Packet[2]; ++ for (int index = 0, len = backingSet.length; index < len; ++index) { ++ Object temp = backingSet[index]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ chunkMap.sendChunk(player, chunkPackets, Chunk.this); ++ } ++ }))); ++ } ++ } ++ // Paper end - no-tick view distance + } + + public final boolean areNeighboursLoaded(final int radius) { +diff --git a/src/main/java/net/minecraft/server/ChunkMapDistance.java b/src/main/java/net/minecraft/server/ChunkMapDistance.java +index 7cd4e29123..942efe62fe 100644 +--- a/src/main/java/net/minecraft/server/ChunkMapDistance.java ++++ b/src/main/java/net/minecraft/server/ChunkMapDistance.java +@@ -252,7 +252,7 @@ public abstract class ChunkMapDistance { + return s; + } + +- protected void a(int i) { ++ protected void setNoTickViewDistance(int i) { // Paper - force abi breakage on usage change + this.g.a(i); + } + +@@ -371,7 +371,7 @@ public abstract class ChunkMapDistance { + + private void a(long i, int j, boolean flag, boolean flag1) { + if (flag != flag1) { +- Ticket ticket = new Ticket<>(TicketType.PLAYER, ChunkMapDistance.b, new ChunkCoordIntPair(i)); ++ Ticket ticket = new Ticket<>(TicketType.PLAYER, 33, new ChunkCoordIntPair(i)); // Paper - no-tick view distance + + if (flag1) { + ChunkMapDistance.this.j.a(ChunkTaskQueueSorter.a(() -> { // CraftBukkit - decompile error +diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java +index 6e8179b465..e32c458dfe 100644 +--- a/src/main/java/net/minecraft/server/EntityPlayer.java ++++ b/src/main/java/net/minecraft/server/EntityPlayer.java +@@ -111,6 +111,8 @@ public class EntityPlayer extends EntityHuman implements ICrafting { + + double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks + ++ boolean needsChunkCenterUpdate; // Paper - no-tick view distance ++ + public EntityPlayer(MinecraftServer minecraftserver, WorldServer worldserver, GameProfile gameprofile, PlayerInteractManager playerinteractmanager) { + super((World) worldserver, gameprofile); + playerinteractmanager.player = this; +diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java +index 8742d13499..72ce7bf0c3 100644 +--- a/src/main/java/net/minecraft/server/PlayerChunk.java ++++ b/src/main/java/net/minecraft/server/PlayerChunk.java +@@ -160,6 +160,18 @@ public class PlayerChunk { + } + // Paper end - optimise isOutsideOfRange + ++ // Paper start - no-tick view distance ++ public final Chunk getSendingChunk() { ++ // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used ++ // in Chunk's neighbour callback ++ Chunk ret = this.chunkMap.world.getChunkProvider().getChunkAtIfLoadedImmediately(this.location.x, this.location.z); ++ if (ret != null && ret.areNeighboursLoaded(1)) { ++ return ret; ++ } ++ return null; ++ } ++ // Paper end - no-tick view distance ++ + public PlayerChunk(ChunkCoordIntPair chunkcoordintpair, int i, LightEngine lightengine, PlayerChunk.c playerchunk_c, PlayerChunk.d playerchunk_d) { + this.statusFutures = new AtomicReferenceArray(PlayerChunk.CHUNK_STATUSES.size()); + this.fullChunkFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE; +@@ -321,7 +333,7 @@ public class PlayerChunk { + } + + public void a(int i, int j, int k) { +- Chunk chunk = this.getChunk(); ++ Chunk chunk = this.getSendingChunk(); // Paper - no-tick view distance + + if (chunk != null) { + this.r |= 1 << (j >> 4); +@@ -341,7 +353,7 @@ public class PlayerChunk { + } + + public void a(EnumSkyBlock enumskyblock, int i) { +- Chunk chunk = this.getChunk(); ++ Chunk chunk = this.getSendingChunk(); // Paper - no-tick view distance + + if (chunk != null) { + chunk.setNeedsSaving(true); +@@ -431,9 +443,48 @@ public class PlayerChunk { + } + + private void a(Packet packet, boolean flag) { +- this.players.a(this.location, flag).forEach((entityplayer) -> { +- entityplayer.playerConnection.sendPacket(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.playerViewDistanceBroadcastMap; ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = viewDistanceMap.getObjectsInRange(this.location); ++ if (players == null) { ++ return; ++ } ++ ++ if (flag) { // flag -> border only ++ Object[] backingSet = players.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ ++ int viewDistance = viewDistanceMap.getLastViewDistance(player); ++ long lastPosition = viewDistanceMap.getLastCoordinate(player); ++ ++ int distX = Math.abs(MCUtil.getCoordinateX(lastPosition) - this.location.x); ++ int distZ = Math.abs(MCUtil.getCoordinateZ(lastPosition) - this.location.z); ++ ++ if (Math.max(distX, distZ) == viewDistance) { ++ player.playerConnection.sendPacket(packet); ++ } ++ } ++ } else { ++ Object[] backingSet = players.getBackingSet(); ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ player.playerConnection.sendPacket(packet); ++ } ++ } ++ ++ return; ++ // Paper end - per player view distance + } + + public CompletableFuture> a(ChunkStatus chunkstatus, PlayerChunkMap playerchunkmap) { +diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java +index 099e612171..345f2689c7 100644 +--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java ++++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java +@@ -71,7 +71,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + private boolean updatingChunksModified; + private final ChunkTaskQueueSorter p; + private final Mailbox> mailboxWorldGen; +- private final Mailbox> mailboxMain; ++ final Mailbox> mailboxMain; // Paper - private -> package private + public final WorldLoadListener worldLoadListener; + public final PlayerChunkMap.a chunkDistanceManager; public final PlayerChunkMap.a getChunkMapDistanceManager() { return this.chunkDistanceManager; } // Paper - OBFHELPER + private final AtomicInteger u; +@@ -141,6 +141,19 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + 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 PlayerChunkMap#isOutsideRange ++ // Paper start - no-tick view distance ++ int noTickViewDistance; ++ public final int getRawNoTickViewDistance() { ++ return this.noTickViewDistance; ++ } ++ public final int getEffectiveNoTickViewDistance() { ++ return this.noTickViewDistance == -1 ? this.getEffectiveViewDistance() : this.noTickViewDistance; ++ } ++ ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceBroadcastMap; ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceTickMap; ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceNoTickMap; ++ // Paper end - no-tick view distance + + void addPlayerToDistanceMaps(EntityPlayer player) { + int chunkX = MCUtil.getChunkCoordinate(player.locX()); +@@ -157,6 +170,19 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + // Paper start - optimise PlayerChunkMap#isOutsideRange + this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, ChunkMapDistance.MOB_SPAWN_RANGE); + // Paper end - optimise PlayerChunkMap#isOutsideRange ++ // Paper start - no-tick view distance ++ int effectiveTickViewDistance = this.getEffectiveViewDistance(); ++ int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); ++ ++ if (!this.cannotLoadChunks(player)) { ++ this.playerViewDistanceTickMap.add(player, chunkX, chunkZ, effectiveTickViewDistance); ++ this.playerViewDistanceNoTickMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) ++ } ++ ++ player.needsChunkCenterUpdate = true; ++ this.playerViewDistanceBroadcastMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured ++ player.needsChunkCenterUpdate = false; ++ // Paper end - no-tick view distance + } + + void removePlayerFromDistanceMaps(EntityPlayer player) { +@@ -169,6 +195,11 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + this.playerMobSpawnMap.remove(player); + this.playerChunkTickRangeMap.remove(player); + // Paper end - optimise PlayerChunkMap#isOutsideRange ++ // Paper start - no-tick view distance ++ this.playerViewDistanceBroadcastMap.remove(player); ++ this.playerViewDistanceTickMap.remove(player); ++ this.playerViewDistanceNoTickMap.remove(player); ++ // Paper end - no-tick view distance + } + + void updateMaps(EntityPlayer player) { +@@ -186,6 +217,19 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + // Paper start - optimise PlayerChunkMap#isOutsideRange + this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, ChunkMapDistance.MOB_SPAWN_RANGE); + // Paper end - optimise PlayerChunkMap#isOutsideRange ++ // Paper start - no-tick view distance ++ int effectiveTickViewDistance = this.getEffectiveViewDistance(); ++ int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance); ++ ++ if (!this.cannotLoadChunks(player)) { ++ this.playerViewDistanceTickMap.update(player, chunkX, chunkZ, effectiveTickViewDistance); ++ this.playerViewDistanceNoTickMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 2); // clients need chunk 1 neighbour, and we need another 1 for sending those extra neighbours (as we require neighbours to send) ++ } ++ ++ player.needsChunkCenterUpdate = true; ++ this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured ++ player.needsChunkCenterUpdate = false; ++ // Paper end - no-tick view distance + } + + +@@ -293,6 +337,45 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + }); + // Paper end - optimise PlayerChunkMap#isOutsideRange ++ // Paper start - no-tick view distance ++ this.setNoTickViewDistance(this.world.paperConfig.noTickViewDistance); ++ this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, ++ (EntityPlayer 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; ++ } ++ Chunk chunk = PlayerChunkMap.this.world.getChunkProvider().getChunkAtIfLoadedMainThreadNoCache(rangeX, rangeZ); ++ if (chunk == null || !chunk.areNeighboursLoaded(2)) { ++ return; ++ } ++ ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ); ++ PlayerChunkMap.this.world.getChunkProvider().addTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update ++ }, ++ (EntityPlayer 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; ++ } ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(rangeX, rangeZ); ++ PlayerChunkMap.this.world.getChunkProvider().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update ++ }); ++ this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); ++ this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, ++ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ if (player.needsChunkCenterUpdate) { ++ player.needsChunkCenterUpdate = false; ++ player.playerConnection.sendPacket(new PacketPlayOutViewCentre(currPosX, currPosZ)); ++ } ++ PlayerChunkMap.this.sendChunk(player, new ChunkCoordIntPair(rangeX, rangeZ), new Packet[2], false, true); // unloaded, loaded ++ }, ++ (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { ++ PlayerChunkMap.this.sendChunk(player, new ChunkCoordIntPair(rangeX, rangeZ), null, true, false); // unloaded, loaded ++ }); ++ // Paper end - no-tick view distance + } + + public void updatePlayerMobTypeMap(Entity entity) { +@@ -1113,15 +1196,11 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + completablefuture1.thenAcceptAsync((either) -> { + either.mapLeft((chunk) -> { + this.u.getAndIncrement(); +- Packet[] apacket = new Packet[2]; +- +- this.a(chunkcoordintpair, false).forEach((entityplayer) -> { +- this.a(entityplayer, apacket, chunk); +- }); ++ // Paper - no-tick view distance - moved to Chunk neighbour update + return Either.left(chunk); + }); + }, (runnable) -> { +- this.mailboxMain.a(ChunkTaskQueueSorter.a(playerchunk, runnable)); // CraftBukkit - decompile error ++ this.mailboxMain.a(ChunkTaskQueueSorter.a(playerchunk, runnable)); // CraftBukkit - decompile error // Paper - diff on change, this is the scheduling method copied in Chunk used to schedule chunk broadcasts (on change it needs to be copied again) + }); + return completablefuture1; + } +@@ -1221,32 +1300,52 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } // Paper + } + +- protected void setViewDistance(int i) { +- int j = MathHelper.clamp(i + 1, 3, 33); ++ public final void setViewDistance(int i) { // Paper - public ++ int j = MathHelper.clamp(i + 1, 3, 33); // Paper - diff on change, these make the lower view distance limit 2 and the upper 32 + + if (j != this.viewDistance) { + int k = this.viewDistance; + + this.viewDistance = j; +- this.chunkDistanceManager.a(this.viewDistance); +- ObjectIterator objectiterator = this.updatingChunks.values().iterator(); ++ if (this.world != null && this.world.players != null) { // this can be called from constructor, where these aren't set ++ // Paper start - no-tick view distance ++ for (EntityPlayer player : this.world.players) { ++ PlayerConnection connection = player.playerConnection; ++ if (connection != null) { ++ // moved in from PlayerList ++ connection.sendPacket(new PacketPlayOutViewDistance(this.getEffectiveNoTickViewDistance())); ++ } ++ this.updateMaps(player); // distance map handles the chunk sending (and ticket level changes) ++ } ++ this.setNoTickViewDistance(this.getRawNoTickViewDistance()); // propagate changes to no-tick, which does the actual chunk loading/sending ++ // Paper end - no-tick view distance ++ } ++ } + +- while (objectiterator.hasNext()) { +- PlayerChunk playerchunk = (PlayerChunk) objectiterator.next(); +- ChunkCoordIntPair chunkcoordintpair = playerchunk.i(); +- Packet[] apacket = new Packet[2]; ++ } ++ ++ // Paper start - no-tick view distance ++ public final void setNoTickViewDistance(int viewDistance) { ++ viewDistance = viewDistance == -1 ? -1 : MathHelper.clamp(viewDistance, 2, 32); ++ if (viewDistance == this.noTickViewDistance && viewDistance != -1) { ++ return; ++ } + +- this.a(chunkcoordintpair, false).forEach((entityplayer) -> { +- int l = b(chunkcoordintpair, entityplayer, true); +- boolean flag = l <= k; +- boolean flag1 = l <= this.viewDistance; ++ this.noTickViewDistance = viewDistance; ++ this.chunkDistanceManager.setNoTickViewDistance(this.getEffectiveNoTickViewDistance() + 2 + 2); // add 2 to account for the change to 31 -> 33 tickets // see notes in the distance map updating for the other + 2 + +- this.sendChunk(entityplayer, chunkcoordintpair, apacket, flag, flag1); +- }); ++ if (this.world != null && this.world.players != null) { // this can be called from constructor, where these aren't set ++ for (EntityPlayer player : this.world.players) { ++ PlayerConnection connection = player.playerConnection; ++ if (connection != null) { ++ // moved in from PlayerList ++ connection.sendPacket(new PacketPlayOutViewDistance(this.getEffectiveNoTickViewDistance())); ++ } ++ this.updateMaps(player); + } + } +- + } ++ // Paper end - no-tick view distance + + protected void sendChunk(EntityPlayer entityplayer, ChunkCoordIntPair chunkcoordintpair, Packet[] apacket, boolean flag, boolean flag1) { + if (entityplayer.world == this.world) { +@@ -1254,7 +1353,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + PlayerChunk playerchunk = this.getVisibleChunk(chunkcoordintpair.pair()); + + if (playerchunk != null) { +- Chunk chunk = playerchunk.getChunk(); ++ Chunk chunk = playerchunk.getSendingChunk(); // Paper - no-tick view distance + + if (chunk != null) { + this.a(entityplayer, apacket, chunk); +@@ -1523,6 +1622,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + } + // Paper end - optimise isOutsideOfRange + ++ private boolean cannotLoadChunks(EntityPlayer entityplayer) { return this.b(entityplayer); } // Paper - OBFHELPER + private boolean b(EntityPlayer entityplayer) { + return entityplayer.isSpectator() && !this.world.getGameRules().getBoolean(GameRules.SPECTATORS_GENERATE_CHUNKS); + } +@@ -1550,13 +1650,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + this.removePlayerFromDistanceMaps(entityplayer); // Paper - distance maps + } + +- for (int k = i - this.viewDistance; k <= i + this.viewDistance; ++k) { +- for (int l = j - this.viewDistance; l <= j + this.viewDistance; ++l) { +- ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(k, l); +- +- this.sendChunk(entityplayer, chunkcoordintpair, new Packet[2], !flag, flag); +- } +- } ++ // Paper - broadcast view distance map handles this (see remove/add calls above) + + } + +@@ -1564,7 +1658,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + SectionPosition sectionposition = SectionPosition.a((Entity) entityplayer); + + entityplayer.a(sectionposition); +- entityplayer.playerConnection.sendPacket(new PacketPlayOutViewCentre(sectionposition.a(), sectionposition.c())); ++ // Paper - distance map handles this now + return sectionposition; + } + +@@ -1609,6 +1703,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + int k1; + int l1; + ++ /* // Paper start - replaced by distance map + if (Math.abs(i1 - i) <= this.viewDistance * 2 && Math.abs(j1 - j) <= this.viewDistance * 2) { + k1 = Math.min(i, i1) - this.viewDistance; + l1 = Math.min(j, j1) - this.viewDistance; +@@ -1646,7 +1741,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + this.sendChunk(entityplayer, chunkcoordintpair1, new Packet[2], false, true); + } + } +- } ++ }*/ // Paper end - replaced by distance map + + this.updateMaps(entityplayer); // Paper - distance maps + +@@ -1654,11 +1749,46 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + @Override + public Stream a(ChunkCoordIntPair chunkcoordintpair, boolean flag) { +- return this.playerMap.a(chunkcoordintpair.pair()).filter((entityplayer) -> { +- int i = b(chunkcoordintpair, entityplayer, true); ++ // 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.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet inRange = this.playerViewDistanceBroadcastMap.getObjectsInRange(chunkcoordintpair); + +- return i > this.viewDistance ? false : !flag || i == this.viewDistance; +- }); ++ if (inRange == null) { ++ return Stream.empty(); ++ } ++ // all current cases are inlined so we wont hit this code, it's just in case plugins or future updates use it ++ List players = new ArrayList<>(); ++ Object[] backingSet = inRange.getBackingSet(); ++ ++ if (flag) { // flag -> border only ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ int viewDistance = this.playerViewDistanceBroadcastMap.getLastViewDistance(player); ++ long lastPosition = this.playerViewDistanceBroadcastMap.getLastCoordinate(player); ++ ++ int distX = Math.abs(MCUtil.getCoordinateX(lastPosition) - chunkcoordintpair.x); ++ int distZ = Math.abs(MCUtil.getCoordinateZ(lastPosition) - chunkcoordintpair.z); ++ if (Math.max(distX, distZ) == viewDistance) { ++ players.add(player); ++ } ++ } ++ } else { ++ for (int i = 0, len = backingSet.length; i < len; ++i) { ++ Object temp = backingSet[i]; ++ if (!(temp instanceof EntityPlayer)) { ++ continue; ++ } ++ EntityPlayer player = (EntityPlayer)temp; ++ players.add(player); ++ } ++ } ++ return players.stream(); ++ // Paper end - per player view distance + } + + protected void addEntity(Entity entity) { +@@ -1829,6 +1959,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + + } + ++ final void sendChunk(EntityPlayer entityplayer, Packet[] apacket, Chunk chunk) { this.a(entityplayer, apacket, chunk); } // Paper - OBFHELPER + private void a(EntityPlayer entityplayer, Packet[] apacket, Chunk chunk) { + if (apacket[0] == null) { + apacket[0] = new PacketPlayOutMapChunk(chunk, 65535, true); // Paper - Anti-Xray +@@ -2014,7 +2145,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(this.tracker.chunkX, this.tracker.chunkZ); + PlayerChunk playerchunk = PlayerChunkMap.this.getVisibleChunk(chunkcoordintpair.pair()); + +- if (playerchunk != null && playerchunk.getChunk() != null) { ++ if (playerchunk != null && playerchunk.getSendingChunk() != null) { // Paper - no-tick view distance + flag1 = PlayerChunkMap.b(chunkcoordintpair, entityplayer, false) <= PlayerChunkMap.this.viewDistance; + } + } +diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java +index edf9df8c8a..ec95b63e51 100644 +--- a/src/main/java/net/minecraft/server/PlayerList.java ++++ b/src/main/java/net/minecraft/server/PlayerList.java +@@ -150,7 +150,7 @@ public abstract class PlayerList { + + // CraftBukkit - getType() + // Spigot - view distance +- playerconnection.sendPacket(new PacketPlayOutLogin(entityplayer.getId(), entityplayer.playerInteractManager.getGameMode(), WorldData.c(worlddata.getSeed()), worlddata.isHardcore(), worldserver.worldProvider.getDimensionManager().getType(), this.getMaxPlayers(), worlddata.getType(), worldserver.spigotConfig.viewDistance, flag1, !flag)); ++ playerconnection.sendPacket(new PacketPlayOutLogin(entityplayer.getId(), entityplayer.playerInteractManager.getGameMode(), WorldData.c(worlddata.getSeed()), worlddata.isHardcore(), worldserver.worldProvider.getDimensionManager().getType(), this.getMaxPlayers(), worlddata.getType(), worldserver.getChunkProvider().playerChunkMap.getEffectiveNoTickViewDistance(), flag1, !flag)); // Paper - no-tick view distance + entityplayer.getBukkitEntity().sendSupportedChannels(); // CraftBukkit + playerconnection.sendPacket(new PacketPlayOutCustomPayload(PacketPlayOutCustomPayload.a, (new PacketDataSerializer(Unpooled.buffer())).a(this.getServer().getServerModName()))); + playerconnection.sendPacket(new PacketPlayOutServerDifficulty(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); +@@ -770,7 +770,7 @@ public abstract class PlayerList { + WorldData worlddata = worldserver.getWorldData(); + + entityplayer1.playerConnection.sendPacket(new PacketPlayOutRespawn(worldserver.worldProvider.getDimensionManager().getType(), WorldData.c(worldserver.getWorldData().getSeed()), worldserver.getWorldData().getType(), entityplayer1.playerInteractManager.getGameMode())); +- entityplayer1.playerConnection.sendPacket(new PacketPlayOutViewDistance(worldserver.spigotConfig.viewDistance)); // Spigot ++ entityplayer1.playerConnection.sendPacket(new PacketPlayOutViewDistance(worldserver.getChunkProvider().playerChunkMap.getEffectiveNoTickViewDistance())); // Paper - no-tick view distance + entityplayer1.spawnIn(worldserver); + entityplayer1.dead = false; + entityplayer1.playerConnection.teleport(new Location(worldserver.getWorld(), entityplayer1.locX(), entityplayer1.locY(), entityplayer1.locZ(), entityplayer1.yaw, entityplayer1.pitch)); +@@ -1254,7 +1254,7 @@ public abstract class PlayerList { + + public void a(int i) { + this.viewDistance = i; +- this.sendAll(new PacketPlayOutViewDistance(i)); ++ //this.sendAll(new PacketPlayOutViewDistance(i)); // Paper - move into setViewDistance + Iterator iterator = this.server.getWorlds().iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java +index 899c535c40..0e6368d0fb 100644 +--- a/src/main/java/net/minecraft/server/World.java ++++ b/src/main/java/net/minecraft/server/World.java +@@ -443,8 +443,13 @@ public abstract class World implements GeneratorAccess, AutoCloseable { + this.b(blockposition, iblockdata1, iblockdata2); + } + +- if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getState() != null && chunk.getState().isAtLeast(PlayerChunk.State.TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement ++ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getState() != null && chunk.getState().isAtLeast(PlayerChunk.State.TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement // Paper - diff on change, see below + this.notify(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 || ((WorldServer)this).getChunkProvider().playerChunkMap.playerViewDistanceBroadcastMap.getObjectsInRange(MCUtil.getCoordinateKey(blockposition)) != null)) { ++ ((WorldServer)this).getChunkProvider().flagDirty(blockposition); ++ // Paper end - per player view distance + } + + if (!this.isClientSide && (i & 1) != 0) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 995f706678..ee7ae46389 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2483,10 +2483,39 @@ public class CraftWorld implements World { + // Spigot start + @Override + public int getViewDistance() { +- return world.spigotConfig.viewDistance; ++ return getHandle().getChunkProvider().playerChunkMap.getEffectiveViewDistance(); // Paper - no-tick view distance + } + // Spigot end + ++ // Paper start - per player view distance ++ @Override ++ public void setViewDistance(int viewDistance) { ++ if (viewDistance < 2 || viewDistance > 32) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ net.minecraft.server.PlayerChunkMap chunkMap = getHandle().getChunkProvider().playerChunkMap; ++ if (viewDistance != chunkMap.getEffectiveViewDistance()) { ++ chunkMap.setViewDistance(viewDistance); ++ } ++ } ++ ++ @Override ++ public int getNoTickViewDistance() { ++ return getHandle().getChunkProvider().playerChunkMap.getEffectiveNoTickViewDistance(); ++ } ++ ++ @Override ++ public void setNoTickViewDistance(int viewDistance) { ++ if ((viewDistance < 2 || viewDistance > 32) && viewDistance != -1) { ++ throw new IllegalArgumentException("View distance " + viewDistance + " is out of range of [2, 32]"); ++ } ++ net.minecraft.server.PlayerChunkMap chunkMap = getHandle().getChunkProvider().playerChunkMap; ++ if (viewDistance != chunkMap.getRawNoTickViewDistance()) { ++ chunkMap.setNoTickViewDistance(viewDistance); ++ } ++ } ++ // Paper end - per player view distance ++ + // Spigot start + private final Spigot spigot = new Spigot() + { +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index d873b8cf3a..f735217e7a 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -201,7 +201,7 @@ public class ActivationRange + maxRange = Math.max( maxRange, waterActivationRange ); + maxRange = Math.max( maxRange, villagerActivationRange ); + // Paper end +- maxRange = Math.min( ( world.spigotConfig.viewDistance << 4 ) - 8, maxRange ); ++ maxRange = Math.min( ( ((net.minecraft.server.WorldServer)world).getChunkProvider().playerChunkMap.getEffectiveViewDistance() << 4 ) - 8, maxRange ); // Paper - no-tick view distance + + for ( EntityHuman player : world.getPlayers() ) + { +-- +2.26.0 +