--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -5,6 +5,7 @@
 import java.io.IOException;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -24,8 +25,10 @@
 import net.minecraft.network.protocol.Packet;
 import net.minecraft.resources.ResourceKey;
 import net.minecraft.resources.ResourceLocation;
+import io.papermc.paper.util.MCUtil;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.level.FullChunkStatus;
+import net.minecraft.server.level.ServerLevel;
 import net.minecraft.sounds.SoundEvent;
 import net.minecraft.sounds.SoundEvents;
 import net.minecraft.sounds.SoundSource;
@@ -40,6 +43,7 @@
 import net.minecraft.world.entity.Entity;
 import net.minecraft.world.entity.boss.EnderDragonPart;
 import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
+import net.minecraft.world.entity.item.ItemEntity;
 import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.item.ItemStack;
 import net.minecraft.world.item.alchemy.PotionBrewing;
@@ -53,11 +57,14 @@
 import net.minecraft.world.level.block.entity.BlockEntity;
 import net.minecraft.world.level.block.entity.TickingBlockEntity;
 import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.border.BorderChangeListener;
 import net.minecraft.world.level.border.WorldBorder;
 import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.ChunkSource;
 import net.minecraft.world.level.chunk.LevelChunk;
 import net.minecraft.world.level.chunk.status.ChunkStatus;
 import net.minecraft.world.level.dimension.DimensionType;
+import net.minecraft.world.level.dimension.LevelStem;
 import net.minecraft.world.level.entity.EntityTypeTest;
 import net.minecraft.world.level.entity.LevelEntityGetter;
 import net.minecraft.world.level.gameevent.GameEvent;
@@ -75,7 +82,27 @@
 import net.minecraft.world.phys.Vec3;
 import net.minecraft.world.scores.Scoreboard;
 
+// CraftBukkit start
+import java.util.HashMap;
+import java.util.Map;
+import net.minecraft.network.protocol.game.ClientboundSetBorderCenterPacket;
+import net.minecraft.network.protocol.game.ClientboundSetBorderLerpSizePacket;
+import net.minecraft.network.protocol.game.ClientboundSetBorderSizePacket;
+import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDelayPacket;
+import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDistancePacket;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.CraftServer;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.block.CapturedBlockState;
+import org.bukkit.craftbukkit.block.CraftBlockState;
+import org.bukkit.craftbukkit.block.data.CraftBlockData;
+import org.bukkit.craftbukkit.util.CraftSpawnCategory;
+import org.bukkit.entity.SpawnCategory;
+import org.bukkit.event.block.BlockPhysicsEvent;
+// CraftBukkit end
+
 public abstract class Level implements LevelAccessor, AutoCloseable {
+
     public static final Codec<ResourceKey<Level>> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION);
     public static final ResourceKey<Level> OVERWORLD = ResourceKey.create(Registries.DIMENSION, new ResourceLocation("overworld"));
     public static final ResourceKey<Level> NETHER = ResourceKey.create(Registries.DIMENSION, new ResourceLocation("the_nether"));
@@ -87,7 +114,7 @@
     public static final int TICKS_PER_DAY = 24000;
     public static final int MAX_ENTITY_SPAWN_Y = 20000000;
     public static final int MIN_ENTITY_SPAWN_Y = -20000000;
-    protected final List<TickingBlockEntity> blockEntityTickers = Lists.newArrayList();
+    public final List<TickingBlockEntity> blockEntityTickers = Lists.newArrayList(); // Paper - public
     protected final NeighborUpdater neighborUpdater;
     private final List<TickingBlockEntity> pendingBlockEntityTickers = Lists.newArrayList();
     private boolean tickingBlockEntities;
@@ -101,6 +128,7 @@
     protected float oThunderLevel;
     public float thunderLevel;
     public final RandomSource random = RandomSource.create();
+    /** @deprecated */
     @Deprecated
     private final RandomSource threadSafeRandom = RandomSource.createThreadSafe();
     private final Holder<DimensionType> dimensionTypeRegistration;
@@ -114,33 +142,100 @@
     private final DamageSources damageSources;
     private long subTickCount;
 
-    protected Level(
-        WritableLevelData properties,
-        ResourceKey<Level> registryRef,
-        RegistryAccess registryManager,
-        Holder<DimensionType> dimensionEntry,
-        Supplier<ProfilerFiller> profiler,
-        boolean isClient,
-        boolean debugWorld,
-        long biomeAccess,
-        int maxChainedNeighborUpdates
-    ) {
-        this.profiler = profiler;
-        this.levelData = properties;
-        this.dimensionTypeRegistration = dimensionEntry;
-        final DimensionType dimensionType = dimensionEntry.value();
-        this.dimension = registryRef;
-        this.isClientSide = isClient;
-        if (dimensionType.coordinateScale() != 1.0) {
-            this.worldBorder = new WorldBorder() {
+    // CraftBukkit start Added the following
+    private final CraftWorld world;
+    public boolean pvpMode;
+    public org.bukkit.generator.ChunkGenerator generator;
+
+    public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
+    public boolean captureBlockStates = false;
+    public boolean captureTreeGeneration = false;
+    public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
+    public Map<BlockPos, org.bukkit.craftbukkit.block.CraftBlockState> capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
+    public Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates
+    public List<ItemEntity> captureDrops;
+    public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
+    // Paper start
+    public int wakeupInactiveRemainingAnimals;
+    public int wakeupInactiveRemainingFlying;
+    public int wakeupInactiveRemainingMonsters;
+    public int wakeupInactiveRemainingVillagers;
+    // Paper end
+    public boolean populating;
+    public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
+    // Paper start - add paper world config
+    private final io.papermc.paper.configuration.WorldConfiguration paperConfig;
+    public io.papermc.paper.configuration.WorldConfiguration paperConfig() {
+        return this.paperConfig;
+    }
+    // Paper end - add paper world config
+
+    public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray
+    public final co.aikar.timings.WorldTimingsHandler timings; // Paper
+    public static BlockPos lastPhysicsProblem; // Spigot
+    private org.spigotmc.TickLimiter entityLimiter;
+    private org.spigotmc.TickLimiter tileLimiter;
+    private int tileTickPosition;
+    public final Map<Explosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions
+    public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here
+
+    public CraftWorld getWorld() {
+        return this.world;
+    }
+
+    public CraftServer getCraftServer() {
+        return (CraftServer) Bukkit.getServer();
+    }
+
+    // Paper start - Use getChunkIfLoadedImmediately
+    @Override
+    public boolean hasChunk(int chunkX, int chunkZ) {
+        return this.getChunkIfLoaded(chunkX, chunkZ) != null;
+    }
+    // Paper end - Use getChunkIfLoadedImmediately
+    // Paper start - per world ticks per spawn
+    private int getTicksPerSpawn(SpawnCategory spawnCategory) {
+        final int perWorld = this.paperConfig().entities.spawning.ticksPerSpawn.getInt(CraftSpawnCategory.toNMS(spawnCategory));
+        if (perWorld >= 0) {
+            return perWorld;
+        }
+        return this.getCraftServer().getTicksPerSpawns(spawnCategory);
+    }
+    // Paper end
+
+    public abstract ResourceKey<LevelStem> getTypeKey();
+
+    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, RegistryAccess iregistrycustom, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config; Async-Anti-Xray: Pass executor
+        this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
+        this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
+        this.generator = gen;
+        this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env);
+
+        // CraftBukkit Ticks things
+        for (SpawnCategory spawnCategory : SpawnCategory.values()) {
+            if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
+                this.ticksPerSpawnCategory.put(spawnCategory, this.getTicksPerSpawn(spawnCategory)); // Paper
+            }
+        }
+
+        // CraftBukkit end
+        this.profiler = supplier;
+        this.levelData = worlddatamutable;
+        this.dimensionTypeRegistration = holder;
+        final DimensionType dimensionmanager = (DimensionType) holder.value();
+
+        this.dimension = resourcekey;
+        this.isClientSide = flag;
+        if (dimensionmanager.coordinateScale() != 1.0D) {
+            this.worldBorder = new WorldBorder() { // CraftBukkit - decompile error
                 @Override
                 public double getCenterX() {
-                    return super.getCenterX() / dimensionType.coordinateScale();
+                    return super.getCenterX(); // CraftBukkit
                 }
 
                 @Override
                 public double getCenterZ() {
-                    return super.getCenterZ() / dimensionType.coordinateScale();
+                    return super.getCenterZ(); // CraftBukkit
                 }
             };
         } else {
@@ -148,14 +243,457 @@
         }
 
         this.thread = Thread.currentThread();
-        this.biomeManager = new BiomeManager(this, biomeAccess);
-        this.isDebug = debugWorld;
-        this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates);
-        this.registryAccess = registryManager;
-        this.damageSources = new DamageSources(registryManager);
+        this.biomeManager = new BiomeManager(this, i);
+        this.isDebug = flag1;
+        this.neighborUpdater = new CollectingNeighborUpdater(this, j);
+        this.registryAccess = iregistrycustom;
+        this.damageSources = new DamageSources(iregistrycustom);
+        // CraftBukkit start
+        this.getWorldBorder().world = (ServerLevel) this;
+        // From PlayerList.setPlayerFileData
+        this.getWorldBorder().addListener(new BorderChangeListener() {
+            @Override
+            public void onBorderSizeSet(WorldBorder border, double size) {
+                Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderSizePacket(border), border.world);
+            }
+
+            @Override
+            public void onBorderSizeLerping(WorldBorder border, double fromSize, double toSize, long time) {
+                Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderLerpSizePacket(border), border.world);
+            }
+
+            @Override
+            public void onBorderCenterSet(WorldBorder border, double centerX, double centerZ) {
+                Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderCenterPacket(border), border.world);
+            }
+
+            @Override
+            public void onBorderSetWarningTime(WorldBorder border, int warningTime) {
+                Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderWarningDelayPacket(border), border.world);
+            }
+
+            @Override
+            public void onBorderSetWarningBlocks(WorldBorder border, int warningBlockDistance) {
+                Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderWarningDistancePacket(border), border.world);
+            }
+
+            @Override
+            public void onBorderSetDamagePerBlock(WorldBorder border, double damagePerBlock) {}
+
+            @Override
+            public void onBorderSetDamageSafeZOne(WorldBorder border, double safeZoneRadius) {}
+        });
+        // CraftBukkit end
+        this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings
+        this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
+        this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
+        this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
+        // Paper start - optimise collisions
+        this.minSection = io.papermc.paper.util.WorldUtil.getMinSection(this);
+        this.maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(this);
+        // Paper end - optimise collisions
+    }
+
+    // Paper start - Cancel hit for vanished players
+    // ret true if no collision
+    public final boolean checkEntityCollision(BlockState data, Entity source, net.minecraft.world.phys.shapes.CollisionContext voxelshapedcollision,
+                                              BlockPos position, boolean checkCanSee) {
+        // Copied from IWorldReader#a(IBlockData, BlockPosition, VoxelShapeCollision) & EntityAccess#a(Entity, VoxelShape)
+        net.minecraft.world.phys.shapes.VoxelShape voxelshape = data.getCollisionShape(this, position, voxelshapedcollision);
+        if (voxelshape.isEmpty()) {
+            return true;
+        }
+
+        voxelshape = voxelshape.move((double) position.getX(), (double) position.getY(), (double) position.getZ());
+        if (voxelshape.isEmpty()) {
+            return true;
+        }
+
+        List<Entity> entities = this.getEntities(null, voxelshape.bounds());
+        for (int i = 0, len = entities.size(); i < len; ++i) {
+            Entity entity = entities.get(i);
+
+            if (checkCanSee && source instanceof net.minecraft.server.level.ServerPlayer && entity instanceof net.minecraft.server.level.ServerPlayer
+                && !((net.minecraft.server.level.ServerPlayer) source).getBukkitEntity().canSee(((net.minecraft.server.level.ServerPlayer) entity).getBukkitEntity())) {
+                continue;
+            }
+
+            // !entity1.dead && entity1.i && (entity == null || !entity1.x(entity));
+            // elide the last check since vanilla calls with entity = null
+            // only we care about the source for the canSee check
+            if (entity.isRemoved() || !entity.blocksBuilding) {
+                continue;
+            }
+
+            if (net.minecraft.world.phys.shapes.Shapes.joinIsNotEmpty(voxelshape, net.minecraft.world.phys.shapes.Shapes.create(entity.getBoundingBox()), net.minecraft.world.phys.shapes.BooleanOp.AND)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+    // Paper end - Cancel hit for vanished players
+    // Paper start - optimise collisions
+    public final int minSection;
+    public final int maxSection;
+
+    @Override
+    public final boolean isUnobstructed(final Entity entity) {
+        final AABB boundingBox = entity.getBoundingBox();
+        if (io.papermc.paper.util.CollisionUtil.isEmpty(boundingBox)) {
+            return false;
+        }
+
+        final List<Entity> entities = this.getEntities(
+                entity,
+                boundingBox.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON),
+                null
+        );
+
+        for (int i = 0, len = entities.size(); i < len; ++i) {
+            final Entity otherEntity = entities.get(i);
+
+            if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) {
+                continue;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) {
+        final Vec3 to = clipContext.getTo();
+        final Vec3 from = clipContext.getFrom();
+
+        return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z));
+    }
+
+    private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState();
+
+    private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level,
+                                                                    final ClipContext clipContext) {
+        final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
+        final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
+        final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
+
+        if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
+            return miss(clipContext);
+        }
+
+        final double toXAdj = to.x - adjX;
+        final double toYAdj = to.y - adjY;
+        final double toZAdj = to.z - adjZ;
+        final double fromXAdj = from.x + adjX;
+        final double fromYAdj = from.y + adjY;
+        final double fromZAdj = from.z + adjZ;
+
+        int currX = Mth.floor(fromXAdj);
+        int currY = Mth.floor(fromYAdj);
+        int currZ = Mth.floor(fromZAdj);
+
+        final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos();
+
+        final double diffX = toXAdj - fromXAdj;
+        final double diffY = toYAdj - fromYAdj;
+        final double diffZ = toZAdj - fromZAdj;
+
+        final double dxDouble = Math.signum(diffX);
+        final double dyDouble = Math.signum(diffY);
+        final double dzDouble = Math.signum(diffZ);
+
+        final int dx = (int)dxDouble;
+        final int dy = (int)dyDouble;
+        final int dz = (int)dzDouble;
+
+        final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
+        final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
+        final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
+
+        double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
+        double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
+        double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
+
+        net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null;
+        net.minecraft.world.level.chunk.PalettedContainer<BlockState> lastSection = null;
+        int lastChunkX = Integer.MIN_VALUE;
+        int lastChunkY = Integer.MIN_VALUE;
+        int lastChunkZ = Integer.MIN_VALUE;
+
+        final int minSection = level.minSection;
+        final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)level.getChunkSource();
+
+        for (;;) {
+            currPos.set(currX, currY, currZ);
+
+            final int newChunkX = currX >> 4;
+            final int newChunkY = currY >> 4;
+            final int newChunkZ = currZ >> 4;
+
+            final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
+            final int chunkYDiff = newChunkY ^ lastChunkY;
+
+            if ((chunkDiff | chunkYDiff) != 0) {
+                if (chunkDiff != 0) {
+                    LevelChunk chunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ);
+                    lastChunk = chunk == null ? null : chunk.getSections(); // diff: don't load chunks for this
+                }
+                final int sectionY = newChunkY - minSection;
+                lastSection = lastChunk != null && sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null;
+
+                lastChunkX = newChunkX;
+                lastChunkY = newChunkY;
+                lastChunkZ = newChunkZ;
+            }
+
+            final BlockState blockState;
+            if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) {
+                final net.minecraft.world.phys.shapes.VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos);
+
+                final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState);
+
+                final net.minecraft.world.phys.shapes.VoxelShape fluidCollision;
+                final FluidState fluidState;
+                if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) {
+                    fluidCollision = clipContext.getFluidShape(fluidState, level, currPos);
+
+                    final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos);
+
+                    if (fluidHit != null) {
+                        if (blockHit == null) {
+                            return fluidHit;
+                        }
+
+                        return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit;
+                    }
+                }
+
+                if (blockHit != null) {
+                    return blockHit;
+                }
+            } // else: usually fall here
+
+            if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
+                return miss(clipContext);
+            }
+
+            // inc the smallest normalized coordinate
+
+            if (normalizedCurrX < normalizedCurrY) {
+                if (normalizedCurrX < normalizedCurrZ) {
+                    currX += dx;
+                    normalizedCurrX += normalizedDiffX;
+                } else {
+                    // x < y && x >= z <--> z < y && z <= x
+                    currZ += dz;
+                    normalizedCurrZ += normalizedDiffZ;
+                }
+            } else if (normalizedCurrY < normalizedCurrZ) {
+                // y <= x && y < z
+                currY += dy;
+                normalizedCurrY += normalizedDiffY;
+            } else {
+                // y <= x && z <= y <--> z <= y && z <= x
+                currZ += dz;
+                normalizedCurrZ += normalizedDiffZ;
+            }
+        }
+    }
+
+    @Override
+    public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) {
+        // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks
+        return fastClip(clipContext.getFrom(), clipContext.getTo(), this, clipContext);
+    }
+
+    @Override
+    public final boolean noCollision(final Entity entity, final AABB box, final boolean loadChunks) {
+        int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
+        if (entity != null) {
+            flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER;
+        }
+        if (loadChunks) {
+            flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_LOAD_CHUNKS;
+        }
+        if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) {
+            return false;
+        }
+
+        return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null);
     }
 
     @Override
+    public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) {
+        return io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null,
+            io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY,
+            (final BlockState state, final BlockPos pos) -> {
+                return state.isSuffocating(Level.this, pos);
+            }
+        );
+    }
+
+    private static net.minecraft.world.phys.shapes.VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) {
+        return net.minecraft.world.phys.shapes.Shapes.create(
+                aabb.minX - x,
+                aabb.minY - y,
+                aabb.minZ - z,
+
+                aabb.maxX + x,
+                aabb.maxY + y,
+                aabb.maxZ + z
+        );
+    }
+
+    @Override
+    public final java.util.Optional<Vec3> findFreePosition(final Entity entity, final net.minecraft.world.phys.shapes.VoxelShape boundsShape, final Vec3 fromPosition,
+                                                           final double rangeX, final double rangeY, final double rangeZ) {
+        if (boundsShape.isEmpty()) {
+            return java.util.Optional.empty();
+        }
+
+        final double expandByX = rangeX * 0.5;
+        final double expandByY = rangeY * 0.5;
+        final double expandByZ = rangeZ * 0.5;
+
+        // note: it is useless to look at shapes outside of range / 2.0
+        final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ);
+
+        final List<AABB> aabbs = new java.util.ArrayList<>();
+        final List<net.minecraft.world.phys.shapes.VoxelShape> voxels = new java.util.ArrayList<>();
+
+        io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
+                this, entity, collectionVolume, voxels, aabbs,
+                io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
+                null
+        );
+
+        // push voxels into aabbs
+        for (int i = 0, len = voxels.size(); i < len; ++i) {
+            aabbs.addAll(voxels.get(i).toAabbs());
+        }
+
+        // expand AABBs
+        final net.minecraft.world.phys.shapes.VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ);
+        final net.minecraft.world.phys.shapes.VoxelShape[] rest = new net.minecraft.world.phys.shapes.VoxelShape[Math.max(0, aabbs.size() - 1)];
+
+        for (int i = 1, len = aabbs.size(); i < len; ++i) {
+            rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ);
+        }
+
+        // use optimized implementation of ORing the shapes together
+        final net.minecraft.world.phys.shapes.VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest);
+
+        // find free space
+        // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs()
+        final net.minecraft.world.phys.shapes.VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(
+            boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST
+        );
+
+        return freeSpace.closestPointTo(fromPosition);
+    }
+
+    @Override
+    public final java.util.Optional<BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) {
+        final int minBlockX = Mth.floor(aabb.minX - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
+        final int maxBlockX = Mth.floor(aabb.maxX + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
+
+        final int minBlockY = Mth.floor(aabb.minY - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
+        final int maxBlockY = Mth.floor(aabb.maxY + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
+
+        final int minBlockZ = Mth.floor(aabb.minZ - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
+        final int maxBlockZ = Mth.floor(aabb.maxZ + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
+
+        io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext collisionContext = null;
+
+        final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
+        BlockPos selected = null;
+        double selectedDistance = Double.MAX_VALUE;
+
+        final Vec3 entityPos = entity.position();
+
+        LevelChunk lastChunk = null;
+        int lastChunkX = Integer.MIN_VALUE;
+        int lastChunkZ = Integer.MIN_VALUE;
+
+        final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource();
+
+        for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) {
+            pos.setZ(currZ);
+            for (int currX = minBlockX; currX <= maxBlockX; ++currX) {
+                pos.setX(currX);
+
+                final int newChunkX = currX >> 4;
+                final int newChunkZ = currZ >> 4;
+
+                final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
+
+                if (chunkDiff != 0) {
+                    lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ);
+                }
+
+                if (lastChunk == null) {
+                    continue;
+                }
+                for (int currY = minBlockY; currY <= maxBlockY; ++currY) {
+                    int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) +
+                            ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) +
+                            ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0);
+                    if (edgeCount == 3) {
+                        continue;
+                    }
+
+                    pos.setY(currY);
+
+                    final double distance = pos.distToCenterSqr(entityPos);
+                    if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) {
+                        continue;
+                    }
+
+                    final BlockState state = lastChunk.getBlockState(currX, currY, currZ);
+                    if (state.emptyCollisionShape()) {
+                        continue;
+                    }
+
+                    if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) {
+                        if (collisionContext == null) {
+                            collisionContext = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(entity);
+                        }
+                        final net.minecraft.world.phys.shapes.VoxelShape blockCollision = state.getCollisionShape(lastChunk, pos, collisionContext);
+                        if (blockCollision.isEmpty()) {
+                            continue;
+                        }
+
+                        // avoid VoxelShape#move by shifting the entity collision shape instead
+                        final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ);
+
+                        final AABB singleAABB = blockCollision.getSingleAABBRepresentation();
+                        if (singleAABB != null) {
+                            if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) {
+                                continue;
+                            }
+
+                            selected = pos.immutable();
+                            selectedDistance = distance;
+                            continue;
+                        }
+
+                        if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) {
+                            continue;
+                        }
+
+                        selected = pos.immutable();
+                        selectedDistance = distance;
+                        continue;
+                    }
+                }
+            }
+        }
+
+        return java.util.Optional.ofNullable(selected);
+    }
+    // Paper end - optimise collisions
+    @Override
     public boolean isClientSide() {
         return this.isClientSide;
     }
@@ -166,130 +704,404 @@
         return null;
     }
 
+    // Paper start - Broken down method of raytracing for EntityLiving#hasLineOfSight, replaces BlockGetter#clip(CollisionContext)
+    public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) {
+        // most of this code comes from BlockGetter#clip(CollisionContext, BiFunction, Function), but removes the needless functions
+        if (start.equals(end)) {
+            return net.minecraft.world.phys.BlockHitResult.Type.MISS;
+        }
+
+        final double endX = Mth.lerp(-1.0E-7D, end.x, start.x);
+        final double endY = Mth.lerp(-1.0E-7D, end.y, start.y);
+        final double endZ = Mth.lerp(-1.0E-7D, end.z, start.z);
+
+        final double startX = Mth.lerp(-1.0E-7D, start.x, end.x);
+        final double startY = Mth.lerp(-1.0E-7D, start.y, end.y);
+        final double startZ = Mth.lerp(-1.0E-7D, start.z, end.z);
+
+        int currentX = Mth.floor(startX);
+        int currentY = Mth.floor(startY);
+        int currentZ = Mth.floor(startZ);
+
+        final BlockPos.MutableBlockPos currentBlock = new BlockPos.MutableBlockPos(currentX, currentY, currentZ);
+
+        LevelChunk chunk = this.getChunkIfLoaded(currentBlock);
+        if (chunk == null) {
+            return net.minecraft.world.phys.BlockHitResult.Type.MISS;
+        }
+
+        final net.minecraft.world.phys.BlockHitResult.Type initialCheck = this.clipDirect(start, end, currentBlock, chunk.getBlockState(currentBlock), context);
+        if (initialCheck != null) {
+            return initialCheck;
+        }
+
+        final double diffX = endX - startX;
+        final double diffY = endY - startY;
+        final double diffZ = endZ - startZ;
+
+        final int xDirection = Mth.sign(diffX);
+        final int yDirection = Mth.sign(diffY);
+        final int zDirection = Mth.sign(diffZ);
+
+        final double normalizedX = xDirection == 0 ? Double.MAX_VALUE : (double) xDirection / diffX;
+        final double normalizedY = yDirection == 0 ? Double.MAX_VALUE : (double) yDirection / diffY;
+        final double normalizedZ = zDirection == 0 ? Double.MAX_VALUE : (double) zDirection / diffZ;
+
+        double normalizedXDirection = normalizedX * (xDirection > 0 ? 1.0D - Mth.frac(startX) : Mth.frac(startX));
+        double normalizedYDirection = normalizedY * (yDirection > 0 ? 1.0D - Mth.frac(startY) : Mth.frac(startY));
+        double normalizedZDirection = normalizedZ * (zDirection > 0 ? 1.0D - Mth.frac(startZ) : Mth.frac(startZ));
+
+        net.minecraft.world.phys.BlockHitResult.Type result;
+
+        do {
+            if (normalizedXDirection > 1.0D && normalizedYDirection > 1.0D && normalizedZDirection > 1.0D) {
+                return net.minecraft.world.phys.BlockHitResult.Type.MISS;
+            }
+
+            if (normalizedXDirection < normalizedYDirection) {
+                if (normalizedXDirection < normalizedZDirection) {
+                    currentX += xDirection;
+                    normalizedXDirection += normalizedX;
+                } else {
+                    currentZ += zDirection;
+                    normalizedZDirection += normalizedZ;
+                }
+            } else if (normalizedYDirection < normalizedZDirection) {
+                currentY += yDirection;
+                normalizedYDirection += normalizedY;
+            } else {
+                currentZ += zDirection;
+                normalizedZDirection += normalizedZ;
+            }
+
+            currentBlock.set(currentX, currentY, currentZ);
+            if (chunk.getPos().x != currentBlock.getX() >> 4 || chunk.getPos().z != currentBlock.getZ() >> 4) {
+                chunk = this.getChunkIfLoaded(currentBlock);
+                if (chunk == null) {
+                    return net.minecraft.world.phys.BlockHitResult.Type.MISS;
+                }
+            }
+            result = this.clipDirect(start, end, currentBlock, chunk.getBlockState(currentBlock), context);
+        } while (result == null);
+
+        return result;
+    }
+    // Paper end
+
     public boolean isInWorldBounds(BlockPos pos) {
-        return !this.isOutsideBuildHeight(pos) && isInWorldBoundsHorizontal(pos);
+        return pos.isInsideBuildHeightAndWorldBoundsHorizontal(this); // Paper - use better/optimized check
     }
 
     public static boolean isInSpawnableBounds(BlockPos pos) {
-        return !isOutsideSpawnableHeight(pos.getY()) && isInWorldBoundsHorizontal(pos);
+        return !Level.isOutsideSpawnableHeight(pos.getY()) && Level.isInWorldBoundsHorizontal(pos);
     }
 
     private static boolean isInWorldBoundsHorizontal(BlockPos pos) {
-        return pos.getX() >= -30000000 && pos.getZ() >= -30000000 && pos.getX() < 30000000 && pos.getZ() < 30000000;
+        return pos.getX() >= -30000000 && pos.getZ() >= -30000000 && pos.getX() < 30000000 && pos.getZ() < 30000000; // Diff on change warnUnsafeChunk()
     }
 
     private static boolean isOutsideSpawnableHeight(int y) {
         return y < -20000000 || y >= 20000000;
     }
 
-    public LevelChunk getChunkAt(BlockPos pos) {
+    public final LevelChunk getChunkAt(BlockPos pos) { // Paper - help inline
         return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
     }
 
     @Override
-    public LevelChunk getChunk(int i, int j) {
-        return (LevelChunk)this.getChunk(i, j, ChunkStatus.FULL);
+    public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline
+        // Paper start - Perf: make sure loaded chunks get the inlined variant of this function
+        net.minecraft.server.level.ServerChunkCache cps = ((ServerLevel)this).getChunkSource();
+        if (cps.mainThread == Thread.currentThread()) {
+            LevelChunk ifLoaded = cps.getChunkAtIfLoadedMainThread(chunkX, chunkZ);
+            if (ifLoaded != null) {
+                return ifLoaded;
+            }
+        }
+        // Paper end - Perf: make sure loaded chunks get the inlined variant of this function
+        return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true); // Paper - avoid a method jump
     }
 
+    // Paper start - if loaded
     @Nullable
     @Override
-    public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
-        ChunkAccess chunkAccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create);
-        if (chunkAccess == null && create) {
+    public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+        return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z);
+    }
+
+    @Override
+    @Nullable
+    public final BlockState getBlockStateIfLoaded(BlockPos pos) {
+        // CraftBukkit start - tree generation
+        if (this.captureTreeGeneration) {
+            CraftBlockState previous = this.capturedBlockStates.get(pos);
+            if (previous != null) {
+                return previous.getHandle();
+            }
+        }
+        // CraftBukkit end
+        if (this.isOutsideBuildHeight(pos)) {
+            return Blocks.VOID_AIR.defaultBlockState();
+        } else {
+            ChunkAccess chunk = this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);
+
+            return chunk == null ? null : chunk.getBlockState(pos);
+        }
+    }
+
+    @Override
+    public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+        ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+
+        return chunk == null ? null : chunk.getFluidState(blockposition);
+    }
+
+    @Override
+    public final boolean hasChunkAt(BlockPos pos) {
+        return getChunkIfLoaded(pos.getX() >> 4, pos.getZ() >> 4) != null; // Paper - Perf: Optimize Level.hasChunkAt(BlockPosition)Z
+    }
+
+    public final boolean isLoadedAndInBounds(BlockPos blockposition) { // Paper - final for inline
+        return getWorldBorder().isWithinBounds(blockposition) && getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) != null;
+    }
+
+    public @Nullable LevelChunk getChunkIfLoaded(int x, int z) { // Overridden in WorldServer for ABI compat which has final
+        return ((ServerLevel) this).getChunkSource().getChunkAtIfLoadedImmediately(x, z);
+    }
+    public final @Nullable LevelChunk getChunkIfLoaded(BlockPos blockposition) {
+        return ((ServerLevel) this).getChunkSource().getChunkAtIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+    }
+
+    //  reduces need to do isLoaded before getType
+    public final @Nullable BlockState getBlockStateIfLoadedAndInBounds(BlockPos blockposition) {
+        return getWorldBorder().isWithinBounds(blockposition) ? getBlockStateIfLoaded(blockposition) : null;
+    }
+
+    @Override
+    public final ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { // Paper - final for inline
+        // Paper end
+        ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create);
+
+        if (ichunkaccess == null && create) {
             throw new IllegalStateException("Should always be able to create a chunk!");
         } else {
-            return chunkAccess;
+            return ichunkaccess;
         }
     }
 
     @Override
-    public boolean setBlock(BlockPos pos, BlockState state, int flags) {
+    public final boolean setBlock(BlockPos pos, BlockState state, int flags) { // Paper - final for inline
         return this.setBlock(pos, state, flags, 512);
     }
 
     @Override
     public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) {
+        // CraftBukkit start - tree generation
+        if (this.captureTreeGeneration) {
+            // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
+            BlockState type = getBlockState(pos);
+            if (!type.isDestroyable()) return false;
+            // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
+            CraftBlockState blockstate = this.capturedBlockStates.get(pos);
+            if (blockstate == null) {
+                blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags);
+                this.capturedBlockStates.put(pos.immutable(), blockstate);
+            }
+            blockstate.setData(state);
+            blockstate.setFlag(flags);
+            return true;
+        }
+        // CraftBukkit end
         if (this.isOutsideBuildHeight(pos)) {
             return false;
         } else if (!this.isClientSide && this.isDebug()) {
             return false;
         } else {
-            LevelChunk levelChunk = this.getChunkAt(pos);
+            LevelChunk chunk = this.getChunkAt(pos);
             Block block = state.getBlock();
-            BlockState blockState = levelChunk.setBlockState(pos, state, (flags & 64) != 0);
-            if (blockState == null) {
+
+            // CraftBukkit start - capture blockstates
+            boolean captured = false;
+            if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) {
+                CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot
+                blockstate.setFlag(flags); // Paper - set flag
+                this.capturedBlockStates.put(pos.immutable(), blockstate);
+                captured = true;
+            }
+            // CraftBukkit end
+
+            BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
+            this.chunkPacketBlockController.onBlockChange(this, pos, state, iblockdata1, flags, maxUpdateDepth); // Paper - Anti-Xray
+
+            if (iblockdata1 == null) {
+                // CraftBukkit start - remove blockstate if failed (or the same)
+                if (this.captureBlockStates && captured) {
+                    this.capturedBlockStates.remove(pos);
+                }
+                // CraftBukkit end
                 return false;
             } else {
-                BlockState blockState2 = this.getBlockState(pos);
-                if (blockState2 == state) {
-                    if (blockState != blockState2) {
-                        this.setBlocksDirty(pos, blockState, blockState2);
+                BlockState iblockdata2 = this.getBlockState(pos);
+
+                /*
+                if (iblockdata2 == iblockdata) {
+                    if (iblockdata1 != iblockdata2) {
+                        this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
                     }
 
-                    if ((flags & 2) != 0
-                        && (!this.isClientSide || (flags & 4) == 0)
-                        && (this.isClientSide || levelChunk.getFullStatus() != null && levelChunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING))) {
-                        this.sendBlockUpdated(pos, blockState, state, flags);
+                    if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING))) {
+                        this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
                     }
 
-                    if ((flags & 1) != 0) {
-                        this.blockUpdated(pos, blockState.getBlock());
-                        if (!this.isClientSide && state.hasAnalogOutputSignal()) {
-                            this.updateNeighbourForOutputSignal(pos, block);
+                    if ((i & 1) != 0) {
+                        this.blockUpdated(blockposition, iblockdata1.getBlock());
+                        if (!this.isClientSide && iblockdata.hasAnalogOutputSignal()) {
+                            this.updateNeighbourForOutputSignal(blockposition, block);
                         }
                     }
 
-                    if ((flags & 16) == 0 && maxUpdateDepth > 0) {
-                        int i = flags & -34;
-                        blockState.updateIndirectNeighbourShapes(this, pos, i, maxUpdateDepth - 1);
-                        state.updateNeighbourShapes(this, pos, i, maxUpdateDepth - 1);
-                        state.updateIndirectNeighbourShapes(this, pos, i, maxUpdateDepth - 1);
+                    if ((i & 16) == 0 && j > 0) {
+                        int k = i & -34;
+
+                        iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1);
+                        iblockdata.updateNeighbourShapes(this, blockposition, k, j - 1);
+                        iblockdata.updateIndirectNeighbourShapes(this, blockposition, k, j - 1);
                     }
 
-                    this.onBlockStateChange(pos, blockState, blockState2);
+                    this.onBlockStateChange(blockposition, iblockdata1, iblockdata2);
                 }
+                */
+
+                // CraftBukkit start
+                if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates
+                    // Modularize client and physic updates
+                    // Spigot start
+                    try {
+                        this.notifyAndUpdatePhysics(pos, chunk, iblockdata1, state, iblockdata2, flags, maxUpdateDepth);
+                    } catch (StackOverflowError ex) {
+                        Level.lastPhysicsProblem = new BlockPos(pos);
+                    }
+                    // Spigot end
+                }
+                // CraftBukkit end
 
                 return true;
             }
         }
     }
 
-    public void onBlockStateChange(BlockPos pos, BlockState oldBlock, BlockState newBlock) {
+    // CraftBukkit start - Split off from above in order to directly send client and physic updates
+    public void notifyAndUpdatePhysics(BlockPos blockposition, LevelChunk chunk, BlockState oldBlock, BlockState newBlock, BlockState actualBlock, int i, int j) {
+        BlockState iblockdata = newBlock;
+        BlockState iblockdata1 = oldBlock;
+        BlockState iblockdata2 = actualBlock;
+        if (iblockdata2 == iblockdata) {
+            if (iblockdata1 != iblockdata2) {
+                this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
+            }
+
+            if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
+                this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
+                // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance
+                // if copied from above
+            } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0)) { // Paper - replace old player chunk management
+                ((ServerLevel)this).getChunkSource().blockChanged(blockposition);
+                // Paper end - per player view distance
+            }
+
+            if ((i & 1) != 0) {
+                this.blockUpdated(blockposition, iblockdata1.getBlock());
+                if (!this.isClientSide && iblockdata.hasAnalogOutputSignal()) {
+                    this.updateNeighbourForOutputSignal(blockposition, newBlock.getBlock());
+                }
+            }
+
+            if ((i & 16) == 0 && j > 0) {
+                int k = i & -34;
+
+                // CraftBukkit start
+                iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam
+                CraftWorld world = ((ServerLevel) this).getWorld();
+                boolean cancelledUpdates = false; // Paper - Fix block place logic
+                if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent
+                    BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata));
+                    this.getCraftServer().getPluginManager().callEvent(event);
+
+                    cancelledUpdates = event.isCancelled(); // Paper - Fix block place logic
+                }
+                // CraftBukkit end
+                if (!cancelledUpdates) { // Paper - Fix block place logic
+                iblockdata.updateNeighbourShapes(this, blockposition, k, j - 1);
+                iblockdata.updateIndirectNeighbourShapes(this, blockposition, k, j - 1);
+                } // Paper - Fix block place logic
+            }
+
+            // CraftBukkit start - SPIGOT-5710
+            if (!this.preventPoiUpdated) {
+                this.onBlockStateChange(blockposition, iblockdata1, iblockdata2);
+            }
+            // CraftBukkit end
+        }
     }
+    // CraftBukkit end
+
+    public void onBlockStateChange(BlockPos pos, BlockState oldBlock, BlockState newBlock) {}
 
     @Override
     public boolean removeBlock(BlockPos pos, boolean move) {
-        FluidState fluidState = this.getFluidState(pos);
-        return this.setBlock(pos, fluidState.createLegacyBlock(), 3 | (move ? 64 : 0));
+        FluidState fluid = this.getFluidState(pos);
+
+        return this.setBlock(pos, fluid.createLegacyBlock(), 3 | (move ? 64 : 0));
     }
 
     @Override
     public boolean destroyBlock(BlockPos pos, boolean drop, @Nullable Entity breakingEntity, int maxUpdateDepth) {
-        BlockState blockState = this.getBlockState(pos);
-        if (blockState.isAir()) {
+        BlockState iblockdata = this.getBlockState(pos);
+
+        if (iblockdata.isAir()) {
             return false;
         } else {
-            FluidState fluidState = this.getFluidState(pos);
-            if (!(blockState.getBlock() instanceof BaseFireBlock)) {
-                this.levelEvent(2001, pos, Block.getId(blockState));
+            FluidState fluid = this.getFluidState(pos);
+            // Paper start - BlockDestroyEvent; while the above setAir method is named same and looks very similar
+            // they are NOT used with same intent and the above should not fire this event. The above method is more of a BlockSetToAirEvent,
+            // it doesn't imply destruction of a block that plays a sound effect / drops an item.
+            boolean playEffect = true;
+            BlockState effectType = iblockdata;
+            int xp = iblockdata.getBlock().getExpDrop(iblockdata, (ServerLevel) this, pos, ItemStack.EMPTY, true);
+            if (com.destroystokyo.paper.event.block.BlockDestroyEvent.getHandlerList().getRegisteredListeners().length > 0) {
+                com.destroystokyo.paper.event.block.BlockDestroyEvent event = new com.destroystokyo.paper.event.block.BlockDestroyEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this, pos), fluid.createLegacyBlock().createCraftBlockData(), effectType.createCraftBlockData(), xp, drop);
+                if (!event.callEvent()) {
+                    return false;
+                }
+                effectType = ((CraftBlockData) event.getEffectBlock()).getState();
+                playEffect = event.playEffect();
+                drop = event.willDrop();
+                xp = event.getExpToDrop();
+            }
+            // Paper end - BlockDestroyEvent
+
+            if (playEffect && !(effectType.getBlock() instanceof BaseFireBlock)) { // Paper - BlockDestroyEvent
+                this.levelEvent(2001, pos, Block.getId(effectType)); // Paper - BlockDestroyEvent
             }
 
             if (drop) {
-                BlockEntity blockEntity = blockState.hasBlockEntity() ? this.getBlockEntity(pos) : null;
-                Block.dropResources(blockState, this, pos, blockEntity, breakingEntity, ItemStack.EMPTY);
+                BlockEntity tileentity = iblockdata.hasBlockEntity() ? this.getBlockEntity(pos) : null;
+
+                Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping
+                iblockdata.getBlock().popExperience((ServerLevel) this, pos, xp, breakingEntity); // Paper - Properly handle xp dropping; custom amount
             }
 
-            boolean bl = this.setBlock(pos, fluidState.createLegacyBlock(), 3, maxUpdateDepth);
-            if (bl) {
-                this.gameEvent(GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(breakingEntity, blockState));
+            boolean flag1 = this.setBlock(pos, fluid.createLegacyBlock(), 3, maxUpdateDepth);
+
+            if (flag1) {
+                this.gameEvent((Holder) GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(breakingEntity, iblockdata));
             }
 
-            return bl;
+            return flag1;
         }
     }
 
-    public void addDestroyBlockEffect(BlockPos pos, BlockState state) {
-    }
+    public void addDestroyBlockEffect(BlockPos pos, BlockState state) {}
 
     public boolean setBlockAndUpdate(BlockPos pos, BlockState state) {
         return this.setBlock(pos, state, 3);
@@ -297,20 +1109,15 @@
 
     public abstract void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags);
 
-    public void setBlocksDirty(BlockPos pos, BlockState old, BlockState updated) {
-    }
+    public void setBlocksDirty(BlockPos pos, BlockState old, BlockState updated) {}
 
-    public void updateNeighborsAt(BlockPos pos, Block sourceBlock) {
-    }
+    public void updateNeighborsAt(BlockPos pos, Block sourceBlock) {}
 
-    public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block sourceBlock, Direction direction) {
-    }
+    public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block sourceBlock, Direction direction) {}
 
-    public void neighborChanged(BlockPos pos, Block sourceBlock, BlockPos sourcePos) {
-    }
+    public void neighborChanged(BlockPos pos, Block sourceBlock, BlockPos sourcePos) {}
 
-    public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) {
-    }
+    public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) {}
 
     @Override
     public void neighborShapeChanged(Direction direction, BlockState neighborState, BlockPos pos, BlockPos neighborPos, int flags, int maxUpdateDepth) {
@@ -319,18 +1126,19 @@
 
     @Override
     public int getHeight(Heightmap.Types heightmap, int x, int z) {
-        int j;
+        int k;
+
         if (x >= -30000000 && z >= -30000000 && x < 30000000 && z < 30000000) {
             if (this.hasChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z))) {
-                j = this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)).getHeight(heightmap, x & 15, z & 15) + 1;
+                k = this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)).getHeight(heightmap, x & 15, z & 15) + 1;
             } else {
-                j = this.getMinBuildHeight();
+                k = this.getMinBuildHeight();
             }
         } else {
-            j = this.getSeaLevel() + 1;
+            k = this.getSeaLevel() + 1;
         }
 
-        return j;
+        return k;
     }
 
     @Override
@@ -340,11 +1148,20 @@
 
     @Override
     public BlockState getBlockState(BlockPos pos) {
+        // CraftBukkit start - tree generation
+        if (this.captureTreeGeneration) {
+            CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper
+            if (previous != null) {
+                return previous.getHandle();
+            }
+        }
+        // CraftBukkit end
         if (this.isOutsideBuildHeight(pos)) {
             return Blocks.VOID_AIR.defaultBlockState();
         } else {
-            LevelChunk levelChunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
-            return levelChunk.getBlockState(pos);
+            ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine
+
+            return chunk.getBlockState(pos);
         }
     }
 
@@ -353,8 +1170,9 @@
         if (this.isOutsideBuildHeight(pos)) {
             return Fluids.EMPTY.defaultFluidState();
         } else {
-            LevelChunk levelChunk = this.getChunkAt(pos);
-            return levelChunk.getFluidState(pos);
+            LevelChunk chunk = this.getChunkAt(pos);
+
+            return chunk.getFluidState(pos);
         }
     }
 
@@ -367,27 +1185,29 @@
     }
 
     public void playSound(@Nullable Entity source, BlockPos pos, SoundEvent sound, SoundSource category, float volume, float pitch) {
-        this.playSound(source instanceof Player player ? player : null, pos, sound, category, volume, pitch);
+        Player entityhuman;
+
+        if (source instanceof Player entityhuman1) {
+            entityhuman = entityhuman1;
+        } else {
+            entityhuman = null;
+        }
+
+        this.playSound(entityhuman, pos, sound, category, volume, pitch);
     }
 
     @Override
     public void playSound(@Nullable Player source, BlockPos pos, SoundEvent sound, SoundSource category, float volume, float pitch) {
-        this.playSound(source, (double)pos.getX() + 0.5, (double)pos.getY() + 0.5, (double)pos.getZ() + 0.5, sound, category, volume, pitch);
+        this.playSound(source, (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, sound, category, volume, pitch);
     }
 
-    public abstract void playSeededSound(
-        @Nullable Player source, double x, double y, double z, Holder<SoundEvent> sound, SoundSource category, float volume, float pitch, long seed
-    );
+    public abstract void playSeededSound(@Nullable Player source, double x, double y, double z, Holder<SoundEvent> sound, SoundSource category, float volume, float pitch, long seed);
 
-    public void playSeededSound(
-        @Nullable Player source, double x, double y, double z, SoundEvent sound, SoundSource category, float volume, float pitch, long seed
-    ) {
+    public void playSeededSound(@Nullable Player source, double x, double y, double z, SoundEvent sound, SoundSource category, float volume, float pitch, long seed) {
         this.playSeededSound(source, x, y, z, BuiltInRegistries.SOUND_EVENT.wrapAsHolder(sound), category, volume, pitch, seed);
     }
 
-    public abstract void playSeededSound(
-        @Nullable Player source, Entity entity, Holder<SoundEvent> sound, SoundSource category, float volume, float pitch, long seed
-    );
+    public abstract void playSeededSound(@Nullable Player source, Entity entity, Holder<SoundEvent> sound, SoundSource category, float volume, float pitch, long seed);
 
     public void playSound(@Nullable Player source, double x, double y, double z, SoundEvent sound, SoundSource category) {
         this.playSound(source, x, y, z, sound, category, 1.0F, 1.0F);
@@ -402,33 +1222,26 @@
     }
 
     public void playLocalSound(BlockPos pos, SoundEvent sound, SoundSource category, float volume, float pitch, boolean useDistance) {
-        this.playLocalSound((double)pos.getX() + 0.5, (double)pos.getY() + 0.5, (double)pos.getZ() + 0.5, sound, category, volume, pitch, useDistance);
+        this.playLocalSound((double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, sound, category, volume, pitch, useDistance);
     }
 
-    public void playLocalSound(Entity entity, SoundEvent sound, SoundSource category, float volume, float pitch) {
-    }
+    public void playLocalSound(Entity entity, SoundEvent sound, SoundSource category, float volume, float pitch) {}
 
-    public void playLocalSound(double x, double y, double z, SoundEvent sound, SoundSource category, float volume, float pitch, boolean useDistance) {
-    }
+    public void playLocalSound(double x, double y, double z, SoundEvent sound, SoundSource category, float volume, float pitch, boolean useDistance) {}
 
     @Override
-    public void addParticle(ParticleOptions parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
-    }
+    public void addParticle(ParticleOptions parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {}
 
-    public void addParticle(ParticleOptions parameters, boolean alwaysSpawn, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
-    }
+    public void addParticle(ParticleOptions parameters, boolean alwaysSpawn, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {}
 
-    public void addAlwaysVisibleParticle(ParticleOptions parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
-    }
+    public void addAlwaysVisibleParticle(ParticleOptions parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {}
 
-    public void addAlwaysVisibleParticle(
-        ParticleOptions parameters, boolean alwaysSpawn, double x, double y, double z, double velocityX, double velocityY, double velocityZ
-    ) {
-    }
+    public void addAlwaysVisibleParticle(ParticleOptions parameters, boolean alwaysSpawn, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {}
 
     public float getSunAngle(float tickDelta) {
-        float f = this.getTimeOfDay(tickDelta);
-        return f * (float) (Math.PI * 2);
+        float f1 = this.getTimeOfDay(tickDelta);
+
+        return f1 * 6.2831855F;
     }
 
     public void addBlockEntityTicker(TickingBlockEntity ticker) {
@@ -436,40 +1249,84 @@
     }
 
     protected void tickBlockEntities() {
-        ProfilerFiller profilerFiller = this.getProfiler();
-        profilerFiller.push("blockEntities");
+        ProfilerFiller gameprofilerfiller = this.getProfiler();
+
+        gameprofilerfiller.push("blockEntities");
+        this.timings.tileEntityPending.startTiming(); // Spigot
         this.tickingBlockEntities = true;
         if (!this.pendingBlockEntityTickers.isEmpty()) {
             this.blockEntityTickers.addAll(this.pendingBlockEntityTickers);
             this.pendingBlockEntityTickers.clear();
         }
+        this.timings.tileEntityPending.stopTiming(); // Spigot
 
-        Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
-        boolean bl = this.tickRateManager().runsNormally();
+        this.timings.tileEntityTick.startTiming(); // Spigot
+        // Spigot start
+        // Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
+        boolean flag = this.tickRateManager().runsNormally();
 
-        while (iterator.hasNext()) {
-            TickingBlockEntity tickingBlockEntity = iterator.next();
-            if (tickingBlockEntity.isRemoved()) {
-                iterator.remove();
-            } else if (bl && this.shouldTickBlocksAt(tickingBlockEntity.getPos())) {
-                tickingBlockEntity.tick();
+        int tilesThisCycle = 0;
+        var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll
+        toRemove.add(null); // Paper - Fix MC-117075
+        for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters
+            this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0;
+            TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(this.tileTickPosition);
+            // Spigot end
+
+            if (tickingblockentity.isRemoved()) {
+                // Spigot start
+                tilesThisCycle--;
+                toRemove.add(tickingblockentity); // Paper - Fix MC-117075; use removeAll
+                // Spigot end
+            } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) {
+                tickingblockentity.tick();
+                // Paper start - execute chunk tasks during tick
+                if ((this.tileTickPosition & 7) == 0) {
+                    MinecraftServer.getServer().executeMidTickTasks();
+                }
+                // Paper end - execute chunk tasks during tick
             }
         }
+        this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
 
+        this.timings.tileEntityTick.stopTiming(); // Spigot
         this.tickingBlockEntities = false;
-        profilerFiller.pop();
+        co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper
+        gameprofilerfiller.pop();
+        this.spigotConfig.currentPrimedTnt = 0; // Spigot
     }
 
     public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, T entity) {
         try {
             tickConsumer.accept(entity);
-        } catch (Throwable var6) {
-            CrashReport crashReport = CrashReport.forThrowable(var6, "Ticking entity");
-            CrashReportCategory crashReportCategory = crashReport.addCategory("Entity being ticked");
-            entity.fillCrashReportCategory(crashReportCategory);
-            throw new ReportedException(crashReport);
+            MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick
+        } catch (Throwable throwable) {
+            if (throwable instanceof ThreadDeath) throw throwable; // Paper
+            // Paper start - Prevent block entity and entity crashes
+            final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
+            MinecraftServer.LOGGER.error(msg, throwable);
+            getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent
+            entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
+            // Paper end - Prevent block entity and entity crashes
         }
     }
+    // Paper start - Option to prevent armor stands from doing entity lookups
+    @Override
+    public boolean noCollision(@Nullable Entity entity, AABB box) {
+        if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false;
+        // Paper start - optimise collisions
+        int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
+        if (entity != null) {
+            flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER;
+        }
+        if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) {
+            return false;
+        }
+
+        return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null);
+        // Paper end - optimise collisions
+    }
+    // Paper end - Option to prevent armor stands from doing entity lookups
 
     public boolean shouldTickDeath(Entity entity) {
         return true;
@@ -484,137 +1341,56 @@
     }
 
     public Explosion explode(@Nullable Entity entity, double x, double y, double z, float power, Level.ExplosionInteraction explosionSourceType) {
-        return this.explode(
-            entity,
-            Explosion.getDefaultDamageSource(this, entity),
-            null,
-            x,
-            y,
-            z,
-            power,
-            false,
-            explosionSourceType,
-            ParticleTypes.EXPLOSION,
-            ParticleTypes.EXPLOSION_EMITTER,
-            SoundEvents.GENERIC_EXPLODE
-        );
+        return this.explode(entity, Explosion.getDefaultDamageSource(this, entity), (ExplosionDamageCalculator) null, x, y, z, power, false, explosionSourceType, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE);
     }
 
-    public Explosion explode(
-        @Nullable Entity entity, double x, double y, double z, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType
-    ) {
-        return this.explode(
-            entity,
-            Explosion.getDefaultDamageSource(this, entity),
-            null,
-            x,
-            y,
-            z,
-            power,
-            createFire,
-            explosionSourceType,
-            ParticleTypes.EXPLOSION,
-            ParticleTypes.EXPLOSION_EMITTER,
-            SoundEvents.GENERIC_EXPLODE
-        );
+    public Explosion explode(@Nullable Entity entity, double x, double y, double z, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType) {
+        return this.explode(entity, Explosion.getDefaultDamageSource(this, entity), (ExplosionDamageCalculator) null, x, y, z, power, createFire, explosionSourceType, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE);
     }
 
-    public Explosion explode(
-        @Nullable Entity entity,
-        @Nullable DamageSource damageSource,
-        @Nullable ExplosionDamageCalculator behavior,
-        Vec3 pos,
-        float power,
-        boolean createFire,
-        Level.ExplosionInteraction explosionSourceType
-    ) {
-        return this.explode(
-            entity,
-            damageSource,
-            behavior,
-            pos.x(),
-            pos.y(),
-            pos.z(),
-            power,
-            createFire,
-            explosionSourceType,
-            ParticleTypes.EXPLOSION,
-            ParticleTypes.EXPLOSION_EMITTER,
-            SoundEvents.GENERIC_EXPLODE
-        );
+    public Explosion explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, Vec3 pos, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType) {
+        return this.explode(entity, damageSource, behavior, pos.x(), pos.y(), pos.z(), power, createFire, explosionSourceType, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE);
     }
 
-    public Explosion explode(
-        @Nullable Entity entity,
-        @Nullable DamageSource damageSource,
-        @Nullable ExplosionDamageCalculator behavior,
-        double x,
-        double y,
-        double z,
-        float power,
-        boolean createFire,
-        Level.ExplosionInteraction explosionSourceType
-    ) {
-        return this.explode(
-            entity,
-            damageSource,
-            behavior,
-            x,
-            y,
-            z,
-            power,
-            createFire,
-            explosionSourceType,
-            ParticleTypes.EXPLOSION,
-            ParticleTypes.EXPLOSION_EMITTER,
-            SoundEvents.GENERIC_EXPLODE
-        );
+    public Explosion explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType) {
+        return this.explode(entity, damageSource, behavior, x, y, z, power, createFire, explosionSourceType, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE);
     }
 
-    public Explosion explode(
-        @Nullable Entity entity,
-        @Nullable DamageSource damageSource,
-        @Nullable ExplosionDamageCalculator behavior,
-        double x,
-        double y,
-        double z,
-        float power,
-        boolean createFire,
-        Level.ExplosionInteraction explosionSourceType,
-        ParticleOptions particle,
-        ParticleOptions emitterParticle,
-        Holder<SoundEvent> soundEvent
-    ) {
+    public Explosion explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType, ParticleOptions particle, ParticleOptions emitterParticle, Holder<SoundEvent> soundEvent) {
         return this.explode(entity, damageSource, behavior, x, y, z, power, createFire, explosionSourceType, true, particle, emitterParticle, soundEvent);
     }
 
-    public Explosion explode(
-        @Nullable Entity entity,
-        @Nullable DamageSource damageSource,
-        @Nullable ExplosionDamageCalculator behavior,
-        double x,
-        double y,
-        double z,
-        float power,
-        boolean createFire,
-        Level.ExplosionInteraction explosionSourceType,
-        boolean particles,
-        ParticleOptions particle,
-        ParticleOptions emitterParticle,
-        Holder<SoundEvent> soundEvent
-    ) {
-        Explosion.BlockInteraction blockInteraction = switch (explosionSourceType) {
-            case NONE -> Explosion.BlockInteraction.KEEP;
-            case BLOCK -> this.getDestroyType(GameRules.RULE_BLOCK_EXPLOSION_DROP_DECAY);
-            case MOB -> this.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)
-            ? this.getDestroyType(GameRules.RULE_MOB_EXPLOSION_DROP_DECAY)
-            : Explosion.BlockInteraction.KEEP;
-            case TNT -> this.getDestroyType(GameRules.RULE_TNT_EXPLOSION_DROP_DECAY);
-            case BLOW -> Explosion.BlockInteraction.TRIGGER_BLOCK;
-        };
-        Explosion explosion = new Explosion(
-            this, entity, damageSource, behavior, x, y, z, power, createFire, blockInteraction, particle, emitterParticle, soundEvent
-        );
+    public Explosion explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType, boolean particles, ParticleOptions particle, ParticleOptions emitterParticle, Holder<SoundEvent> soundEvent) {
+        Explosion.BlockInteraction explosion_effect;
+
+        switch (explosionSourceType.ordinal()) {
+            case 0:
+                explosion_effect = Explosion.BlockInteraction.KEEP;
+                break;
+            case 1:
+                explosion_effect = this.getDestroyType(GameRules.RULE_BLOCK_EXPLOSION_DROP_DECAY);
+                break;
+            case 2:
+                explosion_effect = this.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? this.getDestroyType(GameRules.RULE_MOB_EXPLOSION_DROP_DECAY) : Explosion.BlockInteraction.KEEP;
+                break;
+            case 3:
+                explosion_effect = this.getDestroyType(GameRules.RULE_TNT_EXPLOSION_DROP_DECAY);
+                break;
+            case 4:
+                explosion_effect = Explosion.BlockInteraction.TRIGGER_BLOCK;
+                break;
+            // CraftBukkit start - handle custom explosion type
+            case 5:
+                explosion_effect = Explosion.BlockInteraction.DESTROY;
+                break;
+            // CraftBukkit end
+            default:
+                throw new MatchException((String) null, (Throwable) null);
+        }
+
+        Explosion.BlockInteraction explosion_effect1 = explosion_effect;
+        Explosion explosion = new Explosion(this, entity, damageSource, behavior, x, y, z, power, createFire, explosion_effect1, particle, emitterParticle, soundEvent);
+
         explosion.explode();
         explosion.finalizeExplosion(particles);
         return explosion;
@@ -629,19 +1405,33 @@
     @Nullable
     @Override
     public BlockEntity getBlockEntity(BlockPos pos) {
-        if (this.isOutsideBuildHeight(pos)) {
-            return null;
-        } else {
-            return !this.isClientSide && Thread.currentThread() != this.thread
-                ? null
-                : this.getChunkAt(pos).getBlockEntity(pos, LevelChunk.EntityCreationType.IMMEDIATE);
+        // CraftBukkit start
+        return this.getBlockEntity(pos, true);
+    }
+
+    @Nullable
+    public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) {
+        // Paper start - Perf: Optimize capturedTileEntities lookup
+        net.minecraft.world.level.block.entity.BlockEntity blockEntity;
+        if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) {
+            return blockEntity;
         }
+        // Paper end - Perf: Optimize capturedTileEntities lookup
+        // CraftBukkit end
+        return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system
     }
 
     public void setBlockEntity(BlockEntity blockEntity) {
-        BlockPos blockPos = blockEntity.getBlockPos();
-        if (!this.isOutsideBuildHeight(blockPos)) {
-            this.getChunkAt(blockPos).addAndRegisterBlockEntity(blockEntity);
+        BlockPos blockposition = blockEntity.getBlockPos();
+
+        if (!this.isOutsideBuildHeight(blockposition)) {
+            // CraftBukkit start
+            if (this.captureBlockStates) {
+                this.capturedTileEntities.put(blockposition.immutable(), blockEntity);
+                return;
+            }
+            // CraftBukkit end
+            this.getChunkAt(blockposition).addAndRegisterBlockEntity(blockEntity);
         }
     }
 
@@ -652,18 +1442,16 @@
     }
 
     public boolean isLoaded(BlockPos pos) {
-        return !this.isOutsideBuildHeight(pos)
-            && this.getChunkSource().hasChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
+        return this.isOutsideBuildHeight(pos) ? false : this.getChunkSource().hasChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
     }
 
     public boolean loadedAndEntityCanStandOnFace(BlockPos pos, Entity entity, Direction direction) {
         if (this.isOutsideBuildHeight(pos)) {
             return false;
         } else {
-            ChunkAccess chunkAccess = this.getChunk(
-                SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ChunkStatus.FULL, false
-            );
-            return chunkAccess != null && chunkAccess.getBlockState(pos).entityCanStandOnFace(this, pos, entity, direction);
+            ChunkAccess ichunkaccess = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ChunkStatus.FULL, false);
+
+            return ichunkaccess == null ? false : ichunkaccess.getBlockState(pos).entityCanStandOnFace(this, pos, entity, direction);
         }
     }
 
@@ -672,10 +1460,11 @@
     }
 
     public void updateSkyBrightness() {
-        double d = 1.0 - (double)(this.getRainLevel(1.0F) * 5.0F) / 16.0;
-        double e = 1.0 - (double)(this.getThunderLevel(1.0F) * 5.0F) / 16.0;
-        double f = 0.5 + 2.0 * Mth.clamp((double)Mth.cos(this.getTimeOfDay(1.0F) * (float) (Math.PI * 2)), -0.25, 0.25);
-        this.skyDarken = (int)((1.0 - f * d * e) * 11.0);
+        double d0 = 1.0D - (double) (this.getRainLevel(1.0F) * 5.0F) / 16.0D;
+        double d1 = 1.0D - (double) (this.getThunderLevel(1.0F) * 5.0F) / 16.0D;
+        double d2 = 0.5D + 2.0D * Mth.clamp((double) Mth.cos(this.getTimeOfDay(1.0F) * 6.2831855F), -0.25D, 0.25D);
+
+        this.skyDarken = (int) ((1.0D - d2 * d0 * d1) * 11.0D);
     }
 
     public void setSpawnSettings(boolean spawnMonsters, boolean spawnAnimals) {
@@ -683,14 +1472,13 @@
     }
 
     public BlockPos getSharedSpawnPos() {
-        BlockPos blockPos = this.levelData.getSpawnPos();
-        if (!this.getWorldBorder().isWithinBounds(blockPos)) {
-            blockPos = this.getHeightmapPos(
-                Heightmap.Types.MOTION_BLOCKING, BlockPos.containing(this.getWorldBorder().getCenterX(), 0.0, this.getWorldBorder().getCenterZ())
-            );
+        BlockPos blockposition = this.levelData.getSpawnPos();
+
+        if (!this.getWorldBorder().isWithinBounds(blockposition)) {
+            blockposition = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, BlockPos.containing(this.getWorldBorder().getCenterX(), 0.0D, this.getWorldBorder().getCenterZ()));
         }
 
-        return blockPos;
+        return blockposition;
     }
 
     public float getSharedSpawnAngle() {
@@ -704,9 +1492,9 @@
                 this.thunderLevel = 1.0F;
             }
         }
+
     }
 
-    @Override
     public void close() throws IOException {
         this.getChunkSource().close();
     }
@@ -721,25 +1509,14 @@
     public List<Entity> getEntities(@Nullable Entity except, AABB box, Predicate<? super Entity> predicate) {
         this.getProfiler().incrementCounter("getEntities");
         List<Entity> list = Lists.newArrayList();
-        this.getEntities().get(box, entity -> {
-            if (entity != except && predicate.test(entity)) {
-                list.add(entity);
-            }
-
-            if (entity instanceof EnderDragon) {
-                for (EnderDragonPart enderDragonPart : ((EnderDragon)entity).getSubEntities()) {
-                    if (entity != except && predicate.test(enderDragonPart)) {
-                        list.add(enderDragonPart);
-                    }
-                }
-            }
-        });
+        ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call
         return list;
     }
 
     @Override
     public <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate) {
         List<T> list = Lists.newArrayList();
+
         this.getEntities(filter, box, predicate, list);
         return list;
     }
@@ -750,28 +1527,23 @@
 
     public <T extends Entity> void getEntities(EntityTypeTest<Entity, T> filter, AABB box, Predicate<? super T> predicate, List<? super T> result, int limit) {
         this.getProfiler().incrementCounter("getEntities");
-        this.getEntities().get(filter, box, entity -> {
-            if (predicate.test(entity)) {
-                result.add(entity);
-                if (result.size() >= limit) {
-                    return AbortableIterationConsumer.Continuation.ABORT;
-                }
-            }
-
-            if (entity instanceof EnderDragon enderDragon) {
-                for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) {
-                    T entity2 = filter.tryCast(enderDragonPart);
-                    if (entity2 != null && predicate.test(entity2)) {
-                        result.add(entity2);
-                        if (result.size() >= limit) {
-                            return AbortableIterationConsumer.Continuation.ABORT;
-                        }
-                    }
-                }
+        // Paper start - optimise this call
+        //TODO use limit
+        if (filter instanceof net.minecraft.world.entity.EntityType entityTypeTest) {
+            ((ServerLevel) this).getEntityLookup().getEntities(entityTypeTest, box, result, predicate);
+        } else {
+            Predicate<? super T> test = (obj) -> {
+                return filter.tryCast(obj) != null;
+            };
+            predicate = predicate == null ? test : test.and((Predicate) predicate);
+            Class base;
+            if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) {
+                ((ServerLevel) this).getEntityLookup().getEntities((Entity) null, box, (List) result, (Predicate)predicate);
+            } else {
+                ((ServerLevel) this).getEntityLookup().getEntities(base, null, box, (List) result, (Predicate)predicate); // Paper - optimise this call
             }
-
-            return AbortableIterationConsumer.Continuation.CONTINUE;
-        });
+        }
+        // Paper end - optimise this call
     }
 
     @Nullable
@@ -781,6 +1553,7 @@
         if (this.hasChunkAt(pos)) {
             this.getChunkAt(pos).setUnsaved(true);
         }
+
     }
 
     @Override
@@ -788,8 +1561,7 @@
         return 63;
     }
 
-    public void disconnect() {
-    }
+    public void disconnect() {}
 
     public long getGameTime() {
         return this.levelData.getGameTime();
@@ -803,11 +1575,9 @@
         return true;
     }
 
-    public void broadcastEntityEvent(Entity entity, byte status) {
-    }
+    public void broadcastEntityEvent(Entity entity, byte status) {}
 
-    public void broadcastDamageEvent(Entity entity, DamageSource damageSource) {
-    }
+    public void broadcastDamageEvent(Entity entity, DamageSource damageSource) {}
 
     public void blockEvent(BlockPos pos, Block block, int type, int data) {
         this.getBlockState(pos).triggerEvent(this, pos, type, data);
@@ -829,9 +1599,10 @@
     }
 
     public void setThunderLevel(float thunderGradient) {
-        float f = Mth.clamp(thunderGradient, 0.0F, 1.0F);
-        this.oThunderLevel = f;
-        this.thunderLevel = f;
+        float f1 = Mth.clamp(thunderGradient, 0.0F, 1.0F);
+
+        this.oThunderLevel = f1;
+        this.thunderLevel = f1;
     }
 
     public float getRainLevel(float delta) {
@@ -839,17 +1610,18 @@
     }
 
     public void setRainLevel(float rainGradient) {
-        float f = Mth.clamp(rainGradient, 0.0F, 1.0F);
-        this.oRainLevel = f;
-        this.rainLevel = f;
+        float f1 = Mth.clamp(rainGradient, 0.0F, 1.0F);
+
+        this.oRainLevel = f1;
+        this.rainLevel = f1;
     }
 
     public boolean isThundering() {
-        return this.dimensionType().hasSkyLight() && !this.dimensionType().hasCeiling() && (double)this.getThunderLevel(1.0F) > 0.9;
+        return this.dimensionType().hasSkyLight() && !this.dimensionType().hasCeiling() ? (double) this.getThunderLevel(1.0F) > 0.9D : false;
     }
 
     public boolean isRaining() {
-        return (double)this.getRainLevel(1.0F) > 0.2;
+        return (double) this.getRainLevel(1.0F) > 0.2D;
     }
 
     public boolean isRainingAt(BlockPos pos) {
@@ -860,8 +1632,9 @@
         } else if (this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos).getY() > pos.getY()) {
             return false;
         } else {
-            Biome biome = this.getBiome(pos).value();
-            return biome.getPrecipitationAt(pos) == Biome.Precipitation.RAIN;
+            Biome biomebase = (Biome) this.getBiome(pos).value();
+
+            return biomebase.getPrecipitationAt(pos) == Biome.Precipitation.RAIN;
         }
     }
 
@@ -872,59 +1645,74 @@
 
     public abstract MapId getFreeMapId();
 
-    public void globalLevelEvent(int eventId, BlockPos pos, int data) {
-    }
+    public void globalLevelEvent(int eventId, BlockPos pos, int data) {}
 
     public CrashReportCategory fillReportDetails(CrashReport report) {
-        CrashReportCategory crashReportCategory = report.addCategory("Affected level", 1);
-        crashReportCategory.setDetail("All players", () -> this.players().size() + " total; " + this.players());
-        crashReportCategory.setDetail("Chunk stats", this.getChunkSource()::gatherStats);
-        crashReportCategory.setDetail("Level dimension", () -> this.dimension().location().toString());
+        CrashReportCategory crashreportsystemdetails = report.addCategory("Affected level", 1);
+
+        crashreportsystemdetails.setDetail("All players", () -> {
+            int i = this.players().size();
+
+            return "" + i + " total; " + String.valueOf(this.players());
+        });
+        ChunkSource ichunkprovider = this.getChunkSource();
+
+        Objects.requireNonNull(ichunkprovider);
+        crashreportsystemdetails.setDetail("Chunk stats", ichunkprovider::gatherStats);
+        crashreportsystemdetails.setDetail("Level dimension", () -> {
+            return this.dimension().location().toString();
+        });
 
         try {
-            this.levelData.fillCrashReportCategory(crashReportCategory, this);
-        } catch (Throwable var4) {
-            crashReportCategory.setDetailError("Level Data Unobtainable", var4);
+            this.levelData.fillCrashReportCategory(crashreportsystemdetails, this);
+        } catch (Throwable throwable) {
+            crashreportsystemdetails.setDetailError("Level Data Unobtainable", throwable);
         }
 
-        return crashReportCategory;
+        return crashreportsystemdetails;
     }
 
     public abstract void destroyBlockProgress(int entityId, BlockPos pos, int progress);
 
-    public void createFireworks(double x, double y, double z, double velocityX, double velocityY, double velocityZ, List<FireworkExplosion> explosions) {
-    }
+    public void createFireworks(double x, double y, double z, double velocityX, double velocityY, double velocityZ, List<FireworkExplosion> explosions) {}
 
     public abstract Scoreboard getScoreboard();
 
     public void updateNeighbourForOutputSignal(BlockPos pos, Block block) {
-        for (Direction direction : Direction.Plane.HORIZONTAL) {
-            BlockPos blockPos = pos.relative(direction);
-            if (this.hasChunkAt(blockPos)) {
-                BlockState blockState = this.getBlockState(blockPos);
-                if (blockState.is(Blocks.COMPARATOR)) {
-                    this.neighborChanged(blockState, blockPos, block, pos, false);
-                } else if (blockState.isRedstoneConductor(this, blockPos)) {
-                    blockPos = blockPos.relative(direction);
-                    blockState = this.getBlockState(blockPos);
-                    if (blockState.is(Blocks.COMPARATOR)) {
-                        this.neighborChanged(blockState, blockPos, block, pos, false);
+        Iterator iterator = Direction.Plane.HORIZONTAL.iterator();
+
+        while (iterator.hasNext()) {
+            Direction enumdirection = (Direction) iterator.next();
+            BlockPos blockposition1 = pos.relative(enumdirection);
+
+            if (this.hasChunkAt(blockposition1)) {
+                BlockState iblockdata = this.getBlockState(blockposition1);
+
+                if (iblockdata.is(Blocks.COMPARATOR)) {
+                    this.neighborChanged(iblockdata, blockposition1, block, pos, false);
+                } else if (iblockdata.isRedstoneConductor(this, blockposition1)) {
+                    blockposition1 = blockposition1.relative(enumdirection);
+                    iblockdata = this.getBlockState(blockposition1);
+                    if (iblockdata.is(Blocks.COMPARATOR)) {
+                        this.neighborChanged(iblockdata, blockposition1, block, pos, false);
                     }
                 }
             }
         }
+
     }
 
     @Override
     public DifficultyInstance getCurrentDifficultyAt(BlockPos pos) {
-        long l = 0L;
+        long i = 0L;
         float f = 0.0F;
+
         if (this.hasChunkAt(pos)) {
             f = this.getMoonBrightness();
-            l = this.getChunkAt(pos).getInhabitedTime();
+            i = this.getChunkAt(pos).getInhabitedTime();
         }
 
-        return new DifficultyInstance(this.getDifficulty(), this.getDayTime(), l, f);
+        return new DifficultyInstance(this.getDifficulty(), this.getDayTime(), i, f);
     }
 
     @Override
@@ -932,8 +1720,7 @@
         return this.skyDarken;
     }
 
-    public void setSkyFlashTime(int lightningTicksLeft) {
-    }
+    public void setSkyFlashTime(int lightningTicksLeft) {}
 
     @Override
     public WorldBorder getWorldBorder() {
@@ -946,7 +1733,7 @@
 
     @Override
     public DimensionType dimensionType() {
-        return this.dimensionTypeRegistration.value();
+        return (DimensionType) this.dimensionTypeRegistration.value();
     }
 
     public Holder<DimensionType> dimensionTypeRegistration() {
@@ -974,10 +1761,19 @@
 
     public abstract RecipeManager getRecipeManager();
 
-    public BlockPos getBlockRandomPos(int x, int y, int z, int i) {
+    public BlockPos getBlockRandomPos(int x, int y, int z, int l) {
+        // Paper start - allow use of mutable pos
+        BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos();
+        this.getRandomBlockPosition(x, y, z, l, ret);
+        return ret.immutable();
+    }
+    public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) {
+        // Paper end
         this.randValue = this.randValue * 3 + 1013904223;
-        int j = this.randValue >> 2;
-        return new BlockPos(x + (j & 15), y + (j >> 16 & i), z + (j >> 8 & 15));
+        int i1 = this.randValue >> 2;
+
+        out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call
+        return out; // Paper
     }
 
     public boolean noSave() {
@@ -985,7 +1781,7 @@
     }
 
     public ProfilerFiller getProfiler() {
-        return this.profiler.get();
+        return (ProfilerFiller) this.profiler.get();
     }
 
     public Supplier<ProfilerFiller> getProfilerSupplier() {
@@ -1005,7 +1801,7 @@
 
     @Override
     public long nextSubTickCount() {
-        return this.subTickCount++;
+        return (long) (this.subTickCount++);
     }
 
     @Override
@@ -1020,10 +1816,80 @@
     public abstract PotionBrewing potionBrewing();
 
     public static enum ExplosionInteraction {
-        NONE,
-        BLOCK,
-        MOB,
-        TNT,
-        BLOW;
+
+        NONE, BLOCK, MOB, TNT, BLOW, STANDARD; // CraftBukkit - Add STANDARD which will always use Explosion.Effect.DESTROY
+
+        private ExplosionInteraction() {}
+    }
+    // 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 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
+    // Paper start
+    //protected final io.papermc.paper.world.EntitySliceManager entitySliceManager; // Paper - rewrite chunk system
+
+    public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) {
+        io.papermc.paper.world.ChunkEntitySlices slices = ((ServerLevel)this).getEntityLookup().getChunk(chunkX, chunkZ);
+        if (slices == null) {
+            return new org.bukkit.entity.Entity[0];
+        }
+        return slices.getChunkEntities();
+    }
+
+    @Override
+    public List<Entity> getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate) {
+        List<Entity> ret = new java.util.ArrayList<>();
+        ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, ret, predicate);
+        return ret;
+    }
+
+    @Override
+    public void getEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {
+        ((ServerLevel)this).getEntityLookup().getEntities(except, box, into, predicate);
+    }
+
+    @Override
+    public void getHardCollidingEntities(Entity except, AABB box, Predicate<? super Entity> predicate, List<Entity> into) {
+        ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, into, predicate);
+    }
+
+    @Override
+    public <T> void getEntitiesByClass(Class<? extends T> clazz, Entity except, final AABB box, List<? super T> into,
+                                       Predicate<? super T> predicate) {
+        ((ServerLevel)this).getEntityLookup().getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate);
     }
+
+    @Override
+    public <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box, Predicate<? super T> predicate) {
+        List<T> ret = new java.util.ArrayList<>();
+        ((ServerLevel)this).getEntityLookup().getEntities(entityClass, null, box, ret, predicate);
+        return ret;
+    }
+    // Paper end
+    // Paper start - optimize redstone (Alternate Current)
+    public alternate.current.wire.WireHandler getWireHandler() {
+        // This method is overridden in ServerLevel.
+        // Since Paper is a server platform there is no risk
+        // of this implementation being called. It is here
+        // only so this method can be called without casting
+        // an instance of Level to ServerLevel.
+        return null;
+    }
+    // Paper end - optimize redstone (Alternate Current)
 }
