--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -170,7 +_,7 @@
 import net.minecraft.world.ticks.LevelTicks;
 import org.slf4j.Logger;
 
-public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel {
+public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration
     public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0);
     public static final IntProvider RAIN_DELAY = UniformInt.of(12000, 180000);
     public static final IntProvider RAIN_DURATION = UniformInt.of(12000, 24000);
@@ -180,12 +_,12 @@
     private static final int EMPTY_TIME_NO_TICK = 300;
     private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536;
     final List<ServerPlayer> players = Lists.newArrayList();
-    private final ServerChunkCache chunkSource;
+    public final ServerChunkCache chunkSource;
     private final MinecraftServer server;
-    private final ServerLevelData serverLevelData;
+    public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type
     private int lastSpawnChunkRadius;
     final EntityTickList entityTickList = new EntityTickList();
-    private final PersistentEntitySectionManager<Entity> entityManager;
+    // Paper - rewrite chunk system
     private final GameEventDispatcher gameEventDispatcher;
     public boolean noSave;
     private final SleepStatus sleepStatus;
@@ -209,11 +_,372 @@
     private final boolean tickTime;
     private final RandomSequences randomSequences;
 
+    // CraftBukkit start
+    public final LevelStorageSource.LevelStorageAccess levelStorageAccess;
+    public final UUID uuid;
+    public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent
+    public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent
+    private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current)
+
+    public 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) {
+        // Paper - rewrite chunk system
+        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) {
+        this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); // Paper - rewrite chunk system
+    }
+    // Paper end
+
+    // Paper start - optimise getPlayerByUUID
+    @Nullable
+    @Override
+    public Player getPlayerByUUID(UUID uuid) {
+        final Player player = this.getServer().getPlayerList().getPlayer(uuid);
+        return player != null && player.level() == this ? player : null;
+    }
+    // Paper end - optimise getPlayerByUUID
+    // Paper start - rewrite chunk system
+    private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder();
+    private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader chunkLoader = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader((ServerLevel)(Object)this);
+    private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController entityDataController;
+    private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController poiDataController;
+    private final ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController chunkDataController;
+    private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
+    private long lastMidTickFailure;
+    private long tickedBlocksOrFluids;
+    private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this);
+    private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0];
+    private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
+    private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
+    private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
+
+    @Override
+    public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+        return this.chunkSource.getChunkNow(chunkX, chunkZ);
+    }
+
+    @Override
+    public final ChunkAccess moonrise$getAnyChunkIfLoaded(final int chunkX, final int chunkZ) {
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+        if (newChunkHolder == null) {
+            return null;
+        }
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
+        return lastCompletion == null ? null : lastCompletion.chunk();
+    }
+
+    @Override
+    public final ChunkAccess moonrise$getSpecificChunkIfLoaded(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus leastStatus) {
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder newChunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+        if (newChunkHolder == null) {
+            return null;
+        }
+        return newChunkHolder.getChunkIfPresentUnchecked(leastStatus);
+    }
+
+    @Override
+    public final void moonrise$midTickTasks() {
+        ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+    }
+
+    @Override
+    public final ChunkAccess moonrise$syncLoadNonFull(final int chunkX, final int chunkZ, final net.minecraft.world.level.chunk.status.ChunkStatus status) {
+        return this.moonrise$getChunkTaskScheduler().syncLoadNonFull(chunkX, chunkZ, status);
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() {
+        return this.chunkTaskScheduler;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController  moonrise$getChunkDataController() {
+        return this.chunkDataController;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController() {
+        return this.poiDataController;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController() {
+        return this.entityDataController;
+    }
+
+    @Override
+    public final int moonrise$getRegionChunkShift() {
+        return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift();
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader() {
+        return this.chunkLoader;
+    }
+
+    @Override
+    public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+                                               final ca.spottedleaf.concurrentutil.util.Priority priority,
+                                               final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+        this.moonrise$loadChunksAsync(
+            (pos.getX() - radiusBlocks) >> 4,
+            (pos.getX() + radiusBlocks) >> 4,
+            (pos.getZ() - radiusBlocks) >> 4,
+            (pos.getZ() + radiusBlocks) >> 4,
+            priority, onLoad
+        );
+    }
+
+    @Override
+    public final void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
+                                               final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority,
+                                               final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+        this.moonrise$loadChunksAsync(
+            (pos.getX() - radiusBlocks) >> 4,
+            (pos.getX() + radiusBlocks) >> 4,
+            (pos.getZ() - radiusBlocks) >> 4,
+            (pos.getZ() + radiusBlocks) >> 4,
+            chunkStatus, priority, onLoad
+        );
+    }
+
+    @Override
+    public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+                                               final ca.spottedleaf.concurrentutil.util.Priority priority,
+                                               final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+        this.moonrise$loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad);
+    }
+
+    @Override
+    public final void moonrise$loadChunksAsync(final int minChunkX, final int maxChunkX, final int minChunkZ, final int maxChunkZ,
+                                               final net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, final ca.spottedleaf.concurrentutil.util.Priority priority,
+                                               final java.util.function.Consumer<java.util.List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = this.moonrise$getChunkTaskScheduler();
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager chunkHolderManager = chunkTaskScheduler.chunkHolderManager;
+
+        final int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+        final java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger();
+        final Long holderIdentifier = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getNextChunkLoadId();
+        final int ticketLevel = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getTicketLevel(chunkStatus);
+
+        final List<ChunkAccess> ret = new ArrayList<>(requiredChunks);
+
+        final java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (final ChunkAccess chunk) -> {
+            if (chunk != null) {
+                synchronized (ret) {
+                    ret.add(chunk);
+                }
+                chunkHolderManager.addTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunk.getPos(), ticketLevel, holderIdentifier);
+            }
+            if (loadedChunks.incrementAndGet() == requiredChunks) {
+                try {
+                    onLoad.accept(java.util.Collections.unmodifiableList(ret));
+                } finally {
+                    for (int i = 0, len = ret.size(); i < len; ++i) {
+                        final ChunkPos chunkPos = ret.get(i).getPos();
+
+                        chunkHolderManager.removeTicketAtLevel(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.CHUNK_LOAD, chunkPos, ticketLevel, holderIdentifier);
+                    }
+                }
+            }
+        };
+
+        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
+                );
+            }
+        }
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder() {
+        return this.viewDistanceHolder;
+    }
+
+    @Override
+    public final long moonrise$getLastMidTickFailure() {
+        return this.lastMidTickFailure;
+    }
+
+    @Override
+    public final void moonrise$setLastMidTickFailure(final long time) {
+        this.lastMidTickFailure = time;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() {
+        return this.nearbyPlayers;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks() {
+        return this.loadedChunks;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks() {
+        return this.tickingChunks;
+    }
+
+    @Override
+    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks() {
+        return this.entityTickingChunks;
+    }
+
+    @Override
+    public final boolean moonrise$areChunksLoaded(final int fromX, final int fromZ, final int toX, final int toZ) {
+        final ServerChunkCache chunkSource = this.chunkSource;
+
+        for (int currZ = fromZ; currZ <= toZ; ++currZ) {
+            for (int currX = fromX; currX <= toX; ++currX) {
+                if (!chunkSource.hasChunk(currX, currZ)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+    // Paper end - rewrite chunk system
+    // Paper start - chunk tick iteration
+    private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0];
+    private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS);
+    private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap();
+
+    @Override
+    public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks() {
+        return this.playerTickingChunks;
+    }
+
+    @Override
+    public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) {
+        final ChunkPos pos = chunk.getPos();
+        if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) {
+            return;
+        }
+
+        this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder());
+    }
+
+    @Override
+    public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) {
+        this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder());
+    }
+
+    @Override
+    public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) {
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async");
+
+        final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+        if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) {
+            // already added
+            return;
+        }
+
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler()
+            .chunkHolderManager.getChunkHolder(chunkKey);
+
+        if (chunkHolder == null || !chunkHolder.isTickingReady()) {
+            return;
+        }
+
+        this.playerTickingChunks.add(
+            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder()
+        );
+    }
+
+    @Override
+    public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) {
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async");
+
+        final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
+        final int val = this.playerTickingRequests.addTo(chunkKey, -1);
+
+        if (val <= 0) {
+            throw new IllegalStateException("Negative counter");
+        }
+
+        if (val != 1) {
+            // still has at least one request
+            return;
+        }
+
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler()
+            .chunkHolderManager.getChunkHolder(chunkKey);
+
+        if (chunkHolder == null || !chunkHolder.isTickingReady()) {
+            return;
+        }
+
+        this.playerTickingChunks.remove(
+            ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder()
+        );
+    }
+    // Paper end - chunk tick iteration
+
     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,
@@ -221,28 +_,41 @@
         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())), dispatcher); // Paper - create paper world configs; Async-Anti-Xray: Pass executor
+        this.pvpMode = server.isPvpAllowed();
+        this.levelStorageAccess = levelStorageAccess;
+        this.uuid = org.bukkit.craftbukkit.util.WorldUUID.getUUID(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(
-            new SimpleRegionStorage(
-                new RegionStorageInfo(levelStorageAccess.getLevelId(), dimension, "entities"),
-                levelStorageAccess.getDimensionPath(dimension).resolve("entities"),
-                fixerUpper,
-                flag,
-                DataFixTypes.ENTITY_CHUNK
-            ),
-            this,
-            server
-        );
-        this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entityPersistentStorage);
+        // Paper - rewrite chunk system
         this.chunkSource = new ServerChunkCache(
             this,
             levelStorageAccess,
@@ -250,11 +_,11 @@
             server.getStructureManager(),
             dispatcher,
             chunkGenerator,
-            server.getPlayerList().getViewDistance(),
-            server.getPlayerList().getSimulationDistance(),
+            this.spigotConfig.viewDistance, // Spigot
+            this.spigotConfig.simulationDistance, // Spigot
             flag,
             progressListener,
-            this.entityManager::updateChunkStatus,
+            null, // Paper - rewrite chunk system
             () -> server.overworld().getDataStorage()
         );
         this.chunkSource.getGeneratorState().ensureStructuresGenerated();
@@ -272,7 +_,7 @@
             this.chunkSource.chunkScanner(),
             this.registryAccess(),
             server.getStructureManager(),
-            dimension,
+            getTypeKey(), // Paper - Fix missing CB diff
             chunkGenerator,
             this.chunkSource.randomState(),
             this,
@@ -280,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;
         }
@@ -292,7 +_,29 @@
         this.randomSequences = Objects.requireNonNullElseGet(
             randomSequences, () -> this.getDataStorage().computeIfAbsent(RandomSequences.factory(seed), "random_sequences")
         );
-    }
+        // Paper start - rewrite chunk system
+        this.moonrise$setEntityLookup(new ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup((ServerLevel)(Object)this, ((ServerLevel)(Object)this).new EntityCallbacks()));
+        this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this);
+        this.entityDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController(
+            new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.EntityDataController.EntityRegionFileStorage(
+                new RegionStorageInfo(levelStorageAccess.getLevelId(), dimension, "entities"),
+                levelStorageAccess.getDimensionPath(dimension).resolve("entities"),
+                server.forceSynchronousWrites()
+            ),
+            this.chunkTaskScheduler
+        );
+        this.poiDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.PoiDataController((ServerLevel)(Object)this, this.chunkTaskScheduler);
+        this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler);
+        // Paper end - rewrite chunk system
+        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
@@ -304,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
@@ -332,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();
             }
@@ -352,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();
         }
 
@@ -372,7 +_,7 @@
 
         this.handlingTick = false;
         profilerFiller.pop();
-        boolean flag = !this.players.isEmpty() || !this.getForcedChunks().isEmpty();
+        boolean flag = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this
         if (flag) {
             this.resetEmptyTime();
         }
@@ -385,6 +_,7 @@
                 profilerFiller.pop();
             }
 
+            io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR
             this.entityTickList
                 .forEach(
                     entity -> {
@@ -393,8 +_,7 @@
                                 profilerFiller.push("checkDespawn");
                                 entity.checkDespawn();
                                 profilerFiller.pop();
-                                if (entity instanceof ServerPlayer
-                                    || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) {
+                                if (true) { // Paper - rewrite chunk system
                                     Entity vehicle = entity.getVehicle();
                                     if (vehicle != null) {
                                         if (!vehicle.isRemoved() && vehicle.hasPassenger(entity)) {
@@ -417,13 +_,16 @@
         }
 
         profilerFiller.push("entityManagement");
-        this.entityManager.tick();
+        // Paper - rewrite chunk system
         profilerFiller.pop();
     }
 
     @Override
     public boolean shouldTickBlocksAt(long chunkPos) {
-        return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos);
+        // Paper start - rewrite chunk system
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder holder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+        return holder != null && holder.isTickingReady();
+        // Paper end - rewrite chunk system
     }
 
     protected void tickTime() {
@@ -454,19 +_,72 @@
         this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false));
     }
 
+    // Paper start - optimise random ticking
+    private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed());
+
+    private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) {
+        final LevelChunkSection[] sections = chunk.getSections();
+        final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this);
+        final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom;
+        final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294();
+
+        final ChunkPos cpos = chunk.getPos();
+        final int offsetX = cpos.x << 4;
+        final int offsetZ = cpos.z << 4;
+
+        for (int sectionIndex = 0, sectionsLen = sections.length; sectionIndex < sectionsLen; sectionIndex++) {
+            final int offsetY = (sectionIndex + minSection) << 4;
+            final LevelChunkSection section = sections[sectionIndex];
+            final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> states = section.states;
+            if (!section.isRandomlyTickingBlocks()) {
+                continue;
+            }
+
+            final ca.spottedleaf.moonrise.common.list.ShortList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList();
+
+            for (int i = 0; i < tickSpeed; ++i) {
+                final int tickingBlocks = tickList.size();
+                final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1);
+
+                if (index >= tickingBlocks) {
+                    // most of the time we fall here
+                    continue;
+                }
+
+                final int location = (int)tickList.getRaw(index) & 0xFFFF;
+                final BlockState state = states.get(location);
+
+                // do not use a mutable pos, as some random tick implementations store the input without calling immutable()!
+                final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ);
+
+                state.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
+                if (doubleTickFluids) {
+                    final FluidState fluidState = state.getFluidState();
+                    if (fluidState.isRandomlyTicking()) {
+                        fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
+                    }
+                }
+            }
+        }
+
+        return;
+    }
+    // Paper end - optimise random ticking
+
     public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
+        final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking
         ChunkPos pos = chunk.getPos();
         boolean isRaining = this.isRaining();
         int minBlockX = pos.getMinBlockX();
         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 && simpleRandom.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder // Paper - optimise random ticking
             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);
@@ -474,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
                     }
                 }
 
@@ -482,48 +_,24 @@
                 if (lightningBolt != null) {
                     lightningBolt.moveTo(Vec3.atBottomCenterOf(blockPos));
                     lightningBolt.setVisualOnly(flag);
-                    this.addFreshEntity(lightningBolt);
+                    this.strikeLightning(lightningBolt, org.bukkit.event.weather.LightningStrikeEvent.Cause.WEATHER); // CraftBukkit
                 }
             }
         }
 
         profilerFiller.popPush("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) {
+            if (simpleRandom.nextInt(48) == 0) {  // Paper - optimise random ticking
                 this.tickPrecipitation(this.getBlockRandomPos(minBlockX, 0, minBlockZ, 15));
             }
         }
+        } // Paper - Option to disable ice and snow
 
         profilerFiller.popPush("tickBlocks");
         if (randomTickSpeed > 0) {
-            LevelChunkSection[] sections = chunk.getSections();
-
-            for (int i1 = 0; i1 < sections.length; i1++) {
-                LevelChunkSection levelChunkSection = sections[i1];
-                if (levelChunkSection.isRandomlyTicking()) {
-                    int sectionYFromSectionIndex = chunk.getSectionYFromSectionIndex(i1);
-                    int blockPosCoord = SectionPos.sectionToBlockCoord(sectionYFromSectionIndex);
-
-                    for (int i2 = 0; i2 < randomTickSpeed; i2++) {
-                        BlockPos blockRandomPos = this.getBlockRandomPos(minBlockX, blockPosCoord, minBlockZ, 15);
-                        profilerFiller.push("randomTick");
-                        BlockState blockState = levelChunkSection.getBlockState(
-                            blockRandomPos.getX() - minBlockX, blockRandomPos.getY() - blockPosCoord, blockRandomPos.getZ() - minBlockZ
-                        );
-                        if (blockState.isRandomlyTicking()) {
-                            blockState.randomTick(this, blockRandomPos, this.random);
-                        }
-
-                        FluidState fluidState = blockState.getFluidState();
-                        if (fluidState.isRandomlyTicking()) {
-                            fluidState.randomTick(this, blockRandomPos, this.random);
-                        }
-
-                        profilerFiller.pop();
-                    }
-                }
-            }
+            this.optimiseRandomTick(chunk, randomTickSpeed); // Paper - optimise random ticking
         }
 
         profilerFiller.pop();
@@ -535,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(), null); // CraftBukkit
         }
 
         if (this.isRaining()) {
@@ -547,10 +_,10 @@
                     if (layersValue < Math.min(_int, 8)) {
                         BlockState blockState1 = blockState.setValue(SnowLayerBlock.LAYERS, Integer.valueOf(layersValue + 1));
                         Block.pushEntitiesUp(blockState, blockState1, this, heightmapPos);
-                        this.setBlockAndUpdate(heightmapPos, blockState1);
+                        org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, heightmapPos, blockState1, null); // CraftBukkit
                     }
                 } else {
-                    this.setBlockAndUpdate(heightmapPos, Blocks.SNOW.defaultBlockState());
+                    org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, heightmapPos, Blocks.SNOW.defaultBlockState(), null); // CraftBukkit
                 }
             }
 
@@ -562,7 +_,7 @@
         }
     }
 
-    private Optional<BlockPos> findLightningRod(BlockPos pos) {
+    public Optional<BlockPos> findLightningRod(BlockPos pos) {
         Optional<BlockPos> optional = this.getPoiManager()
             .findClosest(
                 pointOfInterestType -> pointOfInterestType.is(PoiTypes.LIGHTNING_ROD),
@@ -575,6 +_,11 @@
     }
 
     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()) {
@@ -582,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);
                 }
@@ -673,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;
@@ -695,6 +_,7 @@
             this.rainLevel = Mth.clamp(this.rainLevel, 0.0F, 1.0F);
         }
 
+        /* CraftBukkit start
         if (this.oRainLevel != this.rainLevel) {
             this.server
                 .getPlayerList()
@@ -717,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() {
@@ -737,6 +_,12 @@
         if (fluidState.is(fluid)) {
             fluidState.tick(this, pos, blockState);
         }
+        // Paper start - rewrite chunk system
+        if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
+            ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+        }
+        // Paper end - rewrite chunk system
+
     }
 
     private void tickBlock(BlockPos pos, Block block) {
@@ -744,36 +_,84 @@
         if (blockState.is(block)) {
             blockState.tick(this, pos, this.random);
         }
-    }
+        // Paper start - rewrite chunk system
+        if ((++this.tickedBlocksOrFluids & 7L) != 0L) {
+            ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+        }
+        // Paper end - rewrite chunk system
+
+    }
+
+    // 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");
+        final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2
+        if (isActive) { // Paper - EAR 2
         entity.tick();
+        entity.postTick(); // CraftBukkit
+        } else {entity.inactiveTick();} // Paper - EAR 2
         profilerFiller.pop();
 
         for (Entity entity1 : entity.getPassengers()) {
-            this.tickPassenger(entity, entity1);
-        }
+            this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2
+        }
+        // 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) {
+    private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2
         if (passengerEntity.isRemoved() || passengerEntity.getVehicle() != ridingEntity) {
             passengerEntity.stopRiding();
         } 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");
+            // Paper start - EAR 2
+            if (isActive) {
             passengerEntity.rideTick();
+            passengerEntity.postTick(); // CraftBukkit
+            } else {
+                passengerEntity.setDeltaMovement(Vec3.ZERO);
+                passengerEntity.inactiveTick();
+                // copied from inside of if (isPassenger()) of passengerTick, but that ifPassenger is unnecessary
+                ridingEntity.positionRider(passengerEntity);
+            }
+            // Paper end - EAR 2
             profilerFiller.pop();
 
             for (Entity entity : passengerEntity.getPassengers()) {
-                this.tickPassenger(passengerEntity, entity);
+                this.tickPassenger(passengerEntity, entity, isActive); // Paper - EAR 2
             }
         }
     }
@@ -783,9 +_,37 @@
         return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos);
     }
 
+    // Paper start - Incremental chunk and player saving
+    public void saveIncrementally(boolean doFull) {
+        if (doFull) {
+            org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(this.getWorld()));
+        }
+
+        if (doFull) {
+            this.saveLevelData(true);
+        }
+        // chunk autosave is already called by the ChunkSystem during unload processing (ChunkMap#processUnloads)
+        // Copied from save()
+        // CraftBukkit start - moved from MinecraftServer.saveChunks
+        if (doFull) { // Paper
+            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
+    }
+    // Paper end - Incremental chunk and player saving
+
     public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave) {
+        // Paper start - add close param
+        this.save(progress, flush, skipSave, false);
+    }
+    public void save(@Nullable ProgressListener progress, boolean flush, boolean skipSave, boolean close) {
+        // Paper end - add close param
         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"));
             }
@@ -795,18 +_,31 @@
                 progress.progressStage(Component.translatable("menu.savingChunks"));
             }
 
-            chunkSource.save(flush);
-            if (flush) {
-                this.entityManager.saveAll();
-            } else {
-                this.entityManager.autoSave();
+            if (!close) { chunkSource.save(flush); } // Paper - add close param
+            // Paper - rewrite chunk system
+        }
+        // Paper start - add close param
+        if (close) {
+            try {
+                chunkSource.close(!skipSave);
+            } catch (IOException never) {
+                throw new RuntimeException(never);
             }
         }
+        // Paper end - add close param
+
+        // CraftBukkit start - moved from MinecraftServer.saveChunks
+        ServerLevel worldserver1 = this;
+
+        this.serverLevelData.setWorldBorder(worldserver1.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();
@@ -871,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, 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, 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, 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
         }
     }
 
@@ -902,43 +_,122 @@
             this.removePlayerImmediately((ServerPlayer)entity, Entity.RemovalReason.DISCARDED);
         }
 
-        this.entityManager.addNewEntity(player);
+        this.moonrise$getEntityLookup().addNewEntity(player); // Paper - rewrite chunk system
     }
 
-    private boolean addEntity(Entity entity) {
+    // CraftBukkit start
+    private boolean addEntity(Entity entity, 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 {
-            return this.entityManager.addNewEntity(entity);
+            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.moonrise$getEntityLookup().addNewEntity(entity); // Paper - rewrite chunk system
         }
     }
 
     public boolean tryAddFreshEntityWithPassengers(Entity entity) {
-        if (entity.getSelfAndPassengers().map(Entity::getUUID).anyMatch(this.entityManager::isLoaded)) {
+        // 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.moonrise$getEntityLookup()::hasEntity)) { // Paper - rewrite chunk system
             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));
                 }
@@ -1000,7 +_,7 @@
     public void levelEvent(@Nullable Player player, int type, BlockPos pos, int data) {
         this.server
             .getPlayerList()
-            .broadcast(player, pos.getX(), pos.getY(), pos.getZ(), 64.0, this.dimension(), new ClientboundLevelEventPacket(type, pos, data, false));
+            .broadcast(player, pos.getX(), pos.getY(), pos.getZ(), 64.0, this.dimension(), new ClientboundLevelEventPacket(type, pos, data, false)); // 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)
     }
 
     public int getLogicalHeight() {
@@ -1009,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);
     }
 
@@ -1021,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;
@@ -1043,15 +_,18 @@
                 this.isUpdatingNavigations = false;
             }
         }
+        } // Paper - option to disable pathfinding updates
     }
 
     @Override
     public void updateNeighborsAt(BlockPos pos, Block block) {
+        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);
     }
 
@@ -1100,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);
@@ -1108,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) {
@@ -1120,6 +_,8 @@
                 serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound));
             }
         }
+
+        return serverExplosion; // CraftBukkit
     }
 
     private Explosion.BlockInteraction getDestroyType(GameRules.Key<GameRules.BooleanValue> decayGameRule) {
@@ -1190,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(
@@ -1206,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(
+        @javax.annotation.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,
+        @javax.annotation.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++;
             }
@@ -1280,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);
@@ -1327,11 +_,38 @@
     @Nullable
     @Override
     public MapItemSavedData getMapData(MapId mapId) {
-        return this.getServer().overworld().getDataStorage().get(MapItemSavedData.factory(), mapId.key());
+        // 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(mapId.key());
+        if (cacheEntry == null) { // Cache did not contain, try to load and may init
+            final MapItemSavedData mapData = storage.get(MapItemSavedData.factory(), mapId.key()); // 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
     }
 
     @Override
     public void setMapData(MapId mapId, MapItemSavedData mapData) {
+        // CraftBukkit start
+        mapData.id = mapId;
+        org.bukkit.event.server.MapInitializeEvent event = new org.bukkit.event.server.MapInitializeEvent(mapData.mapView);
+        event.callEvent();
+        // CraftBukkit end
         this.getServer().overworld().getDataStorage().set(mapId.key(), mapData);
     }
 
@@ -1344,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().removeRegionTicket(TicketType.START, new ChunkPos(spawnPos), this.lastSpawnChunkRadius, Unit.INSTANCE);
+            // 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, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE);
+            }
+            // Paper end - allow disabling gamerule limits
         }
 
         int i = this.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS) + 1;
         if (i > 1) {
-            this.getChunkSource().addRegionTicket(TicketType.START, new ChunkPos(pos), i, Unit.INSTANCE);
+            // Paper start - allow disabling gamerule limits
+            for (ChunkPos chunkPos : io.papermc.paper.util.MCUtil.getSpiralOutChunks(pos, i - 2)) {
+                this.getChunkSource().addTicketAtLevel(TicketType.START, chunkPos, net.minecraft.server.level.ChunkLevel.ENTITY_TICKING_LEVEL, Unit.INSTANCE);
+            }
+            // Paper end - allow disabling gamerule limits
         }
 
         this.lastSpawnChunkRadius = i;
@@ -1403,6 +_,11 @@
                 DebugPackets.sendPoiRemovedPacket(this, blockPos);
             }));
             optional1.ifPresent(poiType -> 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>)poiType);
                 DebugPackets.sendPoiAddedPacket(this, blockPos);
             }));
@@ -1458,7 +_,7 @@
                 }
             }
 
-            bufferedWriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats()));
+            bufferedWriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo()));  // Paper - rewrite chunk system
             bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
             bufferedWriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
             bufferedWriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
@@ -1476,13 +_,13 @@
         Path path1 = path.resolve("chunks.csv");
 
         try (Writer bufferedWriter2 = Files.newBufferedWriter(path1)) {
-            chunkMap.dumpChunks(bufferedWriter2);
+            //chunkMap.dumpChunks(bufferedWriter2); // Paper - rewrite chunk system
         }
 
         Path path2 = path.resolve("entity_chunks.csv");
 
         try (Writer bufferedWriter3 = Files.newBufferedWriter(path2)) {
-            this.entityManager.dumpSections(bufferedWriter3);
+            //this.entityManager.dumpSections(bufferedWriter3); // Paper - rewrite chunk system
         }
 
         Path path3 = path.resolve("entities.csv");
@@ -1543,6 +_,11 @@
     @Override
     public void blockUpdated(BlockPos pos, Block block) {
         if (!this.isDebug()) {
+            // CraftBukkit start
+            if (this.populating) {
+                return;
+            }
+            // CraftBukkit end
             this.updateNeighborsAt(pos, block);
         }
     }
@@ -1562,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
@@ -1586,8 +_,8 @@
             Locale.ROOT,
             "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s",
             this.players.size(),
-            this.entityManager.gatherStats(),
-            getTypeCount(this.entityManager.getEntityGetter().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()),
+            this.moonrise$getEntityLookup().getDebugInfo(), // Paper - rewrite chunk system
+            getTypeCount(this.moonrise$getEntityLookup().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), // Paper - rewrite chunk system
             this.blockEntityTickers.size(),
             getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType),
             this.getBlockTicks().count(),
@@ -1617,16 +_,27 @@
     }
 
     @Override
-    protected LevelEntityGetter<Entity> getEntities() {
-        return this.entityManager.getEntityGetter();
+    public LevelEntityGetter<Entity> getEntities() {
+        org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
+        return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system
     }
 
     public void addLegacyChunkEntities(Stream<Entity> entities) {
-        this.entityManager.addLegacyChunkEntities(entities);
+        // Paper start - add chunkpos param
+        this.addLegacyChunkEntities(entities, null);
+    }
+    public void addLegacyChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
+        // Paper end - add chunkpos param
+        this.moonrise$getEntityLookup().addLegacyChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
     }
 
     public void addWorldGenChunkEntities(Stream<Entity> entities) {
-        this.entityManager.addWorldGenChunkEntities(entities);
+        // Paper start - add chunkpos param
+        this.addWorldGenChunkEntities(entities, null);
+    }
+    public void addWorldGenChunkEntities(Stream<Entity> entities, ChunkPos chunkPos) {
+        // Paper end - add chunkpos param
+        this.moonrise$getEntityLookup().addWorldGenChunkEntities(entities.toList(), chunkPos); // Paper - rewrite chunk system
     }
 
     public void startTickingChunk(LevelChunk chunk) {
@@ -1644,32 +_,45 @@
     @Override
     public void close() throws IOException {
         super.close();
-        this.entityManager.close();
+        // Paper - rewrite chunk system
     }
 
     @Override
     public String gatherChunkSourceStats() {
-        return "Chunks[S] W: " + this.chunkSource.gatherStats() + " E: " + this.entityManager.gatherStats();
+        return "Chunks[S] W: " + this.chunkSource.gatherStats() + " E: " + this.moonrise$getEntityLookup().getDebugInfo(); // Paper - rewrite chunk system
     }
 
     public boolean areEntitiesLoaded(long chunkPos) {
-        return this.entityManager.areEntitiesLoaded(chunkPos);
+        return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system
     }
 
     private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) {
-        return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos);
+        // Paper start - rewrite chunk system
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
+        // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded
+        return chunkHolder != null && chunkHolder.isTickingReady();
+        // Paper end - rewrite chunk system
     }
 
     public boolean isPositionEntityTicking(BlockPos pos) {
-        return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos));
+        // Paper start - rewrite chunk system
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+        return chunkHolder != null && chunkHolder.isEntityTickingReady();
+        // Paper end - rewrite chunk system
     }
 
     public boolean isNaturalSpawningAllowed(BlockPos pos) {
-        return this.entityManager.canPositionTick(pos);
+        // Paper start - rewrite chunk system
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos));
+        return chunkHolder != null && chunkHolder.isEntityTickingReady();
+        // Paper end - rewrite chunk system
     }
 
     public boolean isNaturalSpawningAllowed(ChunkPos chunkPos) {
-        return this.entityManager.canPositionTick(chunkPos);
+        // Paper start - rewrite chunk system
+        final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkPos));
+        return chunkHolder != null && chunkHolder.isEntityTickingReady();
+        // Paper end - rewrite chunk system
     }
 
     @Override
@@ -1699,10 +_,31 @@
         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
+    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.getFlag(), 512);
+        }
+    }
+    // Paper end - notify observers even if grow failed
+
     @Override
     public CrashReportCategory fillReportDetails(CrashReport report) {
         CrashReportCategory crashReportCategory = super.fillReportDetails(report);
-        crashReportCategory.setDetail("Loaded entity count", () -> String.valueOf(this.entityManager.count()));
+        crashReportCategory.setDetail("Loaded entity count", () -> String.valueOf(this.moonrise$getEntityLookup().getEntityCount())); // Paper - rewrite chunk system
         return crashReportCategory;
     }
 
@@ -1711,6 +_,13 @@
         return this.chunkSource.getGenerator().getSeaLevel();
     }
 
+    // Paper start - optimize redstone (Alternate Current)
+    @Override
+    public alternate.current.wire.WireHandler getWireHandler() {
+        return wireHandler;
+    }
+    // Paper end - optimize redstone (Alternate Current)
+
     final class EntityCallbacks implements LevelCallback<Entity> {
         @Override
         public void onCreated(Entity entity) {
@@ -1723,24 +_,32 @@
 
         @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.cachedOwner = null;
+                pearl.ownerUUID = 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);
                 ServerLevel.this.updateSleepingPlayerList();
             }
 
             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")
@@ -1757,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.getOriginVector() == null) {
+                entity.setOrigin(entity.getBukkitEntity().getLocation());
+            }
+            // Default to current world if unknown, gross assumption but entities rarely change world
+            if (entity.getOriginWorld() == null) {
+                entity.setOrigin(entity.getOriginVector().toLocation(getWorld()));
+            }
+            // 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);
@@ -1768,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")
@@ -1785,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
@@ -1792,4 +_,24 @@
             entity.updateDynamicGameEventListener(DynamicGameEventListener::move);
         }
     }
+
+    // Paper start - check global player list where appropriate
+    @Override
+    @Nullable
+    public Player getGlobalPlayerByUUID(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
 }
