diff --git a/nms-patches/Chunk.patch b/nms-patches/Chunk.patch
index 923879ef94..a617cae68f 100644
--- a/nms-patches/Chunk.patch
+++ b/nms-patches/Chunk.patch
@@ -1,32 +1,18 @@
 --- a/net/minecraft/server/Chunk.java
 +++ b/net/minecraft/server/Chunk.java
-@@ -22,6 +22,13 @@
- import org.apache.logging.log4j.LogManager;
- import org.apache.logging.log4j.Logger;
- 
-+// CraftBukkit start
-+import com.google.common.collect.Lists;
-+import java.util.LinkedList;
-+import org.bukkit.craftbukkit.event.CraftEventFactory;
-+import org.bukkit.event.entity.CreatureSpawnEvent;
-+// CraftBukkit end
-+
- public class Chunk implements IChunkAccess {
- 
-     private static final Logger LOGGER = LogManager.getLogger();
-@@ -95,8 +102,19 @@
+@@ -95,8 +95,19 @@
              }
          }
  
 +        // CraftBukkit start
 +        this.bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
-+    }
-+
+     }
+ 
 +    public org.bukkit.Chunk bukkitChunk;
 +    public org.bukkit.Chunk getBukkitChunk() {
 +        return bukkitChunk;
-     }
- 
++    }
++
 +    public boolean mustNotSave;
 +    public boolean needsDecoration;
 +    // CraftBukkit end
@@ -34,7 +20,7 @@
      public Chunk(World world, ProtoChunk protochunk) {
          this(world, protochunk.getPos(), protochunk.getBiomeIndex(), protochunk.p(), protochunk.n(), protochunk.o(), protochunk.q(), protochunk.getSections(), (Consumer) null);
          Iterator iterator = protochunk.y().iterator();
-@@ -138,6 +156,7 @@
+@@ -138,6 +149,7 @@
  
          this.b(protochunk.r());
          this.s = true;
@@ -42,7 +28,7 @@
      }
  
      @Override
-@@ -228,9 +247,16 @@
+@@ -228,9 +240,16 @@
          }
      }
  
@@ -59,7 +45,7 @@
          int i = blockposition.getX() & 15;
          int j = blockposition.getY();
          int k = blockposition.getZ() & 15;
-@@ -282,7 +308,8 @@
+@@ -282,7 +301,8 @@
                      }
                  }
  
@@ -69,7 +55,7 @@
                      iblockdata.onPlace(this.world, blockposition, iblockdata1, flag);
                  }
  
-@@ -382,7 +409,12 @@
+@@ -382,7 +402,12 @@
  
      @Nullable
      public TileEntity a(BlockPosition blockposition, Chunk.EnumTileEntityState chunk_enumtileentitystate) {
@@ -83,7 +69,7 @@
  
          if (tileentity == null) {
              NBTTagCompound nbttagcompound = (NBTTagCompound) this.e.remove(blockposition);
-@@ -429,6 +461,13 @@
+@@ -429,6 +454,13 @@
                  tileentity1.m();
              }
  
@@ -97,7 +83,7 @@
          }
      }
  
-@@ -457,6 +496,41 @@
+@@ -457,6 +489,50 @@
  
      }
  
@@ -113,6 +99,7 @@
 +            server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(this.bukkitChunk, this.needsDecoration));
 +
 +            if (this.needsDecoration) {
++                this.needsDecoration = false;
 +                java.util.Random random = new java.util.Random();
 +                random.setSeed(world.getSeed());
 +                long xRand = random.nextLong() / 2L * 2L + 1L;
@@ -134,12 +121,20 @@
 +            }
 +        }
 +    }
++
++    public void unloadCallback() {
++        org.bukkit.Server server = this.world.getServer();
++        org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isNeedsSaving());
++        server.getPluginManager().callEvent(unloadEvent);
++        // note: saving can be prevented, but not forced if no saving is actually required
++        this.mustNotSave = !unloadEvent.isSaveChunk();
++    }
 +    // CraftBukkit end
 +
      public void markDirty() {
          this.s = true;
      }
-@@ -531,7 +605,7 @@
+@@ -531,7 +607,7 @@
              Iterator iterator = this.entitySlices[k].a(oclass).iterator();
  
              while (iterator.hasNext()) {
@@ -148,7 +143,7 @@
  
                  if (t0.getBoundingBox().c(axisalignedbb) && (predicate == null || predicate.test(t0))) {
                      list.add(t0);
-@@ -605,7 +679,7 @@
+@@ -605,7 +681,7 @@
  
      @Override
      public boolean isNeedsSaving() {
@@ -157,7 +152,7 @@
      }
  
      public void d(boolean flag) {
-@@ -746,7 +820,7 @@
+@@ -746,7 +822,7 @@
  
      public void B() {
          if (this.o instanceof ProtoChunkTickList) {
@@ -166,7 +161,7 @@
                  return this.getType(blockposition).getBlock();
              });
              this.o = TickListEmpty.a();
-@@ -756,7 +830,7 @@
+@@ -756,7 +832,7 @@
          }
  
          if (this.p instanceof ProtoChunkTickList) {
@@ -175,7 +170,7 @@
                  return this.getFluid(blockposition).getType();
              });
              this.p = TickListEmpty.a();
-@@ -768,12 +842,12 @@
+@@ -768,12 +844,12 @@
      }
  
      public void a(WorldServer worldserver) {
diff --git a/nms-patches/ChunkProviderServer.patch b/nms-patches/ChunkProviderServer.patch
index 02e71aa065..0123c9e34b 100644
--- a/nms-patches/ChunkProviderServer.patch
+++ b/nms-patches/ChunkProviderServer.patch
@@ -1,6 +1,49 @@
 --- a/net/minecraft/server/ChunkProviderServer.java
 +++ b/net/minecraft/server/ChunkProviderServer.java
-@@ -241,6 +241,17 @@
+@@ -81,7 +81,7 @@
+             for (int l = 0; l < 4; ++l) {
+                 if (k == this.n[l] && chunkstatus == this.o[l]) {
+                     ichunkaccess = this.p[l];
+-                    if (ichunkaccess != null || !flag) {
++                    if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
+                         return ichunkaccess;
+                     }
+                 }
+@@ -125,7 +125,15 @@
+         int l = 33 + ChunkStatus.a(chunkstatus);
+         PlayerChunk playerchunk = this.getChunk(k);
+ 
+-        if (flag) {
++        // CraftBukkit start - don't add new ticket for currently unloading chunk
++        boolean currentlyUnloading = false;
++        if (playerchunk != null) {
++            PlayerChunk.State oldChunkState = PlayerChunk.c(playerchunk.oldTicketLevel); // PAIL getChunkState
++            PlayerChunk.State currentChunkState = PlayerChunk.c(playerchunk.getTicketLevel()); // PAIL getChunkState
++            currentlyUnloading = (oldChunkState.a(PlayerChunk.State.BORDER) && !currentChunkState.a(PlayerChunk.State.BORDER)); // PAIL isAtLeast
++        }
++        if (flag && !currentlyUnloading) {
++            // CraftBukkit end
+             this.chunkMapDistance.a(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair);
+             if (this.a(playerchunk, l)) {
+                 GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
+@@ -144,14 +152,14 @@
+     }
+ 
+     private boolean a(@Nullable PlayerChunk playerchunk, int i) {
+-        return playerchunk == null || playerchunk.getTicketLevel() > i;
++        return playerchunk == null || playerchunk.oldTicketLevel > i; // CraftBukkit using oldTicketLevel for isLoaded checks
+     }
+ 
+     public boolean isLoaded(int i, int j) {
+         PlayerChunk playerchunk = this.getChunk((new ChunkCoordIntPair(i, j)).pair());
+         int k = 33 + ChunkStatus.a(ChunkStatus.FULL);
+ 
+-        return playerchunk != null && playerchunk.getTicketLevel() <= k ? ((Either) playerchunk.getStatusFuture(ChunkStatus.FULL).getNow(PlayerChunk.UNLOADED_CHUNK_ACCESS)).left().isPresent() : false;
++        return playerchunk != null && playerchunk.oldTicketLevel <= k ? ((Either) playerchunk.getStatusFuture(ChunkStatus.FULL).getNow(PlayerChunk.UNLOADED_CHUNK_ACCESS)).left().isPresent() : false; // CraftBukkit using oldTicketLevel for isLoaded checks
+     }
+ 
+     @Override
+@@ -241,6 +249,18 @@
          this.playerChunkMap.close();
      }
  
@@ -12,13 +55,14 @@
 +        this.world.getMethodProfiler().exitEnter("unload");
 +        this.playerChunkMap.unloadChunks(() -> true);
 +        this.world.getMethodProfiler().exit();
++        this.l(); // PAIL clearCache
 +    }
 +    // CraftBukkit end
 +
      public void tick(BooleanSupplier booleansupplier) {
          this.world.getMethodProfiler().enter("purge");
          this.chunkMapDistance.purgeTickets();
-@@ -260,13 +271,13 @@
+@@ -260,13 +280,13 @@
          this.lastTickTime = i;
          WorldData worlddata = this.world.getWorldData();
          boolean flag = worlddata.getType() == WorldType.DEBUG_ALL_BLOCK_STATES;
@@ -34,7 +78,7 @@
  
              this.world.getMethodProfiler().enter("naturalSpawnCount");
              int l = this.chunkMapDistance.b();
-@@ -299,8 +310,30 @@
+@@ -299,8 +319,30 @@
                              for (int j1 = 0; j1 < i1; ++j1) {
                                  EnumCreatureType enumcreaturetype = aenumcreaturetype1[j1];
  
diff --git a/nms-patches/Entity.patch b/nms-patches/Entity.patch
index 8691e74483..60a11e7a3b 100644
--- a/nms-patches/Entity.patch
+++ b/nms-patches/Entity.patch
@@ -58,7 +58,7 @@
      protected static final Logger LOGGER = LogManager.getLogger();
      private static final AtomicInteger entityCount = new AtomicInteger();
      private static final List<ItemStack> c = Collections.emptyList();
-@@ -106,6 +155,16 @@
+@@ -106,6 +155,20 @@
      private long aH;
      private EntitySize size;
      private float headHeight;
@@ -71,11 +71,15 @@
 +    public float getBukkitYaw() {
 +        return this.yaw;
 +    }
++
++    public boolean isChunkLoaded() {
++        return world.isChunkLoaded((int) Math.floor(this.locX) >> 4, (int) Math.floor(this.locZ) >> 4);
++    }
 +    // CraftBukkit end
  
      public Entity(EntityTypes<?> entitytypes, World world) {
          this.id = Entity.entityCount.incrementAndGet();
-@@ -204,6 +263,12 @@
+@@ -204,6 +267,12 @@
      }
  
      protected void setPose(EntityPose entitypose) {
@@ -88,7 +92,7 @@
          this.datawatcher.set(Entity.POSE, entitypose);
      }
  
-@@ -212,6 +277,33 @@
+@@ -212,6 +281,33 @@
      }
  
      protected void setYawPitch(float f, float f1) {
@@ -122,7 +126,7 @@
          this.yaw = f % 360.0F;
          this.pitch = f1 % 360.0F;
      }
-@@ -224,6 +316,7 @@
+@@ -224,6 +320,7 @@
          float f1 = this.size.height;
  
          this.a(new AxisAlignedBB(d0 - (double) f, d1, d2 - (double) f, d0 + (double) f, d1 + (double) f1, d2 + (double) f));
@@ -130,7 +134,7 @@
      }
  
      public void tick() {
-@@ -234,6 +327,15 @@
+@@ -234,6 +331,15 @@
          this.entityBaseTick();
      }
  
@@ -146,7 +150,7 @@
      public void entityBaseTick() {
          this.world.getMethodProfiler().enter("entityBaseTick");
          if (this.isPassenger() && this.getVehicle().dead) {
-@@ -250,7 +352,7 @@
+@@ -250,7 +356,7 @@
          this.lastZ = this.locZ;
          this.lastPitch = this.pitch;
          this.lastYaw = this.yaw;
@@ -155,7 +159,7 @@
          this.az();
          this.m();
          if (this.world.isClientSide) {
-@@ -300,12 +402,44 @@
+@@ -300,12 +406,44 @@
  
      protected void burnFromLava() {
          if (!this.isFireProof()) {
@@ -201,7 +205,7 @@
          int j = i * 20;
  
          if (this instanceof EntityLiving) {
-@@ -401,6 +535,28 @@
+@@ -401,6 +539,28 @@
                  block1.a((IBlockAccess) this.world, this);
              }
  
@@ -230,7 +234,7 @@
              if (this.playStepSound() && (!this.onGround || !this.isSneaking() || !(this instanceof EntityHuman)) && !this.isPassenger()) {
                  double d0 = vec3d1.x;
                  double d1 = vec3d1.y;
-@@ -454,7 +610,14 @@
+@@ -454,7 +614,14 @@
                  if (!flag) {
                      ++this.fireTicks;
                      if (this.fireTicks == 0) {
@@ -246,7 +250,7 @@
                      }
                  }
  
-@@ -565,7 +728,7 @@
+@@ -565,7 +732,7 @@
          VoxelShape voxelshape = this.world.getWorldBorder().a();
          Stream<VoxelShape> stream = VoxelShapes.c(voxelshape, VoxelShapes.a(axisalignedbb.shrink(1.0E-7D)), OperatorBoolean.AND) ? Stream.empty() : Stream.of(voxelshape);
          AxisAlignedBB axisalignedbb1 = axisalignedbb.a(vec3d).g(1.0E-7D);
@@ -255,7 +259,7 @@
              return !this.x(entity);
          }).flatMap((entity) -> {
              return Stream.of(entity.ap(), this.j(entity));
-@@ -649,6 +812,7 @@
+@@ -649,6 +816,7 @@
          this.locX = (axisalignedbb.minX + axisalignedbb.maxX) / 2.0D;
          this.locY = axisalignedbb.minY;
          this.locZ = (axisalignedbb.minZ + axisalignedbb.maxZ) / 2.0D;
@@ -263,7 +267,7 @@
      }
  
      protected SoundEffect getSoundSwim() {
-@@ -820,7 +984,7 @@
+@@ -820,7 +988,7 @@
          return null;
      }
  
@@ -272,7 +276,7 @@
          if (!this.isFireProof()) {
              this.damageEntity(DamageSource.FIRE, (float) i);
          }
-@@ -1053,6 +1217,13 @@
+@@ -1053,6 +1221,13 @@
      }
  
      public void spawnIn(World world) {
@@ -286,7 +290,7 @@
          this.world = world;
      }
  
-@@ -1078,6 +1249,7 @@
+@@ -1078,6 +1253,7 @@
              this.lastYaw -= 360.0F;
          }
  
@@ -294,7 +298,7 @@
          this.setPosition(this.locX, this.locY, this.locZ);
          this.setYawPitch(f, f1);
      }
-@@ -1246,7 +1418,7 @@
+@@ -1246,7 +1422,7 @@
      public boolean c(NBTTagCompound nbttagcompound) {
          String s = this.getSaveID();
  
@@ -303,7 +307,7 @@
              nbttagcompound.setString("id", s);
              this.save(nbttagcompound);
              return true;
-@@ -1265,15 +1437,33 @@
+@@ -1265,15 +1441,33 @@
              Vec3D vec3d = this.getMot();
  
              nbttagcompound.set("Motion", this.a(vec3d.x, vec3d.y, vec3d.z));
@@ -338,7 +342,7 @@
              IChatBaseComponent ichatbasecomponent = this.getCustomName();
  
              if (ichatbasecomponent != null) {
-@@ -1331,6 +1521,11 @@
+@@ -1331,6 +1525,11 @@
                  }
              }
  
@@ -350,7 +354,7 @@
              return nbttagcompound;
          } catch (Throwable throwable) {
              CrashReport crashreport = CrashReport.a(throwable, "Saving entity NBT");
-@@ -1371,7 +1566,7 @@
+@@ -1371,7 +1570,7 @@
              this.setAirTicks(nbttagcompound.getShort("Air"));
              this.onGround = nbttagcompound.getBoolean("OnGround");
              if (nbttagcompound.hasKey("Dimension")) {
@@ -359,7 +363,7 @@
              }
  
              this.invulnerable = nbttagcompound.getBoolean("Invulnerable");
-@@ -1414,6 +1609,42 @@
+@@ -1414,6 +1613,42 @@
              } else {
                  throw new IllegalStateException("Entity has invalid position");
              }
@@ -402,7 +406,7 @@
          } catch (Throwable throwable) {
              CrashReport crashreport = CrashReport.a(throwable, "Loading entity NBT");
              CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Entity being loaded");
-@@ -1489,9 +1720,22 @@
+@@ -1489,9 +1724,22 @@
          } else if (this.world.isClientSide) {
              return null;
          } else {
@@ -425,7 +429,7 @@
              this.world.addEntity(entityitem);
              return entityitem;
          }
-@@ -1595,7 +1839,7 @@
+@@ -1595,7 +1843,7 @@
              }
  
              this.vehicle = entity;
@@ -434,7 +438,7 @@
              return true;
          }
      }
-@@ -1620,15 +1864,36 @@
+@@ -1620,15 +1868,36 @@
              Entity entity = this.vehicle;
  
              this.vehicle = null;
@@ -473,7 +477,7 @@
              if (!this.world.isClientSide && entity instanceof EntityHuman && !(this.getRidingPassenger() instanceof EntityHuman)) {
                  this.passengers.add(0, entity);
              } else {
-@@ -1636,15 +1901,33 @@
+@@ -1636,15 +1905,33 @@
              }
  
          }
@@ -508,7 +512,7 @@
      }
  
      protected boolean q(Entity entity) {
-@@ -1687,11 +1970,17 @@
+@@ -1687,11 +1974,17 @@
              int i = this.ab();
  
              if (this.ai) {
@@ -528,7 +532,7 @@
                      this.world.getMethodProfiler().exit();
                  }
  
-@@ -1771,6 +2060,13 @@
+@@ -1771,6 +2064,13 @@
      }
  
      public void setSwimming(boolean flag) {
@@ -542,7 +546,7 @@
          this.setFlag(4, flag);
      }
  
-@@ -1831,16 +2127,56 @@
+@@ -1831,16 +2131,56 @@
      }
  
      public void setAirTicks(int i) {
@@ -577,8 +581,9 @@
 +                this.setOnFire(entityCombustEvent.getDuration(), false);
 +            }
 +            // CraftBukkit end
-+        }
-+
+         }
+ 
+-        this.damageEntity(DamageSource.LIGHTNING, 5.0F);
 +        // CraftBukkit start
 +        if (thisBukkitEntity instanceof Hanging) {
 +            HangingBreakByEntityEvent hangingEvent = new HangingBreakByEntityEvent((Hanging) thisBukkitEntity, stormBukkitEntity);
@@ -587,9 +592,8 @@
 +            if (hangingEvent.isCancelled()) {
 +                return;
 +            }
-         }
- 
--        this.damageEntity(DamageSource.LIGHTNING, 5.0F);
++        }
++
 +        if (this.isFireProof()) {
 +            return;
 +        }
@@ -602,7 +606,7 @@
      }
  
      public void j(boolean flag) {
-@@ -1988,20 +2324,33 @@
+@@ -1988,20 +2328,33 @@
  
      @Nullable
      public Entity a(DimensionManager dimensionmanager) {
@@ -639,7 +643,7 @@
              if (dimensionmanager1 == DimensionManager.THE_END && dimensionmanager == DimensionManager.OVERWORLD) {
                  blockposition = worldserver1.getHighestBlockYAt(HeightMap.Type.MOTION_BLOCKING_NO_LEAVES, worldserver1.getSpawn());
              } else if (dimensionmanager == DimensionManager.THE_END) {
-@@ -2039,6 +2388,25 @@
+@@ -2039,6 +2392,25 @@
                  vec3d = shapedetector_c.b;
                  f = (float) shapedetector_c.c;
              }
@@ -665,7 +669,7 @@
  
              this.world.getMethodProfiler().exitEnter("reloading");
              Entity entity = this.getEntityType().a((World) worldserver1);
-@@ -2048,6 +2416,14 @@
+@@ -2048,6 +2420,14 @@
                  entity.setPositionRotation(blockposition, entity.yaw + f, entity.pitch);
                  entity.setMot(vec3d);
                  worldserver1.addEntityTeleport(entity);
@@ -680,7 +684,7 @@
              }
  
              this.dead = true;
-@@ -2239,7 +2615,26 @@
+@@ -2239,7 +2619,26 @@
      }
  
      public void a(AxisAlignedBB axisalignedbb) {
diff --git a/nms-patches/PlayerChunk.patch b/nms-patches/PlayerChunk.patch
index 27a5ae70b5..e34a359bb2 100644
--- a/nms-patches/PlayerChunk.patch
+++ b/nms-patches/PlayerChunk.patch
@@ -1,5 +1,14 @@
 --- a/net/minecraft/server/PlayerChunk.java
 +++ b/net/minecraft/server/PlayerChunk.java
+@@ -23,7 +23,7 @@
+     private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> tickingFuture;
+     private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> entityTickingFuture;
+     private CompletableFuture<IChunkAccess> chunkSave;
+-    private int oldTicketLevel;
++    public int oldTicketLevel; // CraftBukkit - public
+     private int ticketLevel;
+     private int n;
+     private final ChunkCoordIntPair location;
 @@ -43,7 +43,7 @@
          this.fullChunkFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
          this.tickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
@@ -9,7 +18,31 @@
          this.dirtyBlocks = new short[64];
          this.location = chunkcoordintpair;
          this.lightEngine = lightengine;
-@@ -76,9 +76,9 @@
+@@ -55,6 +55,14 @@
+         this.a(i);
+     }
+ 
++    // CraftBukkit start
++    public Chunk getFullChunk() {
++        CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> statusFuture = this.getStatusFuture(ChunkStatus.FULL);
++        Either<IChunkAccess, PlayerChunk.Failure> either = (Either<IChunkAccess, PlayerChunk.Failure>) statusFuture.getNow(null);
++        return either == null ? null : (Chunk) either.left().orElse(null);
++    }
++    // CraftBukkit end
++
+     public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getStatusFutureUnchecked(ChunkStatus chunkstatus) {
+         CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture = (CompletableFuture) this.statusFutures.get(chunkstatus.c());
+ 
+@@ -62,7 +70,7 @@
+     }
+ 
+     public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getStatusFuture(ChunkStatus chunkstatus) {
+-        return b(this.ticketLevel).b(chunkstatus) ? this.getStatusFutureUnchecked(chunkstatus) : PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE;
++        return b(this.oldTicketLevel).b(chunkstatus) ? this.getStatusFutureUnchecked(chunkstatus) : PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE; // CraftBukkit using oldTicketLevel for isLoaded checks
+     }
+ 
+     public CompletableFuture<Either<Chunk, PlayerChunk.Failure>> a() {
+@@ -76,9 +84,9 @@
      @Nullable
      public Chunk getChunk() {
          CompletableFuture<Either<Chunk, PlayerChunk.Failure>> completablefuture = this.a();
@@ -21,7 +54,7 @@
      }
  
      public CompletableFuture<IChunkAccess> getChunkSave() {
-@@ -201,7 +201,7 @@
+@@ -201,7 +209,7 @@
          CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture = (CompletableFuture) this.statusFutures.get(i);
  
          if (completablefuture != null) {
@@ -30,23 +63,29 @@
  
              if (either == null || either.left().isPresent()) {
                  return completablefuture;
-@@ -213,6 +213,15 @@
- 
-             this.a(completablefuture1);
-             this.statusFutures.set(i, completablefuture1);
-+            // CraftBukkit start
-+            if (chunkstatus == ChunkStatus.FULL) {
-+                completablefuture1.thenAccept((either) -> {
-+                    Chunk chunk = (Chunk) either.left().get();
-+
-+                    chunk.loadCallback();
+@@ -256,6 +264,21 @@
+         boolean flag1 = this.ticketLevel <= PlayerChunkMap.GOLDEN_TICKET;
+         PlayerChunk.State playerchunk_state = c(this.oldTicketLevel);
+         PlayerChunk.State playerchunk_state1 = c(this.ticketLevel);
++        // CraftBukkit start
++        // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
++        if (playerchunk_state.a(PlayerChunk.State.BORDER) && !playerchunk_state1.a(PlayerChunk.State.BORDER)) { // PAIL oldChunkState, newChunkState, isAtLeast
++            this.getStatusFutureUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
++                either.ifLeft((chunkAccess) -> {
++                    Chunk chunk = (Chunk) chunkAccess;
++                    // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
++                    // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
++                    // These actions may however happen deferred, so we manually set the needsSaving flag already here.
++                    chunk.setNeedsSaving(true);
++                    chunk.unloadCallback();
 +                });
-+            }
-+            // CraftBukkit end
-             return completablefuture1;
-         } else {
-             return completablefuture == null ? PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE : completablefuture;
-@@ -294,7 +303,7 @@
++            });
++        }
++        // CraftBukkit end
+ 
+         if (flag1) {
+             for (int i = flag ? chunkstatus.c() + 1 : 0; i <= chunkstatus1.c(); ++i) {
+@@ -294,7 +317,7 @@
          if (flag2 && !flag3) {
              completablefuture = this.fullChunkFuture;
              this.fullChunkFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
@@ -55,3 +94,21 @@
                  playerchunkmap.getClass();
                  return either1.ifLeft(playerchunkmap::a);
              }));
+@@ -332,6 +355,17 @@
+ 
+         this.w.a(this.location, this::j, this.ticketLevel, this::d);
+         this.oldTicketLevel = this.ticketLevel;
++        // CraftBukkit start
++        // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
++        if (!playerchunk_state.a(PlayerChunk.State.BORDER) && playerchunk_state1.a(PlayerChunk.State.BORDER)) { // PAIL oldChunkState, newChunkState, isAtLeast
++            this.getStatusFutureUnchecked(ChunkStatus.FULL).thenAccept((either) -> {
++                either.ifLeft((chunkAccess) -> {
++                    Chunk chunk = (Chunk) chunkAccess;
++                    chunk.loadCallback();
++                });
++            });
++        }
++        // CraftBukkit end
+     }
+ 
+     public static ChunkStatus b(int i) {
diff --git a/nms-patches/PlayerChunkMap.patch b/nms-patches/PlayerChunkMap.patch
index 753e103c6e..0700775c92 100644
--- a/nms-patches/PlayerChunkMap.patch
+++ b/nms-patches/PlayerChunkMap.patch
@@ -1,17 +1,14 @@
 --- a/net/minecraft/server/PlayerChunkMap.java
 +++ b/net/minecraft/server/PlayerChunkMap.java
-@@ -35,6 +35,10 @@
+@@ -35,6 +35,7 @@
  import org.apache.commons.lang3.mutable.MutableBoolean;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
-+// CraftBukkit start
-+import org.bukkit.entity.Player;
-+import org.bukkit.event.world.ChunkUnloadEvent;
-+// CraftBukkit end
++import org.bukkit.entity.Player; // CraftBukkit
  
  public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
  
-@@ -181,9 +185,12 @@
+@@ -181,9 +182,12 @@
  
          return completablefuture1.thenApply((list1) -> {
              List<IChunkAccess> list2 = Lists.newArrayList();
@@ -26,7 +23,7 @@
                  final Either<IChunkAccess, PlayerChunk.Failure> either = (Either) iterator.next();
                  Optional<IChunkAccess> optional = either.left();
  
-@@ -279,7 +286,7 @@
+@@ -279,7 +283,7 @@
              PlayerChunkMap.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", this.x.getName());
          } else {
              this.visibleChunks.values().stream().filter(PlayerChunk::hasBeenLoaded).forEach((playerchunk) -> {
@@ -35,7 +32,7 @@
  
                  if (ichunkaccess instanceof ProtoChunkExtension || ichunkaccess instanceof Chunk) {
                      this.saveChunk(ichunkaccess);
-@@ -290,7 +297,6 @@
+@@ -290,7 +294,6 @@
          }
  
      }
@@ -43,27 +40,7 @@
      protected void unloadChunks(BooleanSupplier booleansupplier) {
          GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
  
-@@ -330,9 +336,19 @@
-                     if (this.loadedChunks.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.setLoaded(false);
-                         this.world.unloadChunk(chunk);
-+                        // CraftBukkit start
-+                    } else {
-+                        this.saveChunk(ichunkaccess);
-                     }
-+                    // CraftBukkit end
- 
-                     this.lightEngine.a(ichunkaccess.getPos());
-                     this.lightEngine.queueUpdate();
-@@ -416,7 +432,7 @@
+@@ -416,7 +419,7 @@
                      return CompletableFuture.completedFuture(Either.right(playerchunk_failure));
                  });
              }, (runnable) -> {
@@ -72,7 +49,7 @@
              });
          }
      }
-@@ -498,7 +514,7 @@
+@@ -498,7 +501,7 @@
              long i = playerchunk.h().pair();
  
              playerchunk.getClass();
@@ -81,7 +58,7 @@
          });
      }
  
-@@ -515,7 +531,7 @@
+@@ -515,7 +518,7 @@
                  return Either.left(chunk);
              });
          }, (runnable) -> {
@@ -90,7 +67,7 @@
          });
  
          completablefuture1.thenAcceptAsync((either) -> {
-@@ -529,7 +545,7 @@
+@@ -529,7 +532,7 @@
                  return Either.left(chunk);
              });
          }, (runnable) -> {
@@ -99,7 +76,7 @@
          });
          return completablefuture1;
      }
-@@ -543,7 +559,7 @@
+@@ -543,7 +546,7 @@
                  return chunk;
              });
          }, (runnable) -> {
@@ -108,23 +85,7 @@
          });
      }
  
-@@ -552,8 +568,14 @@
-     }
- 
-     public boolean saveChunk(IChunkAccess ichunkaccess) {
-+        // CraftBukkit start
-+        return this.saveChunk(ichunkaccess, ichunkaccess.isNeedsSaving());
-+    }
-+
-+    public boolean saveChunk(IChunkAccess ichunkaccess, boolean save) {
-+        // CraftBukkit end
-         this.n.a(ichunkaccess.getPos());
--        if (!ichunkaccess.isNeedsSaving()) {
-+        if (!save) { // CraftBukkit
-             return false;
-         } else {
-             try {
-@@ -607,9 +629,10 @@
+@@ -607,9 +610,10 @@
                  ChunkCoordIntPair chunkcoordintpair = playerchunk.h();
                  Packet<?>[] apacket = new Packet[2];
  
@@ -136,7 +97,7 @@
                      boolean flag1 = i1 <= this.viewDistance;
  
                      this.sendChunk(entityplayer, chunkcoordintpair, apacket, flag, flag1);
-@@ -664,7 +687,7 @@
+@@ -664,7 +668,7 @@
      private NBTTagCompound readChunkData(ChunkCoordIntPair chunkcoordintpair) throws IOException {
          NBTTagCompound nbttagcompound = this.read(chunkcoordintpair);
  
@@ -145,7 +106,7 @@
      }
  
      boolean d(ChunkCoordIntPair chunkcoordintpair) {
-@@ -984,7 +1007,7 @@
+@@ -984,7 +988,7 @@
          public final Set<EntityPlayer> trackedPlayers = Sets.newHashSet();
  
          public EntityTracker(Entity entity, int i, int j, boolean flag) {
@@ -154,7 +115,7 @@
              this.tracker = entity;
              this.trackingDistance = i;
              this.e = SectionPosition.a(entity);
-@@ -1053,6 +1076,17 @@
+@@ -1053,6 +1057,17 @@
                          }
                      }
  
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
index 404f51e140..f4dfbf534d 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -93,6 +93,9 @@ public class CraftChunk implements Chunk {
 
     @Override
     public Entity[] getEntities() {
+        if (!isLoaded()) {
+            getWorld().getChunkAt(x, z); // Transient load for this tick
+        }
         int count = 0, index = 0;
         net.minecraft.server.Chunk chunk = getHandle();
 
@@ -118,6 +121,9 @@ public class CraftChunk implements Chunk {
 
     @Override
     public BlockState[] getTileEntities() {
+        if (!isLoaded()) {
+            getWorld().getChunkAt(x, z); // Transient load for this tick
+        }
         int index = 0;
         net.minecraft.server.Chunk chunk = getHandle();
 
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 950fe8b99f..8b95301c3e 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -330,7 +330,7 @@ public class CraftWorld implements World {
     @Override
     public boolean isChunkLoaded(int x, int z) {
         net.minecraft.server.Chunk chunk = world.getChunkProvider().getChunkAt(x, z, false);
-        return chunk != null && chunk.loaded;
+        return chunk != null;
     }
 
     @Override
@@ -345,8 +345,7 @@ public class CraftWorld implements World {
     @Override
     public Chunk[] getLoadedChunks() {
         Long2ObjectLinkedOpenHashMap<PlayerChunk> chunks = world.getChunkProvider().playerChunkMap.visibleChunks;
-
-        return chunks.values().stream().map(PlayerChunk::getChunk).filter(Objects::nonNull).filter((chunk) -> chunk.loaded).map(net.minecraft.server.Chunk::getBukkitChunk).toArray(Chunk[]::new);
+        return chunks.values().stream().map(PlayerChunk::getFullChunk).filter(Objects::nonNull).map(net.minecraft.server.Chunk::getBukkitChunk).toArray(Chunk[]::new);
     }
 
     @Override
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
index ba50e98462..be8b862d9a 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -530,7 +530,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
 
     @Override
     public boolean isValid() {
-        return entity.isAlive() && entity.valid;
+        return entity.isAlive() && entity.valid && entity.isChunkLoaded();
     }
 
     @Override