--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -180,7 +_,7 @@
     final List<ServerPlayer> players = Lists.newArrayList();
     public final ServerChunkCache chunkSource;
     private final MinecraftServer server;
-    public final ServerLevelData serverLevelData;
+    public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type
     private int lastSpawnChunkRadius;
     final EntityTickList entityTickList = new EntityTickList();
     private final ServerWaypointManager waypointManager;
@@ -208,11 +_,131 @@
     private final boolean tickTime;
     private final RandomSequences randomSequences;
 
+    // CraftBukkit start
+    public final LevelStorageSource.LevelStorageAccess levelStorageAccess;
+    public final java.util.UUID uuid;
+    public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent
+    public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent
+
+    @Override
+    public @Nullable LevelChunk getChunkIfLoaded(int x, int z) {
+        return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately
+    }
+
+    @Override
+    public ResourceKey<LevelStem> getTypeKey() {
+        return this.levelStorageAccess.dimensionType;
+    }
+
+    // Paper start
+    public final boolean areChunksLoadedForMove(AABB axisalignedbb) {
+        // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override
+        // ICollisionAccess methods for VoxelShapes)
+        // be more strict too, add a block (dumb plugins in move events?)
+        int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+        int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+
+        int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+        int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+        int minChunkX = minBlockX >> 4;
+        int maxChunkX = maxBlockX >> 4;
+
+        int minChunkZ = minBlockZ >> 4;
+        int maxChunkZ = maxBlockZ >> 4;
+
+        ServerChunkCache chunkProvider = this.getChunkSource();
+
+        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+                if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.util.Priority priority,
+                                             java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+        if (Thread.currentThread() != this.thread) {
+            this.getChunkSource().mainThreadProcessor.execute(() -> {
+                this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
+            });
+            return;
+        }
+        int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3;
+        int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3;
+
+        int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3;
+        int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3;
+
+        int minChunkX = minBlockX >> 4;
+        int minChunkZ = minBlockZ >> 4;
+
+        int maxChunkX = maxBlockX >> 4;
+        int maxChunkZ = maxBlockZ >> 4;
+
+        this.loadChunks(minChunkX, minChunkZ, maxChunkX, maxChunkZ, priority, onLoad);
+    }
+
+    public final void loadChunks(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ,
+                                 ca.spottedleaf.concurrentutil.util.Priority priority,
+                                 java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+        List<net.minecraft.world.level.chunk.ChunkAccess> ret = new java.util.ArrayList<>();
+        it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList();
+        ServerChunkCache chunkProvider = this.getChunkSource();
+
+        int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+        int[] loadedChunks = new int[1];
+
+        java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
+            if (chunk != null) {
+                int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel());
+                ret.add(chunk);
+                ticketLevels.add(ticketLevel);
+                chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel);
+            }
+            if (++loadedChunks[0] == requiredChunks) {
+                try {
+                    onLoad.accept(java.util.Collections.unmodifiableList(ret));
+                } finally {
+                    for (int i = 0, len = ret.size(); i < len; ++i) {
+                        ChunkPos chunkPos = ret.get(i).getPos();
+                        int ticketLevel = ticketLevels.getInt(i);
+
+                        chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel);
+                        chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel);
+                    }
+                }
+            }
+        };
+
+        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
+            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
+                ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
+                    this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
+                );
+            }
+        }
+    }
+    // Paper end
+
+    // Paper start - optimise getPlayerByUUID
+    @Nullable
+    @Override
+    public Player getPlayerByUUID(java.util.UUID uuid) {
+        final Player player = this.getServer().getPlayerList().getPlayer(uuid);
+        return player != null && player.level() == this ? player : null;
+    }
+    // Paper end - optimise getPlayerByUUID
+
     public ServerLevel(
         MinecraftServer server,
         Executor dispatcher,
         LevelStorageSource.LevelStorageAccess levelStorageAccess,
-        ServerLevelData serverLevelData,
+        net.minecraft.world.level.storage.PrimaryLevelData serverLevelData, // CraftBukkit
         ResourceKey<Level> dimension,
         LevelStem levelStem,
         ChunkProgressListener progressListener,
@@ -220,14 +_,38 @@
         long biomeZoomSeed,
         List<CustomSpawner> customSpawners,
         boolean tickTime,
-        @Nullable RandomSequences randomSequences
+        @Nullable RandomSequences randomSequences,
+        org.bukkit.World.Environment env, // CraftBukkit
+        org.bukkit.generator.ChunkGenerator gen, // CraftBukkit
+        org.bukkit.generator.BiomeProvider biomeProvider // CraftBukkit
     ) {
-        super(serverLevelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates());
+        // CraftBukkit start
+        super(serverLevelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(levelStorageAccess.levelDirectory.path(), serverLevelData.getLevelName(), dimension.location(), spigotConfig, server.registryAccess(), serverLevelData.getGameRules()))); // Paper - create paper world configs
+        this.pvpMode = server.isPvpAllowed();
+        this.levelStorageAccess = levelStorageAccess;
+        this.uuid = org.bukkit.craftbukkit.util.WorldUUID.getOrCreate(levelStorageAccess.levelDirectory.path().toFile());
+        // CraftBukkit end
         this.tickTime = tickTime;
         this.server = server;
         this.customSpawners = customSpawners;
         this.serverLevelData = serverLevelData;
         ChunkGenerator chunkGenerator = levelStem.generator();
+        // CraftBukkit start
+        this.serverLevelData.setWorld(this);
+
+        if (biomeProvider != null) {
+            net.minecraft.world.level.biome.BiomeSource worldChunkManager = new org.bukkit.craftbukkit.generator.CustomWorldChunkManager(this.getWorld(), biomeProvider, this.server.registryAccess().lookupOrThrow(Registries.BIOME), chunkGenerator.getBiomeSource()); // Paper - add vanillaBiomeProvider
+            if (chunkGenerator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator cga) {
+                chunkGenerator = new net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator(worldChunkManager, cga.settings);
+            } else if (chunkGenerator instanceof net.minecraft.world.level.levelgen.FlatLevelSource cpf) {
+                chunkGenerator = new net.minecraft.world.level.levelgen.FlatLevelSource(cpf.settings(), worldChunkManager);
+            }
+        }
+
+        if (gen != null) {
+            chunkGenerator = new org.bukkit.craftbukkit.generator.CustomChunkGenerator(this, chunkGenerator, gen);
+        }
+        // CraftBukkit end
         boolean flag = server.forceSynchronousWrites();
         DataFixer fixerUpper = server.getFixerUpper();
         EntityPersistentStorage<Entity> entityPersistentStorage = new EntityStorage(
@@ -249,8 +_,8 @@
             server.getStructureManager(),
             dispatcher,
             chunkGenerator,
-            server.getPlayerList().getViewDistance(),
-            server.getPlayerList().getSimulationDistance(),
+            this.spigotConfig.viewDistance, // Spigot
+            this.spigotConfig.simulationDistance, // Spigot
             flag,
             progressListener,
             this.entityManager::updateChunkStatus,
@@ -271,7 +_,7 @@
             this.chunkSource.chunkScanner(),
             this.registryAccess(),
             server.getStructureManager(),
-            dimension,
+            getTypeKey(), // Paper - Fix missing CB diff
             chunkGenerator,
             this.chunkSource.randomState(),
             this,
@@ -279,9 +_,9 @@
             seed,
             fixerUpper
         );
-        this.structureManager = new StructureManager(this, server.getWorldData().worldGenOptions(), this.structureCheck);
-        if (this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END)) {
-            this.dragonFight = new EndDragonFight(this, seed, server.getWorldData().endDragonFightData());
+        this.structureManager = new StructureManager(this, this.serverLevelData.worldGenOptions(), this.structureCheck); // CraftBukkit
+        if (this.dimension() == Level.END && this.dimensionTypeRegistration().is(BuiltinDimensionTypes.END) || env == org.bukkit.World.Environment.THE_END) { // CraftBukkit - Allow to create EnderDragonBattle in default and custom END
+            this.dragonFight = new EndDragonFight(this, this.serverLevelData.worldGenOptions().seed(), this.serverLevelData.endDragonFightData()); // CraftBukkit
         } else {
             this.dragonFight = null;
         }
@@ -290,7 +_,15 @@
         this.gameEventDispatcher = new GameEventDispatcher(this);
         this.randomSequences = Objects.requireNonNullElseGet(randomSequences, () -> this.getDataStorage().computeIfAbsent(RandomSequences.TYPE));
         this.waypointManager = new ServerWaypointManager();
-    }
+        this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
+    }
+
+    // Paper start
+    @Override
+    public boolean hasChunk(int chunkX, int chunkZ) {
+        return this.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ) != null;
+    }
+    // Paper end
 
     @Deprecated
     @VisibleForTesting
@@ -302,8 +_,8 @@
         this.serverLevelData.setClearWeatherTime(clearTime);
         this.serverLevelData.setRainTime(weatherTime);
         this.serverLevelData.setThunderTime(weatherTime);
-        this.serverLevelData.setRaining(isRaining);
-        this.serverLevelData.setThundering(isThundering);
+        this.serverLevelData.setRaining(isRaining, org.bukkit.event.weather.WeatherChangeEvent.Cause.COMMAND); // Paper - Add cause to Weather/ThunderChangeEvents
+        this.serverLevelData.setThundering(isThundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.COMMAND); // Paper - Add cause to Weather/ThunderChangeEvents
     }
 
     @Override
@@ -330,12 +_,25 @@
 
         int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE);
         if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) {
+            // Paper start - create time skip event - move up calculations
+            final long newDayTime = this.levelData.getDayTime() + 24000L;
+            org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(
+                this.getWorld(),
+                org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP,
+                (newDayTime - newDayTime % 24000L) - this.getDayTime()
+            );
+            // Paper end - create time skip event - move up calculations
             if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
-                long l = this.levelData.getDayTime() + 24000L;
-                this.setDayTime(l - l % 24000L);
+                // Paper start - call time skip event if gamerule is enabled
+                // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime
+                // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param
+                if (event.callEvent()) {
+                    this.setDayTime(this.getDayTime() + event.getSkipAmount());
+                }
+                // Paper end - call time skip event if gamerule is enabled
             }
 
-            this.wakeUpAllPlayers();
+            if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled
             if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) {
                 this.resetWeatherCycle();
             }
@@ -350,9 +_,9 @@
         if (!this.isDebug() && runsNormally) {
             long l = this.getGameTime();
             profilerFiller.push("blockTicks");
-            this.blockTicks.tick(l, 65536, this::tickBlock);
+            this.blockTicks.tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks
             profilerFiller.popPush("fluidTicks");
-            this.fluidTicks.tick(l, 65536, this::tickFluid);
+            this.fluidTicks.tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks
             profilerFiller.pop();
         }
 
@@ -370,7 +_,7 @@
 
         this.handlingTick = false;
         profilerFiller.pop();
-        boolean flag = !this.players.isEmpty() || !this.getForceLoadedChunks().isEmpty();
+        boolean flag = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForceLoadedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this
         if (flag) {
             this.resetEmptyTime();
         }
@@ -459,11 +_,13 @@
         ProfilerFiller profilerFiller = Profiler.get();
         profilerFiller.push("iceandsnow");
 
+        if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow
         for (int i = 0; i < randomTickSpeed; i++) {
             if (this.random.nextInt(48) == 0) {
                 this.tickPrecipitation(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15));
             }
         }
+        } // Paper - Option to disable ice and snow
 
         profilerFiller.popPush("tickBlocks");
         if (randomTickSpeed > 0) {
@@ -506,12 +_,12 @@
         int minBlockZ = pos.getMinBlockZ();
         ProfilerFiller profilerFiller = Profiler.get();
         profilerFiller.push("thunder");
-        if (isRaining && this.isThundering() && this.random.nextInt(100000) == 0) {
+        if (!this.paperConfig().environment.disableThunder && isRaining && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder
             BlockPos blockPos = this.findLightningTargetAround(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15));
             if (this.isRainingAt(blockPos)) {
                 DifficultyInstance currentDifficultyAt = this.getCurrentDifficultyAt(blockPos);
                 boolean flag = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)
-                    && this.random.nextDouble() < currentDifficultyAt.getEffectiveDifficulty() * 0.01
+                    && this.random.nextDouble() < currentDifficultyAt.getEffectiveDifficulty() * this.paperConfig().entities.spawning.skeletonHorseThunderSpawnChance.or(0.01) // Paper - Configurable spawn chances for skeleton horses
                     && !this.getBlockState(blockPos.below()).is(Blocks.LIGHTNING_ROD);
                 if (flag) {
                     SkeletonHorse skeletonHorse = EntityType.SKELETON_HORSE.create(this, EntitySpawnReason.EVENT);
@@ -519,7 +_,7 @@
                         skeletonHorse.setTrap(true);
                         skeletonHorse.setAge(0);
                         skeletonHorse.setPos(blockPos.getX(), blockPos.getY(), blockPos.getZ());
-                        this.addFreshEntity(skeletonHorse);
+                        this.addFreshEntity(skeletonHorse, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.LIGHTNING); // CraftBukkit
                     }
                 }
 
@@ -527,7 +_,7 @@
                 if (lightningBolt != null) {
                     lightningBolt.snapTo(Vec3.atBottomCenterOf(blockPos));
                     lightningBolt.setVisualOnly(flag);
-                    this.addFreshEntity(lightningBolt);
+                    this.strikeLightning(lightningBolt, org.bukkit.event.weather.LightningStrikeEvent.Cause.WEATHER); // CraftBukkit
                 }
             }
         }
@@ -541,7 +_,7 @@
         BlockPos blockPos1 = heightmapPos.below();
         Biome biome = this.getBiome(heightmapPos).value();
         if (biome.shouldFreeze(this, blockPos1)) {
-            this.setBlockAndUpdate(blockPos1, Blocks.ICE.defaultBlockState());
+            org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockPos1, Blocks.ICE.defaultBlockState(), 3, null); // CraftBukkit
         }
 
         if (this.isRaining()) {
@@ -553,10 +_,10 @@
                     if (layersValue < Math.min(_int, 8)) {
                         BlockState blockState1 = blockState.setValue(SnowLayerBlock.LAYERS, layersValue + 1);
                         Block.pushEntitiesUp(blockState, blockState1, this, heightmapPos);
-                        this.setBlockAndUpdate(heightmapPos, blockState1);
+                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, heightmapPos, blockState1, 3, null); // CraftBukkit
                     }
                 } else {
-                    this.setBlockAndUpdate(heightmapPos, Blocks.SNOW.defaultBlockState());
+                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, heightmapPos, Blocks.SNOW.defaultBlockState(), 3, null); // CraftBukkit
                 }
             }
 
@@ -581,6 +_,12 @@
     }
 
     protected BlockPos findLightningTargetAround(BlockPos pos) {
+        // Paper start - Add methods to find targets for lightning strikes
+        return this.findLightningTargetAround(pos, false);
+    }
+
+    public BlockPos findLightningTargetAround(BlockPos pos, boolean returnNullWhenNoTarget) {
+        // Paper end - Add methods to find targets for lightning strikes
         BlockPos heightmapPos = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos);
         Optional<BlockPos> optional = this.findLightningRod(heightmapPos);
         if (optional.isPresent()) {
@@ -588,11 +_,12 @@
         } else {
             AABB aabb = AABB.encapsulatingFullBlocks(heightmapPos, heightmapPos.atY(this.getMaxY() + 1)).inflate(3.0);
             List<LivingEntity> entitiesOfClass = this.getEntitiesOfClass(
-                LivingEntity.class, aabb, entity -> entity != null && entity.isAlive() && this.canSeeSky(entity.blockPosition())
+                LivingEntity.class, aabb, entity -> entity != null && entity.isAlive() && this.canSeeSky(entity.blockPosition()) && !entity.isSpectator() // Paper - Fix lightning being able to hit spectators (MC-262422)
             );
             if (!entitiesOfClass.isEmpty()) {
                 return entitiesOfClass.get(this.random.nextInt(entitiesOfClass.size())).blockPosition();
             } else {
+                if (returnNullWhenNoTarget) return null; // Paper - Add methods to find targets for lightning strikes
                 if (heightmapPos.getY() == this.getMinY() - 1) {
                     heightmapPos = heightmapPos.above(2);
                 }
@@ -683,8 +_,8 @@
                 this.serverLevelData.setThunderTime(thunderTime);
                 this.serverLevelData.setRainTime(rainTime);
                 this.serverLevelData.setClearWeatherTime(clearWeatherTime);
-                this.serverLevelData.setThundering(isThundering);
-                this.serverLevelData.setRaining(isRaining1);
+                this.serverLevelData.setThundering(isThundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.NATURAL); // Paper - Add cause to Weather/ThunderChangeEvents
+                this.serverLevelData.setRaining(isRaining1, org.bukkit.event.weather.WeatherChangeEvent.Cause.NATURAL); // Paper - Add cause to Weather/ThunderChangeEvents
             }
 
             this.oThunderLevel = this.thunderLevel;
@@ -705,6 +_,7 @@
             this.rainLevel = Mth.clamp(this.rainLevel, 0.0F, 1.0F);
         }
 
+        /* CraftBukkit start
         if (this.oRainLevel != this.rainLevel) {
             this.server
                 .getPlayerList()
@@ -727,14 +_,47 @@
             this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, this.rainLevel));
             this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel));
         }
+        */
+        for (ServerPlayer player : this.players) {
+            if (player.level() == this) {
+                player.tickWeather();
+            }
+        }
+
+        if (isRaining != this.isRaining()) {
+            // Only send weather packets to those affected
+            for (ServerPlayer player : this.players) {
+                if (player.level() == this) {
+                    player.setPlayerWeather((!isRaining ? org.bukkit.WeatherType.DOWNFALL : org.bukkit.WeatherType.CLEAR), false);
+                }
+            }
+        }
+        for (ServerPlayer player : this.players) {
+            if (player.level() == this) {
+                player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel);
+            }
+        }
+        // CraftBukkit end
     }
 
     @VisibleForTesting
     public void resetWeatherCycle() {
-        this.serverLevelData.setRainTime(0);
-        this.serverLevelData.setRaining(false);
-        this.serverLevelData.setThunderTime(0);
-        this.serverLevelData.setThundering(false);
+        // CraftBukkit start
+        this.serverLevelData.setRaining(false, org.bukkit.event.weather.WeatherChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents
+        // If we stop due to everyone sleeping we should reset the weather duration to some other random value.
+        // Not that everyone ever manages to get the whole server to sleep at the same time....
+        if (!this.serverLevelData.isRaining()) {
+            this.serverLevelData.setRainTime(0);
+        }
+        // CraftBukkit end
+        this.serverLevelData.setThundering(false, org.bukkit.event.weather.ThunderChangeEvent.Cause.SLEEP); // Paper - Add cause to Weather/ThunderChangeEvents
+        // CraftBukkit start
+        // If we stop due to everyone sleeping we should reset the weather duration to some other random value.
+        // Not that everyone ever manages to get the whole server to sleep at the same time....
+        if (!this.serverLevelData.isThundering()) {
+            this.serverLevelData.setThunderTime(0);
+        }
+        // CraftBukkit end
     }
 
     public void resetEmptyTime() {
@@ -756,18 +_,46 @@
         }
     }
 
+    // Paper start - log detailed entity tick information
+    // TODO replace with varhandle
+    static final java.util.concurrent.atomic.AtomicReference<Entity> currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>();
+
+    public static List<Entity> getCurrentlyTickingEntities() {
+        Entity ticking = currentlyTickingEntity.get();
+        List<Entity> ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking });
+
+        return ret;
+    }
+    // Paper end - log detailed entity tick information
+
     public void tickNonPassenger(Entity entity) {
+        // Paper start - log detailed entity tick information
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
+        try {
+            if (currentlyTickingEntity.get() == null) {
+                currentlyTickingEntity.lazySet(entity);
+            }
+            // Paper end - log detailed entity tick information
         entity.setOldPosAndRot();
         ProfilerFiller profilerFiller = Profiler.get();
         entity.tickCount++;
+        entity.totalEntityAge++; // Paper - age-like counter for all entities
         profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString());
         profilerFiller.incrementCounter("tickNonPassenger");
         entity.tick();
+        entity.postTick(); // CraftBukkit
         profilerFiller.pop();
 
         for (Entity entity1 : entity.getPassengers()) {
             this.tickPassenger(entity, entity1);
         }
+        // Paper start - log detailed entity tick information
+        } finally {
+            if (currentlyTickingEntity.get() == entity) {
+                currentlyTickingEntity.lazySet(null);
+            }
+        }
+        // Paper end - log detailed entity tick information
     }
 
     private void tickPassenger(Entity ridingEntity, Entity passengerEntity) {
@@ -776,10 +_,12 @@
         } else if (passengerEntity instanceof Player || this.entityTickList.contains(passengerEntity)) {
             passengerEntity.setOldPosAndRot();
             passengerEntity.tickCount++;
+            passengerEntity.totalEntityAge++; // Paper - age-like counter for all entities
             ProfilerFiller profilerFiller = Profiler.get();
             profilerFiller.push(() -> BuiltInRegistries.ENTITY_TYPE.getKey(passengerEntity.getType()).toString());
             profilerFiller.incrementCounter("tickPassenger");
             passengerEntity.rideTick();
+            passengerEntity.postTick(); // CraftBukkit
             profilerFiller.pop();
 
             for (Entity entity : passengerEntity.getPassengers()) {
@@ -810,6 +_,7 @@
     public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave) {
         ServerChunkCache chunkSource = this.getChunkSource();
         if (!skipSave) {
+            org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld())); // CraftBukkit
             if (progress != null) {
                 progress.progressStartNoAbort(Component.translatable("menu.savingLevel"));
             }
@@ -826,11 +_,19 @@
                 this.entityManager.autoSave();
             }
         }
+
+        // CraftBukkit start - moved from MinecraftServer#saveAllChunks
+        ServerLevel serverLevel1 = this;
+
+        this.serverLevelData.setWorldBorder(serverLevel1.getWorldBorder().createSettings());
+        this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess()));
+        this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
+        // CraftBukkit end
     }
 
     private void saveLevelData(boolean join) {
         if (this.dragonFight != null) {
-            this.server.getWorldData().setEndDragonFightData(this.dragonFight.saveData());
+            this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit
         }
 
         DimensionDataStorage dataStorage = this.getChunkSource().getDataStorage();
@@ -895,18 +_,40 @@
 
     @Override
     public boolean addFreshEntity(Entity entity) {
-        return this.addEntity(entity);
+        // CraftBukkit start
+        return this.addFreshEntity(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
+    }
+
+    @Override
+    public boolean addFreshEntity(Entity entity, @Nullable org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
+        return this.addEntity(entity, reason);
+        // CraftBukkit end
     }
 
     public boolean addWithUUID(Entity entity) {
-        return this.addEntity(entity);
+        // CraftBukkit start
+        return this.addWithUUID(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
+    }
+
+    public boolean addWithUUID(Entity entity, @Nullable org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
+        return this.addEntity(entity, reason);
+        // CraftBukkit end
     }
 
     public void addDuringTeleport(Entity entity) {
+        // CraftBukkit start
+        // SPIGOT-6415: Don't call spawn event for entities which travel trough worlds,
+        // since it is only an implementation detail, that a new entity is created when
+        // they are traveling between worlds.
+        this.addDuringTeleport(entity, null);
+    }
+
+    public void addDuringTeleport(Entity entity, @Nullable org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
+        // CraftBukkit end
         if (entity instanceof ServerPlayer serverPlayer) {
             this.addPlayer(serverPlayer);
         } else {
-            this.addEntity(entity);
+            this.addEntity(entity, reason); // CraftBukkit
         }
     }
 
@@ -929,40 +_,119 @@
         this.entityManager.addNewEntity(player);
     }
 
-    private boolean addEntity(Entity entity) {
+    // CraftBukkit start
+    private boolean addEntity(Entity entity, @Nullable org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) {
+        org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot
+        entity.generation = false; // Paper - Don't fire sync event during generation; Reset flag if it was added during a ServerLevel generation process
+        // Paper start - extra debug info
+        if (entity.valid) {
+            MinecraftServer.LOGGER.error("Attempted Double World add on {}", entity, new Throwable());
+            return true;
+        }
+        // Paper end - extra debug info
+        if (entity.spawnReason == null) entity.spawnReason = spawnReason; // Paper - Entity#getEntitySpawnReason
         if (entity.isRemoved()) {
-            LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityType.getKey(entity.getType()));
+            // LOGGER.warn("Tried to add entity {} but it was marked as removed already", EntityType.getKey(entity.getType())); // CraftBukkit - remove warning
             return false;
         } else {
+            if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added
+            // Paper start - capture all item additions to the world
+            if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) {
+                captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity);
+                return true;
+            }
+            // Paper end - capture all item additions to the world
+            // SPIGOT-6415: Don't call spawn event when reason is null. For example when an entity teleports to a new world.
+            if (spawnReason != null && !org.bukkit.craftbukkit.event.CraftEventFactory.doEntityAddEventCalling(this, entity, spawnReason)) {
+                return false;
+            }
+            // CraftBukkit end
+
             return this.entityManager.addNewEntity(entity);
         }
     }
 
     public boolean tryAddFreshEntityWithPassengers(Entity entity) {
+        // CraftBukkit start
+        return this.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
+    }
+
+    public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
+        // CraftBukkit end
         if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.entityManager::isLoaded)) {
             return false;
         } else {
-            this.addFreshEntityWithPassengers(entity);
+            this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
             return true;
         }
     }
 
     public void unload(LevelChunk chunk) {
+        // Spigot start
+        for (net.minecraft.world.level.block.entity.BlockEntity blockEntity : chunk.getBlockEntities().values()) {
+            if (blockEntity instanceof net.minecraft.world.Container) {
+                // Paper start - this area looks like it can load chunks, change the behavior
+                // chests for example can apply physics to the world
+                // so instead we just change the active container and call the event
+                for (org.bukkit.entity.HumanEntity human : Lists.newArrayList(((net.minecraft.world.Container) blockEntity).getViewers())) {
+                    ((org.bukkit.craftbukkit.entity.CraftHumanEntity) human).getHandle().closeUnloadedInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper - Inventory close reason
+                }
+                // Paper end - this area looks like it can load chunks, change the behavior
+            }
+        }
+        // Spigot end
         chunk.clearAllBlockEntities();
         chunk.unregisterTickContainerFromLevel(this);
     }
 
     public void removePlayerImmediately(ServerPlayer player, Entity.RemovalReason reason) {
-        player.remove(reason);
-    }
+        player.remove(reason, null); // CraftBukkit - add Bukkit remove cause
+    }
+
+    // CraftBukkit start
+    public boolean strikeLightning(Entity entitylightning) {
+        return this.strikeLightning(entitylightning, org.bukkit.event.weather.LightningStrikeEvent.Cause.UNKNOWN);
+    }
+
+    public boolean strikeLightning(Entity entitylightning, org.bukkit.event.weather.LightningStrikeEvent.Cause cause) {
+        org.bukkit.event.weather.LightningStrikeEvent lightning = org.bukkit.craftbukkit.event.CraftEventFactory.callLightningStrikeEvent((org.bukkit.entity.LightningStrike) entitylightning.getBukkitEntity(), cause);
+
+        if (lightning.isCancelled()) {
+            return false;
+        }
+
+        return this.addFreshEntity(entitylightning);
+    }
+    // CraftBukkit end
 
     @Override
     public void destroyBlockProgress(int breakerId, BlockPos pos, int progress) {
+        // CraftBukkit start
+        Player breakerPlayer = null;
+        Entity entity = this.getEntity(breakerId);
+        if (entity instanceof Player) breakerPlayer = (Player) entity;
+        // CraftBukkit end
+
+        // Paper start - Add BlockBreakProgressUpdateEvent
+        // If a plugin is using this method to send destroy packets for a client-side only entity id, no block progress occurred on the server.
+        // Hence, do not call the event.
+        if (entity != null) {
+            float progressFloat = Mth.clamp(progress, 0, 10) / 10.0f;
+            org.bukkit.craftbukkit.block.CraftBlock bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(this, pos);
+            new io.papermc.paper.event.block.BlockBreakProgressUpdateEvent(bukkitBlock, progressFloat, entity.getBukkitEntity())
+                .callEvent();
+        }
+        // Paper end - Add BlockBreakProgressUpdateEvent
         for (ServerPlayer serverPlayer : this.server.getPlayerList().getPlayers()) {
             if (serverPlayer != null && serverPlayer.level() == this && serverPlayer.getId() != breakerId) {
                 double d = pos.getX() - serverPlayer.getX();
                 double d1 = pos.getY() - serverPlayer.getY();
                 double d2 = pos.getZ() - serverPlayer.getZ();
+                // CraftBukkit start
+                if (breakerPlayer != null && !serverPlayer.getBukkitEntity().canSee(breakerPlayer.getBukkitEntity())) {
+                    continue;
+                }
+                // CraftBukkit end
                 if (d * d + d1 * d1 + d2 * d2 < 1024.0) {
                     serverPlayer.connection.send(new ClientboundBlockDestructionPacket(breakerId, pos, progress));
                 }
@@ -1037,7 +_,7 @@
                 pos.getX(),
                 pos.getY(),
                 pos.getZ(),
-                64.0,
+                64.0, // Paper - diff on change (the 64.0 distance is used as defaults for sound ranges in spigot config for ender dragon, end portal and wither)
                 this.dimension(),
                 new ClientboundLevelEventPacket(type, pos, data, false)
             );
@@ -1049,6 +_,11 @@
 
     @Override
     public void gameEvent(Holder<GameEvent> gameEvent, Vec3 pos, GameEvent.Context context) {
+        // Paper start - Prevent GameEvents being fired from unloaded chunks
+        if (this.getChunkIfLoadedImmediately((Mth.floor(pos.x) >> 4), (Mth.floor(pos.z) >> 4)) == null) {
+            return;
+        }
+        // Paper end - Prevent GameEvents being fired from unloaded chunks
         this.gameEventDispatcher.post(gameEvent, pos, context);
     }
 
@@ -1061,17 +_,28 @@
 
         this.getChunkSource().blockChanged(pos);
         this.pathTypesByPosCache.invalidate(pos);
+        if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates
         VoxelShape collisionShape = oldState.getCollisionShape(this, pos);
         VoxelShape collisionShape1 = newState.getCollisionShape(this, pos);
         if (Shapes.joinIsNotEmpty(collisionShape, collisionShape1, BooleanOp.NOT_SAME)) {
             List<PathNavigation> list = new ObjectArrayList<>();
 
+            try { // Paper - catch CME see below why
             for (Mob mob : this.navigatingMobs) {
                 PathNavigation navigation = mob.getNavigation();
                 if (navigation.shouldRecomputePath(pos)) {
                     list.add(navigation);
                 }
             }
+            // Paper start - catch CME see below why
+            } catch (final java.util.ConcurrentModificationException concurrentModificationException) {
+                // This can happen because the pathfinder update below may trigger a chunk load, which in turn may cause more navigators to register
+                // In this case we just run the update again across all the iterators as the chunk will then be loaded
+                // As this is a relative edge case it is much faster than copying navigators (on either read or write)
+                this.sendBlockUpdated(pos, oldState, newState, flags);
+                return;
+            }
+            // Paper end - catch CME see below why
 
             try {
                 this.isUpdatingNavigations = true;
@@ -1083,15 +_,23 @@
                 this.isUpdatingNavigations = false;
             }
         }
+        } // Paper - option to disable pathfinding updates
     }
 
     @Override
     public void updateNeighborsAt(BlockPos pos, Block block) {
+        // CraftBukkit start
+        if (this.populating) {
+            return;
+        }
+        // CraftBukkit end
+        if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
         this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, null, null));
     }
 
     @Override
     public void updateNeighborsAt(BlockPos pos, Block block, @Nullable Orientation orientation) {
+        if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
         this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation);
     }
 
@@ -1140,6 +_,42 @@
         ParticleOptions largeExplosionParticles,
         Holder<SoundEvent> explosionSound
     ) {
+        // CraftBukkit start
+        this.explode0(source, damageSource, damageCalculator, x, y, z, radius, fire, explosionInteraction, smallExplosionParticles, largeExplosionParticles, explosionSound);
+    }
+
+    public ServerExplosion explode0(
+        @Nullable Entity source,
+        @Nullable DamageSource damageSource,
+        @Nullable ExplosionDamageCalculator damageCalculator,
+        double x,
+        double y,
+        double z,
+        float radius,
+        boolean fire,
+        Level.ExplosionInteraction explosionInteraction,
+        ParticleOptions smallExplosionParticles,
+        ParticleOptions largeExplosionParticles,
+        Holder<SoundEvent> explosionSound
+    ) {
+        return this.explode0(source, damageSource, damageCalculator, x, y, z, radius, fire, explosionInteraction, smallExplosionParticles, largeExplosionParticles, explosionSound, null);
+    }
+    public ServerExplosion explode0(
+        @Nullable Entity source,
+        @Nullable DamageSource damageSource,
+        @Nullable ExplosionDamageCalculator damageCalculator,
+        double x,
+        double y,
+        double z,
+        float radius,
+        boolean fire,
+        Level.ExplosionInteraction explosionInteraction,
+        ParticleOptions smallExplosionParticles,
+        ParticleOptions largeExplosionParticles,
+        Holder<SoundEvent> explosionSound,
+        @Nullable java.util.function.Consumer<ServerExplosion> configurator
+    ) {
+        // CraftBukkit end
         Explosion.BlockInteraction blockInteraction = switch (explosionInteraction) {
             case NONE -> Explosion.BlockInteraction.KEEP;
             case BLOCK -> this.getDestroyType(GameRules.RULE_BLOCK_EXPLOSION_DROP_DECAY);
@@ -1148,10 +_,17 @@
                 : Explosion.BlockInteraction.KEEP;
             case TNT -> this.getDestroyType(GameRules.RULE_TNT_EXPLOSION_DROP_DECAY);
             case TRIGGER -> Explosion.BlockInteraction.TRIGGER_BLOCK;
+            case STANDARD -> Explosion.BlockInteraction.DESTROY; // CraftBukkit - handle custom explosion type
         };
         Vec3 vec3 = new Vec3(x, y, z);
         ServerExplosion serverExplosion = new ServerExplosion(this, source, damageSource, damageCalculator, vec3, radius, fire, blockInteraction);
+        if (configurator != null) configurator.accept(serverExplosion);// Paper - Allow explosions to damage source
         serverExplosion.explode();
+        // CraftBukkit start
+        if (serverExplosion.wasCanceled) {
+            return serverExplosion;
+        }
+        // CraftBukkit end
         ParticleOptions particleOptions = serverExplosion.isSmall() ? smallExplosionParticles : largeExplosionParticles;
 
         for (ServerPlayer serverPlayer : this.players) {
@@ -1160,6 +_,8 @@
                 serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound));
             }
         }
+
+        return serverExplosion; // CraftBukkit
     }
 
     private Explosion.BlockInteraction getDestroyType(GameRules.Key<GameRules.BooleanValue> decayGameRule) {
@@ -1230,7 +_,7 @@
     public <T extends ParticleOptions> int sendParticles(
         T type, double posX, double posY, double posZ, int particleCount, double xOffset, double yOffset, double zOffset, double speed
     ) {
-        return this.sendParticles(type, false, false, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed);
+        return this.sendParticlesSource(null, type, false, false, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); // CraftBukkit - visibility api support
     }
 
     public <T extends ParticleOptions> int sendParticles(
@@ -1246,13 +_,49 @@
         double zOffset,
         double speed
     ) {
+    // Paper start - visibility api support
+        return this.sendParticlesSource(null, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed);
+    }
+    public <T extends ParticleOptions> int sendParticlesSource(
+        @Nullable Entity sender,
+        T type,
+        boolean overrideLimiter,
+        boolean alwaysShow,
+        double posX,
+        double posY,
+        double posZ,
+        int particleCount,
+        double xOffset,
+        double yOffset,
+        double zOffset,
+        double speed
+    ) {
+        return sendParticlesSource(this.players, sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed);
+    }
+    public <T extends ParticleOptions> int sendParticlesSource(
+        List<ServerPlayer> receivers,
+        @Nullable Entity sender,
+        T type,
+        boolean overrideLimiter,
+        boolean alwaysShow,
+        double posX,
+        double posY,
+        double posZ,
+        int particleCount,
+        double xOffset,
+        double yOffset,
+        double zOffset,
+        double speed
+    ) {
+    // Paper end - visibility api support
         ClientboundLevelParticlesPacket clientboundLevelParticlesPacket = new ClientboundLevelParticlesPacket(
             type, overrideLimiter, alwaysShow, posX, posY, posZ, (float)xOffset, (float)yOffset, (float)zOffset, (float)speed, particleCount
         );
         int i = 0;
 
-        for (int i1 = 0; i1 < this.players.size(); i1++) {
-            ServerPlayer serverPlayer = this.players.get(i1);
+        for (int i1 = 0; i1 < receivers.size(); i1++) { // Paper - particle API
+            ServerPlayer serverPlayer = receivers.get(i1); // Paper - particle API
+            if (sender != null && !serverPlayer.getBukkitEntity().canSee(sender.getBukkitEntity())) continue; // CraftBukkit
             if (this.sendParticles(serverPlayer, overrideLimiter, posX, posY, posZ, clientboundLevelParticlesPacket)) {
                 i++;
             }
@@ -1315,7 +_,7 @@
 
     @Nullable
     public BlockPos findNearestMapStructure(TagKey<Structure> structureTag, BlockPos pos, int radius, boolean skipExistingChunks) {
-        if (!this.server.getWorldData().worldGenOptions().generateStructures()) {
+        if (!this.serverLevelData.worldGenOptions().generateStructures()) { // CraftBukkit
             return null;
         } else {
             Optional<HolderSet.Named<Structure>> optional = this.registryAccess().lookupOrThrow(Registries.STRUCTURE).get(structureTag);
@@ -1362,10 +_,36 @@
     @Nullable
     @Override
     public MapItemSavedData getMapData(MapId mapId) {
-        return this.getServer().overworld().getDataStorage().get(MapItemSavedData.type(mapId));
+        // Paper start - Call missing map initialize event and set id
+        final DimensionDataStorage storage = this.getServer().overworld().getDataStorage();
+
+        final Optional<net.minecraft.world.level.saveddata.SavedData> cacheEntry = storage.cache.get(MapItemSavedData.type(mapId));
+        if (cacheEntry == null) { // Cache did not contain, try to load and may init
+            final MapItemSavedData mapData = storage.get(MapItemSavedData.type(mapId)); // get populates the cache
+            if (mapData != null) { // map was read, init it and return
+                mapData.id = mapId;
+                new org.bukkit.event.server.MapInitializeEvent(mapData.mapView).callEvent();
+                return mapData;
+            }
+
+            return null; // Map does not exist, reading failed.
+        }
+        // Cache entry exists, update it with the id ref and return.
+        if (cacheEntry.orElse(null) instanceof final MapItemSavedData mapItemSavedData) {
+            mapItemSavedData.id = mapId;
+            return mapItemSavedData;
+        }
+
+        return null;
+        // Paper end - Call missing map initialize event and set id
     }
 
     public void setMapData(MapId mapId, MapItemSavedData data) {
+        // CraftBukkit start
+        data.id = mapId;
+        org.bukkit.event.server.MapInitializeEvent event = new org.bukkit.event.server.MapInitializeEvent(data.mapView);
+        event.callEvent();
+        // CraftBukkit end
         this.getServer().overworld().getDataStorage().set(MapItemSavedData.type(mapId), data);
     }
 
@@ -1377,17 +_,27 @@
         BlockPos spawnPos = this.levelData.getSpawnPos();
         float spawnAngle = this.levelData.getSpawnAngle();
         if (!spawnPos.equals(pos) || spawnAngle != angle) {
+            org.bukkit.Location prevSpawnLoc = this.getWorld().getSpawnLocation(); // Paper - Call SpawnChangeEvent
             this.levelData.setSpawn(pos, angle);
+            new org.bukkit.event.world.SpawnChangeEvent(this.getWorld(), prevSpawnLoc).callEvent(); // Paper - Call SpawnChangeEvent
             this.getServer().getPlayerList().broadcastAll(new ClientboundSetDefaultSpawnPositionPacket(pos, angle));
         }
 
         if (this.lastSpawnChunkRadius > 1) {
-            this.getChunkSource().removeTicketWithRadius(TicketType.START, new ChunkPos(spawnPos), this.lastSpawnChunkRadius);
+            // Paper start - allow disabling gamerule limits
+            for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(spawnPos, this.lastSpawnChunkRadius - 2)) {
+                this.getChunkSource().removeTicketAtLevel(TicketType.START, chunkPos, ChunkLevel.ENTITY_TICKING_LEVEL);
+            }
+            // Paper end - allow disabling gamerule limits
         }
 
         int i = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS) + 1;
         if (i > 1) {
-            this.getChunkSource().addTicketWithRadius(TicketType.START, new ChunkPos(pos), i);
+            // Paper start - allow disabling gamerule limits
+            for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(pos, i - 2)) {
+                this.getChunkSource().addTicketAtLevel(TicketType.START, chunkPos, ChunkLevel.ENTITY_TICKING_LEVEL);
+            }
+            // Paper end - allow disabling gamerule limits
         }
 
         this.lastSpawnChunkRadius = i;
@@ -1422,6 +_,11 @@
                 DebugPackets.sendPoiRemovedPacket(this, blockPos);
             }));
             optional1.ifPresent(holder -> this.getServer().execute(() -> {
+                // Paper start - Remove stale POIs
+                if (optional.isEmpty() && this.getPoiManager().exists(blockPos, ignored -> true)) {
+                    this.getPoiManager().remove(blockPos);
+                }
+                // Paper end - Remove stale POIs
                 this.getPoiManager().add(blockPos, (Holder<PoiType>)holder);
                 DebugPackets.sendPoiAddedPacket(this, blockPos);
             }));
@@ -1574,12 +_,12 @@
     }
 
     public boolean isFlat() {
-        return this.server.getWorldData().isFlatWorld();
+        return this.serverLevelData.isFlatWorld(); // CraftBukkit
     }
 
     @Override
     public long getSeed() {
-        return this.server.getWorldData().worldGenOptions().seed();
+        return this.serverLevelData.worldGenOptions().seed(); // CraftBukkit
     }
 
     @Nullable
@@ -1630,6 +_,7 @@
 
     @Override
     public LevelEntityGetter<Entity> getEntities() {
+        org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
         return this.entityManager.getEntityGetter();
     }
 
@@ -1736,6 +_,28 @@
         return this.serverLevelData.getGameRules();
     }
 
+    // Paper start - respect global sound events gamerule
+    public List<net.minecraft.server.level.ServerPlayer> getPlayersForGlobalSoundGamerule() {
+        return this.getGameRules().getBoolean(GameRules.RULE_GLOBAL_SOUND_EVENTS) ? ((ServerLevel) this).getServer().getPlayerList().players : ((ServerLevel) this).players();
+    }
+
+    public double getGlobalSoundRangeSquared(java.util.function.Function<org.spigotmc.SpigotWorldConfig, Integer> rangeFunction) {
+        final double range = rangeFunction.apply(this.spigotConfig);
+        return range <= 0 ? 64.0 * 64.0 : range * range; // 64 is taken from default in ServerLevel#levelEvent
+    }
+    // Paper end - respect global sound events gamerule
+    // Paper start - notify observers even if grow failed
+    @Deprecated
+    public void checkCapturedTreeStateForObserverNotify(final BlockPos pos, final org.bukkit.craftbukkit.block.CraftBlockState craftBlockState) {
+        // notify observers if the block state is the same and the Y level equals the original y level (for mega trees)
+        // blocks at the same Y level with the same state can be assumed to be saplings which trigger observers regardless of if the
+        // tree grew or not
+        if (craftBlockState.getPosition().getY() == pos.getY() && this.getBlockState(craftBlockState.getPosition()) == craftBlockState.getHandle()) {
+            this.notifyAndUpdatePhysics(craftBlockState.getPosition(), null, craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getHandle(), craftBlockState.getFlags(), 512);
+        }
+    }
+    // Paper end - notify observers even if grow failed
+
     @Override
     public CrashReportCategory fillReportDetails(CrashReport report) {
         CrashReportCategory crashReportCategory = super.fillReportDetails(report);
@@ -1754,6 +_,7 @@
             if (entity instanceof WaypointTransmitter waypointTransmitter && waypointTransmitter.isTransmittingWaypoint()) {
                 ServerLevel.this.getWaypointManager().trackWaypoint(waypointTransmitter);
             }
+            entity.setOldPosAndRot(); // Paper - update old pos / rot for new entities as it will default to Vec3.ZERO
         }
 
         @Override
@@ -1767,17 +_,24 @@
 
         @Override
         public void onTickingStart(Entity entity) {
+            if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking
             ServerLevel.this.entityTickList.add(entity);
         }
 
         @Override
         public void onTickingEnd(Entity entity) {
             ServerLevel.this.entityTickList.remove(entity);
+            // Paper start - Reset pearls when they stop being ticked
+            if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) {
+                pearl.setOwner(null);
+            }
+            // Paper end - Reset pearls when they stop being ticked
         }
 
         @Override
         public void onTrackingStart(Entity entity) {
-            ServerLevel.this.getChunkSource().addEntity(entity);
+            org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot
+            // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true
             if (entity instanceof ServerPlayer serverPlayer) {
                 ServerLevel.this.players.add(serverPlayer);
                 if (serverPlayer.isReceivingWaypoints()) {
@@ -1792,7 +_,7 @@
             }
 
             if (entity instanceof Mob mob) {
-                if (ServerLevel.this.isUpdatingNavigations) {
+                if (false && ServerLevel.this.isUpdatingNavigations) { // Paper - Remove unnecessary onTrackingStart during navigation warning
                     String string = "onTrackingStart called during navigation iteration";
                     Util.logAndPauseIfInIde(
                         "onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")
@@ -1809,10 +_,52 @@
             }
 
             entity.updateDynamicGameEventListener(DynamicGameEventListener::add);
+            entity.inWorld = true; // CraftBukkit - Mark entity as in world
+            entity.valid = true; // CraftBukkit
+            ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server
+            // Paper start - Entity origin API
+            if (entity.origin == null) {
+                entity.origin = entity.position();
+            }
+            // Default to current world if unknown, gross assumption but entities rarely change world
+            if (entity.originWorld == null) {
+                entity.originWorld = ServerLevel.this.getWorld().getUID();
+            }
+            // Paper end - Entity origin API
+            new com.destroystokyo.paper.event.entity.EntityAddToWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
         }
 
         @Override
         public void onTrackingEnd(Entity entity) {
+            org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot
+            // Spigot start // TODO I don't think this is needed anymore
+            if (entity instanceof Player player) {
+                for (final ServerLevel level : ServerLevel.this.getServer().getAllLevels()) {
+                    for (final Optional<net.minecraft.world.level.saveddata.SavedData> savedData : level.getDataStorage().cache.values()) {
+                        if (savedData.isEmpty() || !(savedData.get() instanceof MapItemSavedData map)) {
+                            continue;
+                        }
+
+                        map.carriedByPlayers.remove(player);
+                        if (map.carriedBy.removeIf(holdingPlayer -> holdingPlayer.player == player)) {
+                            map.decorations.remove(player.getName().getString());
+                        }
+                    }
+                }
+            }
+            // Spigot end
+            // Spigot start
+            if (entity.getBukkitEntity() instanceof org.bukkit.inventory.InventoryHolder && (!(entity instanceof ServerPlayer) || entity.getRemovalReason() != Entity.RemovalReason.KILLED)) { // SPIGOT-6876: closeInventory clears death message
+                // Paper start - Fix merchant inventory not closing on entity removal
+                if (entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) {
+                    merchant.getTrader().closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED);
+                }
+                // Paper end - Fix merchant inventory not closing on entity removal
+                for (org.bukkit.entity.HumanEntity h : Lists.newArrayList(((org.bukkit.inventory.InventoryHolder) entity.getBukkitEntity()).getInventory().getViewers())) {
+                    h.closeInventory(org.bukkit.event.inventory.InventoryCloseEvent.Reason.UNLOADED); // Paper - Inventory close reason
+                }
+            }
+            // Spigot end
             ServerLevel.this.getChunkSource().removeEntity(entity);
             if (entity instanceof ServerPlayer serverPlayer) {
                 ServerLevel.this.players.remove(serverPlayer);
@@ -1821,7 +_,7 @@
             }
 
             if (entity instanceof Mob mob) {
-                if (ServerLevel.this.isUpdatingNavigations) {
+                if (false && ServerLevel.this.isUpdatingNavigations) { // Paper - Remove unnecessary onTrackingStart during navigation warning
                     String string = "onTrackingStart called during navigation iteration";
                     Util.logAndPauseIfInIde(
                         "onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")
@@ -1838,6 +_,15 @@
             }
 
             entity.updateDynamicGameEventListener(DynamicGameEventListener::remove);
+            // CraftBukkit start
+            entity.valid = false;
+            if (!(entity instanceof ServerPlayer)) {
+                for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players
+                    player.getBukkitEntity().onEntityRemove(entity);
+                }
+            }
+            // CraftBukkit end
+            new com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent(entity.getBukkitEntity(), ServerLevel.this.getWorld()).callEvent(); // Paper - fire while valid
         }
 
         @Override
@@ -1845,4 +_,24 @@
             entity.updateDynamicGameEventListener(DynamicGameEventListener::move);
         }
     }
+
+    // Paper start - check global player list where appropriate
+    @Override
+    @Nullable
+    public Player getGlobalPlayerByUUID(java.util.UUID uuid) {
+        return this.server.getPlayerList().getPlayer(uuid);
+    }
+    // Paper end - check global player list where appropriate
+
+    // Paper start - lag compensation
+    private long lagCompensationTick = MinecraftServer.SERVER_INIT;
+
+    public long getLagCompensationTick() {
+        return this.lagCompensationTick;
+    }
+
+    public void updateLagCompensationTick() {
+        this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L));
+    }
+    // Paper end - lag compensation
 }
