diff --git a/patches/server/Allow-delegation-to-vanilla-chunk-gen.patch b/patches/server/Allow-delegation-to-vanilla-chunk-gen.patch
index 820c8124d3..cb3e9865ef 100644
--- a/patches/server/Allow-delegation-to-vanilla-chunk-gen.patch
+++ b/patches/server/Allow-delegation-to-vanilla-chunk-gen.patch
@@ -52,7 +52,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -    private final LevelChunkSection[] sections;
 +    private LevelChunkSection[] sections; // Paper - remove final
      private Set<BlockPos> tiles;
-     private World world; // Paper - Anti-Xray - Add world
+     private World world; // Paper - Anti-Xray - Add parameters
  
 @@ -0,0 +0,0 @@ public final class CraftChunkData implements ChunkGenerator.ChunkData {
          return this.sections;
diff --git a/patches/server/Anti-Xray.patch b/patches/server/Anti-Xray.patch
index bc7242244d..de91dd5900 100644
--- a/patches/server/Anti-Xray.patch
+++ b/patches/server/Anti-Xray.patch
@@ -59,6 +59,148 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
  }
  
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++public final class BitStorageReader {
++
++    private byte[] buffer;
++    private int bits;
++    private int mask;
++    private int longInBufferIndex;
++    private int bitInLongIndex;
++    private long current;
++
++    public void setBuffer(byte[] buffer) {
++        this.buffer = buffer;
++    }
++
++    public void setBits(int bits) {
++        this.bits = bits;
++        mask = (1 << bits) - 1;
++    }
++
++    public void setIndex(int index) {
++        longInBufferIndex = index;
++        bitInLongIndex = 0;
++        init();
++    }
++
++    private void init() {
++        if (buffer.length > longInBufferIndex + 7) {
++            current = ((((long) buffer[longInBufferIndex]) << 56)
++                | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
++                | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
++                | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
++                | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
++                | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
++                | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
++                | (((long) buffer[longInBufferIndex + 7] & 0xff)));
++        }
++    }
++
++    public int read() {
++        if (bitInLongIndex + bits > 64) {
++            bitInLongIndex = 0;
++            longInBufferIndex += 8;
++            init();
++        }
++
++        int value = (int) (current >>> bitInLongIndex) & mask;
++        bitInLongIndex += bits;
++        return value;
++    }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.antixray;
++
++public final class BitStorageWriter {
++
++    private byte[] buffer;
++    private int bits;
++    private long mask;
++    private int longInBufferIndex;
++    private int bitInLongIndex;
++    private long current;
++    private boolean dirty;
++
++    public void setBuffer(byte[] buffer) {
++        this.buffer = buffer;
++    }
++
++    public void setBits(int bits) {
++        this.bits = bits;
++        mask = (1L << bits) - 1;
++    }
++
++    public void setIndex(int index) {
++        longInBufferIndex = index;
++        bitInLongIndex = 0;
++        init();
++    }
++
++    private void init() {
++        if (buffer.length > longInBufferIndex + 7) {
++            current = ((((long) buffer[longInBufferIndex]) << 56)
++                | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48)
++                | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40)
++                | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32)
++                | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24)
++                | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16)
++                | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8)
++                | (((long) buffer[longInBufferIndex + 7] & 0xff)));
++        }
++
++        dirty = false;
++    }
++
++    public void flush() {
++        if (dirty && buffer.length > longInBufferIndex + 7) {
++            buffer[longInBufferIndex] = (byte) (current >> 56 & 0xff);
++            buffer[longInBufferIndex + 1] = (byte) (current >> 48 & 0xff);
++            buffer[longInBufferIndex + 2] = (byte) (current >> 40 & 0xff);
++            buffer[longInBufferIndex + 3] = (byte) (current >> 32 & 0xff);
++            buffer[longInBufferIndex + 4] = (byte) (current >> 24 & 0xff);
++            buffer[longInBufferIndex + 5] = (byte) (current >> 16 & 0xff);
++            buffer[longInBufferIndex + 6] = (byte) (current >> 8 & 0xff);
++            buffer[longInBufferIndex + 7] = (byte) (current & 0xff);
++        }
++    }
++
++    public void write(int value) {
++        if (bitInLongIndex + bits > 64) {
++            flush();
++            bitInLongIndex = 0;
++            longInBufferIndex += 8;
++            init();
++        }
++
++        current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex;
++        dirty = true;
++        bitInLongIndex += bits;
++    }
++
++    public void skip() {
++        bitInLongIndex += bits;
++
++        if (bitInLongIndex > 64) {
++            flush();
++            bitInLongIndex = bits;
++            longInBufferIndex += 8;
++            init();
++        }
++    }
++}
 diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -70,6 +212,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import net.minecraft.core.BlockPos;
 +import net.minecraft.core.Direction;
 +import net.minecraft.network.protocol.game.ClientboundLevelChunkPacket;
++import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
 +import net.minecraft.server.level.ServerPlayer;
 +import net.minecraft.server.level.ServerPlayerGameMode;
 +import net.minecraft.world.level.Level;
@@ -86,11 +229,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    }
 +
-+    public BlockState[] getPredefinedBlockData(Level world, ChunkAccess chunk, LevelChunkSection chunkSection, boolean initializeBlocks) {
++    public BlockState[] getPresetBlockStates(Level level, ChunkAccess chunk, LevelChunkSection chunkSection, boolean initializeBlocks) {
 +        return null;
 +    }
 +
-+    public boolean shouldModify(ServerPlayer entityPlayer, LevelChunk chunk) {
++    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
 +        return false;
 +    }
 +
@@ -102,11 +245,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        chunkPacket.setReady(true);
 +    }
 +
-+    public void onBlockChange(Level world, BlockPos blockPosition, BlockState newBlockData, BlockState oldBlockData, int flag) {
++    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
 +
 +    }
 +
-+    public void onPlayerLeftClickBlock(ServerPlayerGameMode playerInteractManager, BlockPos blockPosition, Direction enumDirection) {
++    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight) {
 +
 +    }
 +}
@@ -118,18 +261,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package com.destroystokyo.paper.antixray;
 +
-+import java.util.ArrayList;
-+import java.util.LinkedHashSet;
-+import java.util.LinkedList;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.concurrent.Executor;
-+import java.util.concurrent.ThreadLocalRandom;
-+import java.util.function.IntSupplier;
++import com.destroystokyo.paper.PaperWorldConfig;
 +import net.minecraft.core.BlockPos;
 +import net.minecraft.core.Direction;
 +import net.minecraft.core.Registry;
 +import net.minecraft.network.protocol.game.ClientboundLevelChunkPacket;
++import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
 +import net.minecraft.resources.ResourceLocation;
 +import net.minecraft.server.MinecraftServer;
 +import net.minecraft.server.level.ServerLevel;
@@ -139,94 +276,88 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import net.minecraft.world.level.Level;
 +import net.minecraft.world.level.block.Block;
 +import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.block.EntityBlock;
 +import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.EmptyLevelChunk;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.Palette;
++import net.minecraft.world.level.chunk.*;
 +import org.bukkit.Bukkit;
-+import org.bukkit.World.Environment;
 +
-+import com.destroystokyo.paper.PaperWorldConfig;
++import java.util.*;
++import java.util.concurrent.Executor;
++import java.util.concurrent.ThreadLocalRandom;
++import java.util.function.IntSupplier;
 +
 +public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController {
 +
 +    private final Executor executor;
 +    private final EngineMode engineMode;
-+    private final int worldSectionHeight;
-+    private final int maxChunkSectionIndex;
++    private final int maxBlockHeight;
 +    private final int updateRadius;
 +    private final boolean usePermission;
-+    private final BlockState[] predefinedBlockData;
-+    private final BlockState[] predefinedBlockDataFull;
-+    private final BlockState[] predefinedBlockDataStone;
-+    private final BlockState[] predefinedBlockDataNetherrack;
-+    private final BlockState[] predefinedBlockDataEndStone;
-+    private final int[] predefinedBlockDataBitsGlobal;
-+    private final int[] predefinedBlockDataBitsStoneGlobal;
-+    private final int[] predefinedBlockDataBitsNetherrackGlobal;
-+    private final int[] predefinedBlockDataBitsEndStoneGlobal;
++    private final BlockState[] presetBlockStates;
++    private final BlockState[] presetBlockStatesFull;
++    private final BlockState[] presetBlockStatesStone;
++    private final BlockState[] presetBlockStatesNetherrack;
++    private final BlockState[] presetBlockStatesEndStone;
++    private final int[] presetBlockStateBitsGlobal;
++    private final int[] presetBlockStateBitsStoneGlobal;
++    private final int[] presetBlockStateBitsNetherrackGlobal;
++    private final int[] presetBlockStateBitsEndStoneGlobal;
 +    private final boolean[] solidGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
 +    private final boolean[] obfuscateGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
 +    private final LevelChunkSection[] emptyNearbyChunkSections = {LevelChunk.EMPTY_SECTION, LevelChunk.EMPTY_SECTION, LevelChunk.EMPTY_SECTION, LevelChunk.EMPTY_SECTION};
-+    private final int maxBlockYUpdatePosition;
++    private final int maxBlockHeightUpdatePosition;
 +
-+    public ChunkPacketBlockControllerAntiXray(Level world, Executor executor) {
-+        PaperWorldConfig paperWorldConfig = world.paperConfig;
++    public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) {
++        this.executor = executor;
++        PaperWorldConfig paperWorldConfig = level.paperConfig;
 +        engineMode = paperWorldConfig.engineMode;
-+
-+        int minSection = world.getMinSection();
-+        worldSectionHeight = world.getSectionsCount();
-+        maxChunkSectionIndex = (paperWorldConfig.maxBlockHeight >> 4) - minSection;
++        maxBlockHeight = paperWorldConfig.maxBlockHeight >> 4 << 4;
 +        updateRadius = paperWorldConfig.updateRadius;
 +        usePermission = paperWorldConfig.usePermission;
-+
-+        this.executor = executor;
-+
 +        List<String> toObfuscate;
 +
 +        if (engineMode == EngineMode.HIDE) {
 +            toObfuscate = paperWorldConfig.hiddenBlocks;
-+            predefinedBlockData = null;
-+            predefinedBlockDataFull = null;
-+            predefinedBlockDataStone = new BlockState[] {Blocks.STONE.defaultBlockState()};
-+            predefinedBlockDataNetherrack = new BlockState[] {Blocks.NETHERRACK.defaultBlockState()};
-+            predefinedBlockDataEndStone = new BlockState[] {Blocks.END_STONE.defaultBlockState()};
-+            predefinedBlockDataBitsGlobal = null;
-+            predefinedBlockDataBitsStoneGlobal = new int[] {LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())};
-+            predefinedBlockDataBitsNetherrackGlobal = new int[] {LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())};
-+            predefinedBlockDataBitsEndStoneGlobal = new int[] {LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())};
++            presetBlockStates = null;
++            presetBlockStatesFull = null;
++            presetBlockStatesStone = new BlockState[]{Blocks.STONE.defaultBlockState()};
++            presetBlockStatesNetherrack = new BlockState[]{Blocks.NETHERRACK.defaultBlockState()};
++            presetBlockStatesEndStone = new BlockState[]{Blocks.END_STONE.defaultBlockState()};
++            presetBlockStateBitsGlobal = null;
++            presetBlockStateBitsStoneGlobal = new int[]{LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())};
++            presetBlockStateBitsNetherrackGlobal = new int[]{LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())};
++            presetBlockStateBitsEndStoneGlobal = new int[]{LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())};
 +        } else {
 +            toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks);
-+            List<BlockState> predefinedBlockDataList = new LinkedList<BlockState>();
++            List<BlockState> presetBlockStateList = new LinkedList<>();
 +
 +            for (String id : paperWorldConfig.hiddenBlocks) {
 +                Block block = Registry.BLOCK.getOptional(new ResourceLocation(id)).orElse(null);
 +
-+                if (block != null && !(block instanceof net.minecraft.world.level.block.EntityBlock)) {
++                if (block != null && !(block instanceof EntityBlock)) {
 +                    toObfuscate.add(id);
-+                    predefinedBlockDataList.add(block.defaultBlockState());
++                    presetBlockStateList.add(block.defaultBlockState());
 +                }
 +            }
 +
-+            // The doc of the LinkedHashSet(Collection<? extends E> c) constructor doesn't specify that the insertion order is the predictable iteration order of the specified Collection, although it is in the implementation
-+            Set<BlockState> predefinedBlockDataSet = new LinkedHashSet<>(predefinedBlockDataList);
-+            // Therefore addAll(Collection<? extends E> c) is used, which guarantees this order in the doc
-+            predefinedBlockData = predefinedBlockDataSet.isEmpty() ? new BlockState[] {Blocks.DIAMOND_ORE.defaultBlockState()} : predefinedBlockDataSet.toArray(new BlockState[0]);
-+            predefinedBlockDataFull = predefinedBlockDataSet.isEmpty() ? new BlockState[] {Blocks.DIAMOND_ORE.defaultBlockState()} : predefinedBlockDataList.toArray(new BlockState[0]);
-+            predefinedBlockDataStone = null;
-+            predefinedBlockDataNetherrack = null;
-+            predefinedBlockDataEndStone = null;
-+            predefinedBlockDataBitsGlobal = new int[predefinedBlockDataFull.length];
++            // The doc of the LinkedHashSet(Collection<? extends E>) constructor doesn't specify that the insertion order is the predictable iteration order of the specified Collection, although it is in the implementation
++            Set<BlockState> presetBlockStateSet = new LinkedHashSet<>();
++            // Therefore addAll(Collection<? extends E>) is used, which guarantees this order in the doc
++            presetBlockStateSet.addAll(presetBlockStateList);
++            presetBlockStates = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateSet.toArray(new BlockState[0]);
++            presetBlockStatesFull = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateList.toArray(new BlockState[0]);
++            presetBlockStatesStone = null;
++            presetBlockStatesNetherrack = null;
++            presetBlockStatesEndStone = null;
++            presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length];
 +
-+            for (int i = 0; i < predefinedBlockDataFull.length; i++) {
-+                predefinedBlockDataBitsGlobal[i] = LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(predefinedBlockDataFull[i]);
++            for (int i = 0; i < presetBlockStatesFull.length; i++) {
++                presetBlockStateBitsGlobal[i] = LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]);
 +            }
 +
-+            predefinedBlockDataBitsStoneGlobal = null;
-+            predefinedBlockDataBitsNetherrackGlobal = null;
-+            predefinedBlockDataBitsEndStoneGlobal = null;
++            presetBlockStateBitsStoneGlobal = null;
++            presetBlockStateBitsNetherrackGlobal = null;
++            presetBlockStateBitsEndStoneGlobal = null;
 +        }
 +
 +        for (String id : toObfuscate) {
@@ -235,93 +366,86 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // Don't obfuscate air because air causes unnecessary block updates and causes block updates to fail in the void
 +            if (block != null && !block.defaultBlockState().isAir()) {
 +                // Replace all block states of a specified block
-+                for (BlockState blockData : block.getStateDefinition().getPossibleStates()) {
-+                    obfuscateGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockData)] = true;
++                for (BlockState blockState : block.getStateDefinition().getPossibleStates()) {
++                    obfuscateGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
 +                }
 +            }
 +        }
 +
-+        EmptyLevelChunk emptyChunk = new EmptyLevelChunk(world, new ChunkPos(0, 0));
++        EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0));
 +        BlockPos zeroPos = new BlockPos(0, 0, 0);
 +
 +        for (int i = 0; i < solidGlobal.length; i++) {
-+            BlockState blockData = LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.valueFor(i);
++            BlockState blockState = LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.valueFor(i);
 +
-+            if (blockData != null) {
-+                solidGlobal[i] = blockData.isRedstoneConductor(emptyChunk, zeroPos)
-+                    && blockData.getBlock() != Blocks.SPAWNER && blockData.getBlock() != Blocks.BARRIER && blockData.getBlock() != Blocks.SHULKER_BOX && blockData.getBlock() != Blocks.SLIME_BLOCK || paperWorldConfig.lavaObscures && blockData == Blocks.LAVA.defaultBlockState();
-+                // Comparing blockData == Blocks.LAVA.getBlockData() instead of blockData.getBlock() == Blocks.LAVA ensures that only "stationary lava" is used
++            if (blockState != null) {
++                solidGlobal[i] = blockState.isRedstoneConductor(emptyChunk, zeroPos)
++                    && blockState.getBlock() != Blocks.SPAWNER && blockState.getBlock() != Blocks.BARRIER && blockState.getBlock() != Blocks.SHULKER_BOX && blockState.getBlock() != Blocks.SLIME_BLOCK || paperWorldConfig.lavaObscures && blockState == Blocks.LAVA.defaultBlockState();
++                // Comparing blockState == Blocks.LAVA.defaultBlockState() instead of blockState.getBlock() == Blocks.LAVA ensures that only "stationary lava" is used
 +                // shulker box checks TE.
 +            }
 +        }
 +
-+        this.maxBlockYUpdatePosition = (maxChunkSectionIndex + 1) * 16 + updateRadius - 1;
++        maxBlockHeightUpdatePosition = maxBlockHeight + updateRadius - 1;
 +    }
 +
-+    private int getPredefinedBlockDataFullLength() {
-+        return engineMode == EngineMode.HIDE ? 1 : predefinedBlockDataFull.length;
++    private int getPresetBlockStatesFullLength() {
++        return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length;
 +    }
 +
 +    @Override
-+    public BlockState[] getPredefinedBlockData(Level world, ChunkAccess chunk, LevelChunkSection chunkSection, boolean initializeBlocks) {
-+        // Return the block data which should be added to the data palettes so that they can be used for the obfuscation
-+        if (chunkSection.bottomBlockY() >> 4 <= maxChunkSectionIndex) {
++    public BlockState[] getPresetBlockStates(Level level, ChunkAccess chunk, LevelChunkSection chunkSection, boolean initializeBlocks) {
++        // Return the block states to be added to the paletted containers so that they can be used for obfuscation
++        if (chunkSection.bottomBlockY() < maxBlockHeight) {
 +            if (engineMode == EngineMode.HIDE) {
-+                return switch (world.getWorld().getEnvironment()) {
-+                    case NETHER -> predefinedBlockDataNetherrack;
-+                    case THE_END -> predefinedBlockDataEndStone;
-+                    default -> predefinedBlockDataStone;
++                return switch (level.getWorld().getEnvironment()) {
++                    case NETHER -> presetBlockStatesNetherrack;
++                    case THE_END -> presetBlockStatesEndStone;
++                    default -> presetBlockStatesStone;
 +                };
 +            }
-+            return predefinedBlockData;
++
++            return presetBlockStates;
 +        }
 +
 +        return null;
 +    }
 +
 +    @Override
-+    public boolean shouldModify(ServerPlayer entityPlayer, LevelChunk chunk) {
-+        return !usePermission || !entityPlayer.getBukkitEntity().hasPermission("paper.antixray.bypass");
++    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
++        return !usePermission || !player.getBukkitEntity().hasPermission("paper.antixray.bypass");
 +    }
 +
 +    @Override
 +    public ChunkPacketInfoAntiXray getChunkPacketInfo(ClientboundLevelChunkPacket chunkPacket, LevelChunk chunk) {
 +        // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later
-+        // Note: As of 1.14 this has to be moved later due to the chunk system.
 +        return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this);
 +    }
 +
 +    @Override
 +    public void modifyBlocks(ClientboundLevelChunkPacket chunkPacket, ChunkPacketInfo<BlockState> chunkPacketInfo) {
-+        if (chunkPacketInfo == null) {
++        if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) {
 +            chunkPacket.setReady(true);
 +            return;
 +        }
 +
 +        if (!Bukkit.isPrimaryThread()) {
-+            // plugins?
-+            MinecraftServer.getServer().scheduleOnMain(() -> {
-+                this.modifyBlocks(chunkPacket, chunkPacketInfo);
-+            });
++            // Plugins?
++            MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
 +            return;
 +        }
 +
 +        LevelChunk chunk = chunkPacketInfo.getChunk();
 +        int x = chunk.getPos().x;
 +        int z = chunk.getPos().z;
-+        ServerLevel world = chunk.level;
-+        ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(
-+            (LevelChunk) world.getChunkIfLoadedImmediately(x - 1, z),
-+            (LevelChunk) world.getChunkIfLoadedImmediately(x + 1, z),
-+            (LevelChunk) world.getChunkIfLoadedImmediately(x, z - 1),
-+            (LevelChunk) world.getChunkIfLoadedImmediately(x, z + 1));
-+
-+        executor.execute((ChunkPacketInfoAntiXray) chunkPacketInfo);
++        Level level = chunk.level;
++        ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(level.getChunkIfLoaded(x - 1, z), level.getChunkIfLoaded(x + 1, z), level.getChunkIfLoaded(x, z - 1), level.getChunkIfLoaded(x, z + 1));
++        executor.execute((Runnable) chunkPacketInfo);
 +    }
 +
 +    // Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay (even without ThreadLocal)
 +    // If an ExecutorService with multiple threads is used, ThreadLocal must be used here
-+    private final ThreadLocal<int[]> predefinedBlockDataBits = ThreadLocal.withInitial(() -> new int[getPredefinedBlockDataFullLength()]);
++    private final ThreadLocal<int[]> presetBlockStateBits = ThreadLocal.withInitial(() -> new int[getPresetBlockStatesFullLength()]);
 +    private static final ThreadLocal<boolean[]> solid = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
 +    private static final ThreadLocal<boolean[]> obfuscate = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
 +    // These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate
@@ -330,27 +454,30 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private static final ThreadLocal<boolean[][]> nextNext = ThreadLocal.withInitial(() -> new boolean[16][16]);
 +
 +    public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
-+        int[] predefinedBlockDataBits = this.predefinedBlockDataBits.get();
++        int[] presetBlockStateBits = this.presetBlockStateBits.get();
 +        boolean[] solid = ChunkPacketBlockControllerAntiXray.solid.get();
 +        boolean[] obfuscate = ChunkPacketBlockControllerAntiXray.obfuscate.get();
 +        boolean[][] current = ChunkPacketBlockControllerAntiXray.current.get();
 +        boolean[][] next = ChunkPacketBlockControllerAntiXray.next.get();
 +        boolean[][] nextNext = ChunkPacketBlockControllerAntiXray.nextNext.get();
-+        // dataBitsReader, dataBitsWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it
-+        DataBitsReader dataBitsReader = new DataBitsReader();
-+        DataBitsWriter dataBitsWriter = new DataBitsWriter();
++        // bitStorageReader, bitStorageWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it
++        BitStorageReader bitStorageReader = new BitStorageReader();
++        BitStorageWriter bitStorageWriter = new BitStorageWriter();
 +        LevelChunkSection[] nearbyChunkSections = new LevelChunkSection[4];
++        LevelChunk chunk = chunkPacketInfoAntiXray.getChunk();
++        Level level = chunk.level;
++        int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSection(), chunk.getSectionsCount() - 1);
 +        boolean[] solidTemp = null;
 +        boolean[] obfuscateTemp = null;
-+        dataBitsReader.setDataBits(chunkPacketInfoAntiXray.getData());
-+        dataBitsWriter.setDataBits(chunkPacketInfoAntiXray.getData());
-+        int numberOfBlocks = predefinedBlockDataBits.length;
++        bitStorageReader.setBuffer(chunkPacketInfoAntiXray.getBuffer());
++        bitStorageWriter.setBuffer(chunkPacketInfoAntiXray.getBuffer());
++        int numberOfBlocks = presetBlockStateBits.length;
 +        // Keep the lambda expressions as simple as possible. They are used very frequently.
 +        IntSupplier random = numberOfBlocks == 1 ? (() -> 0) : new IntSupplier() {
 +            private int state;
 +
 +            {
-+                while ((state = ThreadLocalRandom.current().nextInt()) == 0);
++                while ((state = ThreadLocalRandom.current().nextInt()) == 0) ;
 +            }
 +
 +            @Override
@@ -365,33 +492,41 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        };
 +
 +        for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
-+            if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex) != null) {
-+                int[] predefinedBlockDataBitsTemp;
++            if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) != null) {
++                int[] presetBlockStateBitsTemp;
 +
-+                if (chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex) == LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE) {
-+                    predefinedBlockDataBitsTemp = engineMode == EngineMode.HIDE ? chunkPacketInfoAntiXray.getChunk().level.getWorld().getEnvironment() == Environment.NETHER ? predefinedBlockDataBitsNetherrackGlobal : chunkPacketInfoAntiXray.getChunk().level.getWorld().getEnvironment() == Environment.THE_END ? predefinedBlockDataBitsEndStoneGlobal : predefinedBlockDataBitsStoneGlobal : predefinedBlockDataBitsGlobal;
++                if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) == LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE) {
++                    if (engineMode == EngineMode.HIDE) {
++                        presetBlockStateBitsTemp = switch (level.getWorld().getEnvironment()) {
++                            case NETHER -> presetBlockStateBitsNetherrackGlobal;
++                            case THE_END -> presetBlockStateBitsEndStoneGlobal;
++                            default -> presetBlockStateBitsStoneGlobal;
++                        };
++                    } else {
++                        presetBlockStateBitsTemp = presetBlockStateBitsGlobal;
++                    }
 +                } else {
-+                    // If it's this.predefinedBlockData, use this.predefinedBlockDataFull instead
-+                    BlockState[] predefinedBlockDataFull = chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex) == predefinedBlockData ? this.predefinedBlockDataFull : chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex);
-+                    predefinedBlockDataBitsTemp = predefinedBlockDataBits;
++                    // If it's presetBlockStates, use this.presetBlockStatesFull instead
++                    BlockState[] presetBlockStatesFull = chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) == presetBlockStates ? this.presetBlockStatesFull : chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex);
++                    presetBlockStateBitsTemp = presetBlockStateBits;
 +
-+                    for (int i = 0; i < predefinedBlockDataBitsTemp.length; i++) {
-+                        predefinedBlockDataBitsTemp[i] = chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex).idFor(predefinedBlockDataFull[i]);
++                    for (int i = 0; i < presetBlockStateBitsTemp.length; i++) {
++                        presetBlockStateBitsTemp[i] = chunkPacketInfoAntiXray.getPalette(chunkSectionIndex).idFor(presetBlockStatesFull[i]);
 +                    }
 +                }
 +
-+                dataBitsWriter.setIndex(chunkPacketInfoAntiXray.getDataBitsIndex(chunkSectionIndex));
++                bitStorageWriter.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
 +
 +                // Check if the chunk section below was not obfuscated
-+                if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex - 1) == null) {
++                if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex - 1) == null) {
 +                    // If so, initialize some stuff
-+                    dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
-+                    dataBitsReader.setIndex(chunkPacketInfoAntiXray.getDataBitsIndex(chunkSectionIndex));
-+                    solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), solid, solidGlobal);
-+                    obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
++                    bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
++                    bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
++                    solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), solid, solidGlobal);
++                    obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
 +                    // Read the blocks of the upper layer of the chunk section below if it exists
 +                    LevelChunkSection belowChunkSection = null;
-+                    boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex - 1]) == LevelChunk.EMPTY_SECTION;
++                    boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunk.getSections()[chunkSectionIndex - 1]) == LevelChunk.EMPTY_SECTION;
 +
 +                    for (int z = 0; z < 16; z++) {
 +                        for (int x = 0; x < 16; x++) {
@@ -401,11 +536,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    }
 +
 +                    // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
-+                    dataBitsWriter.setBitsPerObject(0);
-+                    obfuscateLayer(-1, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random);
++                    bitStorageWriter.setBits(0);
++                    obfuscateLayer(-1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random);
 +                }
 +
-+                dataBitsWriter.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
++                bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
 +                nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? LevelChunk.EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
 +                nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? LevelChunk.EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
 +                nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? LevelChunk.EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
@@ -417,15 +552,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    current = next;
 +                    next = nextNext;
 +                    nextNext = temp;
-+                    obfuscateLayer(y, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, random);
++                    obfuscateLayer(y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
 +                }
 +
 +                // Check if the chunk section above doesn't need obfuscation
-+                if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex + 1) == null) {
++                if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex + 1) == null) {
 +                    // If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists
 +                    LevelChunkSection aboveChunkSection;
 +
-+                    if (chunkSectionIndex != worldSectionHeight && (aboveChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex + 1]) != LevelChunk.EMPTY_SECTION) {
++                    if (chunkSectionIndex != chunk.getSectionsCount() - 1 && (aboveChunkSection = chunk.getSections()[chunkSectionIndex + 1]) != LevelChunk.EMPTY_SECTION) {
 +                        boolean[][] temp = current;
 +                        current = next;
 +                        next = nextNext;
@@ -440,291 +575,290 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                        }
 +
 +                        // There is nothing to read anymore
-+                        dataBitsReader.setBitsPerObject(0);
++                        bitStorageReader.setBits(0);
 +                        solid[0] = true;
-+                        obfuscateLayer(15, dataBitsReader, dataBitsWriter, solid, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, random);
++                        obfuscateLayer(15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
 +                    }
 +                } else {
 +                    // If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section
-+                    dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex + 1));
-+                    dataBitsReader.setIndex(chunkPacketInfoAntiXray.getDataBitsIndex(chunkSectionIndex + 1));
-+                    solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), solid, solidGlobal);
-+                    obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
++                    bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex + 1));
++                    bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex + 1));
++                    solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), solid, solidGlobal);
++                    obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
 +                    boolean[][] temp = current;
 +                    current = next;
 +                    next = nextNext;
 +                    nextNext = temp;
-+                    obfuscateLayer(15, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, random);
++                    obfuscateLayer(15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
 +                }
 +
-+                dataBitsWriter.finish();
++                bitStorageWriter.flush();
 +            }
 +        }
 +
 +        chunkPacketInfoAntiXray.getChunkPacket().setReady(true);
 +    }
 +
-+    private void obfuscateLayer(int y, DataBitsReader dataBitsReader, DataBitsWriter dataBitsWriter, boolean[] solid, boolean[] obfuscate, int[] predefinedBlockDataBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, LevelChunkSection[] nearbyChunkSections, IntSupplier random) {
++    private void obfuscateLayer(int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, boolean[] obfuscate, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, LevelChunkSection[] nearbyChunkSections, IntSupplier random) {
 +        // First block of first line
-+        int dataBits = dataBitsReader.read();
++        int bits = bitStorageReader.read();
 +
-+        if (nextNext[0][0] = !solid[dataBits]) {
-+            dataBitsWriter.skip();
++        if (nextNext[0][0] = !solid[bits]) {
++            bitStorageWriter.skip();
 +            next[0][1] = true;
 +            next[1][0] = true;
 +        } else {
 +            if (nearbyChunkSections[2] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[2].getBlockState(0, y, 15))] || nearbyChunkSections[0] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[0].getBlockState(15, y, 0))] || current[0][0]) {
-+                dataBitsWriter.skip();
++                bitStorageWriter.skip();
 +            } else {
-+                dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +            }
 +        }
 +
-+        if (!obfuscate[dataBits]) {
++        if (!obfuscate[bits]) {
 +            next[0][0] = true;
 +        }
 +
 +        // First line
 +        for (int x = 1; x < 15; x++) {
-+            dataBits = dataBitsReader.read();
++            bits = bitStorageReader.read();
 +
-+            if (nextNext[0][x] = !solid[dataBits]) {
-+                dataBitsWriter.skip();
++            if (nextNext[0][x] = !solid[bits]) {
++                bitStorageWriter.skip();
 +                next[0][x - 1] = true;
 +                next[0][x + 1] = true;
 +                next[1][x] = true;
 +            } else {
 +                if (nearbyChunkSections[2] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[2].getBlockState(x, y, 15))] || current[0][x]) {
-+                    dataBitsWriter.skip();
++                    bitStorageWriter.skip();
 +                } else {
-+                    dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +                }
 +            }
 +
-+            if (!obfuscate[dataBits]) {
++            if (!obfuscate[bits]) {
 +                next[0][x] = true;
 +            }
 +        }
 +
 +        // Last block of first line
-+        dataBits = dataBitsReader.read();
++        bits = bitStorageReader.read();
 +
-+        if (nextNext[0][15] = !solid[dataBits]) {
-+            dataBitsWriter.skip();
++        if (nextNext[0][15] = !solid[bits]) {
++            bitStorageWriter.skip();
 +            next[0][14] = true;
 +            next[1][15] = true;
 +        } else {
 +            if (nearbyChunkSections[2] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[2].getBlockState(15, y, 15))] || nearbyChunkSections[1] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[1].getBlockState(0, y, 0))] || current[0][15]) {
-+                dataBitsWriter.skip();
++                bitStorageWriter.skip();
 +            } else {
-+                dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +            }
 +        }
 +
-+        if (!obfuscate[dataBits]) {
++        if (!obfuscate[bits]) {
 +            next[0][15] = true;
 +        }
 +
 +        // All inner lines
 +        for (int z = 1; z < 15; z++) {
 +            // First block
-+            dataBits = dataBitsReader.read();
++            bits = bitStorageReader.read();
 +
-+            if (nextNext[z][0] = !solid[dataBits]) {
-+                dataBitsWriter.skip();
++            if (nextNext[z][0] = !solid[bits]) {
++                bitStorageWriter.skip();
 +                next[z][1] = true;
 +                next[z - 1][0] = true;
 +                next[z + 1][0] = true;
 +            } else {
 +                if (nearbyChunkSections[0] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[0].getBlockState(15, y, z))] || current[z][0]) {
-+                    dataBitsWriter.skip();
++                    bitStorageWriter.skip();
 +                } else {
-+                    dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +                }
 +            }
 +
-+            if (!obfuscate[dataBits]) {
++            if (!obfuscate[bits]) {
 +                next[z][0] = true;
 +            }
 +
 +            // All inner blocks
 +            for (int x = 1; x < 15; x++) {
-+                dataBits = dataBitsReader.read();
++                bits = bitStorageReader.read();
 +
-+                if (nextNext[z][x] = !solid[dataBits]) {
-+                    dataBitsWriter.skip();
++                if (nextNext[z][x] = !solid[bits]) {
++                    bitStorageWriter.skip();
 +                    next[z][x - 1] = true;
 +                    next[z][x + 1] = true;
 +                    next[z - 1][x] = true;
 +                    next[z + 1][x] = true;
 +                } else {
 +                    if (current[z][x]) {
-+                        dataBitsWriter.skip();
++                        bitStorageWriter.skip();
 +                    } else {
-+                        dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                        bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +                    }
 +                }
 +
-+                if (!obfuscate[dataBits]) {
++                if (!obfuscate[bits]) {
 +                    next[z][x] = true;
 +                }
 +            }
 +
 +            // Last block
-+            dataBits = dataBitsReader.read();
++            bits = bitStorageReader.read();
 +
-+            if (nextNext[z][15] = !solid[dataBits]) {
-+                dataBitsWriter.skip();
++            if (nextNext[z][15] = !solid[bits]) {
++                bitStorageWriter.skip();
 +                next[z][14] = true;
 +                next[z - 1][15] = true;
 +                next[z + 1][15] = true;
 +            } else {
 +                if (nearbyChunkSections[1] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[1].getBlockState(0, y, z))] || current[z][15]) {
-+                    dataBitsWriter.skip();
++                    bitStorageWriter.skip();
 +                } else {
-+                    dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +                }
 +            }
 +
-+            if (!obfuscate[dataBits]) {
++            if (!obfuscate[bits]) {
 +                next[z][15] = true;
 +            }
 +        }
 +
 +        // First block of last line
-+        dataBits = dataBitsReader.read();
++        bits = bitStorageReader.read();
 +
-+        if (nextNext[15][0] = !solid[dataBits]) {
-+            dataBitsWriter.skip();
++        if (nextNext[15][0] = !solid[bits]) {
++            bitStorageWriter.skip();
 +            next[15][1] = true;
 +            next[14][0] = true;
 +        } else {
 +            if (nearbyChunkSections[3] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[3].getBlockState(0, y, 0))] || nearbyChunkSections[0] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[0].getBlockState(15, y, 15))] || current[15][0]) {
-+                dataBitsWriter.skip();
++                bitStorageWriter.skip();
 +            } else {
-+                dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +            }
 +        }
 +
-+        if (!obfuscate[dataBits]) {
++        if (!obfuscate[bits]) {
 +            next[15][0] = true;
 +        }
 +
 +        // Last line
 +        for (int x = 1; x < 15; x++) {
-+            dataBits = dataBitsReader.read();
++            bits = bitStorageReader.read();
 +
-+            if (nextNext[15][x] = !solid[dataBits]) {
-+                dataBitsWriter.skip();
++            if (nextNext[15][x] = !solid[bits]) {
++                bitStorageWriter.skip();
 +                next[15][x - 1] = true;
 +                next[15][x + 1] = true;
 +                next[14][x] = true;
 +            } else {
 +                if (nearbyChunkSections[3] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[3].getBlockState(x, y, 0))] || current[15][x]) {
-+                    dataBitsWriter.skip();
++                    bitStorageWriter.skip();
 +                } else {
-+                    dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +                }
 +            }
 +
-+            if (!obfuscate[dataBits]) {
++            if (!obfuscate[bits]) {
 +                next[15][x] = true;
 +            }
 +        }
 +
 +        // Last block of last line
-+        dataBits = dataBitsReader.read();
++        bits = bitStorageReader.read();
 +
-+        if (nextNext[15][15] = !solid[dataBits]) {
-+            dataBitsWriter.skip();
++        if (nextNext[15][15] = !solid[bits]) {
++            bitStorageWriter.skip();
 +            next[15][14] = true;
 +            next[14][15] = true;
 +        } else {
 +            if (nearbyChunkSections[3] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[3].getBlockState(15, y, 0))] || nearbyChunkSections[1] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[1].getBlockState(0, y, 15))] || current[15][15]) {
-+                dataBitsWriter.skip();
++                bitStorageWriter.skip();
 +            } else {
-+                dataBitsWriter.write(predefinedBlockDataBits[random.getAsInt()]);
++                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
 +            }
 +        }
 +
-+        if (!obfuscate[dataBits]) {
++        if (!obfuscate[bits]) {
 +            next[15][15] = true;
 +        }
 +    }
 +
-+    private boolean[] readDataPalette(Palette<BlockState> dataPalette, boolean[] temp, boolean[] global) {
-+        if (dataPalette == LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE) {
++    private boolean[] readPalette(Palette<BlockState> palette, boolean[] temp, boolean[] global) {
++        if (palette == LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE) {
 +            return global;
 +        }
 +
-+        BlockState blockData;
++        BlockState blockState;
 +
-+        for (int i = 0; (blockData = dataPalette.valueFor(i)) != null; i++) {
-+            temp[i] = global[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockData)];
++        for (int i = 0; (blockState = palette.valueFor(i)) != null; i++) {
++            temp[i] = global[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)];
 +        }
 +
 +        return temp;
 +    }
 +
 +    @Override
-+    public void onBlockChange(Level world, BlockPos blockPosition, BlockState newBlockData, BlockState oldBlockData, int flag) {
-+        if (oldBlockData != null && solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockData)] && !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockData)] && blockPosition.getY() <= maxBlockYUpdatePosition) {
-+            updateNearbyBlocks(world, blockPosition);
++    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
++        if (oldBlockState != null && solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) {
++            updateNearbyBlocks(level, blockPos);
 +        }
 +    }
 +
 +    @Override
-+    public void onPlayerLeftClickBlock(ServerPlayerGameMode playerInteractManager, BlockPos blockPosition, Direction enumDirection) {
-+        if (blockPosition.getY() <= maxBlockYUpdatePosition) {
-+            updateNearbyBlocks(playerInteractManager.level, blockPosition);
++    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight) {
++        if (blockPos.getY() <= maxBlockHeightUpdatePosition) {
++            updateNearbyBlocks(serverPlayerGameMode.level, blockPos);
 +        }
 +    }
 +
-+    private void updateNearbyBlocks(Level world, BlockPos blockPosition) {
++    private void updateNearbyBlocks(Level level, BlockPos blockPos) {
 +        if (updateRadius >= 2) {
-+            BlockPos temp = blockPosition.west();
-+            updateBlock(world, temp);
-+            updateBlock(world, temp.west());
-+            updateBlock(world, temp.below());
-+            updateBlock(world, temp.above());
-+            updateBlock(world, temp.north());
-+            updateBlock(world, temp.south());
-+            updateBlock(world, temp = blockPosition.east());
-+            updateBlock(world, temp.east());
-+            updateBlock(world, temp.below());
-+            updateBlock(world, temp.above());
-+            updateBlock(world, temp.north());
-+            updateBlock(world, temp.south());
-+            updateBlock(world, temp = blockPosition.below());
-+            updateBlock(world, temp.below());
-+            updateBlock(world, temp.north());
-+            updateBlock(world, temp.south());
-+            updateBlock(world, temp = blockPosition.above());
-+            updateBlock(world, temp.above());
-+            updateBlock(world, temp.north());
-+            updateBlock(world, temp.south());
-+            updateBlock(world, temp = blockPosition.north());
-+            updateBlock(world, temp.north());
-+            updateBlock(world, temp = blockPosition.south());
-+            updateBlock(world, temp.south());
++            BlockPos temp = blockPos.west();
++            updateBlock(level, temp);
++            updateBlock(level, temp.west());
++            updateBlock(level, temp.below());
++            updateBlock(level, temp.above());
++            updateBlock(level, temp.north());
++            updateBlock(level, temp.south());
++            updateBlock(level, temp = blockPos.east());
++            updateBlock(level, temp.east());
++            updateBlock(level, temp.below());
++            updateBlock(level, temp.above());
++            updateBlock(level, temp.north());
++            updateBlock(level, temp.south());
++            updateBlock(level, temp = blockPos.below());
++            updateBlock(level, temp.below());
++            updateBlock(level, temp.north());
++            updateBlock(level, temp.south());
++            updateBlock(level, temp = blockPos.above());
++            updateBlock(level, temp.above());
++            updateBlock(level, temp.north());
++            updateBlock(level, temp.south());
++            updateBlock(level, temp = blockPos.north());
++            updateBlock(level, temp.north());
++            updateBlock(level, temp = blockPos.south());
++            updateBlock(level, temp.south());
 +        } else if (updateRadius == 1) {
-+            updateBlock(world, blockPosition.west());
-+            updateBlock(world, blockPosition.east());
-+            updateBlock(world, blockPosition.below());
-+            updateBlock(world, blockPosition.above());
-+            updateBlock(world, blockPosition.north());
-+            updateBlock(world, blockPosition.south());
++            updateBlock(level, blockPos.west());
++            updateBlock(level, blockPos.east());
++            updateBlock(level, blockPos.below());
++            updateBlock(level, blockPos.above());
++            updateBlock(level, blockPos.north());
++            updateBlock(level, blockPos.south());
 +        } else {
 +            // Do nothing if updateRadius <= 0 (test mode)
 +        }
 +    }
 +
-+    private void updateBlock(Level world, BlockPos blockPosition) {
-+        BlockState blockData = world.getTypeIfLoaded(blockPosition);
++    private void updateBlock(Level level, BlockPos blockPos) {
++        BlockState blockState = level.getTypeIfLoaded(blockPos);
 +
-+        if (blockData != null && obfuscateGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockData)]) {
-+            // world.notify(blockPosition, blockData, blockData, 3);
-+            ((ServerLevel)world).getChunkSource().blockChanged(blockPosition); // We only need to re-send to client
++        if (blockState != null && obfuscateGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) {
++            ((ServerLevel) level).getChunkSource().blockChanged(blockPos);
 +        }
 +    }
 +
@@ -776,21 +910,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    private final ClientboundLevelChunkPacket chunkPacket;
 +    private final LevelChunk chunk;
-+    private byte[] data;
-+    private final int[] bitsPerObject;
-+    private final Object[] dataPalettes;
-+    private final int[] dataBitsIndexes;
-+    private final Object[][] predefinedObjects;
++    private final int[] bits;
++    private final Object[] palettes;
++    private final int[] indexes;
++    private final Object[][] presetValues;
++    private byte[] buffer;
 +
 +    public ChunkPacketInfo(ClientboundLevelChunkPacket chunkPacket, LevelChunk chunk) {
 +        this.chunkPacket = chunkPacket;
 +        this.chunk = chunk;
-+
 +        int sections = chunk.getSectionsCount();
-+        this.bitsPerObject = new int[sections];
-+        this.dataPalettes = new Object[sections];
-+        this.dataBitsIndexes = new int[sections];
-+        this.predefinedObjects = new Object[sections][];
++        bits = new int[sections];
++        palettes = new Object[sections];
++        indexes = new int[sections];
++        presetValues = new Object[sections][];
 +    }
 +
 +    public ClientboundLevelChunkPacket getChunkPacket() {
@@ -801,50 +934,50 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return chunk;
 +    }
 +
-+    public byte[] getData() {
-+        return data;
++    public byte[] getBuffer() {
++        return buffer;
 +    }
 +
-+    public void setData(byte[] data) {
-+        this.data = data;
++    public void setBuffer(byte[] buffer) {
++        this.buffer = buffer;
 +    }
 +
-+    public int getBitsPerObject(int chunkSectionIndex) {
-+        return bitsPerObject[chunkSectionIndex];
++    public int getBits(int chunkSectionIndex) {
++        return bits[chunkSectionIndex];
 +    }
 +
-+    public void setBitsPerObject(int chunkSectionIndex, int bitsPerObject) {
-+        this.bitsPerObject[chunkSectionIndex] = bitsPerObject;
++    public void setBits(int chunkSectionIndex, int bits) {
++        this.bits[chunkSectionIndex] = bits;
 +    }
 +
 +    @SuppressWarnings("unchecked")
-+    public Palette<T> getDataPalette(int chunkSectionIndex) {
-+        return (Palette<T>) dataPalettes[chunkSectionIndex];
++    public Palette<T> getPalette(int chunkSectionIndex) {
++        return (Palette<T>) palettes[chunkSectionIndex];
 +    }
 +
-+    public void setDataPalette(int chunkSectionIndex, Palette<T> dataPalette) {
-+        dataPalettes[chunkSectionIndex] = dataPalette;
++    public void setPalette(int chunkSectionIndex, Palette<T> palette) {
++        palettes[chunkSectionIndex] = palette;
 +    }
 +
-+    public int getDataBitsIndex(int chunkSectionIndex) {
-+        return dataBitsIndexes[chunkSectionIndex];
++    public int getIndex(int chunkSectionIndex) {
++        return indexes[chunkSectionIndex];
 +    }
 +
-+    public void setDataBitsIndex(int chunkSectionIndex, int dataBitsIndex) {
-+        dataBitsIndexes[chunkSectionIndex] = dataBitsIndex;
++    public void setIndex(int chunkSectionIndex, int index) {
++        indexes[chunkSectionIndex] = index;
 +    }
 +
 +    @SuppressWarnings("unchecked")
-+    public T[] getPredefinedObjects(int chunkSectionIndex) {
-+        return (T[]) predefinedObjects[chunkSectionIndex];
++    public T[] getPresetValues(int chunkSectionIndex) {
++        return (T[]) presetValues[chunkSectionIndex];
 +    }
 +
-+    public void setPredefinedObjects(int chunkSectionIndex, T[] predefinedObjects) {
-+        this.predefinedObjects[chunkSectionIndex] = predefinedObjects;
++    public void setPresetValues(int chunkSectionIndex, T[] presetValues) {
++        this.presetValues[chunkSectionIndex] = presetValues;
 +    }
 +
 +    public boolean isWritten(int chunkSectionIndex) {
-+        return bitsPerObject[chunkSectionIndex] != 0;
++        return bits[chunkSectionIndex] != 0;
 +    }
 +}
 diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
@@ -861,12 +994,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +public final class ChunkPacketInfoAntiXray extends ChunkPacketInfo<BlockState> implements Runnable {
 +
-+    private LevelChunk[] nearbyChunks;
 +    private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray;
++    private LevelChunk[] nearbyChunks;
 +
-+    public ChunkPacketInfoAntiXray(ClientboundLevelChunkPacket packetPlayOutMapChunk, LevelChunk chunk,
-+                                   ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
-+        super(packetPlayOutMapChunk, chunk);
++    public ChunkPacketInfoAntiXray(ClientboundLevelChunkPacket chunkPacket, LevelChunk chunk, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
++        super(chunkPacket, chunk);
 +        this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray;
 +    }
 +
@@ -883,148 +1015,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        chunkPacketBlockControllerAntiXray.obfuscate(this);
 +    }
 +}
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/DataBitsReader.java b/src/main/java/com/destroystokyo/paper/antixray/DataBitsReader.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/DataBitsReader.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+public final class DataBitsReader {
-+
-+    private byte[] dataBits;
-+    private int bitsPerObject;
-+    private int mask;
-+    private int longInDataBitsIndex;
-+    private int bitInLongIndex;
-+    private long current;
-+
-+    public void setDataBits(byte[] dataBits) {
-+        this.dataBits = dataBits;
-+    }
-+
-+    public void setBitsPerObject(int bitsPerObject) {
-+        this.bitsPerObject = bitsPerObject;
-+        mask = (1 << bitsPerObject) - 1;
-+    }
-+
-+    public void setIndex(int index) {
-+        this.longInDataBitsIndex = index;
-+        bitInLongIndex = 0;
-+        init();
-+    }
-+
-+    private void init() {
-+        if (dataBits.length > longInDataBitsIndex + 7) {
-+            current = ((((long) dataBits[longInDataBitsIndex]) << 56)
-+                    | (((long) dataBits[longInDataBitsIndex + 1] & 0xff) << 48)
-+                    | (((long) dataBits[longInDataBitsIndex + 2] & 0xff) << 40)
-+                    | (((long) dataBits[longInDataBitsIndex + 3] & 0xff) << 32)
-+                    | (((long) dataBits[longInDataBitsIndex + 4] & 0xff) << 24)
-+                    | (((long) dataBits[longInDataBitsIndex + 5] & 0xff) << 16)
-+                    | (((long) dataBits[longInDataBitsIndex + 6] & 0xff) << 8)
-+                    | (((long) dataBits[longInDataBitsIndex + 7] & 0xff)));
-+        }
-+    }
-+
-+    public int read() {
-+        if (bitInLongIndex + bitsPerObject > 64) {
-+            bitInLongIndex = 0;
-+            longInDataBitsIndex += 8;
-+            init();
-+        }
-+
-+        int value = (int) (current >>> bitInLongIndex) & mask;
-+        bitInLongIndex += bitsPerObject;
-+        return value;
-+    }
-+}
-diff --git a/src/main/java/com/destroystokyo/paper/antixray/DataBitsWriter.java b/src/main/java/com/destroystokyo/paper/antixray/DataBitsWriter.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/com/destroystokyo/paper/antixray/DataBitsWriter.java
-@@ -0,0 +0,0 @@
-+package com.destroystokyo.paper.antixray;
-+
-+public final class DataBitsWriter {
-+
-+    private byte[] dataBits;
-+    private int bitsPerObject;
-+    private long mask;
-+    private int longInDataBitsIndex;
-+    private int bitInLongIndex;
-+    private long current;
-+    private boolean dirty;
-+
-+    public void setDataBits(byte[] dataBits) {
-+        this.dataBits = dataBits;
-+    }
-+
-+    public void setBitsPerObject(int bitsPerObject) {
-+        this.bitsPerObject = bitsPerObject;
-+        mask = (1 << bitsPerObject) - 1;
-+    }
-+
-+    public void setIndex(int index) {
-+        this.longInDataBitsIndex = index;
-+        bitInLongIndex = 0;
-+        init();
-+    }
-+
-+    private void init() {
-+        if (dataBits.length > longInDataBitsIndex + 7) {
-+            current = ((((long) dataBits[longInDataBitsIndex]) << 56)
-+                    | (((long) dataBits[longInDataBitsIndex + 1] & 0xff) << 48)
-+                    | (((long) dataBits[longInDataBitsIndex + 2] & 0xff) << 40)
-+                    | (((long) dataBits[longInDataBitsIndex + 3] & 0xff) << 32)
-+                    | (((long) dataBits[longInDataBitsIndex + 4] & 0xff) << 24)
-+                    | (((long) dataBits[longInDataBitsIndex + 5] & 0xff) << 16)
-+                    | (((long) dataBits[longInDataBitsIndex + 6] & 0xff) << 8)
-+                    | (((long) dataBits[longInDataBitsIndex + 7] & 0xff)));
-+        }
-+
-+        dirty = false;
-+    }
-+
-+    public void finish() {
-+        if (dirty && dataBits.length > longInDataBitsIndex + 7) {
-+            dataBits[longInDataBitsIndex] = (byte) (current >> 56 & 0xff);
-+            dataBits[longInDataBitsIndex + 1] = (byte) (current >> 48 & 0xff);
-+            dataBits[longInDataBitsIndex + 2] = (byte) (current >> 40 & 0xff);
-+            dataBits[longInDataBitsIndex + 3] = (byte) (current >> 32 & 0xff);
-+            dataBits[longInDataBitsIndex + 4] = (byte) (current >> 24 & 0xff);
-+            dataBits[longInDataBitsIndex + 5] = (byte) (current >> 16 & 0xff);
-+            dataBits[longInDataBitsIndex + 6] = (byte) (current >> 8 & 0xff);
-+            dataBits[longInDataBitsIndex + 7] = (byte) (current & 0xff);
-+        }
-+    }
-+
-+    public void write(int value) {
-+        if (bitInLongIndex + bitsPerObject > 64) {
-+            finish();
-+            bitInLongIndex = 0;
-+            longInDataBitsIndex += 8;
-+            init();
-+        }
-+
-+        current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex;
-+        dirty = true;
-+        bitInLongIndex += bitsPerObject;
-+    }
-+
-+    public void skip() {
-+        bitInLongIndex += bitsPerObject;
-+
-+        if (bitInLongIndex > 64) {
-+            finish();
-+            bitInLongIndex = bitsPerObject;
-+            longInDataBitsIndex += 8;
-+            init();
-+        }
-+    }
-+}
 diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacket.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacket.java
@@ -1034,9 +1024,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      // Paper end
  
 -    public ClientboundLevelChunkPacket(LevelChunk chunk) {
-+    private volatile boolean ready; // Paper - Async-Anti-Xray - Ready flag for the network manager
++    // Paper start - Async-Anti-Xray - Ready flag for the connection
++    private volatile boolean ready;
 +
-+    // Paper start
++    @Override
++    public boolean isReady() {
++        return this.ready;
++    }
++
++    public void setReady(boolean ready) {
++        this.ready = ready;
++    }
++    // Paper end
++
++    // Paper start - Anti-Xray - Add chunk packet info
 +    @Deprecated public ClientboundLevelChunkPacket(LevelChunk chunk) { this(chunk, true); } // Notice for updates: Please make sure this constructor isn't used anywhere
 +    public ClientboundLevelChunkPacket(LevelChunk chunk, boolean modifyBlocks) {
 +        com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo = modifyBlocks ? chunk.level.chunkPacketBlockController.getChunkPacketInfo(this, chunk) : null;
@@ -1049,20 +1050,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          this.biomes = chunk.getBiomes().writeBiomes();
          this.buffer = new byte[this.calculateChunkSize(chunk)];
 -        this.availableSections = this.extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk);
++
 +        // Paper start - Anti-Xray - Add chunk packet info
 +        if (chunkPacketInfo != null) {
-+            chunkPacketInfo.setData(this.buffer);
++            chunkPacketInfo.setBuffer(this.buffer);
 +        }
++
 +        this.availableSections = this.extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo);
 +        // Paper end
          this.blockEntitiesTags = Lists.newArrayList();
          int totalTileEntities = 0; // Paper
  
 @@ -0,0 +0,0 @@ public class ClientboundLevelChunkPacket implements Packet<ClientGamePacketListe
-             if (blockEntity instanceof net.minecraft.world.level.block.entity.SkullBlockEntity) { net.minecraft.world.level.block.entity.SkullBlockEntity.sanitizeTileEntityUUID(compoundTag); } // Paper
              this.blockEntitiesTags.add(compoundTag);
          }
--
+ 
 +        chunk.level.chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks
      }
  
@@ -1088,22 +1090,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              }
          }
  
-@@ -0,0 +0,0 @@ public class ClientboundLevelChunkPacket implements Packet<ClientGamePacketListe
-     public int[] getBiomes() {
-         return this.biomes;
-     }
-+
-+    // Paper start - Async-Anti-Xray - Getter and Setter for the ready flag
-+    @Override
-+    public boolean isReady() {
-+        return this.ready;
-+    }
-+
-+    public void setReady(boolean ready) {
-+        this.ready = ready;
-+    }
-+    // Paper end
- }
 diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1126,7 +1112,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, ServerLevelData iworlddataserver, ResourceKey<net.minecraft.world.level.Level> resourcekey, DimensionType dimensionmanager, ChunkProgressListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<CustomSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
          // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error
 -        super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getProfiler, false, flag, i, gen, env);
-+        super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getProfiler, false, flag, i, gen, env, executor); // Paper - pass executor
++        super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getProfiler, false, flag, i, gen, env, executor); // Paper - Anti-Xray - Pass executor
          this.pvpMode = minecraftserver.isPvpAllowed();
          this.convertable = convertable_conversionsession;
          this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelPath.toFile());
@@ -1139,7 +1125,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      private static final Logger LOGGER = LogManager.getLogger();
 -    protected ServerLevel level;
-+    public ServerLevel level; // Paper - protected->public
++    public ServerLevel level; // Paper - Anti-Xray - protected -> public
      protected final ServerPlayer player;
      private GameType gameModeForPlayer;
      @Nullable
@@ -1148,7 +1134,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          }
 +
-+        this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, direction); // Paper - Anti-Xray
++        this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, action, direction, worldHeight); // Paper - Anti-Xray
      }
  
      public void destroyAndAck(BlockPos pos, ServerboundPlayerActionPacket.Action action, String reason) {
@@ -1169,7 +1155,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
 -    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, final DimensionType dimensionmanager, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env) {
-+    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, final DimensionType dimensionmanager, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper
++    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, final DimensionType dimensionmanager, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper - Anti-Xray - Pass executor
          this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
          this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName(), this.spigotConfig); // Paper
          this.generator = gen;
@@ -1187,7 +1173,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              // CraftBukkit end
  
              BlockState iblockdata1 = chunk.setType(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
-+            this.chunkPacketBlockController.onBlockChange(this, pos, state, iblockdata1, flags); // Paper - Anti-Xray
++            this.chunkPacketBlockController.onBlockChange(this, pos, state, iblockdata1, flags, maxUpdateDepth); // Paper - Anti-Xray
  
              if (iblockdata1 == null) {
                  // CraftBukkit start - remove blockstate if failed (or the same)
@@ -1200,17 +1186,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          LevelChunkSection[] levelChunkSections = this.getSections();
          if (levelChunkSections[yIndex] == LevelChunk.EMPTY_SECTION) {
 -            levelChunkSections[yIndex] = new LevelChunkSection(this.getSectionYFromSectionIndex(yIndex));
-+            levelChunkSections[yIndex] = new LevelChunkSection(this.getSectionYFromSectionIndex(yIndex), this, getServerLevel(), true);
++            levelChunkSections[yIndex] = new LevelChunkSection(this.getSectionYFromSectionIndex(yIndex), this, this.getLevel(), true); // Paper - Anti-Xray - Add parameters
          }
  
          return levelChunkSections[yIndex];
      }
  
-+    // Paper start
-+    default net.minecraft.server.level.ServerLevel getServerLevel() {
-+        return null;
-+    }
-+    // Paper end
++    net.minecraft.world.level.Level getLevel(); // Paper - Anti-Xray - Add parameters
 +
      Collection<Entry<Heightmap.Types, Heightmap>> getHeightmaps();
  
@@ -1224,7 +1206,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          public EmptyChunkBiomeContainer(Level world) {
 -            super(world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), world, EMPTY_BIOMES);
-+            super(net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), world, EMPTY_BIOMES); // Paper - world isnt ready yet for anti xray use here, use server singleton for registry
++            super(net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registry.BIOME_REGISTRY), world, EMPTY_BIOMES); // Paper - Anti-Xray - The world isnt ready yet, use server singleton for registry
          }
  
          @Override
@@ -1241,18 +1223,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.sections[j] = chunksection;
          }
  
-@@ -0,0 +0,0 @@ public class LevelChunk implements ChunkAccess {
-             return "Level ticker for " + s + "@" + this.getPos();
-         }
-     }
-+
-+    // Paper start
-+    @Override
-+    public net.minecraft.server.level.ServerLevel getServerLevel() {
-+        return level;
-+    }
-+    // Paper end
- }
 diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -1265,9 +1235,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        this(yOffset, (short)0, (short)0, (short)0);
 +    // Paper start - Anti-Xray - Add parameters
 +    @Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere
-+    public LevelChunkSection(int yOffset, ChunkAccess chunk, net.minecraft.server.level.ServerLevel world, boolean initializeBlocks) {
++    public LevelChunkSection(int yOffset, ChunkAccess chunk, net.minecraft.world.level.Level level, boolean initializeBlocks) {
++        this(yOffset, (short) 0, (short) 0, (short) 0, chunk, level, initializeBlocks);
 +        // Paper end
-+        this(yOffset, (short) 0, (short) 0, (short) 0, chunk, world, initializeBlocks);
      }
  
 -    public LevelChunkSection(int yOffset, short nonEmptyBlockCount, short randomTickableBlockCount, short nonEmptyFluidCount) {
@@ -1275,7 +1245,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    @Deprecated public LevelChunkSection(int yOffset, short nonEmptyBlockCount, short randomTickableBlockCount, short nonEmptyFluidCount) { // Notice for updates: Please make sure this constructor isn't used anywhere
 +        this(yOffset, nonEmptyBlockCount, randomTickableBlockCount, nonEmptyFluidCount, null, null, true);
 +    }
-+    public LevelChunkSection(int yOffset, short nonEmptyBlockCount, short randomTickableBlockCount, short nonEmptyFluidCount, ChunkAccess chunk, net.minecraft.server.level.ServerLevel world, boolean initializeBlocks) {
++    public LevelChunkSection(int yOffset, short nonEmptyBlockCount, short randomTickableBlockCount, short nonEmptyFluidCount, ChunkAccess chunk, net.minecraft.world.level.Level level, boolean initializeBlocks) {
 +        // Paper end
          this.bottomBlockY = getBottomBlockY(yOffset);
          this.nonEmptyBlockCount = nonEmptyBlockCount;
@@ -1283,7 +1253,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          this.tickingFluidCount = nonEmptyFluidCount;
 -        this.states = new PalettedContainer<>(GLOBAL_BLOCKSTATE_PALETTE, Block.BLOCK_STATE_REGISTRY, NbtUtils::readBlockState, NbtUtils::writeBlockState, Blocks.AIR.defaultBlockState());
 +        this.states = new PalettedContainer<>(GLOBAL_BLOCKSTATE_PALETTE, Block.BLOCK_STATE_REGISTRY, NbtUtils::readBlockState, NbtUtils::writeBlockState, Blocks.AIR.defaultBlockState(),
-+            world == null ? null : world.chunkPacketBlockController.getPredefinedBlockData(world, chunk, this, initializeBlocks), initializeBlocks); // Paper - Anti-Xray - Add predefined block data
++            level == null ? null : level.chunkPacketBlockController.getPresetBlockStates(level, chunk, this, initializeBlocks), initializeBlocks); // Paper - Anti-Xray - Add preset block states
      }
  
      public static int getBottomBlockY(int chunkPos) {
@@ -1292,7 +1262,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
 -    public void write(FriendlyByteBuf buf) {
-+    // Paper start
++    // Paper start - Anti-Xray - Add chunk packet info
 +    @Deprecated public void write(FriendlyByteBuf buf) { write(buf, null); } // Notice for updates: Please make sure this method isn't used anywhere
 +    public void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo<BlockState> chunkPacketInfo) {
 +        // Paper end
@@ -1310,7 +1280,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final Function<CompoundTag, T> reader;
      private final Function<T, CompoundTag> writer;
      private final T defaultValue;
-+    private final T[] predefinedObjects; // Paper - Anti-Xray - Add predefined objects
++    private final T[] presetValues; // Paper - Anti-Xray - Add preset values
      protected BitStorage storage;
      private Palette<T> palette;
      private int bits;
@@ -1319,87 +1289,95 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
 -    public PalettedContainer(Palette<T> fallbackPalette, IdMapper<T> idList, Function<CompoundTag, T> elementDeserializer, Function<T, CompoundTag> elementSerializer, T defaultElement) {
-+    // Paper start - Anti-Xray - Add predefined objects
++    // Paper start - Anti-Xray - Add preset values
 +    @Deprecated public PalettedContainer(Palette<T> fallbackPalette, IdMapper<T> idList, Function<CompoundTag, T> elementDeserializer, Function<T, CompoundTag> elementSerializer, T defaultElement) { // Notice for updates: Please make sure this constructor isn't used anywhere
 +        this(fallbackPalette, idList, elementDeserializer, elementSerializer, defaultElement, null, true);
 +    }
-+    public PalettedContainer(Palette<T> fallbackPalette, IdMapper<T> idList, Function<CompoundTag, T> elementDeserializer, Function<T, CompoundTag> elementSerializer, T defaultElement, T[] predefinedObjects, boolean initialize) {
++    public PalettedContainer(Palette<T> fallbackPalette, IdMapper<T> idList, Function<CompoundTag, T> elementDeserializer, Function<T, CompoundTag> elementSerializer, T defaultElement, T[] presetValues, boolean initialize) {
 +        // Paper end
          this.globalPalette = fallbackPalette;
          this.registry = idList;
          this.reader = elementDeserializer;
          this.writer = elementSerializer;
          this.defaultValue = defaultElement;
-         this.setBits(4);
-+        // Paper start - Anti-Xray - Add predefined objects
-+        this.predefinedObjects = predefinedObjects;
+-        this.setBits(4);
++        // Paper start - Anti-Xray - Add preset values
++        this.presetValues = presetValues;
 +
 +        if (initialize) {
-+            if (predefinedObjects == null) {
++            if (presetValues == null) {
 +                // Default
 +                this.setBits(4);
 +            } else {
-+                // MathHelper.d() is trailingBits(roundCeilPow2(n)), alternatively; (int)ceil(log2(n)); however it's trash, use numberOfLeadingZeros instead
-+                // Count the bits of the maximum array index to initialize a data palette with enough space from the beginning
-+                // The length of the array is used because air is also added to the data palette from the beginning
-+                // Start with at least 4
-+                int maxIndex = predefinedObjects.length >> 4;
-+                int bitCount = (32 - Integer.numberOfLeadingZeros(Math.max(16, maxIndex) - 1));
-+
-+                // Initialize with at least 15 free indixes
-+                this.setBits((1 << bitCount) - predefinedObjects.length < 16 ? bitCount + 1 : bitCount);
-+                this.addPredefinedObjects();
++                // Count the number of required bits
++                // Preset values:   presetValues.length - 1
++                // Air:                                 + 1
++                // Extra:                              + 15
++                // Air and extra correspond to the default behavior this.setBits(4)
++                this.setBits(32 - Integer.numberOfLeadingZeros(presetValues.length + 15));
++                this.addPresetValues();
 +            }
 +        }
 +        // Paper end
-     }
- 
-+    // Paper start - Anti-Xray - Add predefined objects
-+    private void addPredefinedObjects() {
-+        if (this.predefinedObjects != null && this.palette != this.globalPalette) {
-+            for (T predefinedObject : this.predefinedObjects) {
-+                this.palette.idFor(predefinedObject);
++    }
++
++    // Paper start - Anti-Xray - Add preset values
++    private void addPresetValues() {
++        if (this.presetValues != null && this.palette != this.globalPalette) {
++            for (T presetValue : this.presetValues) {
++                this.palette.idFor(presetValue);
 +            }
 +        }
-+    }
+     }
 +    // Paper end
-+
+ 
      private static int getIndex(int x, int y, int z) {
          return y << 8 | z << 4 | x;
-     }
 @@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T> {
+         BitStorage bitStorage = this.storage;
          Palette<T> palette = this.palette;
          this.setBits(newSize);
++        this.addPresetValues(); // Paper - Anti-Xray - Add preset values
  
-+        this.addPredefinedObjects(); // Paper - Anti-Xray - Add predefined objects
          for(int i = 0; i < bitStorage.getSize(); ++i) {
              T object = palette.valueFor(bitStorage.get(i));
-             if (object != null) {
 @@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T> {
  
+             this.palette.read(buf);
+             buf.readLongArray(this.storage.getRaw());
++            // Paper start - Anti-Xray - Add preset values
++            // If there are many preset values this may require several resize operations
++            // This can be avoided by calculating the required bits in advance, as it is done in #read(ListTag, long[])
++            // However, this method is only used by the client, so it does not matter
++            this.addPresetValues();
++            // Paper end
+         } finally {
+             this.release();
+         }
+ 
      }
  
 -    public void write(FriendlyByteBuf buf) {
 +    // Paper start - Anti-Xray - Add chunk packet info
-+    @Deprecated public void write(FriendlyByteBuf buf) {
-+        write(buf, null, 0);
-+    }
++    @Deprecated public void write(FriendlyByteBuf buf) { write(buf, null, 0); } // Notice for updates: Please make sure this method isn't used anywhere
 +    public void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int bottomBlockY) {
 +        // Paper end
          try {
              this.acquire();
              buf.writeByte(this.bits);
              this.palette.write(buf);
++
 +            // Paper start - Anti-Xray - Add chunk packet info
 +            if (chunkPacketInfo != null) {
 +                // Bottom block to 0 based chunk section index
-+                int section = (bottomBlockY >> 4) - chunkPacketInfo.getChunk().getMinSection();
-+                chunkPacketInfo.setBitsPerObject(section, this.bits);
-+                chunkPacketInfo.setDataPalette(section, this.palette);
-+                chunkPacketInfo.setDataBitsIndex(section, buf.writerIndex() + FriendlyByteBuf.getVarIntSize(this.storage.getRaw().length));
-+                chunkPacketInfo.setPredefinedObjects(section, this.predefinedObjects);
++                int chunkSectionIndex = (bottomBlockY >> 4) - chunkPacketInfo.getChunk().getMinSection();
++                chunkPacketInfo.setBits(chunkSectionIndex, this.bits);
++                chunkPacketInfo.setPalette(chunkSectionIndex, this.palette);
++                chunkPacketInfo.setIndex(chunkSectionIndex, buf.writerIndex() + FriendlyByteBuf.getVarIntSize(this.storage.getRaw().length));
++                chunkPacketInfo.setPresetValues(chunkSectionIndex, this.presetValues);
 +            }
 +            // Paper end
++
              buf.writeLongArray(this.storage.getRaw());
          } finally {
              this.release();
@@ -1409,14 +1387,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.acquire();
 -            int i = Math.max(4, Mth.ceillog2(paletteNbt.size()));
 -            if (i != this.bits) {
-+            // Paper - Anti-Xray - TODO: Should this.predefinedObjects.length just be added here (faster) or should the contents be compared to calculate the size (less RAM)?
-+            int i = Math.max(4, Mth.ceillog2(paletteNbt.size() + (this.predefinedObjects == null ? 0 : this.predefinedObjects.length))); // Paper - Anti-Xray - Calculate the size with predefined objects
++            // Paper - Anti-Xray - TODO: Should this.presetValues.length just be added here (faster) or should the contents be compared to calculate the size (less RAM)?
++            int i = Math.max(4, Mth.ceillog2(paletteNbt.size() + (this.presetValues == null ? 0 : this.presetValues.length))); // Paper - Anti-Xray - Calculate the size with preset values
 +            if (true || i != this.bits) { // Paper - Anti-Xray - Not initialized yet
                  this.setBits(i);
              }
  
              this.palette.read(paletteNbt);
-+            this.addPredefinedObjects(); // Paper - Anti-Xray - Add predefined objects
++            this.addPresetValues(); // Paper - Anti-Xray - Add preset values
              int j = data.length * 64 / 4096;
              if (this.palette == this.globalPalette) {
                  Palette<T> palette = new HashMapPalette<>(this.registry, i, this.dummyPaletteResize, this.reader, this.writer);
@@ -1425,28 +1403,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
 +++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
 @@ -0,0 +0,0 @@ public class ProtoChunk implements ChunkAccess {
-     private long inhabitedTime;
-     private final Map<GenerationStep.Carving, BitSet> carvingMasks = new Object2ObjectArrayMap<>();
-     private volatile boolean isLightCorrect;
--    final net.minecraft.world.level.Level level; // Paper - Add level
-+    final net.minecraft.server.level.ServerLevel level; // Paper - Add level
- 
-     // Paper start - add level
-     @Deprecated public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) { this(pos, upgradeData, world, null); }
-@@ -0,0 +0,0 @@ public class ProtoChunk implements ChunkAccess {
-         this.postProcessing = new ShortList[world.getSectionsCount()];
+     public int getHeight() {
+         return this.levelHeightAccessor.getHeight();
      }
- 
-+    // Paper start
++
++    // Paper start - Anti-Xray - Add parameters
 +    @Override
-+    public net.minecraft.server.level.ServerLevel getServerLevel() {
++    public net.minecraft.world.level.Level getLevel() {
 +        return level;
 +    }
 +    // Paper end
-+
-     // Paper start - If loaded util
-     @Override
-     public FluidState getFluidIfLoaded(BlockPos blockposition) {
+ }
 diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
@@ -1456,7 +1423,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
              if (nbttagcompound2.contains("Palette", 9) && nbttagcompound2.contains("BlockStates", 12)) {
 -                LevelChunkSection chunksection = new LevelChunkSection(b0);
-+                LevelChunkSection chunksection = new LevelChunkSection(b0, null, world, false); // Paper - Anti-Xray - Add parameters
++                LevelChunkSection chunksection = new LevelChunkSection(b0, null, world, false); // Paper - Anti-Xray - Add parameters and don't initialize because it's done in the line below internally
  
                  chunksection.getStates().read(nbttagcompound2.getList("Palette", 10), nbttagcompound2.getLongArray("BlockStates"));
                  chunksection.recalcBlockCounts();
@@ -1478,7 +1445,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  cs[i].getStates().write(data, "Palette", "BlockStates");
  
 -                PalettedContainer blockids = new PalettedContainer<>(LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE, net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, NbtUtils::readBlockState, NbtUtils::writeBlockState, Blocks.AIR.defaultBlockState()); // TODO: snapshot whole ChunkSection
-+                PalettedContainer blockids = new PalettedContainer<>(LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE, net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, NbtUtils::readBlockState, NbtUtils::writeBlockState, Blocks.AIR.defaultBlockState(), null, false); // TODO: snapshot whole ChunkSection // Paper - Anti-Xray - Add no predefined block data and don't initialize because it's done in the line below internally
++                PalettedContainer blockids = new PalettedContainer<>(LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE, net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, NbtUtils::readBlockState, NbtUtils::writeBlockState, Blocks.AIR.defaultBlockState(), null, false); // TODO: snapshot whole ChunkSection // Paper - Anti-Xray - Add no preset block states and don't initialize because it's done in the line below internally
                  blockids.read(data.getList("Palette", CraftMagicNumbers.NBT.TAG_COMPOUND), data.getLongArray("BlockStates"));
  
                  sectionBlockIDs[i] = blockids;
@@ -1490,11 +1457,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private final int maxHeight;
      private final LevelChunkSection[] sections;
      private Set<BlockPos> tiles;
-+    private World world; // Paper - Anti-Xray - Add world
++    private World world; // Paper - Anti-Xray - Add parameters
  
      public CraftChunkData(World world) {
          this(world.getMinHeight(), world.getMaxHeight());
-+        this.world = world; // Paper - Anti-Xray - Add world
++        this.world = world; // Paper - Anti-Xray - Add parameters
      }
  
      /* pp for tests */ CraftChunkData(int minHeight, int maxHeight) {
@@ -1503,7 +1470,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          LevelChunkSection section = this.sections[offset];
          if (create && section == null) {
 -            this.sections[offset] = section = new LevelChunkSection(offset + (this.minHeight >> 4));
-+            this.sections[offset] = section = new LevelChunkSection(offset + (this.minHeight >> 4), null, world instanceof org.bukkit.craftbukkit.CraftWorld ? ((org.bukkit.craftbukkit.CraftWorld) world).getHandle() : null, true); // Paper - Anti-Xray - Add parameters
++            this.sections[offset] = section = new LevelChunkSection(offset + (this.minHeight >> 4), null, this.world instanceof org.bukkit.craftbukkit.CraftWorld ? ((org.bukkit.craftbukkit.CraftWorld) this.world).getHandle() : null, true); // Paper - Anti-Xray - Add parameters
          }
          return section;
      }
diff --git a/patches/server/Synchronize-PalettedContainer-instead-of-ReentrantLo.patch b/patches/server/Synchronize-PalettedContainer-instead-of-ReentrantLo.patch
index b5588eae44..0741d92a5d 100644
--- a/patches/server/Synchronize-PalettedContainer-instead-of-ReentrantLo.patch
+++ b/patches/server/Synchronize-PalettedContainer-instead-of-ReentrantLo.patch
@@ -35,7 +35,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        //this.lock.release(); // Paper - disable this
      }
  
-     // Paper start - Anti-Xray - Add predefined objects
+     // Paper start - Anti-Xray - Add preset values
 @@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T> {
          return this.palette.idFor(objectAdded);
      }
@@ -64,9 +64,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.acquire();
              int i = buf.readByte();
 @@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T> {
-     @Deprecated public void write(FriendlyByteBuf buf) {
-         write(buf, null, 0);
-     }
+ 
+     // Paper start - Anti-Xray - Add chunk packet info
+     @Deprecated public void write(FriendlyByteBuf buf) { write(buf, null, 0); } // Notice for updates: Please make sure this method isn't used anywhere
 -    public void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int bottomBlockY) {
 +    public synchronized void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int bottomBlockY) { // Paper - synchronize
          // Paper end
@@ -80,7 +80,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public synchronized void read(ListTag paletteNbt, long[] data) { // Paper - synchronize
          try {
              this.acquire();
-             // Paper - Anti-Xray - TODO: Should this.predefinedObjects.length just be added here (faster) or should the contents be compared to calculate the size (less RAM)?
+             // Paper - Anti-Xray - TODO: Should this.presetValues.length just be added here (faster) or should the contents be compared to calculate the size (less RAM)?
 @@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T> {
  
      }