--- a/net/minecraft/server/level/ChunkMap.java
+++ b/net/minecraft/server/level/ChunkMap.java
@@ -1,7 +1,10 @@
 package net.minecraft.server.level;
 
+import co.aikar.timings.Timing; // Paper
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.ComparisonChain; // Paper
 import com.google.common.collect.Lists;
 import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
@@ -18,16 +21,18 @@
 import it.unimi.dsi.fastutil.longs.Long2LongMap;
 import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
 import it.unimi.dsi.fastutil.longs.LongIterator;
 import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 import it.unimi.dsi.fastutil.longs.LongSet;
+import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator;
 import it.unimi.dsi.fastutil.objects.ObjectIterator;
 import java.io.IOException;
 import java.io.Writer;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.BitSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
@@ -37,6 +42,7 @@
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BooleanSupplier;
@@ -59,6 +65,8 @@
 import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket;
 import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket;
 import net.minecraft.network.protocol.game.DebugPackets;
+import io.papermc.paper.util.MCUtil;
+import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.level.progress.ChunkProgressListener;
 import net.minecraft.server.network.ServerPlayerConnection;
 import net.minecraft.util.CsvOutput;
@@ -85,6 +93,7 @@
 import net.minecraft.world.level.chunk.storage.ChunkSerializer;
 import net.minecraft.world.level.chunk.storage.ChunkStorage;
 import net.minecraft.world.level.entity.ChunkStatusUpdateListener;
+import net.minecraft.world.level.entity.EntityAccess;
 import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
 import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
 import net.minecraft.world.level.levelgen.RandomState;
@@ -97,8 +106,14 @@
 import org.apache.commons.lang3.mutable.MutableBoolean;
 import org.apache.commons.lang3.mutable.MutableObject;
 import org.slf4j.Logger;
+import org.bukkit.craftbukkit.v1_19_R1.generator.CustomChunkGenerator;
+import org.bukkit.entity.Player;
+// CraftBukkit end
+
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper
 
 public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider {
+
     private static final byte CHUNK_TYPE_REPLACEABLE = -1;
     private static final byte CHUNK_TYPE_UNKNOWN = 0;
     private static final byte CHUNK_TYPE_FULL = 1;
@@ -110,62 +125,304 @@
     public static final int MAX_VIEW_DISTANCE = 33;
     public static final int MAX_CHUNK_DISTANCE = 33 + ChunkStatus.maxDistance();
     public static final int FORCED_TICKET_LEVEL = 31;
-    public final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap<>();
-    public volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap = this.updatingChunkMap.clone();
-    private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads = new Long2ObjectLinkedOpenHashMap<>();
-    private final LongSet entitiesInLevel = new LongOpenHashSet();
+    // Paper - rewrite chunk system
     public final ServerLevel level;
     private final ThreadedLevelLightEngine lightEngine;
-    private final BlockableEventLoop<Runnable> mainThreadExecutor;
+    public final BlockableEventLoop<Runnable> mainThreadExecutor; // Paper - public
     public ChunkGenerator generator;
     private RandomState randomState;
     public final Supplier<DimensionDataStorage> overworldDataStorage;
     private final PoiManager poiManager;
-    public final LongSet toDrop = new LongOpenHashSet();
+    // Paper - rewrite chunk system
     private boolean modified;
-    private final ChunkTaskPriorityQueueSorter queueSorter;
-    private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> worldgenMailbox;
-    private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> mainThreadMailbox;
+    // Paper - rewrite chunk system
     public final ChunkProgressListener progressListener;
     private final ChunkStatusUpdateListener chunkStatusListener;
-    public final ChunkMap.DistanceManager distanceManager;
-    private final AtomicInteger tickingGenerated = new AtomicInteger();
-    private final StructureTemplateManager structureTemplateManager;
+    public final ChunkMap.ChunkDistanceManager distanceManager;
+    private final AtomicInteger tickingGenerated;
+    public final StructureTemplateManager structureTemplateManager; // Paper - rewrite chunk system
     private final String storageName;
-    private final PlayerMap playerMap = new PlayerMap();
-    public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap = new Int2ObjectOpenHashMap<>();
-    private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap();
-    private final Long2LongMap chunkSaveCooldowns = new Long2LongOpenHashMap();
-    private final Queue<Runnable> unloadQueue = Queues.newConcurrentLinkedQueue();
+    private final PlayerMap playerMap;
+    public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap;
+    private final Long2ByteMap chunkTypeCache;
+    private final Long2LongMap chunkSaveCooldowns;
+    private final Queue<Runnable> unloadQueue;
     int viewDistance;
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper
+    public final ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new ReferenceOpenHashSet<>();
+
+    // Paper - rewrite chunk system
+    // Paper start - optimise checkDespawn
+    public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40;
+    public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1);
+    public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE;
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap;
+    // Paper end - optimise checkDespawn
+
+    // Paper start - distance maps
+    private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
+    public final io.papermc.paper.chunk.PlayerChunkLoader playerChunkManager = new io.papermc.paper.chunk.PlayerChunkLoader(this, this.pooledLinkedPlayerHashSets); // Paper - replace chunk loader
+    // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+    // A note about the naming used here:
+    // Previously, mojang used a "spawn range" of 8 for controlling both ticking and
+    // mob spawn range. However, spigot makes the spawn range configurable by
+    // checking if the chunk is in the tick range (8) and the spawn range
+    // obviously this means a spawn range > 8 cannot be implemented
+
+    // these maps are named after spigot's uses
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap;
+    // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+    // Paper start - use distance map to optimise tracker
+    public static boolean isLegacyTrackingEntity(Entity entity) {
+        return entity.isLegacyTrackingEntity;
+    }
+
+    // inlined EnumMap, TrackingRange.TrackingRangeType
+    static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values();
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps;
+    final int[] entityTrackerTrackRanges;
+    public final int getEntityTrackerRange(final int ordinal) {
+        return this.entityTrackerTrackRanges[ordinal];
+    }
+
+    private int convertSpigotRangeToVanilla(final int vanilla) {
+        return MinecraftServer.getServer().getScaledTrackingDistance(vanilla);
+    }
+    // Paper end - use distance map to optimise tracker
+
+    void addPlayerToDistanceMaps(ServerPlayer player) {
+        this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader
+        int chunkX = MCUtil.getChunkCoordinate(player.getX());
+        int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
+        // Note: players need to be explicitly added to distance maps before they can be updated
+        this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        // Paper start - per player mob spawning
+        if (this.playerMobDistanceMap != null) {
+            this.playerMobDistanceMap.add(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player));
+        }
+        // Paper end - per player mob spawning
+        // Paper start - use distance map to optimise entity tracker
+        for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) {
+            com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
+            int trackRange = this.entityTrackerTrackRanges[i];
+
+            trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player)));
+        }
+        // Paper end - use distance map to optimise entity tracker
+        this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn
+    }
+
+    void removePlayerFromDistanceMaps(ServerPlayer player) {
+        this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader
+
+        // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        this.playerMobSpawnMap.remove(player);
+        this.playerChunkTickRangeMap.remove(player);
+        // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns
+        // Paper start - per player mob spawning
+        if (this.playerMobDistanceMap != null) {
+            this.playerMobDistanceMap.remove(player);
+        }
+        // Paper end - per player mob spawning
+        // Paper start - use distance map to optimise tracker
+        for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) {
+            this.playerEntityTrackerTrackMaps[i].remove(player);
+        }
+        // Paper end - use distance map to optimise tracker
+    }
+
+    void updateMaps(ServerPlayer player) {
+        int chunkX = MCUtil.getChunkCoordinate(player.getX());
+        int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
+        // Note: players need to be explicitly added to distance maps before they can be updated
+        this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader
+        this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        // Paper start - per player mob spawning
+        if (this.playerMobDistanceMap != null) {
+            this.playerMobDistanceMap.update(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player));
+        }
+        // Paper end - per player mob spawning
+        // Paper start - use distance map to optimise entity tracker
+        for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) {
+            com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i];
+            int trackRange = this.entityTrackerTrackRanges[i];
+
+            trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player)));
+        }
+        // Paper end - use distance map to optimise entity tracker
+        this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn
+    }
+    // Paper end
+    // Paper start
+    public final List<io.papermc.paper.chunk.SingleThreadChunkRegionManager> regionManagers = new java.util.ArrayList<>();
+    public final io.papermc.paper.chunk.SingleThreadChunkRegionManager dataRegionManager;
+
+    public static final class DataRegionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionData {
+    }
+
+    public static final class DataRegionSectionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSectionData {
+
+        @Override
+        public void removeFromRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section,
+                                     final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region from) {
+            final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
+            final DataRegionData fromData = (DataRegionData)from.regionData;
+        }
+
+        @Override
+        public void addToRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section,
+                                final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region oldRegion,
+                                final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region newRegion) {
+            final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData;
+            final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData;
+            final DataRegionData newRegionData = (DataRegionData)newRegion.regionData;
+        }
+    }
+
+    public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
+        return null; // Paper - rewrite chunk system
+    }
+    // Paper end
 
     public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
         super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
+        // Paper - rewrite chunk system
+        this.tickingGenerated = new AtomicInteger();
+        this.playerMap = new PlayerMap();
+        this.entityMap = new Int2ObjectOpenHashMap();
+        this.chunkTypeCache = new Long2ByteOpenHashMap();
+        this.chunkSaveCooldowns = new Long2LongOpenHashMap();
+        this.unloadQueue = Queues.newConcurrentLinkedQueue();
         this.structureTemplateManager = structureTemplateManager;
         Path path = session.getDimensionPath(world.dimension());
+
         this.storageName = path.getFileName().toString();
         this.level = world;
         this.generator = chunkGenerator;
-        if (chunkGenerator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
-            this.randomState = RandomState.create(noiseBasedChunkGenerator.generatorSettings().value(), world.registryAccess().registryOrThrow(Registry.NOISE_REGISTRY), world.getSeed());
+        // CraftBukkit start - SPIGOT-7051: It's a rigged game! Use delegate for random state creation, otherwise it is not so random.
+        if (chunkGenerator instanceof CustomChunkGenerator) {
+            chunkGenerator = ((CustomChunkGenerator) chunkGenerator).getDelegate();
+        }
+        // CraftBukkit end
+        if (chunkGenerator instanceof NoiseBasedChunkGenerator) {
+            NoiseBasedChunkGenerator chunkgeneratorabstract = (NoiseBasedChunkGenerator) chunkGenerator;
+
+            this.randomState = RandomState.create((NoiseGeneratorSettings) chunkgeneratorabstract.generatorSettings().value(), world.registryAccess().registryOrThrow(Registry.NOISE_REGISTRY), world.getSeed());
         } else {
             this.randomState = RandomState.create(NoiseGeneratorSettings.dummy(), world.registryAccess().registryOrThrow(Registry.NOISE_REGISTRY), world.getSeed());
         }
 
         this.mainThreadExecutor = mainThreadExecutor;
-        ProcessorMailbox<Runnable> processorMailbox = ProcessorMailbox.create(executor, "worldgen");
-        ProcessorHandle<Runnable> processorHandle = ProcessorHandle.of("main", mainThreadExecutor::tell);
+        // Paper - rewrite chunk system
+
+        Objects.requireNonNull(mainThreadExecutor);
+        // Paper - rewrite chunk system
+
         this.progressListener = worldGenerationProgressListener;
         this.chunkStatusListener = chunkStatusChangeListener;
-        ProcessorMailbox<Runnable> processorMailbox2 = ProcessorMailbox.create(executor, "light");
-        this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(processorMailbox, processorHandle, processorMailbox2), executor, Integer.MAX_VALUE);
-        this.worldgenMailbox = this.queueSorter.getProcessor(processorMailbox, false);
-        this.mainThreadMailbox = this.queueSorter.getProcessor(processorHandle, false);
-        this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), processorMailbox2, this.queueSorter.getProcessor(processorMailbox2, false));
-        this.distanceManager = new ChunkMap.DistanceManager(executor, mainThreadExecutor);
+        // Paper - rewrite chunk system
+
+        // Paper - rewrite chunk system
+        this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), null, null); // Paper - rewrite chunk system
+        this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor);
         this.overworldDataStorage = persistentStateManagerFactory;
         this.poiManager = new PoiManager(path.resolve("poi"), dataFixer, dsync, world.registryAccess(), world);
         this.setViewDistance(viewDistance);
+        // Paper start
+        this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new);
+        this.regionManagers.add(this.dataRegionManager);
+        // Paper end
+        this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper
+        // Paper start - use distance map to optimise entity tracker
+        this.playerEntityTrackerTrackMaps = new com.destroystokyo.paper.util.misc.PlayerAreaMap[TRACKING_RANGE_TYPES.length];
+        this.entityTrackerTrackRanges = new int[TRACKING_RANGE_TYPES.length];
+
+        org.spigotmc.SpigotWorldConfig spigotWorldConfig = this.level.spigotConfig;
+
+        for (int ordinal = 0, len = TRACKING_RANGE_TYPES.length; ordinal < len; ++ordinal) {
+            org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = TRACKING_RANGE_TYPES[ordinal];
+            int configuredSpigotValue;
+            switch (trackingRangeType) {
+                case PLAYER:
+                    configuredSpigotValue = spigotWorldConfig.playerTrackingRange;
+                    break;
+                case ANIMAL:
+                    configuredSpigotValue = spigotWorldConfig.animalTrackingRange;
+                    break;
+                case MONSTER:
+                    configuredSpigotValue = spigotWorldConfig.monsterTrackingRange;
+                    break;
+                case MISC:
+                    configuredSpigotValue = spigotWorldConfig.miscTrackingRange;
+                    break;
+                case OTHER:
+                    configuredSpigotValue = spigotWorldConfig.otherTrackingRange;
+                    break;
+                case ENDERDRAGON:
+                    configuredSpigotValue = EntityType.ENDER_DRAGON.clientTrackingRange() * 16;
+                    break;
+                default:
+                    throw new IllegalStateException("Missing case for enum " + trackingRangeType);
+            }
+            configuredSpigotValue = convertSpigotRangeToVanilla(configuredSpigotValue);
+
+            int trackRange = (configuredSpigotValue >>> 4) + ((configuredSpigotValue & 15) != 0 ? 1 : 0);
+            this.entityTrackerTrackRanges[ordinal] = trackRange;
+
+            this.playerEntityTrackerTrackMaps[ordinal] = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
+        }
+        // Paper end - use distance map to optimise entity tracker
+        // Paper start - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        this.playerChunkTickRangeMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInChunkTickRange = newState;
+                }
+            },
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInChunkTickRange = newState;
+                }
+            });
+        this.playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInMobSpawnRange = newState;
+                }
+            },
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInMobSpawnRange = newState;
+                }
+            });
+        // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        // Paper start - optimise checkDespawn
+        this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ);
+                if (chunk != null) {
+                    chunk.updateGeneralAreaCache(newState);
+                }
+            },
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ);
+                if (chunk != null) {
+                    chunk.updateGeneralAreaCache(newState);
+                }
+            });
+        // Paper end - optimise checkDespawn
     }
 
     protected ChunkGenerator generator() {
@@ -177,46 +434,64 @@
     }
 
     public void debugReloadGenerator() {
-        DataResult<JsonElement> dataResult = ChunkGenerator.CODEC.encodeStart(JsonOps.INSTANCE, this.generator);
-        DataResult<ChunkGenerator> dataResult2 = dataResult.flatMap((json) -> {
-            return ChunkGenerator.CODEC.parse(JsonOps.INSTANCE, json);
+        DataResult<JsonElement> dataresult = ChunkGenerator.CODEC.encodeStart(JsonOps.INSTANCE, this.generator);
+        DataResult<ChunkGenerator> dataresult1 = dataresult.flatMap((jsonelement) -> {
+            return ChunkGenerator.CODEC.parse(JsonOps.INSTANCE, jsonelement);
         });
-        dataResult2.result().ifPresent((chunkGenerator) -> {
-            this.generator = chunkGenerator;
+
+        dataresult1.result().ifPresent((chunkgenerator) -> {
+            this.generator = chunkgenerator;
         });
     }
 
+    // Paper start
+    public void updatePlayerMobTypeMap(Entity entity) {
+        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
+            return;
+        }
+        int index = entity.getType().getCategory().ordinal();
+
+        final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> inRange = this.playerMobDistanceMap.getObjectsInRange(entity.chunkPosition());
+        if (inRange == null) {
+            return;
+        }
+        final Object[] backingSet = inRange.getBackingSet();
+        for (int i = 0; i < backingSet.length; i++) {
+            if (!(backingSet[i] instanceof final ServerPlayer player)) {
+                continue;
+            }
+            ++player.mobCounts[index];
+        }
+    }
+
+    public int getMobCountNear(ServerPlayer entityPlayer, net.minecraft.world.entity.MobCategory mobCategory) {
+        return entityPlayer.mobCounts[mobCategory.ordinal()];
+    }
+    // Paper end
+
     private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) {
-        double d = (double)SectionPos.sectionToBlockCoord(pos.x, 8);
-        double e = (double)SectionPos.sectionToBlockCoord(pos.z, 8);
-        double f = d - entity.getX();
-        double g = e - entity.getZ();
-        return f * f + g * g;
+        double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8);
+        double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8);
+        double d2 = d0 - entity.getX();
+        double d3 = d1 - entity.getZ();
+
+        return d2 * d2 + d3 * d3;
     }
 
     public static boolean isChunkInRange(int x1, int z1, int x2, int z2, int distance) {
-        int i = Math.max(0, Math.abs(x1 - x2) - 1);
-        int j = Math.max(0, Math.abs(z1 - z2) - 1);
-        long l = (long)Math.max(0, Math.max(i, j) - 1);
-        long m = (long)Math.min(i, j);
-        long n = m * m + l * l;
-        int k = distance - 1;
-        int o = k * k;
-        return n <= (long)o;
+        int j1 = Math.max(0, Math.abs(x1 - x2) - 1);
+        int k1 = Math.max(0, Math.abs(z1 - z2) - 1);
+        long l1 = (long) Math.max(0, Math.max(j1, k1) - 1);
+        long i2 = (long) Math.min(j1, k1);
+        long j2 = i2 * i2 + l1 * l1;
+        int k2 = distance - 1;
+        int l2 = k2 * k2;
+
+        return j2 <= (long) l2;
     }
 
     private static boolean isChunkOnRangeBorder(int x1, int z1, int x2, int z2, int distance) {
-        if (!isChunkInRange(x1, z1, x2, z2, distance)) {
-            return false;
-        } else if (!isChunkInRange(x1 + 1, z1, x2, z2, distance)) {
-            return true;
-        } else if (!isChunkInRange(x1, z1 + 1, x2, z2, distance)) {
-            return true;
-        } else if (!isChunkInRange(x1 - 1, z1, x2, z2, distance)) {
-            return true;
-        } else {
-            return !isChunkInRange(x1, z1 - 1, x2, z2, distance);
-        }
+        return !ChunkMap.isChunkInRange(x1, z1, x2, z2, distance) ? false : (!ChunkMap.isChunkInRange(x1 + 1, z1, x2, z2, distance) ? true : (!ChunkMap.isChunkInRange(x1, z1 + 1, x2, z2, distance) ? true : (!ChunkMap.isChunkInRange(x1 - 1, z1, x2, z2, distance) ? true : !ChunkMap.isChunkInRange(x1, z1 - 1, x2, z2, distance))));
     }
 
     protected ThreadedLevelLightEngine getLightEngine() {
@@ -225,368 +500,170 @@
 
     @Nullable
     protected ChunkHolder getUpdatingChunkIfPresent(long pos) {
-        return this.updatingChunkMap.get(pos);
+        // Paper start - rewrite chunk system
+        io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos);
+        return holder == null ? null : holder.vanillaChunkHolder;
+        // Paper end - rewrite chunk system
     }
 
     @Nullable
     public ChunkHolder getVisibleChunkIfPresent(long pos) {
-        return this.visibleChunkMap.get(pos);
+        // Paper start - rewrite chunk system
+        io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos);
+        return holder == null ? null : holder.vanillaChunkHolder;
+        // Paper end - rewrite chunk system
     }
 
     protected IntSupplier getChunkQueueLevel(long pos) {
-        return () -> {
-            ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(pos);
-            return chunkHolder == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(chunkHolder.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
-        };
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     public String getChunkDebugData(ChunkPos chunkPos) {
-        ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(chunkPos.toLong());
-        if (chunkHolder == null) {
+        ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkPos.toLong());
+
+        if (playerchunk == null) {
             return "null";
         } else {
-            String string = chunkHolder.getTicketLevel() + "\n";
-            ChunkStatus chunkStatus = chunkHolder.getLastAvailableStatus();
-            ChunkAccess chunkAccess = chunkHolder.getLastAvailable();
-            if (chunkStatus != null) {
-                string = string + "St: \u00a7" + chunkStatus.getIndex() + chunkStatus + "\u00a7r\n";
-            }
-
-            if (chunkAccess != null) {
-                string = string + "Ch: \u00a7" + chunkAccess.getStatus().getIndex() + chunkAccess.getStatus() + "\u00a7r\n";
-            }
-
-            ChunkHolder.FullChunkStatus fullChunkStatus = chunkHolder.getFullStatus();
-            string = string + "\u00a7" + fullChunkStatus.ordinal() + fullChunkStatus;
-            return string + "\u00a7r";
-        }
-    }
-
-    private CompletableFuture<Either<List<ChunkAccess>, ChunkHolder.ChunkLoadingFailure>> getChunkRangeFuture(ChunkPos centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
-        List<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> list = new ArrayList<>();
-        List<ChunkHolder> list2 = new ArrayList<>();
-        int i = centerChunk.x;
-        int j = centerChunk.z;
-
-        for(int k = -margin; k <= margin; ++k) {
-            for(int l = -margin; l <= margin; ++l) {
-                int m = Math.max(Math.abs(l), Math.abs(k));
-                final ChunkPos chunkPos = new ChunkPos(i + l, j + k);
-                long n = chunkPos.toLong();
-                ChunkHolder chunkHolder = this.getUpdatingChunkIfPresent(n);
-                if (chunkHolder == null) {
-                    return CompletableFuture.completedFuture(Either.right(new ChunkHolder.ChunkLoadingFailure() {
-                        @Override
-                        public String toString() {
-                            return "Unloaded " + chunkPos;
-                        }
-                    }));
-                }
+            String s = playerchunk.getTicketLevel() + "\n";
+            ChunkStatus chunkstatus = playerchunk.getLastAvailableStatus();
+            ChunkAccess ichunkaccess = playerchunk.getLastAvailable();
 
-                ChunkStatus chunkStatus = distanceToStatus.apply(m);
-                CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture = chunkHolder.getOrScheduleFuture(chunkStatus, this);
-                list2.add(chunkHolder);
-                list.add(completableFuture);
+            if (chunkstatus != null) {
+                s = s + "St: \u00a7" + chunkstatus.getIndex() + chunkstatus + "\u00a7r\n";
             }
-        }
 
-        CompletableFuture<List<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> completableFuture2 = Util.sequence(list);
-        CompletableFuture<Either<List<ChunkAccess>, ChunkHolder.ChunkLoadingFailure>> completableFuture3 = completableFuture2.thenApply((chunks) -> {
-            List<ChunkAccess> list = Lists.newArrayList();
-            int l = 0;
-
-            for(final Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either : chunks) {
-                if (either == null) {
-                    throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
-                }
-
-                Optional<ChunkAccess> optional = either.left();
-                if (!optional.isPresent()) {
-                    final int m = l;
-                    return Either.right(new ChunkHolder.ChunkLoadingFailure() {
-                        @Override
-                        public String toString() {
-                            return "Unloaded " + new ChunkPos(i + m % (margin * 2 + 1), j + m / (margin * 2 + 1)) + " " + either.right().get();
-                        }
-                    });
-                }
-
-                list.add(optional.get());
-                ++l;
+            if (ichunkaccess != null) {
+                s = s + "Ch: \u00a7" + ichunkaccess.getStatus().getIndex() + ichunkaccess.getStatus() + "\u00a7r\n";
             }
 
-            return Either.left(list);
-        });
+            ChunkHolder.FullChunkStatus playerchunk_state = playerchunk.getFullStatus();
 
-        for(ChunkHolder chunkHolder2 : list2) {
-            chunkHolder2.addSaveDependency("getChunkRangeFuture " + centerChunk + " " + margin, completableFuture3);
+            s = s + "\u00a7" + playerchunk_state.ordinal() + playerchunk_state;
+            return s + "\u00a7r";
         }
+    }
 
-        return completableFuture3;
+    // Paper start
+    public final int getEffectiveViewDistance() {
+        // TODO this needs to be checked on update
+        // Mojang currently sets it to +1 of the configured view distance. So subtract one to get the one we really want.
+        return this.viewDistance - 1;
+    }
+    // Paper end
+
+    private CompletableFuture<Either<List<ChunkAccess>, ChunkHolder.ChunkLoadingFailure>> getChunkRangeFuture(ChunkPos centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
-        StringBuilder stringBuilder = new StringBuilder();
-        Consumer<ChunkHolder> consumer = (chunkHolder) -> {
-            chunkHolder.getAllFutures().forEach((pair) -> {
-                ChunkStatus chunkStatus = pair.getFirst();
-                CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture = pair.getSecond();
-                if (completableFuture != null && completableFuture.isDone() && completableFuture.join() == null) {
-                    stringBuilder.append((Object)chunkHolder.getPos()).append(" - status: ").append((Object)chunkStatus).append(" future: ").append((Object)completableFuture).append(System.lineSeparator());
+        StringBuilder stringbuilder = new StringBuilder();
+        Consumer<ChunkHolder> consumer = (playerchunk) -> {
+            playerchunk.getAllFutures().forEach((pair) -> {
+                ChunkStatus chunkstatus = (ChunkStatus) pair.getFirst();
+                CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = (CompletableFuture) pair.getSecond();
+
+                if (completablefuture != null && completablefuture.isDone() && completablefuture.join() == null) {
+                    stringbuilder.append(playerchunk.getPos()).append(" - status: ").append(chunkstatus).append(" future: ").append(completablefuture).append(System.lineSeparator());
                 }
 
             });
         };
-        stringBuilder.append("Updating:").append(System.lineSeparator());
-        this.updatingChunkMap.values().forEach(consumer);
-        stringBuilder.append("Visible:").append(System.lineSeparator());
-        this.visibleChunkMap.values().forEach(consumer);
-        CrashReport crashReport = CrashReport.forThrowable(exception, "Chunk loading");
-        CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk loading");
-        crashReportCategory.setDetail("Details", details);
-        crashReportCategory.setDetail("Futures", stringBuilder);
-        return new ReportedException(crashReport);
+
+        stringbuilder.append("Updating:").append(System.lineSeparator());
+        io.papermc.paper.chunk.system.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
+        stringbuilder.append("Visible:").append(System.lineSeparator());
+        io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
+        CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
+        CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
+
+        crashreportsystemdetails.setDetail("Details", (Object) details);
+        crashreportsystemdetails.setDetail("Futures", (Object) stringbuilder);
+        return new ReportedException(crashreport);
     }
 
     public CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> prepareEntityTickingChunk(ChunkPos pos) {
-        return this.getChunkRangeFuture(pos, 2, (distance) -> {
-            return ChunkStatus.FULL;
-        }).thenApplyAsync((either) -> {
-            return either.mapLeft((chunks) -> {
-                return (LevelChunk)chunks.get(chunks.size() / 2);
-            });
-        }, this.mainThreadExecutor);
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     @Nullable
-    ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int i) {
-        if (i > MAX_CHUNK_DISTANCE && level > MAX_CHUNK_DISTANCE) {
-            return holder;
-        } else {
-            if (holder != null) {
-                holder.setTicketLevel(level);
-            }
-
-            if (holder != null) {
-                if (level > MAX_CHUNK_DISTANCE) {
-                    this.toDrop.add(pos);
-                } else {
-                    this.toDrop.remove(pos);
-                }
-            }
-
-            if (level <= MAX_CHUNK_DISTANCE && holder == null) {
-                holder = this.pendingUnloads.remove(pos);
-                if (holder != null) {
-                    holder.setTicketLevel(level);
-                } else {
-                    holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
-                }
-
-                this.updatingChunkMap.put(pos, holder);
-                this.modified = true;
-            }
-
-            return holder;
-        }
+    ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     @Override
     public void close() throws IOException {
-        try {
-            this.queueSorter.close();
-            this.poiManager.close();
-        } finally {
-            super.close();
-        }
+        throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system
+    }
 
+    // Paper start - rewrite chunk system
+    protected void saveIncrementally() {
+        this.level.chunkTaskScheduler.chunkHolderManager.autoSave(); // Paper - rewrite chunk system
     }
+    // Paper end - - rewrite chunk system
 
     protected void saveAllChunks(boolean flush) {
-        if (flush) {
-            List<ChunkHolder> list = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList());
-            MutableBoolean mutableBoolean = new MutableBoolean();
-
-            do {
-                mutableBoolean.setFalse();
-                list.stream().map((chunkHolder) -> {
-                    CompletableFuture<ChunkAccess> completableFuture;
-                    do {
-                        completableFuture = chunkHolder.getChunkToSave();
-                        this.mainThreadExecutor.managedBlock(completableFuture::isDone);
-                    } while(completableFuture != chunkHolder.getChunkToSave());
-
-                    return completableFuture.join();
-                }).filter((chunk) -> {
-                    return chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk;
-                }).filter(this::save).forEach((chunkAccess) -> {
-                    mutableBoolean.setTrue();
-                });
-            } while(mutableBoolean.isTrue());
-
-            this.processUnloads(() -> {
-                return true;
-            });
-            this.flushWorker();
-        } else {
-            this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
-        }
-
+        this.level.chunkTaskScheduler.chunkHolderManager.saveAllChunks(flush, false, false); // Paper - rewrite chunk system
     }
 
     protected void tick(BooleanSupplier shouldKeepTicking) {
-        ProfilerFiller profilerFiller = this.level.getProfiler();
-        profilerFiller.push("poi");
+        ProfilerFiller gameprofilerfiller = this.level.getProfiler();
+
+        try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper
+        gameprofilerfiller.push("poi");
         this.poiManager.tick(shouldKeepTicking);
-        profilerFiller.popPush("chunk_unload");
+        } // Paper
+        gameprofilerfiller.popPush("chunk_unload");
         if (!this.level.noSave()) {
+            try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper
             this.processUnloads(shouldKeepTicking);
+            } // Paper
         }
 
-        profilerFiller.pop();
+        gameprofilerfiller.pop();
     }
 
     public boolean hasWork() {
-        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets();
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     private void processUnloads(BooleanSupplier shouldKeepTicking) {
-        LongIterator longIterator = this.toDrop.iterator();
-
-        for(int i = 0; longIterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longIterator.remove()) {
-            long l = longIterator.nextLong();
-            ChunkHolder chunkHolder = this.updatingChunkMap.remove(l);
-            if (chunkHolder != null) {
-                this.pendingUnloads.put(l, chunkHolder);
-                this.modified = true;
-                ++i;
-                this.scheduleUnload(l, chunkHolder);
-            }
-        }
-
-        int j = Math.max(0, this.unloadQueue.size() - 2000);
-
-        Runnable runnable;
-        while((shouldKeepTicking.getAsBoolean() || j > 0) && (runnable = this.unloadQueue.poll()) != null) {
-            --j;
-            runnable.run();
-        }
-
-        int k = 0;
-        ObjectIterator<ChunkHolder> objectIterator = this.visibleChunkMap.values().iterator();
-
-        while(k < 20 && shouldKeepTicking.getAsBoolean() && objectIterator.hasNext()) {
-            if (this.saveChunkIfNeeded(objectIterator.next())) {
-                ++k;
-            }
-        }
+        this.level.chunkTaskScheduler.chunkHolderManager.processUnloads(); // Paper - rewrite chunk system
 
     }
 
     private void scheduleUnload(long pos, ChunkHolder holder) {
-        CompletableFuture<ChunkAccess> completableFuture = holder.getChunkToSave();
-        completableFuture.thenAcceptAsync((chunk) -> {
-            CompletableFuture<ChunkAccess> completableFuture2 = holder.getChunkToSave();
-            if (completableFuture2 != completableFuture) {
-                this.scheduleUnload(pos, holder);
-            } else {
-                if (this.pendingUnloads.remove(pos, holder) && chunk != null) {
-                    if (chunk instanceof LevelChunk) {
-                        ((LevelChunk)chunk).setLoaded(false);
-                    }
-
-                    this.save(chunk);
-                    if (this.entitiesInLevel.remove(pos) && chunk instanceof LevelChunk) {
-                        LevelChunk levelChunk = (LevelChunk)chunk;
-                        this.level.unload(levelChunk);
-                    }
-
-                    this.lightEngine.updateChunkStatus(chunk.getPos());
-                    this.lightEngine.tryScheduleUpdate();
-                    this.progressListener.onStatusChange(chunk.getPos(), (ChunkStatus)null);
-                    this.chunkSaveCooldowns.remove(chunk.getPos().toLong());
-                }
-
-            }
-        }, this.unloadQueue::add).whenComplete((void_, throwable) -> {
-            if (throwable != null) {
-                LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable);
-            }
-
-        });
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     protected boolean promoteChunkMap() {
-        if (!this.modified) {
-            return false;
-        } else {
-            this.visibleChunkMap = this.updatingChunkMap.clone();
-            this.modified = false;
-            return true;
-        }
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     public CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> schedule(ChunkHolder holder, ChunkStatus requiredStatus) {
-        ChunkPos chunkPos = holder.getPos();
-        if (requiredStatus == ChunkStatus.EMPTY) {
-            return this.scheduleChunkLoad(chunkPos);
-        } else {
-            if (requiredStatus == ChunkStatus.LIGHT) {
-                this.distanceManager.addTicket(TicketType.LIGHT, chunkPos, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkPos);
-            }
-
-            Optional<ChunkAccess> optional = holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK).left();
-            if (optional.isPresent() && optional.get().getStatus().isOrAfter(requiredStatus)) {
-                CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture = requiredStatus.load(this.level, this.structureTemplateManager, this.lightEngine, (chunkAccess) -> {
-                    return this.protoChunkToFullChunk(holder);
-                }, optional.get());
-                this.progressListener.onStatusChange(chunkPos, requiredStatus);
-                return completableFuture;
-            } else {
-                return this.scheduleChunkGeneration(holder, requiredStatus);
-            }
-        }
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> scheduleChunkLoad(ChunkPos pos) {
-        return this.readChunk(pos).thenApply((nbt) -> {
-            return nbt.filter((nbt2) -> {
-                boolean bl = isChunkDataValid(nbt2);
-                if (!bl) {
-                    LOGGER.error("Chunk file at {} is missing level data, skipping", (Object)pos);
-                }
-
-                return bl;
-            });
-        }).thenApplyAsync((nbt) -> {
-            this.level.getProfiler().incrementCounter("chunkLoad");
-            if (nbt.isPresent()) {
-                ChunkAccess chunkAccess = ChunkSerializer.read(this.level, this.poiManager, pos, nbt.get());
-                this.markPosition(pos, chunkAccess.getStatus().getChunkType());
-                return Either.left(chunkAccess);
-            } else {
-                return Either.left(this.createEmptyChunk(pos));
-            }
-        }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> {
-            return this.handleChunkLoadFailure(throwable, pos);
-        }, this.mainThreadExecutor);
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
-    private static boolean isChunkDataValid(CompoundTag nbt) {
+    public static boolean isChunkDataValid(CompoundTag nbt) { // Paper - async chunk loading
         return nbt.contains("Status", 8);
     }
 
     private Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> handleChunkLoadFailure(Throwable throwable, ChunkPos chunkPos) {
-        if (throwable instanceof ReportedException reportedException) {
-            Throwable throwable2 = reportedException.getCause();
-            if (!(throwable2 instanceof IOException)) {
+        if (throwable instanceof ReportedException) {
+            ReportedException reportedexception = (ReportedException) throwable;
+            Throwable throwable1 = reportedexception.getCause();
+
+            if (!(throwable1 instanceof IOException)) {
                 this.markPositionReplaceable(chunkPos);
-                throw reportedException;
+                throw reportedexception;
             }
 
-            LOGGER.error("Couldn't load chunk {}", chunkPos, throwable2);
+            ChunkMap.LOGGER.error("Couldn't load chunk {}", chunkPos, throwable1);
         } else if (throwable instanceof IOException) {
-            LOGGER.error("Couldn't load chunk {}", chunkPos, throwable);
+            ChunkMap.LOGGER.error("Couldn't load chunk {}", chunkPos, throwable);
         }
 
         return Either.left(this.createEmptyChunk(chunkPos));
@@ -594,53 +671,19 @@
 
     private ChunkAccess createEmptyChunk(ChunkPos chunkPos) {
         this.markPositionReplaceable(chunkPos);
-        return new ProtoChunk(chunkPos, UpgradeData.EMPTY, this.level, this.level.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), (BlendingData)null);
+        return new ProtoChunk(chunkPos, UpgradeData.EMPTY, this.level, this.level.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), (BlendingData) null);
     }
 
     private void markPositionReplaceable(ChunkPos pos) {
-        this.chunkTypeCache.put(pos.toLong(), (byte)-1);
+        this.chunkTypeCache.put(pos.toLong(), (byte) -1);
     }
 
     private byte markPosition(ChunkPos pos, ChunkStatus.ChunkType type) {
-        return this.chunkTypeCache.put(pos.toLong(), (byte)(type == ChunkStatus.ChunkType.PROTOCHUNK ? -1 : 1));
+        return this.chunkTypeCache.put(pos.toLong(), (byte) (type == ChunkStatus.ChunkType.PROTOCHUNK ? -1 : 1));
     }
 
     private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> scheduleChunkGeneration(ChunkHolder holder, ChunkStatus requiredStatus) {
-        ChunkPos chunkPos = holder.getPos();
-        CompletableFuture<Either<List<ChunkAccess>, ChunkHolder.ChunkLoadingFailure>> completableFuture = this.getChunkRangeFuture(chunkPos, requiredStatus.getRange(), (distance) -> {
-            return this.getDependencyStatus(requiredStatus, distance);
-        });
-        this.level.getProfiler().incrementCounter(() -> {
-            return "chunkGenerate " + requiredStatus.getName();
-        });
-        Executor executor = (task) -> {
-            this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, task));
-        };
-        return completableFuture.thenComposeAsync((either) -> {
-            return either.map((chunks) -> {
-                try {
-                    CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture = requiredStatus.generate(executor, this.level, this.generator, this.structureTemplateManager, this.lightEngine, (chunk) -> {
-                        return this.protoChunkToFullChunk(holder);
-                    }, chunks, false);
-                    this.progressListener.onStatusChange(chunkPos, requiredStatus);
-                    return completableFuture;
-                } catch (Exception var9) {
-                    var9.getStackTrace();
-                    CrashReport crashReport = CrashReport.forThrowable(var9, "Exception generating new chunk");
-                    CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk to be generated");
-                    crashReportCategory.setDetail("Location", String.format(Locale.ROOT, "%d,%d", chunkPos.x, chunkPos.z));
-                    crashReportCategory.setDetail("Position hash", ChunkPos.asLong(chunkPos.x, chunkPos.z));
-                    crashReportCategory.setDetail("Generator", this.generator);
-                    this.mainThreadExecutor.execute(() -> {
-                        throw new ReportedException(crashReport);
-                    });
-                    throw new ReportedException(crashReport);
-                }
-            }, (unloaded) -> {
-                this.releaseLightTicket(chunkPos);
-                return CompletableFuture.completedFuture(Either.right(unloaded));
-            });
-        }, executor);
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     protected void releaseLightTicket(ChunkPos pos) {
@@ -651,97 +694,93 @@
         }));
     }
 
-    private ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) {
-        ChunkStatus chunkStatus;
+    public static ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { // Paper -> public, static
+        ChunkStatus chunkstatus1;
+
         if (distance == 0) {
-            chunkStatus = centerChunkTargetStatus.getParent();
+            chunkstatus1 = centerChunkTargetStatus.getParent();
         } else {
-            chunkStatus = ChunkStatus.getStatusAroundFullChunk(ChunkStatus.getDistance(centerChunkTargetStatus) + distance);
+            chunkstatus1 = ChunkStatus.getStatusAroundFullChunk(ChunkStatus.getDistance(centerChunkTargetStatus) + distance);
         }
 
-        return chunkStatus;
+        return chunkstatus1;
     }
 
-    private static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> nbt) {
+    public static void postLoadProtoChunk(ServerLevel world, List<CompoundTag> nbt) { // Paper - public
         if (!nbt.isEmpty()) {
-            world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(nbt, world));
+            // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities
+            world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(nbt, world).filter((entity) -> {
+                boolean needsRemoval = false;
+                net.minecraft.server.dedicated.DedicatedServer server = world.getCraftServer().getServer();
+                if (!server.areNpcsEnabled() && entity instanceof net.minecraft.world.entity.npc.Npc) {
+                    entity.discard();
+                    needsRemoval = true;
+                }
+                if (!server.isSpawningAnimals() && (entity instanceof net.minecraft.world.entity.animal.Animal || entity instanceof net.minecraft.world.entity.animal.WaterAnimal)) {
+                    entity.discard();
+                    needsRemoval = true;
+                }
+                checkDupeUUID(world, entity); // Paper
+                return !needsRemoval;
+            }));
+            // CraftBukkit end
         }
 
     }
 
     private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> protoChunkToFullChunk(ChunkHolder chunkHolder) {
-        CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture = chunkHolder.getFutureIfPresentUnchecked(ChunkStatus.FULL.getParent());
-        return completableFuture.thenApplyAsync((either) -> {
-            ChunkStatus chunkStatus = ChunkHolder.getStatus(chunkHolder.getTicketLevel());
-            return !chunkStatus.isOrAfter(ChunkStatus.FULL) ? ChunkHolder.UNLOADED_CHUNK : either.mapLeft((protoChunk) -> {
-                ChunkPos chunkPos = chunkHolder.getPos();
-                ProtoChunk protoChunk2 = (ProtoChunk)protoChunk;
-                LevelChunk levelChunk;
-                if (protoChunk2 instanceof ImposterProtoChunk) {
-                    levelChunk = ((ImposterProtoChunk)protoChunk2).getWrapped();
-                } else {
-                    levelChunk = new LevelChunk(this.level, protoChunk2, (chunk) -> {
-                        postLoadProtoChunk(this.level, protoChunk2.getEntities());
-                    });
-                    chunkHolder.replaceProtoChunk(new ImposterProtoChunk(levelChunk, false));
-                }
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
+    }
 
-                levelChunk.setFullStatus(() -> {
-                    return ChunkHolder.getFullChunkStatus(chunkHolder.getTicketLevel());
-                });
-                levelChunk.runPostLoad();
-                if (this.entitiesInLevel.add(chunkPos.toLong())) {
-                    levelChunk.setLoaded(true);
-                    levelChunk.registerAllBlockEntitiesAfterLevelLoad();
-                    levelChunk.registerTickContainerInLevel(this.level);
-                }
+    // Paper start
+    // rets true if to prevent the entity from being added
+    public static boolean checkDupeUUID(ServerLevel level, Entity entity) {
+        io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode mode = level.paperConfig().entities.spawning.duplicateUuid.mode;
+        if (mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN
+            && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE
+            && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN) {
+            return false;
+        }
+        Entity other = level.getEntity(entity.getUUID());
 
-                return levelChunk;
-            });
-        }, (task) -> {
-            this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(task, chunkHolder.getPos().toLong(), chunkHolder::getTicketLevel));
-        });
-    }
+        if (other == null || other == entity) {
+            return false;
+        }
 
+        if (mode == io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN && other != null && !other.isRemoved()
+            && Objects.equals(other.getEncodeId(), entity.getEncodeId())
+            && entity.getBukkitEntity().getLocation().distance(other.getBukkitEntity().getLocation()) < level.paperConfig().entities.spawning.duplicateUuid.safeRegenDeleteRange
+        ) {
+            if (ServerLevel.DEBUG_ENTITIES) LOGGER.warn("[DUPE-UUID] Duplicate UUID found used by " + other + ", deleted entity " + entity + " because it was near the duplicate and likely an actual duplicate. See https://github.com/PaperMC/Paper/issues/1223 for discussion on what this is about.");
+            entity.discard();
+            return true;
+        }
+        if (other != null && !other.isRemoved()) {
+            switch (mode) {
+                case SAFE_REGEN: {
+                    entity.setUUID(java.util.UUID.randomUUID());
+                    if (ServerLevel.DEBUG_ENTITIES) LOGGER.warn("[DUPE-UUID] Duplicate UUID found used by " + other + ", regenerated UUID for " + entity + ". See https://github.com/PaperMC/Paper/issues/1223 for discussion on what this is about.");
+                    break;
+                }
+                case DELETE: {
+                    if (ServerLevel.DEBUG_ENTITIES) LOGGER.warn("[DUPE-UUID] Duplicate UUID found used by " + other + ", deleted entity " + entity + ". See https://github.com/PaperMC/Paper/issues/1223 for discussion on what this is about.");
+                    entity.discard();
+                    return true;
+                }
+                default:
+                    if (ServerLevel.DEBUG_ENTITIES) LOGGER.warn("[DUPE-UUID] Duplicate UUID found used by " + other + ", doing nothing to " + entity + ". See https://github.com/PaperMC/Paper/issues/1223 for discussion on what this is about.");
+                    break;
+            }
+        }
+        return false;
+    }
+    // Paper end
     public CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> prepareTickingChunk(ChunkHolder holder) {
-        ChunkPos chunkPos = holder.getPos();
-        CompletableFuture<Either<List<ChunkAccess>, ChunkHolder.ChunkLoadingFailure>> completableFuture = this.getChunkRangeFuture(chunkPos, 1, (i) -> {
-            return ChunkStatus.FULL;
-        });
-        CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> completableFuture2 = completableFuture.thenApplyAsync((either) -> {
-            return either.mapLeft((list) -> {
-                return (LevelChunk)list.get(list.size() / 2);
-            });
-        }, (task) -> {
-            this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, task));
-        }).thenApplyAsync((either) -> {
-            return either.ifLeft((chunk) -> {
-                chunk.postProcessGeneration();
-                this.level.startTickingChunk(chunk);
-            });
-        }, this.mainThreadExecutor);
-        completableFuture2.thenAcceptAsync((either) -> {
-            either.ifLeft((chunk) -> {
-                this.tickingGenerated.getAndIncrement();
-                MutableObject<ClientboundLevelChunkWithLightPacket> mutableObject = new MutableObject<>();
-                this.getPlayers(chunkPos, false).forEach((player) -> {
-                    this.playerLoadedChunk(player, mutableObject, chunk);
-                });
-            });
-        }, (task) -> {
-            this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, task));
-        });
-        return completableFuture2;
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     public CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> prepareAccessibleChunk(ChunkHolder holder) {
-        return this.getChunkRangeFuture(holder.getPos(), 1, ChunkStatus::getStatusAroundFullChunk).thenApplyAsync((either) -> {
-            return either.mapLeft((chunks) -> {
-                return (LevelChunk)chunks.get(chunks.size() / 2);
-            });
-        }, (task) -> {
-            this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, task));
-        });
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     public int getTickingGenerated() {
@@ -749,115 +788,44 @@
     }
 
     private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) {
-        if (!chunkHolder.wasAccessibleSinceLastSave()) {
-            return false;
-        } else {
-            ChunkAccess chunkAccess = chunkHolder.getChunkToSave().getNow((ChunkAccess)null);
-            if (!(chunkAccess instanceof ImposterProtoChunk) && !(chunkAccess instanceof LevelChunk)) {
-                return false;
-            } else {
-                long l = chunkAccess.getPos().toLong();
-                long m = this.chunkSaveCooldowns.getOrDefault(l, -1L);
-                long n = System.currentTimeMillis();
-                if (n < m) {
-                    return false;
-                } else {
-                    boolean bl = this.save(chunkAccess);
-                    chunkHolder.refreshAccessibility();
-                    if (bl) {
-                        this.chunkSaveCooldowns.put(l, n + 10000L);
-                    }
-
-                    return bl;
-                }
-            }
-        }
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     public boolean save(ChunkAccess chunk) {
-        this.poiManager.flush(chunk.getPos());
-        if (!chunk.isUnsaved()) {
-            return false;
-        } else {
-            chunk.setUnsaved(false);
-            ChunkPos chunkPos = chunk.getPos();
-
-            try {
-                ChunkStatus chunkStatus = chunk.getStatus();
-                if (chunkStatus.getChunkType() != ChunkStatus.ChunkType.LEVELCHUNK) {
-                    if (this.isExistingChunkFull(chunkPos)) {
-                        return false;
-                    }
-
-                    if (chunkStatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
-                        return false;
-                    }
-                }
-
-                this.level.getProfiler().incrementCounter("chunkSave");
-                CompoundTag compoundTag = ChunkSerializer.write(this.level, chunk);
-                this.write(chunkPos, compoundTag);
-                this.markPosition(chunkPos, chunkStatus.getChunkType());
-                return true;
-            } catch (Exception var5) {
-                LOGGER.error("Failed to save chunk {},{}", chunkPos.x, chunkPos.z, var5);
-                return false;
-            }
-        }
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     private boolean isExistingChunkFull(ChunkPos pos) {
-        byte b = this.chunkTypeCache.get(pos.toLong());
-        if (b != 0) {
-            return b == 1;
-        } else {
-            CompoundTag compoundTag;
-            try {
-                compoundTag = this.readChunk(pos).join().orElse((CompoundTag)null);
-                if (compoundTag == null) {
-                    this.markPositionReplaceable(pos);
-                    return false;
-                }
-            } catch (Exception var5) {
-                LOGGER.error("Failed to read chunk {}", pos, var5);
-                this.markPositionReplaceable(pos);
-                return false;
-            }
-
-            ChunkStatus.ChunkType chunkType = ChunkSerializer.getChunkTypeFromTag(compoundTag);
-            return this.markPosition(pos, chunkType) == 1;
-        }
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
+    // Paper start - replace player loader system
+    public void setTickViewDistance(int distance) {
+        this.playerChunkManager.setTickDistance(distance);
+    }
+    // Paper end - replace player loader system
     public void setViewDistance(int watchDistance) {
-        int i = Mth.clamp(watchDistance + 1, 3, 33);
-        if (i != this.viewDistance) {
-            int j = this.viewDistance;
-            this.viewDistance = i;
-            this.distanceManager.updatePlayerTickets(this.viewDistance + 1);
+        int j = Mth.clamp(watchDistance + 1, (int) 3, (int) 33);
 
-            for(ChunkHolder chunkHolder : this.updatingChunkMap.values()) {
-                ChunkPos chunkPos = chunkHolder.getPos();
-                MutableObject<ClientboundLevelChunkWithLightPacket> mutableObject = new MutableObject<>();
-                this.getPlayers(chunkPos, false).forEach((player) -> {
-                    SectionPos sectionPos = player.getLastSectionPos();
-                    boolean bl = isChunkInRange(chunkPos.x, chunkPos.z, sectionPos.x(), sectionPos.z(), j);
-                    boolean bl2 = isChunkInRange(chunkPos.x, chunkPos.z, sectionPos.x(), sectionPos.z(), this.viewDistance);
-                    this.updateChunkTracking(player, chunkPos, mutableObject, bl, bl2);
-                });
-            }
+        if (j != this.viewDistance) {
+            int k = this.viewDistance;
+
+            this.viewDistance = j;
+            this.playerChunkManager.setLoadDistance(this.viewDistance); // Paper - replace player loader system
         }
 
     }
 
-    protected void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject<ClientboundLevelChunkWithLightPacket> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) {
+    public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject<java.util.Map<Object, ClientboundLevelChunkWithLightPacket>> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public // Paper - Anti-Xray - Bypass
         if (player.level == this.level) {
             if (newWithinViewDistance && !oldWithinViewDistance) {
-                ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(pos.toLong());
-                if (chunkHolder != null) {
-                    LevelChunk levelChunk = chunkHolder.getTickingChunk();
-                    if (levelChunk != null) {
-                        this.playerLoadedChunk(player, packet, levelChunk);
+                ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong());
+
+                if (playerchunk != null) {
+                    LevelChunk chunk = playerchunk.getSendingChunk(); // Paper - replace chunk loader system
+
+                    if (chunk != null) {
+                        this.playerLoadedChunk(player, packet, chunk);
                     }
 
                     DebugPackets.sendPoiPacketsForChunk(this.level, pos);
@@ -872,90 +840,202 @@
     }
 
     public int size() {
-        return this.visibleChunkMap.size();
+        return io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
     }
 
-    public net.minecraft.server.level.DistanceManager getDistanceManager() {
+    public DistanceManager getDistanceManager() {
         return this.distanceManager;
     }
 
     protected Iterable<ChunkHolder> getChunks() {
-        return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
+        return Iterables.unmodifiableIterable(io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
     }
 
     void dumpChunks(Writer writer) throws IOException {
-        CsvOutput csvOutput = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
-        TickingTracker tickingTracker = this.distanceManager.tickingTracker();
-
-        for(Long2ObjectMap.Entry<ChunkHolder> entry : this.visibleChunkMap.long2ObjectEntrySet()) {
-            long l = entry.getLongKey();
-            ChunkPos chunkPos = new ChunkPos(l);
-            ChunkHolder chunkHolder = entry.getValue();
-            Optional<ChunkAccess> optional = Optional.ofNullable(chunkHolder.getLastAvailable());
-            Optional<LevelChunk> optional2 = optional.flatMap((chunk) -> {
-                return chunk instanceof LevelChunk ? Optional.of((LevelChunk)chunk) : Optional.empty();
-            });
-            csvOutput.writeRow(chunkPos.x, chunkPos.z, chunkHolder.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getStatus).orElse((ChunkStatus)null), optional2.map(LevelChunk::getFullStatus).orElse((ChunkHolder.FullChunkStatus)null), printFuture(chunkHolder.getFullChunkFuture()), printFuture(chunkHolder.getTickingChunkFuture()), printFuture(chunkHolder.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(l), this.anyPlayerCloseEnoughForSpawning(chunkPos), optional2.map((levelChunk) -> {
-                return levelChunk.getBlockEntities().size();
-            }).orElse(0), tickingTracker.getTicketDebugString(l), tickingTracker.getLevel(l), optional2.map((levelChunk) -> {
-                return levelChunk.getBlockTicks().count();
-            }).orElse(0), optional2.map((levelChunk) -> {
-                return levelChunk.getFluidTicks().count();
-            }).orElse(0));
-        }
-
+        throw new UnsupportedOperationException(); // Paper - rewrite chunk system
     }
 
     private static String printFuture(CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> future) {
         try {
-            Either<LevelChunk, ChunkHolder.ChunkLoadingFailure> either = future.getNow((Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>)null);
-            return either != null ? either.map((chunk) -> {
+            Either<LevelChunk, ChunkHolder.ChunkLoadingFailure> either = (Either) future.getNow(null); // CraftBukkit - decompile error
+
+            return either != null ? (String) either.map((chunk) -> {
                 return "done";
-            }, (unloaded) -> {
+            }, (playerchunk_failure) -> {
                 return "unloaded";
             }) : "not completed";
-        } catch (CompletionException var2) {
-            return "failed " + var2.getCause().getMessage();
-        } catch (CancellationException var3) {
+        } catch (CompletionException completionexception) {
+            return "failed " + completionexception.getCause().getMessage();
+        } catch (CancellationException cancellationexception) {
             return "cancelled";
         }
     }
 
+    // Paper start - Asynchronous chunk io
+    @Nullable
+    @Override
+    public CompoundTag readSync(ChunkPos chunkcoordintpair) throws IOException {
+        // Paper start - rewrite chunk system
+        if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
+            return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData(
+                this.level, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA,
+                io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
+            );
+        }
+        // Paper end - rewrite chunk system
+        return super.readSync(chunkcoordintpair);
+    }
+
+    @Override
+    public void write(ChunkPos chunkcoordintpair, CompoundTag nbttagcompound) throws IOException {
+        // Paper start - rewrite chunk system
+        if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) {
+            io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave(
+                this.level, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound,
+                io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA);
+            return;
+        }
+        // Paper end - rewrite chunk system
+        super.write(chunkcoordintpair, nbttagcompound);
+    }
+    // Paper end
+
     private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos chunkPos) {
-        return this.read(chunkPos).thenApplyAsync((nbt) -> {
-            return nbt.map(this::upgradeChunkTag);
-        }, Util.backgroundExecutor());
+        // Paper start - Cache chunk status on disk
+        try {
+            return CompletableFuture.completedFuture(Optional.ofNullable(this.readConvertChunkSync(chunkPos)));
+        } catch (Throwable thr) {
+            return CompletableFuture.failedFuture(thr);
+        }
+        // Paper end - Cache chunk status on disk
     }
 
-    private CompoundTag upgradeChunkTag(CompoundTag nbt) {
-        return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, nbt, this.generator.getTypeNameForDataFixer());
+    // CraftBukkit start
+    private CompoundTag upgradeChunkTag(CompoundTag nbttagcompound, ChunkPos chunkcoordintpair) {
+        return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, this.generator.getTypeNameForDataFixer(), chunkcoordintpair, level);
+        // CraftBukkit end
+    }
+
+    // Paper start - Cache chunk status on disk
+    @Nullable
+    public CompoundTag readConvertChunkSync(ChunkPos pos) throws IOException {
+        CompoundTag nbttagcompound = this.readSync(pos);
+        // Paper start - Cache chunk status on disk
+        if (nbttagcompound == null) {
+            return null;
+        }
+
+        nbttagcompound = this.upgradeChunkTag(nbttagcompound, pos); // CraftBukkit
+        if (nbttagcompound == null) {
+            return null;
+        }
+
+        this.updateChunkStatusOnDisk(pos, nbttagcompound);
+
+        return nbttagcompound;
+        // Paper end
+    }
+
+    // Paper start - chunk status cache "api"
+    public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) {
+        net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos);
+
+        return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+    }
+
+    public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException {
+        net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, true);
+
+        if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) {
+            return null;
+        }
+
+        ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+
+        if (status != null) {
+            return status;
+        }
+
+        this.readChunk(chunkPos);
+
+        return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
+    }
+
+    public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException {
+        net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, false);
+
+        regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound));
+    }
+
+    public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) {
+        ChunkHolder chunkHolder = io.papermc.paper.chunk.system.ChunkSystem.getUnloadingChunkHolder(this.level, chunkX, chunkZ);
+        return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow();
     }
+    // Paper end
 
     boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) {
-        long l = pos.toLong();
-        if (!this.distanceManager.hasPlayersNearby(l)) {
+        // Spigot start
+        return this.anyPlayerCloseEnoughForSpawning(pos, false);
+    }
+
+    // Paper start - optimise anyPlayerCloseEnoughForSpawning
+    final boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkcoordintpair, boolean reducedRange) {
+        return this.anyPlayerCloseEnoughForSpawning(this.getUpdatingChunkIfPresent(chunkcoordintpair.toLong()), chunkcoordintpair, reducedRange);
+    }
+
+    final boolean anyPlayerCloseEnoughForSpawning(ChunkHolder playerchunk, ChunkPos chunkcoordintpair, boolean reducedRange) {
+        // this function is so hot that removing the map lookup call can have an order of magnitude impact on its performance
+        // tested and confirmed via System.nanoTime()
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange;
+        if (playersInRange == null) {
             return false;
+        }
+        Object[] backingSet = playersInRange.getBackingSet();
+
+        if (reducedRange) {
+            for (int i = 0, len = backingSet.length; i < len; ++i) {
+                Object raw = backingSet[i];
+                if (!(raw instanceof ServerPlayer player)) {
+                    continue;
+                }
+                // don't check spectator and whatnot, already handled by mob spawn map update
+                if (euclideanDistanceSquared(chunkcoordintpair, player) < player.lastEntitySpawnRadiusSquared) {
+                    return true; // in range
+                }
+            }
         } else {
-            for(ServerPlayer serverPlayer : this.playerMap.getPlayers(l)) {
-                if (this.playerIsCloseEnoughForSpawning(serverPlayer, pos)) {
-                    return true;
+            final double range = (DistanceManager.MOB_SPAWN_RANGE * 16) * (DistanceManager.MOB_SPAWN_RANGE * 16);
+            // before spigot, mob spawn range was actually mob spawn range + tick range, but it was split
+            for (int i = 0, len = backingSet.length; i < len; ++i) {
+                Object raw = backingSet[i];
+                if (!(raw instanceof ServerPlayer player)) {
+                    continue;
+                }
+                // don't check spectator and whatnot, already handled by mob spawn map update
+                if (euclideanDistanceSquared(chunkcoordintpair, player) < range) {
+                    return true; // in range
                 }
             }
-
-            return false;
         }
+        // no players in range
+        return false;
+        // Paper end - optimise anyPlayerCloseEnoughForSpawning
     }
 
     public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos pos) {
-        long l = pos.toLong();
-        if (!this.distanceManager.hasPlayersNearby(l)) {
+        long i = pos.toLong();
+
+        if (!this.distanceManager.hasPlayersNearby(i)) {
             return List.of();
         } else {
-            ImmutableList.Builder<ServerPlayer> builder = ImmutableList.builder();
+            Builder<ServerPlayer> builder = ImmutableList.builder();
+            Iterator iterator = this.playerMap.getPlayers(i).iterator();
 
-            for(ServerPlayer serverPlayer : this.playerMap.getPlayers(l)) {
-                if (this.playerIsCloseEnoughForSpawning(serverPlayer, pos)) {
-                    builder.add(serverPlayer);
+            while (iterator.hasNext()) {
+                ServerPlayer entityplayer = (ServerPlayer) iterator.next();
+
+                if (this.playerIsCloseEnoughForSpawning(entityplayer, pos, 16384.0D)) { // Spigot
+                    builder.add(entityplayer);
                 }
             }
 
@@ -963,12 +1043,13 @@
         }
     }
 
-    private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos pos) {
-        if (player.isSpectator()) {
+    private boolean playerIsCloseEnoughForSpawning(ServerPlayer entityplayer, ChunkPos chunkcoordintpair, double range) { // Spigot
+        if (entityplayer.isSpectator()) {
             return false;
         } else {
-            double d = euclideanDistanceSquared(pos, player);
-            return d < 16384.0D;
+            double d0 = ChunkMap.euclideanDistanceSquared(chunkcoordintpair, entityplayer);
+
+            return d0 < range; // Spigot
         }
     }
 
@@ -977,156 +1058,152 @@
     }
 
     void updatePlayerStatus(ServerPlayer player, boolean added) {
-        boolean bl = this.skipPlayer(player);
-        boolean bl2 = this.playerMap.ignoredOrUnknown(player);
+        boolean flag1 = this.skipPlayer(player);
+        boolean flag2 = this.playerMap.ignoredOrUnknown(player);
         int i = SectionPos.blockToSectionCoord(player.getBlockX());
         int j = SectionPos.blockToSectionCoord(player.getBlockZ());
+
         if (added) {
-            this.playerMap.addPlayer(ChunkPos.asLong(i, j), player, bl);
+            this.playerMap.addPlayer(ChunkPos.asLong(i, j), player, flag1);
             this.updatePlayerPos(player);
-            if (!bl) {
-                this.distanceManager.addPlayer(SectionPos.of(player), player);
+            if (!flag1) {
+                this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
             }
+            this.addPlayerToDistanceMaps(player); // Paper - distance maps
         } else {
-            SectionPos sectionPos = player.getLastSectionPos();
-            this.playerMap.removePlayer(sectionPos.chunk().toLong(), player);
-            if (!bl2) {
-                this.distanceManager.removePlayer(sectionPos, player);
-            }
-        }
+            SectionPos sectionposition = player.getLastSectionPos();
 
-        for(int k = i - this.viewDistance - 1; k <= i + this.viewDistance + 1; ++k) {
-            for(int l = j - this.viewDistance - 1; l <= j + this.viewDistance + 1; ++l) {
-                if (isChunkInRange(k, l, i, j, this.viewDistance)) {
-                    ChunkPos chunkPos = new ChunkPos(k, l);
-                    this.updateChunkTracking(player, chunkPos, new MutableObject<>(), !added, added);
-                }
+            this.playerMap.removePlayer(sectionposition.chunk().toLong(), player);
+            if (!flag2) {
+                this.distanceManager.removePlayer(sectionposition, player);
             }
+            this.removePlayerFromDistanceMaps(player); // Paper - distance maps
         }
 
+        // Paper - handled by player chunk loader
+
     }
 
     private SectionPos updatePlayerPos(ServerPlayer player) {
-        SectionPos sectionPos = SectionPos.of(player);
-        player.setLastSectionPos(sectionPos);
-        player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionPos.x(), sectionPos.z()));
-        return sectionPos;
+        SectionPos sectionposition = SectionPos.of((EntityAccess) player);
+
+        player.setLastSectionPos(sectionposition);
+        //player.connection.send(new ClientboundSetChunkCacheCenterPacket(sectionposition.x(), sectionposition.z())); // Paper - handled by player chunk loader
+        return sectionposition;
     }
 
     public void move(ServerPlayer player) {
-        for(ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
-            if (trackedEntity.entity == player) {
-                trackedEntity.updatePlayers(this.level.players());
-            } else {
-                trackedEntity.updatePlayer(player);
-            }
-        }
+        // Paper - delay this logic for the entity tracker tick, no need to duplicate it
 
         int i = SectionPos.blockToSectionCoord(player.getBlockX());
         int j = SectionPos.blockToSectionCoord(player.getBlockZ());
-        SectionPos sectionPos = player.getLastSectionPos();
-        SectionPos sectionPos2 = SectionPos.of(player);
-        long l = sectionPos.chunk().toLong();
-        long m = sectionPos2.chunk().toLong();
-        boolean bl = this.playerMap.ignored(player);
-        boolean bl2 = this.skipPlayer(player);
-        boolean bl3 = sectionPos.asLong() != sectionPos2.asLong();
-        if (bl3 || bl != bl2) {
+        SectionPos sectionposition = player.getLastSectionPos();
+        SectionPos sectionposition1 = SectionPos.of((EntityAccess) player);
+        long k = sectionposition.chunk().toLong();
+        long l = sectionposition1.chunk().toLong();
+        boolean flag = this.playerMap.ignored(player);
+        boolean flag1 = this.skipPlayer(player);
+        boolean flag2 = sectionposition.asLong() != sectionposition1.asLong();
+
+        if (flag2 || flag != flag1) {
             this.updatePlayerPos(player);
-            if (!bl) {
-                this.distanceManager.removePlayer(sectionPos, player);
+            if (!flag) {
+                this.distanceManager.removePlayer(sectionposition, player);
             }
 
-            if (!bl2) {
-                this.distanceManager.addPlayer(sectionPos2, player);
+            if (!flag1) {
+                this.distanceManager.addPlayer(sectionposition1, player);
             }
 
-            if (!bl && bl2) {
+            if (!flag && flag1) {
                 this.playerMap.ignorePlayer(player);
             }
 
-            if (bl && !bl2) {
+            if (flag && !flag1) {
                 this.playerMap.unIgnorePlayer(player);
             }
 
-            if (l != m) {
-                this.playerMap.updatePlayer(l, m, player);
+            if (k != l) {
+                this.playerMap.updatePlayer(k, l, player);
             }
         }
 
-        int k = sectionPos.x();
-        int n = sectionPos.z();
-        if (Math.abs(k - i) <= this.viewDistance * 2 && Math.abs(n - j) <= this.viewDistance * 2) {
-            int o = Math.min(i, k) - this.viewDistance - 1;
-            int p = Math.min(j, n) - this.viewDistance - 1;
-            int q = Math.max(i, k) + this.viewDistance + 1;
-            int r = Math.max(j, n) + this.viewDistance + 1;
+        int i1 = sectionposition.x();
+        int j1 = sectionposition.z();
+        int k1;
+        int l1;
 
-            for(int s = o; s <= q; ++s) {
-                for(int t = p; t <= r; ++t) {
-                    boolean bl4 = isChunkInRange(s, t, k, n, this.viewDistance);
-                    boolean bl5 = isChunkInRange(s, t, i, j, this.viewDistance);
-                    this.updateChunkTracking(player, new ChunkPos(s, t), new MutableObject<>(), bl4, bl5);
-                }
-            }
-        } else {
-            for(int u = k - this.viewDistance - 1; u <= k + this.viewDistance + 1; ++u) {
-                for(int v = n - this.viewDistance - 1; v <= n + this.viewDistance + 1; ++v) {
-                    if (isChunkInRange(u, v, k, n, this.viewDistance)) {
-                        boolean bl6 = true;
-                        boolean bl7 = false;
-                        this.updateChunkTracking(player, new ChunkPos(u, v), new MutableObject<>(), true, false);
-                    }
-                }
-            }
+        // Paper - replaced by PlayerChunkLoader
 
-            for(int w = i - this.viewDistance - 1; w <= i + this.viewDistance + 1; ++w) {
-                for(int x = j - this.viewDistance - 1; x <= j + this.viewDistance + 1; ++x) {
-                    if (isChunkInRange(w, x, i, j, this.viewDistance)) {
-                        boolean bl8 = false;
-                        boolean bl9 = true;
-                        this.updateChunkTracking(player, new ChunkPos(w, x), new MutableObject<>(), false, true);
-                    }
-                }
-            }
-        }
+        this.updateMaps(player); // Paper - distance maps
+        this.playerChunkManager.updatePlayer(player); // Paper - respond to movement immediately
 
     }
 
     @Override
     public List<ServerPlayer> getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) {
-        Set<ServerPlayer> set = this.playerMap.getPlayers(chunkPos.toLong());
-        ImmutableList.Builder<ServerPlayer> builder = ImmutableList.builder();
+        // Paper start - per player view distance
+        // there can be potential desync with player's last mapped section and the view distance map, so use the
+        // view distance map here.
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> players = this.playerChunkManager.broadcastMap.getObjectsInRange(chunkPos);
+        if (players == null) {
+            return java.util.Collections.emptyList();
+        }
 
-        for(ServerPlayer serverPlayer : set) {
-            SectionPos sectionPos = serverPlayer.getLastSectionPos();
-            if (onlyOnWatchDistanceEdge && isChunkOnRangeBorder(chunkPos.x, chunkPos.z, sectionPos.x(), sectionPos.z(), this.viewDistance) || !onlyOnWatchDistanceEdge && isChunkInRange(chunkPos.x, chunkPos.z, sectionPos.x(), sectionPos.z(), this.viewDistance)) {
-                builder.add(serverPlayer);
+        List<ServerPlayer> ret = new java.util.ArrayList<>(players.size());
+
+        Object[] backingSet = players.getBackingSet();
+        for (int i = 0, len = backingSet.length; i < len; ++i) {
+            if (!(backingSet[i] instanceof ServerPlayer player)) {
+                continue;
             }
+            if (!this.playerChunkManager.isChunkSent(player, chunkPos.x, chunkPos.z, onlyOnWatchDistanceEdge)) {
+                continue;
+            }
+            ret.add(player);
         }
 
-        return builder.build();
+        return ret;
+        // Paper end - per player view distance
     }
 
     public void addEntity(Entity entity) {
+        org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot
+        // Paper start - ignore and warn about illegal addEntity calls instead of crashing server
+        if (!entity.valid || entity.level != this.level || this.entityMap.containsKey(entity.getId())) {
+            LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName()
+                + ": " + entity  + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable());
+            return;
+        }
+        if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Delay adding to tracker until after list packets
+        // Paper end
         if (!(entity instanceof EnderDragonPart)) {
-            EntityType<?> entityType = entity.getType();
-            int i = entityType.clientTrackingRange() * 16;
+            EntityType<?> entitytypes = entity.getType();
+            int i = entitytypes.clientTrackingRange() * 16;
+            i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot
+
             if (i != 0) {
-                int j = entityType.updateInterval();
+                int j = entitytypes.updateInterval();
+
                 if (this.entityMap.containsKey(entity.getId())) {
-                    throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Entity is already tracked!"));
+                    throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Entity is already tracked!"));
                 } else {
-                    ChunkMap.TrackedEntity trackedEntity = new ChunkMap.TrackedEntity(entity, i, j, entityType.trackDeltas());
-                    this.entityMap.put(entity.getId(), trackedEntity);
-                    trackedEntity.updatePlayers(this.level.players());
+                    ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas());
+
+                    entity.tracker = playerchunkmap_entitytracker; // Paper - Fast access to tracker
+                    this.entityMap.put(entity.getId(), playerchunkmap_entitytracker);
+                    playerchunkmap_entitytracker.updatePlayers(entity.getPlayersInTrackRange()); // Paper - don't search all players
                     if (entity instanceof ServerPlayer) {
-                        ServerPlayer serverPlayer = (ServerPlayer)entity;
-                        this.updatePlayerStatus(serverPlayer, true);
+                        ServerPlayer entityplayer = (ServerPlayer) entity;
 
-                        for(ChunkMap.TrackedEntity trackedEntity2 : this.entityMap.values()) {
-                            if (trackedEntity2.entity != serverPlayer) {
-                                trackedEntity2.updatePlayer(serverPlayer);
+                        this.updatePlayerStatus(entityplayer, true);
+                        ObjectIterator objectiterator = this.entityMap.values().iterator();
+
+                        while (objectiterator.hasNext()) {
+                            ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) objectiterator.next();
+
+                            if (playerchunkmap_entitytracker1.entity != entityplayer) {
+                                playerchunkmap_entitytracker1.updatePlayer(entityplayer);
                             }
                         }
                     }
@@ -1137,104 +1214,213 @@
     }
 
     protected void removeEntity(Entity entity) {
-        if (entity instanceof ServerPlayer serverPlayer) {
-            this.updatePlayerStatus(serverPlayer, false);
+        org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot
+        if (entity instanceof ServerPlayer) {
+            ServerPlayer entityplayer = (ServerPlayer) entity;
 
-            for(ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
-                trackedEntity.removePlayer(serverPlayer);
+            this.updatePlayerStatus(entityplayer, false);
+            ObjectIterator objectiterator = this.entityMap.values().iterator();
+
+            while (objectiterator.hasNext()) {
+                ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
+
+                playerchunkmap_entitytracker.removePlayer(entityplayer);
             }
         }
 
-        ChunkMap.TrackedEntity trackedEntity2 = this.entityMap.remove(entity.getId());
-        if (trackedEntity2 != null) {
-            trackedEntity2.broadcastRemoved();
+        ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) this.entityMap.remove(entity.getId());
+
+        if (playerchunkmap_entitytracker1 != null) {
+            playerchunkmap_entitytracker1.broadcastRemoved();
         }
+        entity.tracker = null; // Paper - We're no longer tracked
+    }
 
+    // Paper start - optimised tracker
+    private final void processTrackQueue() {
+        this.level.timings.tracker1.startTiming();
+        try {
+            for (TrackedEntity tracker : this.entityMap.values()) {
+                // update tracker entry
+                tracker.updatePlayers(tracker.entity.getPlayersInTrackRange());
+            }
+        } finally {
+            this.level.timings.tracker1.stopTiming();
+        }
+
+
+        this.level.timings.tracker2.startTiming();
+        try {
+            for (TrackedEntity tracker : this.entityMap.values()) {
+                tracker.serverEntity.sendChanges();
+            }
+        } finally {
+            this.level.timings.tracker2.stopTiming();
+        }
     }
+    // Paper end - optimised tracker
 
     protected void tick() {
+        // Paper start - optimized tracker
+        if (true) {
+            this.processTrackQueue();
+            return;
+        }
+        // Paper end - optimized tracker
         List<ServerPlayer> list = Lists.newArrayList();
-        List<ServerPlayer> list2 = this.level.players();
+        List<ServerPlayer> list1 = this.level.players();
+        ObjectIterator objectiterator = this.entityMap.values().iterator();
+        level.timings.tracker1.startTiming(); // Paper
+
+        ChunkMap.TrackedEntity playerchunkmap_entitytracker;
+
+        while (objectiterator.hasNext()) {
+            playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
+            SectionPos sectionposition = playerchunkmap_entitytracker.lastSectionPos;
+            SectionPos sectionposition1 = SectionPos.of((EntityAccess) playerchunkmap_entitytracker.entity);
+            boolean flag = !Objects.equals(sectionposition, sectionposition1);
+
+            if (flag) {
+                playerchunkmap_entitytracker.updatePlayers(list1);
+                Entity entity = playerchunkmap_entitytracker.entity;
 
-        for(ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
-            SectionPos sectionPos = trackedEntity.lastSectionPos;
-            SectionPos sectionPos2 = SectionPos.of(trackedEntity.entity);
-            boolean bl = !Objects.equals(sectionPos, sectionPos2);
-            if (bl) {
-                trackedEntity.updatePlayers(list2);
-                Entity entity = trackedEntity.entity;
                 if (entity instanceof ServerPlayer) {
-                    list.add((ServerPlayer)entity);
+                    list.add((ServerPlayer) entity);
                 }
 
-                trackedEntity.lastSectionPos = sectionPos2;
+                playerchunkmap_entitytracker.lastSectionPos = sectionposition1;
             }
 
-            if (bl || this.distanceManager.inEntityTickingRange(sectionPos2.chunk().toLong())) {
-                trackedEntity.serverEntity.sendChanges();
+            if (flag || this.distanceManager.inEntityTickingRange(sectionposition1.chunk().toLong())) {
+                playerchunkmap_entitytracker.serverEntity.sendChanges();
             }
         }
+        level.timings.tracker1.stopTiming(); // Paper
 
         if (!list.isEmpty()) {
-            for(ChunkMap.TrackedEntity trackedEntity2 : this.entityMap.values()) {
-                trackedEntity2.updatePlayers(list);
+            objectiterator = this.entityMap.values().iterator();
+
+            level.timings.tracker2.startTiming(); // Paper
+            while (objectiterator.hasNext()) {
+                playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next();
+                playerchunkmap_entitytracker.updatePlayers(list);
             }
+            level.timings.tracker2.stopTiming(); // Paper
         }
 
     }
 
     public void broadcast(Entity entity, Packet<?> packet) {
-        ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId());
-        if (trackedEntity != null) {
-            trackedEntity.broadcast(packet);
+        ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId());
+
+        if (playerchunkmap_entitytracker != null) {
+            playerchunkmap_entitytracker.broadcast(packet);
         }
 
     }
 
     protected void broadcastAndSend(Entity entity, Packet<?> packet) {
-        ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId());
-        if (trackedEntity != null) {
-            trackedEntity.broadcastAndSend(packet);
-        }
-
-    }
+        ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId());
 
-    private void playerLoadedChunk(ServerPlayer player, MutableObject<ClientboundLevelChunkWithLightPacket> cachedDataPacket, LevelChunk chunk) {
-        if (cachedDataPacket.getValue() == null) {
-            cachedDataPacket.setValue(new ClientboundLevelChunkWithLightPacket(chunk, this.lightEngine, (BitSet)null, (BitSet)null, true));
+        if (playerchunkmap_entitytracker != null) {
+            playerchunkmap_entitytracker.broadcastAndSend(packet);
         }
 
-        player.trackChunk(chunk.getPos(), cachedDataPacket.getValue());
-        DebugPackets.sendPoiPacketsForChunk(this.level, chunk.getPos());
-        List<Entity> list = Lists.newArrayList();
-        List<Entity> list2 = Lists.newArrayList();
+    }
 
-        for(ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
-            Entity entity = trackedEntity.entity;
-            if (entity != player && entity.chunkPosition().equals(chunk.getPos())) {
-                trackedEntity.updatePlayer(player);
-                if (entity instanceof Mob && ((Mob)entity).getLeashHolder() != null) {
-                    list.add(entity);
-                }
+    // Paper start - Fix MC-162253
+    /**
+     * Returns the light mask for the given chunk consisting of all non-empty sections that may need sending.
+     */
+    private BitSet lightMask(final LevelChunk chunk) {
+        final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();
+        final BitSet mask = new BitSet(this.lightEngine.getLightSectionCount());
 
-                if (!entity.getPassengers().isEmpty()) {
-                    list2.add(entity);
-                }
+        // There are 2 more light sections than chunk sections so when iterating over
+        // sections we have to increment the index by 1
+        for (int i = 0; i < sections.length; i++) {
+            if (!sections[i].hasOnlyAir()) {
+                // Whenever a section is not empty, it can change lighting for the section itself (i + 1), the section below, and the section above
+                mask.set(i);
+                mask.set(i + 1);
+                mask.set(i + 2);
+                i++; // We can skip the already set upper section
             }
         }
+        return mask;
+    }
 
-        if (!list.isEmpty()) {
-            for(Entity entity2 : list) {
-                player.connection.send(new ClientboundSetEntityLinkPacket(entity2, ((Mob)entity2).getLeashHolder()));
+    /**
+     * Returns the ceiling light mask of all sections that are equal or lower to the highest non-empty section.
+     */
+    private BitSet ceilingLightMask(final LevelChunk chunk) {
+        final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();
+        for (int i = sections.length - 1; i >= 0; i--) {
+            if (!sections[i].hasOnlyAir()) {
+                // Add one to get the light section, one because blocks in the section above may change, and another because BitSet's toIndex is exclusive
+                final int highest = i + 3;
+                final BitSet mask = new BitSet(highest);
+                mask.set(0, highest);
+                return mask;
             }
         }
+        return new BitSet();
+    }
+    // Paper end - Fix MC-162253
 
-        if (!list2.isEmpty()) {
-            for(Entity entity3 : list2) {
-                player.connection.send(new ClientboundSetPassengersPacket(entity3));
-            }
+    // Paper start - Anti-Xray - Bypass
+    private void playerLoadedChunk(ServerPlayer player, MutableObject<java.util.Map<Object, ClientboundLevelChunkWithLightPacket>> cachedDataPackets, LevelChunk chunk) {
+        if (cachedDataPackets.getValue() == null) {
+            cachedDataPackets.setValue(new java.util.HashMap<>());
         }
 
+        Boolean shouldModify = chunk.getLevel().chunkPacketBlockController.shouldModify(player, chunk);
+        player.trackChunk(chunk.getPos(), (Packet) cachedDataPackets.getValue().computeIfAbsent(shouldModify, (s) -> {
+            // Paper start - Fix MC-162253
+            final int viewDistance = getEffectiveViewDistance();
+            final int playerChunkX = player.getBlockX() >> 4;
+            final int playerChunkZ = player.getBlockZ() >> 4;
+
+            // For all loaded neighbours, send sky light for empty sections above highest non-empty section (+1) of the center chunk
+            // otherwise the client will try to calculate lighting there on its own
+            final BitSet lightMask = lightMask(chunk);
+            if (!lightMask.isEmpty()) {
+                for (int x = -1; x <= 1; x++) {
+                    for (int z = -1; z <= 1; z++) {
+                        if (x == 0 && z == 0) {
+                            continue;
+                        }
+
+                        if (!chunk.isNeighbourLoaded(x, z)) {
+                            continue;
+                        }
+
+                        final int neighborChunkX = chunk.getPos().x + x;
+                        final int neighborChunkZ = chunk.getPos().z + z;
+                        final int distX = Math.abs(playerChunkX - neighborChunkX);
+                        final int distZ = Math.abs(playerChunkZ - neighborChunkZ);
+                        if (Math.max(distX, distZ) > viewDistance) {
+                            continue;
+                        }
+
+                        final LevelChunk neighbor = chunk.getRelativeNeighbourIfLoaded(x, z);
+                        final BitSet updateLightMask = (BitSet) lightMask.clone();
+                        updateLightMask.andNot(ceilingLightMask(neighbor));
+                        if (updateLightMask.isEmpty()) {
+                            continue;
+                        }
+
+                        player.connection.send(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(new ChunkPos(neighborChunkX, neighborChunkZ), this.lightEngine, updateLightMask, null, true));
+                    }
+                }
+            }
+            // Paper end - Fix MC-162253
+            return new ClientboundLevelChunkWithLightPacket(chunk, this.lightEngine, (BitSet) null, (BitSet) null, true, (Boolean) s);
+        }));
+        // Paper end
+        DebugPackets.sendPoiPacketsForChunk(this.level, chunk.getPos());
+        // Paper - no longer needed - this was used to account for clients bugging out since they needed a chunk to store entities, but they no longer need a chunk
+
     }
 
     public PoiManager getPoiManager() {
@@ -1249,14 +1435,15 @@
         this.chunkStatusListener.onChunkStatusChange(chunkPos, levelType);
     }
 
-    class DistanceManager extends net.minecraft.server.level.DistanceManager {
-        protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) {
-            super(workerExecutor, mainThreadExecutor);
+    private class ChunkDistanceManager extends DistanceManager {
+
+        protected ChunkDistanceManager(Executor workerExecutor, Executor mainThreadExecutor) {
+            super(workerExecutor, mainThreadExecutor, ChunkMap.this);
         }
 
         @Override
         protected boolean isChunkToRemove(long pos) {
-            return ChunkMap.this.toDrop.contains(pos);
+            throw new UnsupportedOperationException(); // Paper - rewrite chunk system
         }
 
         @Nullable
@@ -1267,42 +1454,77 @@
 
         @Nullable
         @Override
-        protected ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int i) {
-            return ChunkMap.this.updateChunkScheduling(pos, level, holder, i);
+        protected ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) {
+            return ChunkMap.this.updateChunkScheduling(pos, level, holder, k);
         }
     }
 
     public class TrackedEntity {
+
         final ServerEntity serverEntity;
         final Entity entity;
         private final int range;
         SectionPos lastSectionPos;
-        public final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
+        public final Set<ServerPlayerConnection> seenBy = new ReferenceOpenHashSet<>(); // Paper - optimise map impl
 
-        public TrackedEntity(Entity entity, int maxDistance, int tickInterval, boolean alwaysUpdateVelocity) {
-            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, tickInterval, alwaysUpdateVelocity, this::broadcast);
+        public TrackedEntity(Entity entity, int i, int j, boolean flag) {
+            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit
             this.entity = entity;
-            this.range = maxDistance;
-            this.lastSectionPos = SectionPos.of(entity);
+            this.range = i;
+            this.lastSectionPos = SectionPos.of((EntityAccess) entity);
         }
 
-        @Override
-        public boolean equals(Object object) {
-            if (object instanceof ChunkMap.TrackedEntity) {
-                return ((ChunkMap.TrackedEntity)object).entity.getId() == this.entity.getId();
-            } else {
-                return false;
+        // Paper start - use distance map to optimise tracker
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> lastTrackerCandidates;
+
+        final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newTrackerCandidates) {
+            com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> oldTrackerCandidates = this.lastTrackerCandidates;
+            this.lastTrackerCandidates = newTrackerCandidates;
+
+            if (newTrackerCandidates != null) {
+                Object[] rawData = newTrackerCandidates.getBackingSet();
+                for (int i = 0, len = rawData.length; i < len; ++i) {
+                    Object raw = rawData[i];
+                    if (!(raw instanceof ServerPlayer)) {
+                        continue;
+                    }
+                    ServerPlayer player = (ServerPlayer)raw;
+                    this.updatePlayer(player);
+                }
+            }
+
+            if (oldTrackerCandidates == newTrackerCandidates) {
+                // this is likely the case.
+                // means there has been no range changes, so we can just use the above for tracking.
+                return;
+            }
+
+            // stuff could have been removed, so we need to check the trackedPlayers set
+            // for players that were removed
+
+            for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME
+                if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) {
+                    this.updatePlayer(conn.getPlayer());
+                }
             }
         }
+        // Paper end - use distance map to optimise tracker
+
+        public boolean equals(Object object) {
+            return object instanceof ChunkMap.TrackedEntity ? ((ChunkMap.TrackedEntity) object).entity.getId() == this.entity.getId() : false;
+        }
 
-        @Override
         public int hashCode() {
             return this.entity.getId();
         }
 
         public void broadcast(Packet<?> packet) {
-            for(ServerPlayerConnection serverPlayerConnection : this.seenBy) {
-                serverPlayerConnection.send(packet);
+            Iterator iterator = this.seenBy.iterator();
+
+            while (iterator.hasNext()) {
+                ServerPlayerConnection serverplayerconnection = (ServerPlayerConnection) iterator.next();
+
+                serverplayerconnection.send(packet);
             }
 
         }
@@ -1310,19 +1532,24 @@
         public void broadcastAndSend(Packet<?> packet) {
             this.broadcast(packet);
             if (this.entity instanceof ServerPlayer) {
-                ((ServerPlayer)this.entity).connection.send(packet);
+                ((ServerPlayer) this.entity).connection.send(packet);
             }
 
         }
 
         public void broadcastRemoved() {
-            for(ServerPlayerConnection serverPlayerConnection : this.seenBy) {
-                this.serverEntity.removePairing(serverPlayerConnection.getPlayer());
+            Iterator iterator = this.seenBy.iterator();
+
+            while (iterator.hasNext()) {
+                ServerPlayerConnection serverplayerconnection = (ServerPlayerConnection) iterator.next();
+
+                this.serverEntity.removePairing(serverplayerconnection.getPlayer());
             }
 
         }
 
         public void removePlayer(ServerPlayer player) {
+            org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot
             if (this.seenBy.remove(player.connection)) {
                 this.serverEntity.removePairing(player);
             }
@@ -1330,13 +1557,24 @@
         }
 
         public void updatePlayer(ServerPlayer player) {
+            org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
             if (player != this.entity) {
-                Vec3 vec3 = player.position().subtract(this.entity.position());
-                double d = (double)Math.min(this.getEffectiveRange(), (ChunkMap.this.viewDistance - 1) * 16);
-                double e = vec3.x * vec3.x + vec3.z * vec3.z;
-                double f = d * d;
-                boolean bl = e <= f && this.entity.broadcastToPlayer(player);
-                if (bl) {
+                // Paper start - remove allocation of Vec3D here
+                // Vec3 vec3d = player.position().subtract(this.entity.position());
+                double vec3d_dx = player.getX() - this.entity.getX();
+                double vec3d_dz = player.getZ() - this.entity.getZ();
+                // Paper end - remove allocation of Vec3D here
+                double d0 = (double) Math.min(this.getEffectiveRange(), io.papermc.paper.chunk.PlayerChunkLoader.getSendViewDistance(player) * 16); // Paper - per player view distance
+                double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper
+                double d2 = d0 * d0;
+                boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player);
+
+                // CraftBukkit start - respect vanish API
+                if (!player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) {
+                    flag = false;
+                }
+                // CraftBukkit end
+                if (flag) {
                     if (this.seenBy.add(player.connection)) {
                         this.serverEntity.addPairing(player);
                     }
@@ -1353,9 +1591,13 @@
 
         private int getEffectiveRange() {
             int i = this.range;
+            Iterator iterator = this.entity.getIndirectPassengers().iterator();
 
-            for(Entity entity : this.entity.getIndirectPassengers()) {
+            while (iterator.hasNext()) {
+                Entity entity = (Entity) iterator.next();
                 int j = entity.getType().clientTrackingRange() * 16;
+                j = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, j); // Paper
+
                 if (j > i) {
                     i = j;
                 }
@@ -1365,10 +1607,33 @@
         }
 
         public void updatePlayers(List<ServerPlayer> players) {
-            for(ServerPlayer serverPlayer : players) {
-                this.updatePlayer(serverPlayer);
+            Iterator iterator = players.iterator();
+
+            while (iterator.hasNext()) {
+                ServerPlayer entityplayer = (ServerPlayer) iterator.next();
+
+                this.updatePlayer(entityplayer);
+            }
+
+        }
+        // Paper start
+        // This method should only be used if the data of an entity could have became desynced
+        // due to interactions on the client.
+        public void resendPossiblyDesyncedEntity(ServerPlayer player) {
+            if (player.getBukkitEntity().canSee(entity.getBukkitEntity())) {
+                // This will prevent the entity data from being cleared, so that ONLY this player
+                // will have its entity data updated and it won't dirty the datatracker for other players.
+                this.serverEntity.clearDirtyEntityData = false;
+                this.serverEntity.sendPairingData(player.connection::send, player);
+                this.serverEntity.clearDirtyEntityData = true;
             }
+        }
 
+        public void resendPossiblyDesyncedEntityData(ServerPlayer player) {
+            if (player.getBukkitEntity().canSee(entity.getBukkitEntity())) {
+                player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket(entity.getId(), entity.getEntityData(), true, false));
+            }
         }
+        // Paper end
     }
 }
