--- a/net/minecraft/server/PlayerChunkMap.java
+++ b/net/minecraft/server/PlayerChunkMap.java
@@ -33,6 +33,12 @@
 import javax.annotation.Nullable;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+// CraftBukkit start
+import org.bukkit.craftbukkit.event.CraftEventFactory;
+import org.bukkit.entity.Player;
+import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.bukkit.event.world.ChunkUnloadEvent;
+// CraftBukkit end
 
 public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
 
@@ -179,9 +185,12 @@
 
         return completablefuture1.thenApply((list1) -> {
             List<IChunkAccess> list2 = Lists.newArrayList();
-            final int l1 = 0;
+            // CraftBukkit start - decompile error
+            int cnt = 0;
 
-            for (Iterator iterator = list1.iterator(); iterator.hasNext(); ++l1) {
+            for (Iterator iterator = list1.iterator(); iterator.hasNext(); ++cnt) {
+                final int l1 = cnt;
+                // CraftBukkit end
                 final Either<IChunkAccess, PlayerChunk.Failure> either = (Either) iterator.next();
                 Optional<IChunkAccess> optional = either.left();
 
@@ -257,9 +266,9 @@
         }).forEach((completablefuture) -> {
             if (flag) {
                 this.executor.awaitTasks(completablefuture::isDone);
-                ((Either) completablefuture.join()).ifLeft(this::saveChunk);
+                (completablefuture.join()).ifLeft(this::saveChunk); // CraftBukkit - decompile error
             } else {
-                ((Either) completablefuture.getNow(PlayerChunk.UNLOADED_CHUNK_ACCESS)).ifLeft(this::saveChunk);
+                (completablefuture.getNow(PlayerChunk.UNLOADED_CHUNK_ACCESS)).ifLeft(this::saveChunk); // CraftBukkit - decompile error
             }
 
         });
@@ -268,7 +277,6 @@
         }
 
     }
-
     protected void unloadChunks(BooleanSupplier booleansupplier) {
         GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
 
@@ -304,13 +312,22 @@
                 this.a(i, playerchunk);
             } else {
                 if (this.g.remove(i, playerchunk) && ichunkaccess != null) {
-                    this.saveChunk(ichunkaccess);
                     if (this.h.remove(i) && ichunkaccess instanceof Chunk) {
                         Chunk chunk = (Chunk) ichunkaccess;
 
+                        // CraftBukkit start
+                        ChunkUnloadEvent event = new ChunkUnloadEvent(chunk.bukkitChunk, chunk.isNeedsSaving());
+                        this.world.getServer().getPluginManager().callEvent(event);
+                        this.saveChunk(ichunkaccess, event.isSaveChunk());
+                        // CraftBukkit end
+
                         chunk.c(false);
                         this.world.unloadChunk(chunk);
+                        // CraftBukkit start
+                    } else {
+                        this.saveChunk(ichunkaccess);
                     }
+                    // CraftBukkit end
 
                     this.lightEngine.a(ichunkaccess.getPos());
                     this.lightEngine.queueUpdate();
@@ -394,7 +411,7 @@
                     return CompletableFuture.completedFuture(Either.right(playerchunk_failure));
                 });
             }, (runnable) -> {
-                this.mailboxWorldGen.a((Object) ChunkTaskQueueSorter.a(playerchunk, runnable));
+                this.mailboxWorldGen.a(ChunkTaskQueueSorter.a(playerchunk, runnable)); // CraftBukkit - decompile error
             });
         }
     }
@@ -476,7 +493,7 @@
             long i = playerchunk.h().pair();
 
             playerchunk.getClass();
-            mailbox.a((Object) ChunkTaskQueueSorter.a(runnable, i, playerchunk::i));
+            mailbox.a(ChunkTaskQueueSorter.a(runnable, i, playerchunk::i)); // CraftBukkit - decompile error
         });
     }
 
@@ -493,7 +510,7 @@
                 return Either.left(chunk);
             });
         }, (runnable) -> {
-            this.mailboxMain.a((Object) ChunkTaskQueueSorter.a(playerchunk, runnable));
+            this.mailboxMain.a(ChunkTaskQueueSorter.a(playerchunk, runnable)); // CraftBukkit - decompile error
         });
 
         completablefuture1.thenAcceptAsync((either) -> {
@@ -507,7 +524,7 @@
                 return Either.left(chunk);
             });
         }, (runnable) -> {
-            this.mailboxMain.a((Object) ChunkTaskQueueSorter.a(playerchunk, runnable));
+            this.mailboxMain.a(ChunkTaskQueueSorter.a(playerchunk, runnable)); // CraftBukkit - decompile error
         });
         return completablefuture1;
     }
@@ -517,8 +534,14 @@
     }
 
     public void saveChunk(IChunkAccess ichunkaccess) {
+        // CraftBukkit start
+        this.saveChunk(ichunkaccess, ichunkaccess.isNeedsSaving());
+    }
+
+    public void saveChunk(IChunkAccess ichunkaccess, boolean save) {
+        // CraftBukkit end
         this.n.a(ichunkaccess.getPos());
-        if (ichunkaccess.isNeedsSaving()) {
+        if (save) { // CraftBukkit
             try {
                 this.world.checkSession();
             } catch (ExceptionWorldConflict exceptionworldconflict) {
@@ -569,9 +592,10 @@
                 ChunkCoordIntPair chunkcoordintpair = playerchunk.h();
                 Packet<?>[] apacket = new Packet[2];
 
+                int finall = l; // CraftBukkit - decompile error
                 this.a(chunkcoordintpair, false).forEach((entityplayer) -> {
                     int i1 = b(chunkcoordintpair, entityplayer, true);
-                    boolean flag = i1 <= l;
+                    boolean flag = i1 <= finall; // CraftBukkit - decompile error
                     boolean flag1 = i1 <= this.A;
 
                     this.sendChunk(entityplayer, chunkcoordintpair, apacket, flag, flag1);
@@ -626,7 +650,7 @@
     private NBTTagCompound f(ChunkCoordIntPair chunkcoordintpair) throws IOException {
         NBTTagCompound nbttagcompound = this.read(chunkcoordintpair);
 
-        return nbttagcompound == null ? null : this.getChunkData(this.world.getWorldProvider().getDimensionManager(), this.m, nbttagcompound);
+        return nbttagcompound == null ? null : this.getChunkData(this.world.getWorldProvider().getDimensionManager(), this.m, nbttagcompound, chunkcoordintpair, world); // CraftBukkit
     }
 
     boolean d(ChunkCoordIntPair chunkcoordintpair) {
@@ -946,7 +970,7 @@
         public final Set<EntityPlayer> trackedPlayers = Sets.newHashSet();
 
         public EntityTracker(Entity entity, int i, int j, boolean flag) {
-            this.trackerEntry = new EntityTrackerEntry(PlayerChunkMap.this.world, entity, j, flag, this::broadcast);
+            this.trackerEntry = new EntityTrackerEntry(PlayerChunkMap.this.world, entity, j, flag, this::broadcast, trackedPlayers); // CraftBukkit
             this.tracker = entity;
             this.trackingDistance = i;
             this.e = SectionPosition.a(entity);
@@ -1015,6 +1039,17 @@
                         }
                     }
 
+                    // CraftBukkit start - respect vanish API
+                    if (this.tracker instanceof EntityPlayer) {
+                        Player player = ((EntityPlayer) this.tracker).getBukkitEntity();
+                        if (!entityplayer.getBukkitEntity().canSee(player)) {
+                            flag1 = false;
+                        }
+                    }
+
+                    entityplayer.removeQueue.remove(Integer.valueOf(this.tracker.getId()));
+                    // CraftBukkit end
+
                     if (flag1 && this.trackedPlayers.add(entityplayer)) {
                         this.trackerEntry.b(entityplayer);
                     }