From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
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 4612697569fd6e3683b0e58453b61a9a8d077229..5c8a946d5c895fc2622c7df656cc462c58104cf7 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -679,4 +679,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 750fb07756f7e40b21f8ab0925f2e842aae50f7b..8c1f3290d23795b58a30274c9437dc7dc43fa3a1 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<EntityPlayer> 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 b98abed74f214932b4a226f12645f987191b0097..716d4eab382244ee9bc4855bf0f026e65ec057d6 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 0560eca744cb2032bb6a3faf5aeafa95a7a6815e..07a6fc3d88e7d44bfab7f3d6a0eef7dc132ab422 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 c4aad20a2952cc34e334ba665a6e0910d5609497..845e5d2a8ee025ac61cf916de04e0797e32db568 100644
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
@@ -47,14 +47,28 @@ public class PlayerChunk {
     // cached here to avoid a map lookup
     com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> playersInMobSpawnRange;
     com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> playersInChunkTickRange;
+    com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> playersInTickingRange;
 
     void updateRanges() {
         long key = net.minecraft.server.MCUtil.getCoordinateKey(this.location);
         this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
         this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+        this.playersInTickingRange = this.chunkMap.playerViewDistanceTickMap.getObjectsInRange(key);
     }
     // 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;
@@ -210,7 +224,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);
@@ -230,7 +244,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);
@@ -315,9 +329,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<EntityPlayer> 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<Either<IChunkAccess, PlayerChunk.Failure>> 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 9d414b6bbdda2a4d5a4ecdad6abb7d53860e7e71..abfe75750ea564cd56b53d3f09eb247478384e2f 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<ChunkTaskQueueSorter.a<Runnable>> mailboxWorldGen;
-    private final Mailbox<ChunkTaskQueueSorter.a<Runnable>> mailboxMain;
+    final Mailbox<ChunkTaskQueueSorter.a<Runnable>> 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,22 @@ 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 int getLoadViewDistance() {
+        return Math.max(this.getEffectiveViewDistance(), this.getEffectiveNoTickViewDistance());
+    }
+
+    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 +173,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 +198,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 +220,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 +340,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<EntityPlayer> 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<EntityPlayer> 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<EntityPlayer> 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<EntityPlayer> 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) {
@@ -1101,15 +1187,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;
     }
@@ -1209,32 +1291,38 @@ 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();
+            this.setNoTickViewDistance(this.getRawNoTickViewDistance()); //Paper - no-tick view distance - propagate changes to no-tick, which does the actual chunk loading/sending
+        }
 
-            while (objectiterator.hasNext()) {
-                PlayerChunk playerchunk = (PlayerChunk) objectiterator.next();
-                ChunkCoordIntPair chunkcoordintpair = playerchunk.i();
-                Packet<?>[] apacket = new Packet[2];
+    }
 
-                this.a(chunkcoordintpair, false).forEach((entityplayer) -> {
-                    int l = b(chunkcoordintpair, entityplayer, true);
-                    boolean flag = l <= k;
-                    boolean flag1 = l <= this.viewDistance;
+    // Paper start - no-tick view distance
+    public final void setNoTickViewDistance(int viewDistance) {
+        viewDistance = viewDistance == -1 ? -1 : MathHelper.clamp(viewDistance, 2, 32);
 
-                    this.sendChunk(entityplayer, chunkcoordintpair, apacket, flag, flag1);
-                });
+        this.noTickViewDistance = viewDistance;
+        int loadViewDistance = this.getLoadViewDistance();
+        this.chunkDistanceManager.setNoTickViewDistance(loadViewDistance + 2 + 2); // add 2 to account for the change to 31 -> 33 tickets // see notes in the distance map updating for the other + 2
+
+        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(loadViewDistance));
+                }
+                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) {
@@ -1242,7 +1330,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);
@@ -1511,6 +1599,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);
     }
@@ -1538,13 +1627,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)
 
     }
 
@@ -1552,7 +1635,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;
     }
 
@@ -1597,6 +1680,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;
@@ -1634,7 +1718,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
 
@@ -1642,11 +1726,46 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
 
     @Override
     public Stream<EntityPlayer> 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<EntityPlayer> 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<EntityPlayer> 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) {
@@ -1806,6 +1925,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);
@@ -1991,7 +2111,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 60af90bf8c376ab8ab61b16ae38886149faa88cc..9b726de6daeba42120f3a948fbdcf080d0e72917 100644
--- a/src/main/java/net/minecraft/server/PlayerList.java
+++ b/src/main/java/net/minecraft/server/PlayerList.java
@@ -151,7 +151,7 @@ public abstract class PlayerList {
         // CraftBukkit - getType()
         // Spigot - view distance
         networkmanager.queueImmunity = true; // Paper
-        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.getLoadViewDistance(), 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()));
@@ -773,7 +773,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.getLoadViewDistance())); // 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));
@@ -1259,7 +1259,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 899c535c4056cd2375ab8f834f03267d405f4bda..0e6368d0fb3beccb492ae3867fb4e22825f928a2 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 b184f8ed8ce86965c3ef9aef179126a4d69275e8..bbfbfeed12890c9d20d78a9661ab172c901f008c 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -2482,10 +2482,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 d873b8cf3aec01b791565c33b252889f99f181f9..f735217e7a99bf8286ea60158f9fe137e84ad75c 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() )
         {
