--- a/net/minecraft/server/level/ChunkMap.java
+++ b/net/minecraft/server/level/ChunkMap.java
@@ -145,6 +_,33 @@
     public int serverViewDistance;
     private final WorldGenContext worldGenContext;
 
+    // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
+    public final CallbackExecutor callbackExecutor = new CallbackExecutor();
+    public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable {
+
+        private final java.util.Queue<Runnable> queue = new java.util.ArrayDeque<>();
+
+        @Override
+        public void execute(Runnable runnable) {
+            this.queue.add(runnable);
+        }
+
+        @Override
+        public void run() {
+            Runnable task;
+            while ((task = this.queue.poll()) != null) {
+                task.run();
+            }
+        }
+    };
+    // CraftBukkit end
+
+    // Paper start
+    public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
+        return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+    }
+    // Paper end
+
     public ChunkMap(
         ServerLevel level,
         LevelStorageSource.LevelStorageAccess levelStorageAccess,
@@ -171,13 +_,19 @@
         this.level = level;
         RegistryAccess registryAccess = level.registryAccess();
         long seed = level.getSeed();
-        if (generator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
+        // CraftBukkit start - SPIGOT-7051: It's a rigged game! Use delegate for random state creation, otherwise it is not so random.
+        ChunkGenerator randomGenerator = generator;
+        if (randomGenerator instanceof org.bukkit.craftbukkit.generator.CustomChunkGenerator customChunkGenerator) {
+            randomGenerator = customChunkGenerator.getDelegate();
+        }
+        if (randomGenerator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
+        // CraftBukkit end
             this.randomState = RandomState.create(noiseBasedChunkGenerator.generatorSettings().value(), registryAccess.lookupOrThrow(Registries.NOISE), seed);
         } else {
             this.randomState = RandomState.create(NoiseGeneratorSettings.dummy(), registryAccess.lookupOrThrow(Registries.NOISE), seed);
         }
 
-        this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, seed);
+        this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, seed, level.spigotConfig); // Spigot
         this.mainThreadExecutor = mainThreadExecutor;
         ConsecutiveExecutor consecutiveExecutor = new ConsecutiveExecutor(dispatcher, "worldgen");
         this.progressListener = progressListener;
@@ -207,6 +_,12 @@
         this.chunksToEagerlySave.add(chunkPos.toLong());
     }
 
+    // Paper start
+    public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
+        return -1;
+    }
+    // Paper end
+
     protected ChunkGenerator generator() {
         return this.worldGenContext.generator();
     }
@@ -354,9 +_,9 @@
                 }
             );
         stringBuilder.append("Updating:").append(System.lineSeparator());
-        this.updatingChunkMap.values().forEach(consumer);
+        ca.spottedleaf.moonrise.common.PlatformHooks.get().getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
         stringBuilder.append("Visible:").append(System.lineSeparator());
-        this.visibleChunkMap.values().forEach(consumer);
+        ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level).forEach(consumer); // Paper
         CrashReport crashReport = CrashReport.forThrowable(exception, "Chunk loading");
         CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk loading");
         crashReportCategory.setDetail("Details", details);
@@ -392,6 +_,9 @@
                     holder.setTicketLevel(newLevel);
                 } else {
                     holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this);
+                    // Paper start
+                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderCreate(this.level, holder);
+                    // Paper end
                 }
 
                 this.updatingChunkMap.put(chunkPos, holder);
@@ -420,8 +_,8 @@
 
     protected void saveAllChunks(boolean flush) {
         if (flush) {
-            List<ChunkHolder> list = this.visibleChunkMap
-                .values()
+            List<ChunkHolder> list = ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level) // Paper - moonrise
+                //.values() // Paper - moonrise
                 .stream()
                 .filter(ChunkHolder::wasAccessibleSinceLastSave)
                 .peek(ChunkHolder::refreshAccessibility)
@@ -447,7 +_,7 @@
             this.nextChunkSaveTime.clear();
             long millis = Util.getMillis();
 
-            for (ChunkHolder chunkHolder : this.visibleChunkMap.values()) {
+            for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper
                 this.saveChunkIfNeeded(chunkHolder, millis);
             }
         }
@@ -468,6 +_,7 @@
     public boolean hasWork() {
         return this.lightEngine.hasLightWork()
             || !this.pendingUnloads.isEmpty()
+            || ca.spottedleaf.moonrise.common.PlatformHooks.get().hasAnyChunkHolders(this.level) // Paper - moonrise
             || !this.updatingChunkMap.isEmpty()
             || this.poiManager.hasWork()
             || !this.toDrop.isEmpty()
@@ -526,7 +_,11 @@
                 this.scheduleUnload(chunkPos, chunkHolder);
             } else {
                 ChunkAccess latestChunk = chunkHolder.getLatestChunk();
-                if (this.pendingUnloads.remove(chunkPos, chunkHolder) && latestChunk != null) {
+                // Paper start
+                boolean removed;
+                if ((removed = this.pendingUnloads.remove(chunkPos, chunkHolder)) && latestChunk != null) {
+                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
+                    // Paper end
                     if (latestChunk instanceof LevelChunk levelChunk) {
                         levelChunk.setLoaded(false);
                     }
@@ -540,7 +_,9 @@
                     this.lightEngine.tryScheduleUpdate();
                     this.progressListener.onStatusChange(latestChunk.getPos(), null);
                     this.nextChunkSaveTime.remove(latestChunk.getPos().toLong());
-                }
+                } else if (removed) { // Paper start
+                    ca.spottedleaf.moonrise.common.PlatformHooks.get().onChunkHolderDelete(this.level, chunkHolder);
+                } // Paper end
             }
         }, this.unloadQueue::add).whenComplete((_void, error) -> {
             if (error != null) {
@@ -856,7 +_,7 @@
     }
 
     public int size() {
-        return this.visibleChunkMap.size();
+        return ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolderCount(this.level); // Paper
     }
 
     public net.minecraft.server.level.DistanceManager getDistanceManager() {
@@ -864,7 +_,7 @@
     }
 
     protected Iterable<ChunkHolder> getChunks() {
-        return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
+        return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)); // Paper
     }
 
     void dumpChunks(Writer writer) throws IOException {
@@ -888,10 +_,10 @@
             .build(writer);
         TickingTracker tickingTracker = this.distanceManager.tickingTracker();
 
-        for (Entry<ChunkHolder> entry : this.visibleChunkMap.long2ObjectEntrySet()) {
-            long longKey = entry.getLongKey();
+        for (ChunkHolder entry : ca.spottedleaf.moonrise.common.PlatformHooks.get().getVisibleChunkHolders(this.level)) { // Paper - Moonrise
+            long longKey = entry.pos.toLong(); // Paper - Moonrise
             ChunkPos chunkPos = new ChunkPos(longKey);
-            ChunkHolder chunkHolder = entry.getValue();
+            ChunkHolder chunkHolder = entry; // Paper - Moonrise
             Optional<ChunkAccess> optional = Optional.ofNullable(chunkHolder.getLatestChunk());
             Optional<LevelChunk> optional1 = optional.flatMap(chunk -> chunk instanceof LevelChunk ? Optional.of((LevelChunk)chunk) : Optional.empty());
             csvOutput.writeRow(
@@ -931,11 +_,13 @@
     }
 
     private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos pos) {
-        return this.read(pos).thenApplyAsync(optional -> optional.map(this::upgradeChunkTag), Util.backgroundExecutor().forName("upgradeChunk"));
+        return this.read(pos).thenApplyAsync(optional -> optional.map(tag -> upgradeChunkTag(tag, pos)), Util.backgroundExecutor().forName("upgradeChunk")); // CraftBukkit
     }
 
-    private CompoundTag upgradeChunkTag(CompoundTag tag) {
-        return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer());
+    // CraftBukkit start
+    private CompoundTag upgradeChunkTag(CompoundTag tag, ChunkPos pos) {
+        return this.upgradeChunkTag(this.level.getTypeKey(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer(), pos, this.level);
+    // CraftBukkit end
     }
 
     void forEachSpawnCandidateChunk(Consumer<ChunkHolder> action) {
@@ -951,12 +_,34 @@
     }
 
     public boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos) {
-        return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos);
+        // Spigot start
+        return this.anyPlayerCloseEnoughForSpawning(chunkPos, false);
+    }
+
+    boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos, boolean reducedRange) {
+        return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, reducedRange);
+        // Spigot end
     }
 
     private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos) {
+        // Spigot start
+        return this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, false);
+    }
+
+    private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos, boolean reducedRange) {
+        double blockRange; // Paper - use from event
+        // Spigot end
         for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
-            if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
+            // Paper start - PlayerNaturallySpawnCreaturesEvent
+            com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event;
+            blockRange = 16384.0D;
+            if (reducedRange) {
+                event = serverPlayer.playerNaturallySpawnedEvent;
+                if (event == null || event.isCancelled()) continue;
+                blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4));
+            }
+            // Paper end - PlayerNaturallySpawnCreaturesEvent
+            if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, blockRange)) {
                 return true;
             }
         }
@@ -972,7 +_,7 @@
             Builder<ServerPlayer> builder = ImmutableList.builder();
 
             for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
-                if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
+                if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, 16384.0D)) { // Spigot
                     builder.add(serverPlayer);
                 }
             }
@@ -981,12 +_,12 @@
         }
     }
 
-    private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos) {
+    private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos, double range) { // Spigot
         if (player.isSpectator()) {
             return false;
         } else {
             double d = euclideanDistanceSquared(chunkPos, player);
-            return d < 16384.0;
+            return d < range; // Spigot
         }
     }
 
@@ -1100,9 +_,19 @@
     }
 
     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;
+        }
+        // Paper end - ignore and warn about illegal addEntity calls instead of crashing server
+        if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Paper - Fire PlayerJoinEvent when Player is actually ready; Delay adding to tracker until after list packets
         if (!(entity instanceof EnderDragonPart)) {
             EntityType<?> type = entity.getType();
             int i = type.clientTrackingRange() * 16;
+            i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot
             if (i != 0) {
                 int updateInterval = type.updateInterval();
                 if (this.entityMap.containsKey(entity.getId())) {
@@ -1126,6 +_,7 @@
     }
 
     protected void removeEntity(Entity entity) {
+        org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot
         if (entity instanceof ServerPlayer serverPlayer) {
             this.updatePlayerStatus(serverPlayer, false);
 
@@ -1230,7 +_,7 @@
         });
     }
 
-    class DistanceManager extends net.minecraft.server.level.DistanceManager {
+    public class DistanceManager extends net.minecraft.server.level.DistanceManager { // Paper - public
         protected DistanceManager(final Executor dispatcher, final Executor mainThreadExecutor) {
             super(dispatcher, mainThreadExecutor);
         }
@@ -1258,10 +_,10 @@
         final Entity entity;
         private final int range;
         SectionPos lastSectionPos;
-        public final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
+        public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
 
         public TrackedEntity(final Entity entity, final int range, final int updateInterval, final boolean trackDelta) {
-            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast);
+            this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast, this.seenBy); // CraftBukkit
             this.entity = entity;
             this.range = range;
             this.lastSectionPos = SectionPos.of(entity);
@@ -1297,24 +_,47 @@
         }
 
         public void removePlayer(ServerPlayer player) {
+            org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot
             if (this.seenBy.remove(player.connection)) {
                 this.serverEntity.removePairing(player);
             }
         }
 
         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());
+                // Paper start - remove allocation of Vec3D here
+                // Vec3 vec3 = player.position().subtract(this.entity.position());
+                double vec3_dx = player.getX() - this.entity.getX();
+                double vec3_dz = player.getZ() - this.entity.getZ();
+                // Paper end - remove allocation of Vec3D here
                 int playerViewDistance = ChunkMap.this.getPlayerViewDistance(player);
                 double d = Math.min(this.getEffectiveRange(), playerViewDistance * 16);
-                double d1 = vec3.x * vec3.x + vec3.z * vec3.z;
+                double d1 = vec3_dx * vec3_dx + vec3_dz * vec3_dz; // Paper
                 double d2 = d * d;
-                boolean flag = d1 <= d2
-                    && this.entity.broadcastToPlayer(player)
-                    && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
+                // Paper start - Configurable entity tracking range by Y
+                boolean flag = d1 <= d2;
+                if (flag && level.paperConfig().entities.trackingRangeY.enabled) {
+                    double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1);
+                    if (rangeY != -1) {
+                        double vec3_dy = player.getY() - this.entity.getY();
+                        flag = vec3_dy * vec3_dy <= rangeY * rangeY;
+                    }
+                }
+                flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
+                // Paper end - Configurable entity tracking range by Y
+                // CraftBukkit start - respect vanish API
+                if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits
+                    flag = false;
+                }
+                // CraftBukkit end
                 if (flag) {
                     if (this.seenBy.add(player.connection)) {
+                        // Paper start - entity tracking events
+                        if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length == 0 || new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.entity.getBukkitEntity()).callEvent()) {
                         this.serverEntity.addPairing(player);
+                        }
+                        // Paper end - entity tracking events
                     }
                 } else if (this.seenBy.remove(player.connection)) {
                     this.serverEntity.removePairing(player);
@@ -1331,6 +_,7 @@
 
             for (Entity entity : this.entity.getIndirectPassengers()) {
                 int i1 = entity.getType().clientTrackingRange() * 16;
+                i1 = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i1); // Paper
                 if (i1 > i) {
                     i = i1;
                 }
