--- a/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/net/minecraft/world/level/chunk/LevelChunk.java
@@ -1,11 +1,16 @@
 package net.minecraft.world.level.chunk;
 
 import com.google.common.collect.ImmutableList;
+import com.destroystokyo.paper.exception.ServerInternalException;
 import com.google.common.collect.Maps;
+import com.google.common.collect.UnmodifiableIterator;
 import com.mojang.logging.LogUtils;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortListIterator;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
@@ -21,6 +26,7 @@
 import net.minecraft.network.FriendlyByteBuf;
 import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
 import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ServerChunkCache;
 import net.minecraft.server.level.ServerLevel;
 import net.minecraft.util.profiling.ProfilerFiller;
 import net.minecraft.world.entity.Entity;
@@ -49,11 +55,11 @@
 import org.slf4j.Logger;
 
 public class LevelChunk extends ChunkAccess {
+
     static final Logger LOGGER = LogUtils.getLogger();
     private static final TickingBlockEntity NULL_TICKER = new TickingBlockEntity() {
         @Override
-        public void tick() {
-        }
+        public void tick() {}
 
         @Override
         public boolean isRemoved() {
@@ -70,10 +76,10 @@
             return "<null>";
         }
     };
-    private final Map<BlockPos, LevelChunk.RebindableTickingBlockEntityWrapper> tickersInLevel = Maps.newHashMap();
+    private final Map<BlockPos, LevelChunk.RebindableTickingBlockEntityWrapper> tickersInLevel;
     public boolean loaded;
-    private boolean clientLightReady = false;
-    public final Level level;
+    private boolean clientLightReady;
+    public final ServerLevel level; // CraftBukkit - type
     @Nullable
     private Supplier<ChunkHolder.FullChunkStatus> fullStatus;
     @Nullable
@@ -81,51 +87,328 @@
     private final Int2ObjectMap<GameEventDispatcher> gameEventDispatcherSections;
     private final LevelChunkTicks<Block> blockTicks;
     private final LevelChunkTicks<Fluid> fluidTicks;
+    // Paper start - track last save time
+    public long lastSaveTime;
+    public void setLastSaved(long ticks) {
+        this.lastSaveTime = ticks;
+    }
+    // Paper end
 
     public LevelChunk(Level world, ChunkPos pos) {
-        this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[])null, (LevelChunk.PostLoadProcessor)null, (BlendingData)null);
+        this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null);
     }
 
     public LevelChunk(Level world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks<Block> blockTickScheduler, LevelChunkTicks<Fluid> fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) {
-        super(pos, upgradeData, world, world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), inhabitedTime, sectionArrayInitializer, blendingData);
-        this.level = world;
-        this.gameEventDispatcherSections = new Int2ObjectOpenHashMap<>();
+        super(pos, upgradeData, world, net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), inhabitedTime, sectionArrayInitializer, blendingData); // Paper - Anti-Xray - The world isn't ready yet, use server singleton for registry
+        // Paper start - rewrite light engine
+        this.setBlockNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world));
+        this.setSkyNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world));
+        // Paper end - rewrite light engine
+        this.tickersInLevel = Maps.newHashMap();
+        this.clientLightReady = false;
+        this.level = (ServerLevel) world; // CraftBukkit - type
+        this.gameEventDispatcherSections = new Int2ObjectOpenHashMap();
+        Heightmap.Types[] aheightmap_type = Heightmap.Types.values();
+        int j = aheightmap_type.length;
 
-        for(Heightmap.Types types : Heightmap.Types.values()) {
-            if (ChunkStatus.FULL.heightmapsAfter().contains(types)) {
-                this.heightmaps.put(types, new Heightmap(this, types));
+        for (int k = 0; k < j; ++k) {
+            Heightmap.Types heightmap_type = aheightmap_type[k];
+
+            if (ChunkStatus.FULL.heightmapsAfter().contains(heightmap_type)) {
+                this.heightmaps.put(heightmap_type, new Heightmap(this, heightmap_type));
             }
         }
 
         this.postLoad = entityLoader;
         this.blockTicks = blockTickScheduler;
         this.fluidTicks = fluidTickScheduler;
+        // CraftBukkit start
+        this.bukkitChunk = new org.bukkit.craftbukkit.v1_19_R1.CraftChunk(this);
+    }
+
+    public org.bukkit.Chunk bukkitChunk;
+    public org.bukkit.Chunk getBukkitChunk() {
+        return this.bukkitChunk;
+    }
+
+    public boolean mustNotSave;
+    public boolean needsDecoration;
+
+    // CraftBukkit end
+
+    // Paper start
+    public final com.destroystokyo.paper.util.maplist.EntityList entities = new com.destroystokyo.paper.util.maplist.EntityList();
+    public @Nullable ChunkHolder playerChunk;
+
+    static final int NEIGHBOUR_CACHE_RADIUS = 3;
+    public static int getNeighbourCacheRadius() {
+        return NEIGHBOUR_CACHE_RADIUS;
+    }
+
+    boolean loadedTicketLevel; public final boolean wasLoadCallbackInvoked() { return this.loadedTicketLevel; } // Paper - public accessor
+    private long neighbourChunksLoadedBitset;
+    private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)];
+
+    private static int getNeighbourIndex(final int relativeX, final int relativeZ) {
+        // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)
+        // optimised variant of the above by moving some of the ops to compile time
+        return relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1)));
+    }
+
+    public final LevelChunk getRelativeNeighbourIfLoaded(final int relativeX, final int relativeZ) {
+        return this.loadedNeighbourChunks[getNeighbourIndex(relativeX, relativeZ)];
+    }
+
+    public final boolean isNeighbourLoaded(final int relativeX, final int relativeZ) {
+        return (this.neighbourChunksLoadedBitset & (1L << getNeighbourIndex(relativeX, relativeZ))) != 0;
+    }
+
+    public final void setNeighbourLoaded(final int relativeX, final int relativeZ, final LevelChunk chunk) {
+        if (chunk == null) {
+            throw new IllegalArgumentException("Chunk must be non-null, neighbour: (" + relativeX + "," + relativeZ + "), chunk: " + this.chunkPos);
+        }
+        final long before = this.neighbourChunksLoadedBitset;
+        final int index = getNeighbourIndex(relativeX, relativeZ);
+        this.loadedNeighbourChunks[index] = chunk;
+        this.neighbourChunksLoadedBitset |= (1L << index);
+        this.onNeighbourChange(before, this.neighbourChunksLoadedBitset);
+    }
+
+    public final void setNeighbourUnloaded(final int relativeX, final int relativeZ) {
+        final long before = this.neighbourChunksLoadedBitset;
+        final int index = getNeighbourIndex(relativeX, relativeZ);
+        this.loadedNeighbourChunks[index] = null;
+        this.neighbourChunksLoadedBitset &= ~(1L << index);
+        this.onNeighbourChange(before, this.neighbourChunksLoadedBitset);
+    }
+
+    public final void resetNeighbours() {
+        final long before = this.neighbourChunksLoadedBitset;
+        this.neighbourChunksLoadedBitset = 0L;
+        java.util.Arrays.fill(this.loadedNeighbourChunks, null);
+        this.onNeighbourChange(before, 0L);
+    }
+
+    protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) {
+
+        // Paper start - no-tick view distance
+        ServerChunkCache chunkProviderServer = ((ServerLevel)this.level).getChunkSource();
+        net.minecraft.server.level.ChunkMap chunkMap = chunkProviderServer.chunkMap;
+        // this code handles the addition of ticking tickets - the distance map handles the removal
+        if (!areNeighboursLoaded(bitsetBefore, 2) && areNeighboursLoaded(bitsetAfter, 2)) {
+            if (chunkMap.playerChunkManager.tickMap.getObjectsInRange(this.coordinateKey) != null) { // Paper - replace old player chunk loading system
+                // now we're ready for entity ticking
+                chunkProviderServer.mainThreadProcessor.execute(() -> {
+                    // double check that this condition still holds.
+                    if (LevelChunk.this.areNeighboursLoaded(2) && chunkMap.playerChunkManager.tickMap.getObjectsInRange(LevelChunk.this.coordinateKey) != null) { // Paper - replace old player chunk loading system
+                        chunkMap.playerChunkManager.onChunkPlayerTickReady(this.chunkPos.x, this.chunkPos.z); // Paper - replace old player chunk
+                        chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.PLAYER, LevelChunk.this.chunkPos, 31, LevelChunk.this.chunkPos); // 31 -> entity ticking, TODO check on update
+                    }
+                });
+            }
+        }
+
+        // this code handles the chunk sending
+        if (!areNeighboursLoaded(bitsetBefore, 1) && areNeighboursLoaded(bitsetAfter, 1)) {
+            // Paper start - replace old player chunk loading system
+            if (chunkMap.playerChunkManager.isChunkNearPlayers(this.chunkPos.x, this.chunkPos.z)) {
+                // the post processing is expensive, so we don't want to run it unless we're actually near
+                // a player.
+                chunkProviderServer.mainThreadProcessor.execute(() -> {
+                    if (!LevelChunk.this.areNeighboursLoaded(1)) {
+                        return;
+                    }
+                    LevelChunk.this.postProcessGeneration();
+                    if (!LevelChunk.this.areNeighboursLoaded(1)) {
+                        return;
+                    }
+                    chunkMap.playerChunkManager.onChunkSendReady(this.chunkPos.x, this.chunkPos.z);
+                });
+            }
+            // Paper end - replace old player chunk loading system
+        }
+        // Paper end - no-tick view distance
+    }
+
+    public final boolean isAnyNeighborsLoaded() {
+        return neighbourChunksLoadedBitset != 0;
+    }
+    public final boolean areNeighboursLoaded(final int radius) {
+        return LevelChunk.areNeighboursLoaded(this.neighbourChunksLoadedBitset, radius);
+    }
+
+    public static boolean areNeighboursLoaded(final long bitset, final int radius) {
+        // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1)))
+        switch (radius) {
+            case 0: {
+                return (bitset & (1L << getNeighbourIndex(0, 0))) != 0;
+            }
+            case 1: {
+                long mask = 0L;
+                for (int dx = -1; dx <= 1; ++dx) {
+                    for (int dz = -1; dz <= 1; ++dz) {
+                        mask |= (1L << getNeighbourIndex(dx, dz));
+                    }
+                }
+                return (bitset & mask) == mask;
+            }
+            case 2: {
+                long mask = 0L;
+                for (int dx = -2; dx <= 2; ++dx) {
+                    for (int dz = -2; dz <= 2; ++dz) {
+                        mask |= (1L << getNeighbourIndex(dx, dz));
+                    }
+                }
+                return (bitset & mask) == mask;
+            }
+            case 3: {
+                long mask = 0L;
+                for (int dx = -3; dx <= 3; ++dx) {
+                    for (int dz = -3; dz <= 3; ++dz) {
+                        mask |= (1L << getNeighbourIndex(dx, dz));
+                    }
+                }
+                return (bitset & mask) == mask;
+            }
+
+            default:
+                throw new IllegalArgumentException("Radius not recognized: " + radius);
+        }
     }
+    // Paper end
+    // Paper start - optimise checkDespawn
+    private boolean playerGeneralAreaCacheSet;
+    private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> playerGeneralAreaCache;
+
+    public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> getPlayerGeneralAreaCache() {
+        if (!this.playerGeneralAreaCacheSet) {
+            this.updateGeneralAreaCache();
+        }
+        return this.playerGeneralAreaCache;
+    }
+
+    public void updateGeneralAreaCache() {
+        this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey));
+    }
+
+    public void removeGeneralAreaCache() {
+        this.playerGeneralAreaCacheSet = false;
+        this.playerGeneralAreaCache = null;
+    }
+
+    public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> value) {
+        this.playerGeneralAreaCacheSet = true;
+        this.playerGeneralAreaCache = value;
+    }
+
+    public net.minecraft.server.level.ServerPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ,
+                                                                     double maxRange, java.util.function.Predicate<Entity> predicate) {
+        if (!this.playerGeneralAreaCacheSet) {
+            this.updateGeneralAreaCache();
+        }
+
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> nearby = this.playerGeneralAreaCache;
+
+        if (nearby == null) {
+            return null;
+        }
+
+        Object[] backingSet = nearby.getBackingSet();
+        double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange;
+        net.minecraft.server.level.ServerPlayer closest = null;
+        for (int i = 0, len = backingSet.length; i < len; ++i) {
+            Object _player = backingSet[i];
+            if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) {
+                continue;
+            }
+            net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player;
+
+            double distance = player.distanceToSqr(sourceX, sourceY, sourceZ);
+            if (distance < closestDistance && predicate.test(player)) {
+                closest = player;
+                closestDistance = distance;
+            }
+        }
+
+        return closest;
+    }
+
+    public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, java.util.function.Predicate<Entity> predicate,
+                                  double range, java.util.List<net.minecraft.server.level.ServerPlayer> ret) {
+        if (!this.playerGeneralAreaCacheSet) {
+            this.updateGeneralAreaCache();
+        }
+
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> nearby = this.playerGeneralAreaCache;
+
+        if (nearby == null) {
+            return;
+        }
+
+        double rangeSquared = range * range;
+
+        Object[] backingSet = nearby.getBackingSet();
+        for (int i = 0, len = backingSet.length; i < len; ++i) {
+            Object _player = backingSet[i];
+            if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) {
+                continue;
+            }
+            net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player;
+
+            if (range >= 0.0) {
+                double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ);
+                if (distanceSquared > rangeSquared) {
+                    continue;
+                }
+            }
+
+            if (predicate == null || predicate.test(player)) {
+                ret.add(player);
+            }
+        }
+    }
+    // Paper end - optimise checkDespawn
 
     public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
         this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
+        // Paper start - rewrite light engine
+        this.setBlockNibbles(protoChunk.getBlockNibbles());
+        this.setSkyNibbles(protoChunk.getSkyNibbles());
+        this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap());
+        this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap());
+        // Paper end - rewrite light engine
+        Iterator iterator = protoChunk.getBlockEntities().values().iterator();
 
-        for(BlockEntity blockEntity : protoChunk.getBlockEntities().values()) {
-            this.setBlockEntity(blockEntity);
+        while (iterator.hasNext()) {
+            BlockEntity tileentity = (BlockEntity) iterator.next();
+
+            this.setBlockEntity(tileentity);
         }
 
         this.pendingBlockEntities.putAll(protoChunk.getBlockEntityNbts());
 
-        for(int i = 0; i < protoChunk.getPostProcessing().length; ++i) {
+        for (int i = 0; i < protoChunk.getPostProcessing().length; ++i) {
             this.postProcessing[i] = protoChunk.getPostProcessing()[i];
         }
 
         this.setAllStarts(protoChunk.getAllStarts());
         this.setAllReferences(protoChunk.getAllReferences());
+        iterator = protoChunk.getHeightmaps().iterator();
+
+        while (iterator.hasNext()) {
+            Entry<Heightmap.Types, Heightmap> entry = (Entry) iterator.next();
 
-        for(Map.Entry<Heightmap.Types, Heightmap> entry : protoChunk.getHeightmaps()) {
             if (ChunkStatus.FULL.heightmapsAfter().contains(entry.getKey())) {
-                this.setHeightmap(entry.getKey(), entry.getValue().getRawData());
+                this.setHeightmap((Heightmap.Types) entry.getKey(), ((Heightmap) entry.getValue()).getRawData());
             }
         }
 
         this.setLightCorrect(protoChunk.isLightCorrect());
         this.unsaved = true;
+        this.needsDecoration = true; // CraftBukkit
+        // CraftBukkit start
+        this.persistentDataContainer = protoChunk.persistentDataContainer; // SPIGOT-6814: copy PDC to account for 1.17 to 1.18 chunk upgrading.
+        // CraftBukkit end
     }
 
     @Override
@@ -143,137 +426,201 @@
         return new ChunkAccess.TicksToSave(this.blockTicks, this.fluidTicks);
     }
 
+    // Paper start
+    @Override
+    public long getInhabitedTime() {
+        return this.level.paperConfig().chunks.fixedChunkInhabitedTime < 0 ? super.getInhabitedTime() : this.level.paperConfig().chunks.fixedChunkInhabitedTime;
+    }
+    // Paper end
+
     @Override
     public GameEventDispatcher getEventDispatcher(int ySectionCoord) {
-        Level var3 = this.level;
-        if (var3 instanceof ServerLevel serverLevel) {
-            return this.gameEventDispatcherSections.computeIfAbsent(ySectionCoord, (sectionCoord) -> {
-                return new EuclideanGameEventDispatcher(serverLevel);
+        Level world = this.level;
+
+        if (world instanceof ServerLevel) {
+            ServerLevel worldserver = (ServerLevel) world;
+
+            return (GameEventDispatcher) this.gameEventDispatcherSections.computeIfAbsent(ySectionCoord, (j) -> {
+                return new EuclideanGameEventDispatcher(worldserver);
             });
         } else {
             return super.getEventDispatcher(ySectionCoord);
         }
     }
 
+    // Paper start - Optimize getBlockData to reduce instructions
     @Override
     public BlockState getBlockState(BlockPos pos) {
-        int i = pos.getX();
-        int j = pos.getY();
-        int k = pos.getZ();
+        return this.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ());
+    }
+
+    @Override
+    public BlockState getBlockState(final int x, final int y, final int z) {
+        return this.getBlockStateFinal(x, y, z);
+    }
+    public final BlockState getBlockStateFinal(final int x, final int y, final int z) {
+        // Method body / logic copied from below
+        final int i = this.getSectionIndex(y);
+        if (i < 0 || i >= this.sections.length || this.sections[i].nonEmptyBlockCount == 0 || this.sections[i].hasOnlyAir()) {
+            return Blocks.AIR.defaultBlockState();
+        }
+        // Inlined ChunkSection.getType() and DataPaletteBlock.a(int,int,int)
+        return this.sections[i].states.get((y & 15) << 8 | (z & 15) << 4 | x & 15);
+
+    }
+
+    public BlockState getBlockState_unused(int i, int j, int k) {
+        // Paper end
         if (this.level.isDebug()) {
-            BlockState blockState = null;
+            BlockState iblockdata = null;
+
             if (j == 60) {
-                blockState = Blocks.BARRIER.defaultBlockState();
+                iblockdata = Blocks.BARRIER.defaultBlockState();
             }
 
             if (j == 70) {
-                blockState = DebugLevelSource.getBlockStateFor(i, k);
+                iblockdata = DebugLevelSource.getBlockStateFor(i, k);
             }
 
-            return blockState == null ? Blocks.AIR.defaultBlockState() : blockState;
+            return iblockdata == null ? Blocks.AIR.defaultBlockState() : iblockdata;
         } else {
             try {
                 int l = this.getSectionIndex(j);
+
                 if (l >= 0 && l < this.sections.length) {
-                    LevelChunkSection levelChunkSection = this.sections[l];
-                    if (!levelChunkSection.hasOnlyAir()) {
-                        return levelChunkSection.getBlockState(i & 15, j & 15, k & 15);
+                    LevelChunkSection chunksection = this.sections[l];
+
+                    if (!chunksection.hasOnlyAir()) {
+                        return chunksection.getBlockState(i & 15, j & 15, k & 15);
                     }
                 }
 
                 return Blocks.AIR.defaultBlockState();
-            } catch (Throwable var8) {
-                CrashReport crashReport = CrashReport.forThrowable(var8, "Getting block state");
-                CrashReportCategory crashReportCategory = crashReport.addCategory("Block being got");
-                crashReportCategory.setDetail("Location", () -> {
+            } catch (Throwable throwable) {
+                CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting block state");
+                CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block being got");
+
+                crashreportsystemdetails.setDetail("Location", () -> {
                     return CrashReportCategory.formatLocation(this, i, j, k);
                 });
-                throw new ReportedException(crashReport);
+                throw new ReportedException(crashreport);
             }
         }
     }
 
+    // Paper start - If loaded util
+    @Override
+    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+        return this.getFluidState(blockposition);
+    }
+
+    @Override
+    public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+        return this.getBlockState(blockposition);
+    }
+    // Paper end
+
     @Override
     public FluidState getFluidState(BlockPos pos) {
         return this.getFluidState(pos.getX(), pos.getY(), pos.getZ());
     }
 
     public FluidState getFluidState(int x, int y, int z) {
-        try {
-            int i = this.getSectionIndex(y);
-            if (i >= 0 && i < this.sections.length) {
-                LevelChunkSection levelChunkSection = this.sections[i];
-                if (!levelChunkSection.hasOnlyAir()) {
-                    return levelChunkSection.getFluidState(x & 15, y & 15, z & 15);
+        // try {  // Paper - remove try catch
+        // Paper start - reduce the number of ops in this call
+        int index = this.getSectionIndex(y);
+            if (index >= 0 && index < this.sections.length) {
+                LevelChunkSection chunksection = this.sections[index];
+
+                if (!chunksection.hasOnlyAir()) {
+                    return chunksection.states.get((y & 15) << 8 | (z & 15) << 4 | x & 15).getFluidState();
+                    // Paper end
                 }
             }
 
             return Fluids.EMPTY.defaultFluidState();
-        } catch (Throwable var7) {
-            CrashReport crashReport = CrashReport.forThrowable(var7, "Getting fluid state");
-            CrashReportCategory crashReportCategory = crashReport.addCategory("Block being got");
-            crashReportCategory.setDetail("Location", () -> {
+        /* // Paper - remove try catch
+        } catch (Throwable throwable) {
+            CrashReport crashreport = CrashReport.forThrowable(throwable, "Getting fluid state");
+            CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Block being got");
+
+            crashreportsystemdetails.setDetail("Location", () -> {
                 return CrashReportCategory.formatLocation(this, x, y, z);
             });
-            throw new ReportedException(crashReport);
+            throw new ReportedException(crashreport);
         }
+         */  // Paper - remove try catch
     }
 
+    // CraftBukkit start
     @Nullable
     @Override
     public BlockState setBlockState(BlockPos pos, BlockState state, boolean moved) {
-        int i = pos.getY();
-        LevelChunkSection levelChunkSection = this.getSection(this.getSectionIndex(i));
-        boolean bl = levelChunkSection.hasOnlyAir();
-        if (bl && state.isAir()) {
+        return this.setBlockState(pos, state, moved, true);
+    }
+
+    @Nullable
+    public BlockState setBlockState(BlockPos blockposition, BlockState iblockdata, boolean flag, boolean doPlace) {
+        // CraftBukkit end
+        int i = blockposition.getY();
+        LevelChunkSection chunksection = this.getSection(this.getSectionIndex(i));
+        boolean flag1 = chunksection.hasOnlyAir();
+
+        if (flag1 && iblockdata.isAir()) {
             return null;
         } else {
-            int j = pos.getX() & 15;
+            int j = blockposition.getX() & 15;
             int k = i & 15;
-            int l = pos.getZ() & 15;
-            BlockState blockState = levelChunkSection.setBlockState(j, k, l, state);
-            if (blockState == state) {
+            int l = blockposition.getZ() & 15;
+            BlockState iblockdata1 = chunksection.setBlockState(j, k, l, iblockdata);
+
+            if (iblockdata1 == iblockdata) {
                 return null;
             } else {
-                Block block = state.getBlock();
-                this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING).update(j, i, l, state);
-                this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES).update(j, i, l, state);
-                this.heightmaps.get(Heightmap.Types.OCEAN_FLOOR).update(j, i, l, state);
-                this.heightmaps.get(Heightmap.Types.WORLD_SURFACE).update(j, i, l, state);
-                boolean bl2 = levelChunkSection.hasOnlyAir();
-                if (bl != bl2) {
-                    this.level.getChunkSource().getLightEngine().updateSectionStatus(pos, bl2);
+                Block block = iblockdata.getBlock();
+
+                ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING)).update(j, i, l, iblockdata);
+                ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES)).update(j, i, l, iblockdata);
+                ((Heightmap) this.heightmaps.get(Heightmap.Types.OCEAN_FLOOR)).update(j, i, l, iblockdata);
+                ((Heightmap) this.heightmaps.get(Heightmap.Types.WORLD_SURFACE)).update(j, i, l, iblockdata);
+                boolean flag2 = chunksection.hasOnlyAir();
+
+                if (flag1 != flag2) {
+                    this.level.getChunkSource().getLightEngine().updateSectionStatus(blockposition, flag2);
                 }
 
-                boolean bl3 = blockState.hasBlockEntity();
+                boolean flag3 = iblockdata1.hasBlockEntity();
+
                 if (!this.level.isClientSide) {
-                    blockState.onRemove(this.level, pos, state, moved);
-                } else if (!blockState.is(block) && bl3) {
-                    this.removeBlockEntity(pos);
+                    iblockdata1.onRemove(this.level, blockposition, iblockdata, flag);
+                } else if (!iblockdata1.is(block) && flag3) {
+                    this.removeBlockEntity(blockposition);
                 }
 
-                if (!levelChunkSection.getBlockState(j, k, l).is(block)) {
+                if (!chunksection.getBlockState(j, k, l).is(block)) {
                     return null;
                 } else {
-                    if (!this.level.isClientSide) {
-                        state.onPlace(this.level, pos, blockState, moved);
+                    // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled.
+                    if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) {
+                        iblockdata.onPlace(this.level, blockposition, iblockdata1, flag);
                     }
 
-                    if (state.hasBlockEntity()) {
-                        BlockEntity blockEntity = this.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK);
-                        if (blockEntity == null) {
-                            blockEntity = ((EntityBlock)block).newBlockEntity(pos, state);
-                            if (blockEntity != null) {
-                                this.addAndRegisterBlockEntity(blockEntity);
+                    if (iblockdata.hasBlockEntity()) {
+                        BlockEntity tileentity = this.getBlockEntity(blockposition, LevelChunk.EntityCreationType.CHECK);
+
+                        if (tileentity == null) {
+                            tileentity = ((EntityBlock) block).newBlockEntity(blockposition, iblockdata);
+                            if (tileentity != null) {
+                                this.addAndRegisterBlockEntity(tileentity);
                             }
                         } else {
-                            blockEntity.setBlockState(state);
-                            this.updateBlockEntityTicker(blockEntity);
+                            tileentity.setBlockState(iblockdata);
+                            this.updateBlockEntityTicker(tileentity);
                         }
                     }
 
                     this.unsaved = true;
-                    return blockState;
+                    return iblockdata1;
                 }
             }
         }
@@ -282,13 +629,13 @@
     /** @deprecated */
     @Deprecated
     @Override
-    public void addEntity(Entity entity) {
-    }
+    public void addEntity(Entity entity) {}
 
     @Nullable
     private BlockEntity createBlockEntity(BlockPos pos) {
-        BlockState blockState = this.getBlockState(pos);
-        return !blockState.hasBlockEntity() ? null : ((EntityBlock)blockState.getBlock()).newBlockEntity(pos, blockState);
+        BlockState iblockdata = this.getBlockState(pos);
+
+        return !iblockdata.hasBlockEntity() ? null : ((EntityBlock) iblockdata.getBlock()).newBlockEntity(pos, iblockdata);
     }
 
     @Nullable
@@ -297,44 +644,55 @@
         return this.getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK);
     }
 
+    @Deprecated @Nullable public final BlockEntity getTileEntityImmediately(BlockPos pos) { return this.getBlockEntity(pos, EntityCreationType.IMMEDIATE); } // Paper - OBFHELPER
     @Nullable
     public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) {
-        BlockEntity blockEntity = this.blockEntities.get(pos);
-        if (blockEntity == null) {
-            CompoundTag compoundTag = this.pendingBlockEntities.remove(pos);
-            if (compoundTag != null) {
-                BlockEntity blockEntity2 = this.promotePendingBlockEntity(pos, compoundTag);
-                if (blockEntity2 != null) {
-                    return blockEntity2;
+        // CraftBukkit start
+        BlockEntity tileentity = level.capturedTileEntities.get(pos);
+        if (tileentity == null) {
+            tileentity = (BlockEntity) this.blockEntities.get(pos);
+        }
+        // CraftBukkit end
+
+        if (tileentity == null) {
+            CompoundTag nbttagcompound = (CompoundTag) this.pendingBlockEntities.remove(pos);
+
+            if (nbttagcompound != null) {
+                BlockEntity tileentity1 = this.promotePendingBlockEntity(pos, nbttagcompound);
+
+                if (tileentity1 != null) {
+                    return tileentity1;
                 }
             }
         }
 
-        if (blockEntity == null) {
+        if (tileentity == null) {
             if (creationType == LevelChunk.EntityCreationType.IMMEDIATE) {
-                blockEntity = this.createBlockEntity(pos);
-                if (blockEntity != null) {
-                    this.addAndRegisterBlockEntity(blockEntity);
+                tileentity = this.createBlockEntity(pos);
+                if (tileentity != null) {
+                    this.addAndRegisterBlockEntity(tileentity);
                 }
             }
-        } else if (blockEntity.isRemoved()) {
+        } else if (tileentity.isRemoved()) {
             this.blockEntities.remove(pos);
             return null;
         }
 
-        return blockEntity;
+        return tileentity;
     }
 
-    public void addAndRegisterBlockEntity(BlockEntity blockEntity) {
-        this.setBlockEntity(blockEntity);
+    public void addAndRegisterBlockEntity(BlockEntity tileentity) {
+        this.setBlockEntity(tileentity);
         if (this.isInLevel()) {
-            Level var3 = this.level;
-            if (var3 instanceof ServerLevel) {
-                ServerLevel serverLevel = (ServerLevel)var3;
-                this.addGameEventListener(blockEntity, serverLevel);
+            Level world = this.level;
+
+            if (world instanceof ServerLevel) {
+                ServerLevel worldserver = (ServerLevel) world;
+
+                this.addGameEventListener(tileentity, worldserver);
             }
 
-            this.updateBlockEntityTicker(blockEntity);
+            this.updateBlockEntityTicker(tileentity);
         }
 
     }
@@ -347,76 +705,107 @@
         if (!this.level.getWorldBorder().isWithinBounds(pos)) {
             return false;
         } else {
-            Level var3 = this.level;
-            if (!(var3 instanceof ServerLevel)) {
+            Level world = this.level;
+
+            if (!(world instanceof ServerLevel)) {
                 return true;
             } else {
-                ServerLevel serverLevel = (ServerLevel)var3;
-                return this.getFullStatus().isOrAfter(ChunkHolder.FullChunkStatus.TICKING) && serverLevel.areEntitiesLoaded(ChunkPos.asLong(pos));
+                ServerLevel worldserver = (ServerLevel) world;
+
+                return this.getFullStatus().isOrAfter(ChunkHolder.FullChunkStatus.TICKING) && worldserver.areEntitiesLoaded(ChunkPos.asLong(pos));
             }
         }
     }
 
     @Override
     public void setBlockEntity(BlockEntity blockEntity) {
-        BlockPos blockPos = blockEntity.getBlockPos();
-        if (this.getBlockState(blockPos).hasBlockEntity()) {
+        BlockPos blockposition = blockEntity.getBlockPos();
+
+        if (this.getBlockState(blockposition).hasBlockEntity()) {
             blockEntity.setLevel(this.level);
             blockEntity.clearRemoved();
-            BlockEntity blockEntity2 = this.blockEntities.put(blockPos.immutable(), blockEntity);
-            if (blockEntity2 != null && blockEntity2 != blockEntity) {
-                blockEntity2.setRemoved();
+            BlockEntity tileentity1 = (BlockEntity) this.blockEntities.put(blockposition.immutable(), blockEntity);
+
+            if (tileentity1 != null && tileentity1 != blockEntity) {
+                tileentity1.setRemoved();
             }
 
+            // CraftBukkit start
+        } else {
+            // Paper start
+            ServerInternalException e = new ServerInternalException(
+                "Attempted to place a tile entity (" + blockEntity + ") at " + blockEntity.getBlockPos().getX() + ","
+                    + blockEntity.getBlockPos().getY() + "," + blockEntity.getBlockPos().getZ()
+                    + " (" + getBlockState(blockposition) + ") where there was no entity tile!\n" +
+                    "Chunk coordinates: " + (this.chunkPos.x * 16) + "," + (this.chunkPos.z * 16) +
+                    "\nWorld: " + level.getLevel().dimension().location());
+            e.printStackTrace();
+            ServerInternalException.reportInternalException(e);
+            // Paper end
+            // CraftBukkit end
         }
     }
 
     @Nullable
     @Override
     public CompoundTag getBlockEntityNbtForSaving(BlockPos pos) {
-        BlockEntity blockEntity = this.getBlockEntity(pos);
-        if (blockEntity != null && !blockEntity.isRemoved()) {
-            CompoundTag compoundTag = blockEntity.saveWithFullMetadata();
-            compoundTag.putBoolean("keepPacked", false);
-            return compoundTag;
+        BlockEntity tileentity = this.getBlockEntity(pos);
+        CompoundTag nbttagcompound;
+
+        if (tileentity != null && !tileentity.isRemoved()) {
+            nbttagcompound = tileentity.saveWithFullMetadata();
+            nbttagcompound.putBoolean("keepPacked", false);
+            return nbttagcompound;
         } else {
-            CompoundTag compoundTag2 = this.pendingBlockEntities.get(pos);
-            if (compoundTag2 != null) {
-                compoundTag2 = compoundTag2.copy();
-                compoundTag2.putBoolean("keepPacked", true);
+            nbttagcompound = (CompoundTag) this.pendingBlockEntities.get(pos);
+            if (nbttagcompound != null) {
+                nbttagcompound = nbttagcompound.copy();
+                nbttagcompound.putBoolean("keepPacked", true);
             }
 
-            return compoundTag2;
+            return nbttagcompound;
         }
     }
 
     @Override
     public void removeBlockEntity(BlockPos pos) {
         if (this.isInLevel()) {
-            BlockEntity blockEntity = this.blockEntities.remove(pos);
-            if (blockEntity != null) {
-                Level var4 = this.level;
-                if (var4 instanceof ServerLevel) {
-                    ServerLevel serverLevel = (ServerLevel)var4;
-                    this.removeGameEventListener(blockEntity, serverLevel);
+            BlockEntity tileentity = (BlockEntity) this.blockEntities.remove(pos);
+
+            // CraftBukkit start - SPIGOT-5561: Also remove from pending map
+            if (!pendingBlockEntities.isEmpty()) {
+                pendingBlockEntities.remove(pos);
+            }
+            // CraftBukkit end
+
+            if (tileentity != null) {
+                Level world = this.level;
+
+                if (world instanceof ServerLevel) {
+                    ServerLevel worldserver = (ServerLevel) world;
+
+                    this.removeGameEventListener(tileentity, worldserver);
                 }
 
-                blockEntity.setRemoved();
+                tileentity.setRemoved();
             }
         }
 
         this.removeBlockEntityTicker(pos);
     }
 
-    private <T extends BlockEntity> void removeGameEventListener(T blockEntity, ServerLevel serverLevel) {
+    private <T extends BlockEntity> void removeGameEventListener(T blockEntity, ServerLevel worldserver) {
         Block block = blockEntity.getBlockState().getBlock();
+
         if (block instanceof EntityBlock) {
-            GameEventListener gameEventListener = ((EntityBlock)block).getListener(serverLevel, blockEntity);
-            if (gameEventListener != null) {
+            GameEventListener gameeventlistener = ((EntityBlock) block).getListener(worldserver, blockEntity);
+
+            if (gameeventlistener != null) {
                 int i = SectionPos.blockToSectionCoord(blockEntity.getBlockPos().getY());
-                GameEventDispatcher gameEventDispatcher = this.getEventDispatcher(i);
-                gameEventDispatcher.unregister(gameEventListener);
-                if (gameEventDispatcher.isEmpty()) {
+                GameEventDispatcher gameeventdispatcher = this.getEventDispatcher(i);
+
+                gameeventdispatcher.unregister(gameeventlistener);
+                if (gameeventdispatcher.isEmpty()) {
                     this.gameEventDispatcherSections.remove(i);
                 }
             }
@@ -425,9 +814,10 @@
     }
 
     private void removeBlockEntityTicker(BlockPos pos) {
-        LevelChunk.RebindableTickingBlockEntityWrapper rebindableTickingBlockEntityWrapper = this.tickersInLevel.remove(pos);
-        if (rebindableTickingBlockEntityWrapper != null) {
-            rebindableTickingBlockEntityWrapper.rebind(NULL_TICKER);
+        LevelChunk.RebindableTickingBlockEntityWrapper chunk_d = (LevelChunk.RebindableTickingBlockEntityWrapper) this.tickersInLevel.remove(pos);
+
+        if (chunk_d != null) {
+            chunk_d.rebind(LevelChunk.NULL_TICKER);
         }
 
     }
@@ -440,28 +830,129 @@
 
     }
 
+    // CraftBukkit start
+    public void loadCallback() {
+        if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper
+        // Paper start - neighbour cache
+        int chunkX = this.chunkPos.x;
+        int chunkZ = this.chunkPos.z;
+        ServerChunkCache chunkProvider = this.level.getChunkSource();
+        for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) {
+            for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) {
+                LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz);
+                if (neighbour != null) {
+                    neighbour.setNeighbourLoaded(-dx, -dz, this);
+                    // should be in cached already
+                    this.setNeighbourLoaded(dx, dz, neighbour);
+                }
+            }
+        }
+        this.setNeighbourLoaded(0, 0, this);
+        this.loadedTicketLevel = true;
+        // Paper end - neighbour cache
+        org.bukkit.Server server = this.level.getCraftServer();
+        this.level.getChunkSource().addLoadedChunk(this); // Paper
+        ((ServerLevel)this.level).getChunkSource().chunkMap.playerChunkManager.onChunkLoad(this.chunkPos.x, this.chunkPos.z); // Paper - rewrite player chunk management
+        if (server != null) {
+            /*
+             * If it's a new world, the first few chunks are generated inside
+             * the World constructor. We can't reliably alter that, so we have
+             * no way of creating a CraftWorld/CraftServer at that point.
+             */
+            server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(this.bukkitChunk, this.needsDecoration));
+
+            if (this.needsDecoration) {
+                try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper
+                this.needsDecoration = false;
+                java.util.Random random = new java.util.Random();
+                random.setSeed(this.level.getSeed());
+                long xRand = random.nextLong() / 2L * 2L + 1L;
+                long zRand = random.nextLong() / 2L * 2L + 1L;
+                random.setSeed((long) this.chunkPos.x * xRand + (long) this.chunkPos.z * zRand ^ this.level.getSeed());
+
+                org.bukkit.World world = this.level.getWorld();
+                if (world != null) {
+                    this.level.populating = true;
+                    try {
+                        for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) {
+                            populator.populate(world, random, bukkitChunk);
+                        }
+                    } finally {
+                        this.level.populating = false;
+                    }
+                }
+                server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(this.bukkitChunk));
+                } // Paper
+            }
+        }
+    }
+
+    public void unloadCallback() {
+        if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
+        org.bukkit.Server server = this.level.getCraftServer();
+        org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved());
+        server.getPluginManager().callEvent(unloadEvent);
+        // note: saving can be prevented, but not forced if no saving is actually required
+        this.mustNotSave = !unloadEvent.isSaveChunk();
+        this.level.getChunkSource().removeLoadedChunk(this); // Paper
+        // Paper start - neighbour cache
+        int chunkX = this.chunkPos.x;
+        int chunkZ = this.chunkPos.z;
+        ServerChunkCache chunkProvider = this.level.getChunkSource();
+        for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) {
+            for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) {
+                LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz);
+                if (neighbour != null) {
+                    neighbour.setNeighbourUnloaded(-dx, -dz);
+                }
+            }
+        }
+        this.loadedTicketLevel = false;
+        this.resetNeighbours();
+        // Paper end
+    }
+
+    @Override
+    public boolean isUnsaved() {
+        return super.isUnsaved() && !this.mustNotSave;
+    }
+    // CraftBukkit end
+
     public boolean isEmpty() {
         return false;
     }
 
     public void replaceWithPacketData(FriendlyByteBuf buf, CompoundTag nbt, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> consumer) {
         this.clearAllBlockEntities();
+        LevelChunkSection[] achunksection = this.sections;
+        int i = achunksection.length;
 
-        for(LevelChunkSection levelChunkSection : this.sections) {
-            levelChunkSection.read(buf);
+        int j;
+
+        for (j = 0; j < i; ++j) {
+            LevelChunkSection chunksection = achunksection[j];
+
+            chunksection.read(buf);
         }
 
-        for(Heightmap.Types types : Heightmap.Types.values()) {
-            String string = types.getSerializationKey();
-            if (nbt.contains(string, 12)) {
-                this.setHeightmap(types, nbt.getLongArray(string));
+        Heightmap.Types[] aheightmap_type = Heightmap.Types.values();
+
+        i = aheightmap_type.length;
+
+        for (j = 0; j < i; ++j) {
+            Heightmap.Types heightmap_type = aheightmap_type[j];
+            String s = heightmap_type.getSerializationKey();
+
+            if (nbt.contains(s, 12)) {
+                this.setHeightmap(heightmap_type, nbt.getLongArray(s));
             }
         }
 
-        consumer.accept((pos, blockEntityType, nbtx) -> {
-            BlockEntity blockEntity = this.getBlockEntity(pos, LevelChunk.EntityCreationType.IMMEDIATE);
-            if (blockEntity != null && nbtx != null && blockEntity.getType() == blockEntityType) {
-                blockEntity.load(nbtx);
+        consumer.accept((blockposition, tileentitytypes, nbttagcompound1) -> {
+            BlockEntity tileentity = this.getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE);
+
+            if (tileentity != null && nbttagcompound1 != null && tileentity.getType() == tileentitytypes) {
+                tileentity.load(nbttagcompound1);
             }
 
         });
@@ -481,27 +972,35 @@
 
     @Override
     public Stream<BlockPos> getLights() {
-        return StreamSupport.stream(BlockPos.betweenClosed(this.chunkPos.getMinBlockX(), this.getMinBuildHeight(), this.chunkPos.getMinBlockZ(), this.chunkPos.getMaxBlockX(), this.getMaxBuildHeight() - 1, this.chunkPos.getMaxBlockZ()).spliterator(), false).filter((blockPos) -> {
-            return this.getBlockState(blockPos).getLightEmission() != 0;
+        return StreamSupport.stream(BlockPos.betweenClosed(this.chunkPos.getMinBlockX(), this.getMinBuildHeight(), this.chunkPos.getMinBlockZ(), this.chunkPos.getMaxBlockX(), this.getMaxBuildHeight() - 1, this.chunkPos.getMaxBlockZ()).spliterator(), false).filter((blockposition) -> {
+            return this.getBlockState(blockposition).getLightEmission() != 0;
         });
     }
 
+    public boolean isPostProcessingDone; // Paper - replace chunk loader system
+
     public void postProcessGeneration() {
-        ChunkPos chunkPos = this.getPos();
+        try { // Paper - replace chunk loader system
+        ChunkPos chunkcoordintpair = this.getPos();
 
-        for(int i = 0; i < this.postProcessing.length; ++i) {
+        for (int i = 0; i < this.postProcessing.length; ++i) {
             if (this.postProcessing[i] != null) {
-                for(Short short_ : this.postProcessing[i]) {
-                    BlockPos blockPos = ProtoChunk.unpackOffsetCoordinates(short_, this.getSectionYFromSectionIndex(i), chunkPos);
-                    BlockState blockState = this.getBlockState(blockPos);
-                    FluidState fluidState = blockState.getFluidState();
-                    if (!fluidState.isEmpty()) {
-                        fluidState.tick(this.level, blockPos);
+                ShortListIterator shortlistiterator = this.postProcessing[i].iterator();
+
+                while (shortlistiterator.hasNext()) {
+                    Short oshort = (Short) shortlistiterator.next();
+                    BlockPos blockposition = ProtoChunk.unpackOffsetCoordinates(oshort, this.getSectionYFromSectionIndex(i), chunkcoordintpair);
+                    BlockState iblockdata = this.getBlockState(blockposition);
+                    FluidState fluid = iblockdata.getFluidState();
+
+                    if (!fluid.isEmpty()) {
+                        fluid.tick(this.level, blockposition);
                     }
 
-                    if (!(blockState.getBlock() instanceof LiquidBlock)) {
-                        BlockState blockState2 = Block.updateFromNeighbourShapes(blockState, this.level, blockPos);
-                        this.level.setBlock(blockPos, blockState2, 20);
+                    if (!(iblockdata.getBlock() instanceof LiquidBlock)) {
+                        BlockState iblockdata1 = Block.updateFromNeighbourShapes(iblockdata, this.level, blockposition);
+
+                        this.level.setBlock(blockposition, iblockdata1, 20);
                     }
                 }
 
@@ -509,37 +1008,47 @@
             }
         }
 
-        for(BlockPos blockPos2 : ImmutableList.copyOf(this.pendingBlockEntities.keySet())) {
-            this.getBlockEntity(blockPos2);
+        UnmodifiableIterator unmodifiableiterator = ImmutableList.copyOf(this.pendingBlockEntities.keySet()).iterator();
+
+        while (unmodifiableiterator.hasNext()) {
+            BlockPos blockposition1 = (BlockPos) unmodifiableiterator.next();
+
+            this.getBlockEntity(blockposition1);
         }
 
         this.pendingBlockEntities.clear();
         this.upgradeData.upgrade(this);
+        } finally { // Paper start - replace chunk loader system
+            this.isPostProcessingDone = true;
+            this.level.getChunkSource().chunkMap.playerChunkManager.onChunkPostProcessing(this.chunkPos.x, this.chunkPos.z);
+        }
+        // Paper end - replace chunk loader system
     }
 
     @Nullable
     private BlockEntity promotePendingBlockEntity(BlockPos pos, CompoundTag nbt) {
-        BlockState blockState = this.getBlockState(pos);
-        BlockEntity blockEntity;
+        BlockState iblockdata = this.getBlockState(pos);
+        BlockEntity tileentity;
+
         if ("DUMMY".equals(nbt.getString("id"))) {
-            if (blockState.hasBlockEntity()) {
-                blockEntity = ((EntityBlock)blockState.getBlock()).newBlockEntity(pos, blockState);
+            if (iblockdata.hasBlockEntity()) {
+                tileentity = ((EntityBlock) iblockdata.getBlock()).newBlockEntity(pos, iblockdata);
             } else {
-                blockEntity = null;
-                LOGGER.warn("Tried to load a DUMMY block entity @ {} but found not block entity block {} at location", pos, blockState);
+                tileentity = null;
+                LevelChunk.LOGGER.warn("Tried to load a DUMMY block entity @ {} but found not block entity block {} at location", pos, iblockdata);
             }
         } else {
-            blockEntity = BlockEntity.loadStatic(pos, blockState, nbt);
+            tileentity = BlockEntity.loadStatic(pos, iblockdata, nbt);
         }
 
-        if (blockEntity != null) {
-            blockEntity.setLevel(this.level);
-            this.addAndRegisterBlockEntity(blockEntity);
+        if (tileentity != null) {
+            tileentity.setLevel(this.level);
+            this.addAndRegisterBlockEntity(tileentity);
         } else {
-            LOGGER.warn("Tried to load a block entity for block {} but failed at location {}", blockState, pos);
+            LevelChunk.LOGGER.warn("Tried to load a block entity for block {} but failed at location {}", iblockdata, pos);
         }
 
-        return blockEntity;
+        return tileentity;
     }
 
     public void unpackTicks(long time) {
@@ -563,7 +1072,7 @@
     }
 
     public ChunkHolder.FullChunkStatus getFullStatus() {
-        return this.fullStatus == null ? ChunkHolder.FullChunkStatus.BORDER : this.fullStatus.get();
+        return this.fullStatus == null ? ChunkHolder.FullChunkStatus.BORDER : (ChunkHolder.FullChunkStatus) this.fullStatus.get();
     }
 
     public void setFullStatus(Supplier<ChunkHolder.FullChunkStatus> levelTypeProvider) {
@@ -573,50 +1082,59 @@
     public void clearAllBlockEntities() {
         this.blockEntities.values().forEach(BlockEntity::setRemoved);
         this.blockEntities.clear();
-        this.tickersInLevel.values().forEach((ticker) -> {
-            ticker.rebind(NULL_TICKER);
+        this.tickersInLevel.values().forEach((chunk_d) -> {
+            chunk_d.rebind(LevelChunk.NULL_TICKER);
         });
         this.tickersInLevel.clear();
     }
 
     public void registerAllBlockEntitiesAfterLevelLoad() {
-        this.blockEntities.values().forEach((blockEntity) -> {
-            Level level = this.level;
-            if (level instanceof ServerLevel serverLevel) {
-                this.addGameEventListener(blockEntity, serverLevel);
+        this.blockEntities.values().forEach((tileentity) -> {
+            Level world = this.level;
+
+            if (world instanceof ServerLevel) {
+                ServerLevel worldserver = (ServerLevel) world;
+
+                this.addGameEventListener(tileentity, worldserver);
             }
 
-            this.updateBlockEntityTicker(blockEntity);
+            this.updateBlockEntityTicker(tileentity);
         });
     }
 
-    private <T extends BlockEntity> void addGameEventListener(T blockEntity, ServerLevel serverLevel) {
+    private <T extends BlockEntity> void addGameEventListener(T blockEntity, ServerLevel worldserver) {
         Block block = blockEntity.getBlockState().getBlock();
+
         if (block instanceof EntityBlock) {
-            GameEventListener gameEventListener = ((EntityBlock)block).getListener(serverLevel, blockEntity);
-            if (gameEventListener != null) {
-                GameEventDispatcher gameEventDispatcher = this.getEventDispatcher(SectionPos.blockToSectionCoord(blockEntity.getBlockPos().getY()));
-                gameEventDispatcher.register(gameEventListener);
+            GameEventListener gameeventlistener = ((EntityBlock) block).getListener(worldserver, blockEntity);
+
+            if (gameeventlistener != null) {
+                GameEventDispatcher gameeventdispatcher = this.getEventDispatcher(SectionPos.blockToSectionCoord(blockEntity.getBlockPos().getY()));
+
+                gameeventdispatcher.register(gameeventlistener);
             }
         }
 
     }
 
     private <T extends BlockEntity> void updateBlockEntityTicker(T blockEntity) {
-        BlockState blockState = blockEntity.getBlockState();
-        BlockEntityTicker<T> blockEntityTicker = blockState.getTicker(this.level, blockEntity.getType());
-        if (blockEntityTicker == null) {
+        BlockState iblockdata = blockEntity.getBlockState();
+        BlockEntityTicker<T> blockentityticker = iblockdata.getTicker(this.level, (BlockEntityType<T>) blockEntity.getType()); // CraftBukkit - decompile error
+
+        if (blockentityticker == null) {
             this.removeBlockEntityTicker(blockEntity.getBlockPos());
         } else {
-            this.tickersInLevel.compute(blockEntity.getBlockPos(), (pos, rebindableTickingBlockEntityWrapper) -> {
-                TickingBlockEntity tickingBlockEntity = this.createTicker(blockEntity, blockEntityTicker);
-                if (rebindableTickingBlockEntityWrapper != null) {
-                    rebindableTickingBlockEntityWrapper.rebind(tickingBlockEntity);
-                    return rebindableTickingBlockEntityWrapper;
+            this.tickersInLevel.compute(blockEntity.getBlockPos(), (blockposition, chunk_d) -> {
+                TickingBlockEntity tickingblockentity = this.createTicker(blockEntity, blockentityticker);
+
+                if (chunk_d != null) {
+                    chunk_d.rebind(tickingblockentity);
+                    return chunk_d;
                 } else if (this.isInLevel()) {
-                    LevelChunk.RebindableTickingBlockEntityWrapper rebindableTickingBlockEntityWrapper2 = new LevelChunk.RebindableTickingBlockEntityWrapper(tickingBlockEntity);
-                    this.level.addBlockEntityTicker(rebindableTickingBlockEntityWrapper2);
-                    return rebindableTickingBlockEntityWrapper2;
+                    LevelChunk.RebindableTickingBlockEntityWrapper chunk_d1 = new LevelChunk.RebindableTickingBlockEntityWrapper(tickingblockentity);
+
+                    this.level.addBlockEntityTicker(chunk_d1);
+                    return chunk_d1;
                 } else {
                     return null;
                 }
@@ -637,111 +1155,126 @@
         this.clientLightReady = shouldRenderOnUpdate;
     }
 
-    class BoundTickingBlockEntity<T extends BlockEntity> implements TickingBlockEntity {
-        private final T blockEntity;
-        private final BlockEntityTicker<T> ticker;
-        private boolean loggedInvalidBlockState;
+    @FunctionalInterface
+    public interface PostLoadProcessor {
 
-        BoundTickingBlockEntity(T blockEntity, BlockEntityTicker<T> ticker) {
-            this.blockEntity = blockEntity;
-            this.ticker = ticker;
+        void run(LevelChunk chunk);
+    }
+
+    public static enum EntityCreationType {
+
+        IMMEDIATE, QUEUED, CHECK;
+
+        private EntityCreationType() {}
+    }
+
+    private class RebindableTickingBlockEntityWrapper implements TickingBlockEntity {
+
+        private TickingBlockEntity ticker;
+
+        RebindableTickingBlockEntityWrapper(TickingBlockEntity tickingblockentity) {
+            this.ticker = tickingblockentity;
+        }
+
+        void rebind(TickingBlockEntity wrapped) {
+            this.ticker = wrapped;
         }
 
         @Override
         public void tick() {
-            if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) {
-                BlockPos blockPos = this.blockEntity.getBlockPos();
-                if (LevelChunk.this.isTicking(blockPos)) {
-                    try {
-                        ProfilerFiller profilerFiller = LevelChunk.this.level.getProfiler();
-                        profilerFiller.push(this::getType);
-                        BlockState blockState = LevelChunk.this.getBlockState(blockPos);
-                        if (this.blockEntity.getType().isValid(blockState)) {
-                            this.ticker.tick(LevelChunk.this.level, this.blockEntity.getBlockPos(), blockState, this.blockEntity);
-                            this.loggedInvalidBlockState = false;
-                        } else if (!this.loggedInvalidBlockState) {
-                            this.loggedInvalidBlockState = true;
-                            LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", LogUtils.defer(this::getType), LogUtils.defer(this::getPos), blockState);
-                        }
-
-                        profilerFiller.pop();
-                    } catch (Throwable var5) {
-                        CrashReport crashReport = CrashReport.forThrowable(var5, "Ticking block entity");
-                        CrashReportCategory crashReportCategory = crashReport.addCategory("Block entity being ticked");
-                        this.blockEntity.fillCrashReportCategory(crashReportCategory);
-                        throw new ReportedException(crashReport);
-                    }
-                }
-            }
-
+            this.ticker.tick();
         }
 
         @Override
         public boolean isRemoved() {
-            return this.blockEntity.isRemoved();
+            return this.ticker.isRemoved();
         }
 
         @Override
         public BlockPos getPos() {
-            return this.blockEntity.getBlockPos();
+            return this.ticker.getPos();
         }
 
         @Override
         public String getType() {
-            return BlockEntityType.getKey(this.blockEntity.getType()).toString();
+            return this.ticker.getType();
         }
 
-        @Override
         public String toString() {
-            return "Level ticker for " + this.getType() + "@" + this.getPos();
+            return this.ticker.toString() + " <wrapped>";
         }
     }
 
-    public static enum EntityCreationType {
-        IMMEDIATE,
-        QUEUED,
-        CHECK;
-    }
-
-    @FunctionalInterface
-    public interface PostLoadProcessor {
-        void run(LevelChunk chunk);
-    }
-
-    class RebindableTickingBlockEntityWrapper implements TickingBlockEntity {
-        private TickingBlockEntity ticker;
+    private class BoundTickingBlockEntity<T extends BlockEntity> implements TickingBlockEntity {
 
-        RebindableTickingBlockEntityWrapper(TickingBlockEntity wrapped) {
-            this.ticker = wrapped;
-        }
+        private final T blockEntity;
+        private final BlockEntityTicker<T> ticker;
+        private boolean loggedInvalidBlockState;
 
-        void rebind(TickingBlockEntity wrapped) {
-            this.ticker = wrapped;
+        BoundTickingBlockEntity(BlockEntity tileentity, BlockEntityTicker blockentityticker) {
+            this.blockEntity = (T) tileentity; // CraftBukkit - decompile error
+            this.ticker = blockentityticker;
         }
 
         @Override
         public void tick() {
-            this.ticker.tick();
+            if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) {
+                BlockPos blockposition = this.blockEntity.getBlockPos();
+
+                if (LevelChunk.this.isTicking(blockposition)) {
+                    try {
+                        ProfilerFiller gameprofilerfiller = LevelChunk.this.level.getProfiler();
+
+                        gameprofilerfiller.push(this::getType);
+                        this.blockEntity.tickTimer.startTiming(); // Spigot
+                        BlockState iblockdata = LevelChunk.this.getBlockState(blockposition);
+
+                        if (this.blockEntity.getType().isValid(iblockdata)) {
+                            this.ticker.tick(LevelChunk.this.level, this.blockEntity.getBlockPos(), iblockdata, this.blockEntity);
+                            this.loggedInvalidBlockState = false;
+                        } else if (!this.loggedInvalidBlockState) {
+                            this.loggedInvalidBlockState = true;
+                            LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata});
+                        }
+
+                        gameprofilerfiller.pop();
+                    } catch (Throwable throwable) {
+                        if (throwable instanceof ThreadDeath) throw throwable; // Paper
+                        // Paper start - Prevent tile entity and entity crashes
+                        final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ());
+                        net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable);
+                        net.minecraft.world.level.chunk.LevelChunk.this.level.getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new ServerInternalException(msg, throwable)));
+                        LevelChunk.this.removeBlockEntity(this.getPos());
+                        // Paper end
+                        // Spigot start
+                    } finally {
+                        this.blockEntity.tickTimer.stopTiming();
+                        // Spigot end
+                    }
+                }
+            }
+
         }
 
         @Override
         public boolean isRemoved() {
-            return this.ticker.isRemoved();
+            return this.blockEntity.isRemoved();
         }
 
         @Override
         public BlockPos getPos() {
-            return this.ticker.getPos();
+            return this.blockEntity.getBlockPos();
         }
 
         @Override
         public String getType() {
-            return this.ticker.getType();
+            return BlockEntityType.getKey(this.blockEntity.getType()).toString();
         }
 
-        @Override
         public String toString() {
-            return this.ticker.toString() + " <wrapped>";
+            String s = this.getType();
+
+            return "Level ticker for " + s + "@" + this.getPos();
         }
     }
 }
