From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: stonar96 <minecraft.stonar96@gmail.com>
Date: Thu, 25 Nov 2021 13:27:51 +0100
Subject: [PATCH] Anti-Xray


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..e448c26327b5f6189c3c52e698cff66c8f9ad81a
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java
@@ -0,0 +1,51 @@
+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..e4540ea278f2dc871cb6a3cb8897559bfd65e134
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java
@@ -0,0 +1,79 @@
+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..bd86dc2ad2f87969da4add06de2a629f69d4b5de
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
@@ -0,0 +1,45 @@
+package com.destroystokyo.paper.antixray;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ServerPlayerGameMode;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.LevelChunk;
+
+public class ChunkPacketBlockController {
+
+    public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController();
+
+    protected ChunkPacketBlockController() {
+
+    }
+
+    public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int bottomBlockY) {
+        return null;
+    }
+
+    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
+        return false;
+    }
+
+    public ChunkPacketInfo<BlockState> getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
+        return null;
+    }
+
+    public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo<BlockState> chunkPacketInfo) {
+        chunkPacket.setReady(true);
+    }
+
+    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
+
+    }
+
+    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
+
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f3670b2bdb8b1b252e9f074a6af56a018a8c465
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
@@ -0,0 +1,636 @@
+package com.destroystokyo.paper.antixray;
+
+import io.papermc.paper.configuration.WorldConfiguration;
+import io.papermc.paper.configuration.type.EngineMode;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ServerPlayerGameMode;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.biome.Biomes;
+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.*;
+import org.bukkit.Bukkit;
+
+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 static final Palette<BlockState> GLOBAL_BLOCKSTATE_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
+    private static final LevelChunkSection EMPTY_SECTION = null;
+    private final Executor executor;
+    private final EngineMode engineMode;
+    private final int maxBlockHeight;
+    private final int updateRadius;
+    private final boolean usePermission;
+    private final BlockState[] presetBlockStates;
+    private final BlockState[] presetBlockStatesFull;
+    private final BlockState[] presetBlockStatesStone;
+    private final BlockState[] presetBlockStatesDeepslate;
+    private final BlockState[] presetBlockStatesNetherrack;
+    private final BlockState[] presetBlockStatesEndStone;
+    private final int[] presetBlockStateBitsGlobal;
+    private final int[] presetBlockStateBitsStoneGlobal;
+    private final int[] presetBlockStateBitsDeepslateGlobal;
+    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 = {EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION};
+    private final int maxBlockHeightUpdatePosition;
+
+    public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) {
+        this.executor = executor;
+        WorldConfiguration.Anticheat.AntiXray paperWorldConfig = level.paperConfig().anticheat.antiXray;
+        engineMode = paperWorldConfig.engineMode;
+        maxBlockHeight = paperWorldConfig.maxBlockHeight >> 4 << 4;
+        updateRadius = paperWorldConfig.updateRadius;
+        usePermission = paperWorldConfig.usePermission;
+        List<String> toObfuscate;
+
+        if (engineMode == EngineMode.HIDE) {
+            toObfuscate = paperWorldConfig.hiddenBlocks;
+            presetBlockStates = null;
+            presetBlockStatesFull = null;
+            presetBlockStatesStone = new BlockState[]{Blocks.STONE.defaultBlockState()};
+            presetBlockStatesDeepslate = new BlockState[]{Blocks.DEEPSLATE.defaultBlockState()};
+            presetBlockStatesNetherrack = new BlockState[]{Blocks.NETHERRACK.defaultBlockState()};
+            presetBlockStatesEndStone = new BlockState[]{Blocks.END_STONE.defaultBlockState()};
+            presetBlockStateBitsGlobal = null;
+            presetBlockStateBitsStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())};
+            presetBlockStateBitsDeepslateGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.DEEPSLATE.defaultBlockState())};
+            presetBlockStateBitsNetherrackGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())};
+            presetBlockStateBitsEndStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())};
+        } else {
+            toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks);
+            List<BlockState> presetBlockStateList = new LinkedList<>();
+
+            for (String id : paperWorldConfig.hiddenBlocks) {
+                Block block = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(id)).orElse(null);
+
+                if (block != null && !(block instanceof EntityBlock)) {
+                    toObfuscate.add(id);
+                    presetBlockStateList.add(block.defaultBlockState());
+                }
+            }
+
+            // 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;
+            presetBlockStatesDeepslate = null;
+            presetBlockStatesNetherrack = null;
+            presetBlockStatesEndStone = null;
+            presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length];
+
+            for (int i = 0; i < presetBlockStatesFull.length; i++) {
+                presetBlockStateBitsGlobal[i] = GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]);
+            }
+
+            presetBlockStateBitsStoneGlobal = null;
+            presetBlockStateBitsDeepslateGlobal = null;
+            presetBlockStateBitsNetherrackGlobal = null;
+            presetBlockStateBitsEndStoneGlobal = null;
+        }
+
+        for (String id : toObfuscate) {
+            Block block = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(id)).orElse(null);
+
+            // 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 blockState : block.getStateDefinition().getPossibleStates()) {
+                    obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
+                }
+            }
+        }
+
+        EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS));
+        BlockPos zeroPos = new BlockPos(0, 0, 0);
+
+        for (int i = 0; i < solidGlobal.length; i++) {
+            BlockState blockState = GLOBAL_BLOCKSTATE_PALETTE.valueFor(i);
+
+            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 && blockState.getBlock() != Blocks.MANGROVE_ROOTS || 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.
+            }
+        }
+
+        maxBlockHeightUpdatePosition = maxBlockHeight + updateRadius - 1;
+    }
+
+    private int getPresetBlockStatesFullLength() {
+        return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length;
+    }
+
+    @Override
+    public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int bottomBlockY) {
+        // Return the block states to be added to the paletted containers so that they can be used for obfuscation
+        if (bottomBlockY < maxBlockHeight) {
+            if (engineMode == EngineMode.HIDE) {
+                return switch (level.getWorld().getEnvironment()) {
+                    case NETHER -> presetBlockStatesNetherrack;
+                    case THE_END -> presetBlockStatesEndStone;
+                    default -> bottomBlockY < 0 ? presetBlockStatesDeepslate : presetBlockStatesStone;
+                };
+            }
+
+            return presetBlockStates;
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
+        return !usePermission || !player.getBukkitEntity().hasPermission("paper.antixray.bypass");
+    }
+
+    @Override
+    public ChunkPacketInfoAntiXray getChunkPacketInfo(ClientboundLevelChunkWithLightPacket 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
+        return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this);
+    }
+
+    @Override
+    public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo<BlockState> chunkPacketInfo) {
+        if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) {
+            chunkPacket.setReady(true);
+            return;
+        }
+
+        if (!Bukkit.isPrimaryThread()) {
+            // Plugins?
+            MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
+            return;
+        }
+
+        LevelChunk chunk = chunkPacketInfo.getChunk();
+        int x = chunk.getPos().x;
+        int z = chunk.getPos().z;
+        Level level = chunk.getLevel();
+        ((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[]> 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
+    private static final ThreadLocal<boolean[][]> CURRENT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+    private static final ThreadLocal<boolean[][]> NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+    private static final ThreadLocal<boolean[][]> NEXT_NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+
+    public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
+        int[] presetBlockStateBits = this.presetBlockStateBits.get();
+        boolean[] solid = SOLID.get();
+        boolean[] obfuscate = OBFUSCATE.get();
+        boolean[][] current = CURRENT.get();
+        boolean[][] next = NEXT.get();
+        boolean[][] nextNext = NEXT_NEXT.get();
+        // 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.getLevel();
+        int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSection(), chunk.getSectionsCount()) - 1;
+        boolean[] solidTemp = null;
+        boolean[] obfuscateTemp = null;
+        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) ;
+            }
+
+            @Override
+            public int getAsInt() {
+                // https://en.wikipedia.org/wiki/Xorshift
+                state ^= state << 13;
+                state ^= state >>> 17;
+                state ^= state << 5;
+                // https://www.pcg-random.org/posts/bounded-rands.html
+                return (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32);
+            }
+        };
+
+        for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
+            if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) != null) {
+                int[] presetBlockStateBitsTemp;
+
+                if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) instanceof GlobalPalette) {
+                    if (engineMode == EngineMode.HIDE) {
+                        presetBlockStateBitsTemp = switch (level.getWorld().getEnvironment()) {
+                            case NETHER -> presetBlockStateBitsNetherrackGlobal;
+                            case THE_END -> presetBlockStateBitsEndStoneGlobal;
+                            default -> chunkSectionIndex + chunk.getMinSection() < 0 ? presetBlockStateBitsDeepslateGlobal : presetBlockStateBitsStoneGlobal;
+                        };
+                    } else {
+                        presetBlockStateBitsTemp = presetBlockStateBitsGlobal;
+                    }
+                } else {
+                    // 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 < presetBlockStateBitsTemp.length; i++) {
+                        // This is thread safe because we only request IDs that are guaranteed to be in the palette and are visible
+                        // For more details see the comments in the readPalette method
+                        presetBlockStateBitsTemp[i] = chunkPacketInfoAntiXray.getPalette(chunkSectionIndex).idFor(presetBlockStatesFull[i]);
+                    }
+                }
+
+                bitStorageWriter.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex));
+
+                // Check if the chunk section below was not obfuscated
+                if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex - 1) == null) {
+                    // If so, initialize some stuff
+                    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 = chunk.getSections()[chunkSectionIndex - 1]) == EMPTY_SECTION;
+
+                    for (int z = 0; z < 16; z++) {
+                        for (int x = 0; x < 16; x++) {
+                            current[z][x] = true;
+                            next[z][x] = skipFirstLayer || isTransparent(belowChunkSection, x, 15, z);
+                        }
+                    }
+
+                    // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
+                    bitStorageWriter.setBits(0);
+                    obfuscateLayer(-1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random);
+                }
+
+                bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
+                nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
+                nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
+                nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
+                nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex];
+
+                // Obfuscate all layers of the current chunk section except the upper one
+                for (int y = 0; y < 15; y++) {
+                    boolean[][] temp = current;
+                    current = next;
+                    next = nextNext;
+                    nextNext = temp;
+                    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.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 != chunk.getSectionsCount() - 1 && (aboveChunkSection = chunk.getSections()[chunkSectionIndex + 1]) != EMPTY_SECTION) {
+                        boolean[][] temp = current;
+                        current = next;
+                        next = nextNext;
+                        nextNext = temp;
+
+                        for (int z = 0; z < 16; z++) {
+                            for (int x = 0; x < 16; x++) {
+                                if (isTransparent(aboveChunkSection, x, 0, z)) {
+                                    current[z][x] = true;
+                                }
+                            }
+                        }
+
+                        // There is nothing to read anymore
+                        bitStorageReader.setBits(0);
+                        solid[0] = true;
+                        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
+                    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, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random);
+                }
+
+                bitStorageWriter.flush();
+            }
+        }
+
+        chunkPacketInfoAntiXray.getChunkPacket().setReady(true);
+    }
+
+    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 bits = bitStorageReader.read();
+
+        if (nextNext[0][0] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[0][1] = true;
+            next[1][0] = true;
+        } else {
+            if (current[0][0] || isTransparent(nearbyChunkSections[2], 0, y, 15) || isTransparent(nearbyChunkSections[0], 15, y, 0)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[0][0] = true;
+        }
+
+        // First line
+        for (int x = 1; x < 15; x++) {
+            bits = bitStorageReader.read();
+
+            if (nextNext[0][x] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[0][x - 1] = true;
+                next[0][x + 1] = true;
+                next[1][x] = true;
+            } else {
+                if (current[0][x] || isTransparent(nearbyChunkSections[2], x, y, 15)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[0][x] = true;
+            }
+        }
+
+        // Last block of first line
+        bits = bitStorageReader.read();
+
+        if (nextNext[0][15] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[0][14] = true;
+            next[1][15] = true;
+        } else {
+            if (current[0][15] || isTransparent(nearbyChunkSections[2], 15, y, 15) || isTransparent(nearbyChunkSections[1], 0, y, 0)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[0][15] = true;
+        }
+
+        // All inner lines
+        for (int z = 1; z < 15; z++) {
+            // First block
+            bits = bitStorageReader.read();
+
+            if (nextNext[z][0] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[z][1] = true;
+                next[z - 1][0] = true;
+                next[z + 1][0] = true;
+            } else {
+                if (current[z][0] || isTransparent(nearbyChunkSections[0], 15, y, z)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[z][0] = true;
+            }
+
+            // All inner blocks
+            for (int x = 1; x < 15; x++) {
+                bits = bitStorageReader.read();
+
+                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]) {
+                        bitStorageWriter.skip();
+                    } else {
+                        bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                    }
+                }
+
+                if (!obfuscate[bits]) {
+                    next[z][x] = true;
+                }
+            }
+
+            // Last block
+            bits = bitStorageReader.read();
+
+            if (nextNext[z][15] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[z][14] = true;
+                next[z - 1][15] = true;
+                next[z + 1][15] = true;
+            } else {
+                if (current[z][15] || isTransparent(nearbyChunkSections[1], 0, y, z)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[z][15] = true;
+            }
+        }
+
+        // First block of last line
+        bits = bitStorageReader.read();
+
+        if (nextNext[15][0] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[15][1] = true;
+            next[14][0] = true;
+        } else {
+            if (current[15][0] || isTransparent(nearbyChunkSections[3], 0, y, 0) || isTransparent(nearbyChunkSections[0], 15, y, 15)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[15][0] = true;
+        }
+
+        // Last line
+        for (int x = 1; x < 15; x++) {
+            bits = bitStorageReader.read();
+
+            if (nextNext[15][x] = !solid[bits]) {
+                bitStorageWriter.skip();
+                next[15][x - 1] = true;
+                next[15][x + 1] = true;
+                next[14][x] = true;
+            } else {
+                if (current[15][x] || isTransparent(nearbyChunkSections[3], x, y, 0)) {
+                    bitStorageWriter.skip();
+                } else {
+                    bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+                }
+            }
+
+            if (!obfuscate[bits]) {
+                next[15][x] = true;
+            }
+        }
+
+        // Last block of last line
+        bits = bitStorageReader.read();
+
+        if (nextNext[15][15] = !solid[bits]) {
+            bitStorageWriter.skip();
+            next[15][14] = true;
+            next[14][15] = true;
+        } else {
+            if (current[15][15] || isTransparent(nearbyChunkSections[3], 15, y, 0) || isTransparent(nearbyChunkSections[1], 0, y, 15)) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[15][15] = true;
+        }
+    }
+
+    private boolean isTransparent(LevelChunkSection chunkSection, int x, int y, int z) {
+        if (chunkSection == EMPTY_SECTION) {
+            return true;
+        }
+
+        try {
+            return !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(chunkSection.getBlockState(x, y, z))];
+        } catch (MissingPaletteEntryException e) {
+            // Race condition / visibility issue / no happens-before relationship
+            // We don't care and treat the block as transparent
+            // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur
+            return true;
+        }
+    }
+
+    private boolean[] readPalette(Palette<BlockState> palette, boolean[] temp, boolean[] global) {
+        if (palette instanceof GlobalPalette) {
+            return global;
+        }
+
+        try {
+            for (int i = 0; i < palette.getSize(); i++) {
+                temp[i] = global[GLOBAL_BLOCKSTATE_PALETTE.idFor(palette.valueFor(i))];
+            }
+        } catch (MissingPaletteEntryException e) {
+            // Race condition / visibility issue / no happens-before relationship
+            // We don't care because we at least see the state as it was when the chunk packet was created
+            // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur until we have all the data that we need here
+            // Since all palettes have a fixed initial maximum size and there is no internal restructuring and no values are removed from palettes, we are also guaranteed to see the data
+        }
+
+        return temp;
+    }
+
+    @Override
+    public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
+        if (oldBlockState != null && solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) {
+            updateNearbyBlocks(level, blockPos);
+        }
+    }
+
+    @Override
+    public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) {
+        if (blockPos.getY() <= maxBlockHeightUpdatePosition) {
+            updateNearbyBlocks(serverPlayerGameMode.level, blockPos);
+        }
+    }
+
+    private void updateNearbyBlocks(Level level, BlockPos blockPos) {
+        if (updateRadius >= 2) {
+            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(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 level, BlockPos blockPos) {
+        BlockState blockState = level.getBlockStateIfLoaded(blockPos);
+
+        if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) {
+            ((ServerLevel) level).getChunkSource().blockChanged(blockPos);
+        }
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..d98a3f5c54c67a673eb7dc456dd039cd78f9c34d
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java
@@ -0,0 +1,80 @@
+package com.destroystokyo.paper.antixray;
+
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.Palette;
+
+public class ChunkPacketInfo<T> {
+
+    private final ClientboundLevelChunkWithLightPacket chunkPacket;
+    private final LevelChunk chunk;
+    private final int[] bits;
+    private final Object[] palettes;
+    private final int[] indexes;
+    private final Object[][] presetValues;
+    private byte[] buffer;
+
+    public ChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) {
+        this.chunkPacket = chunkPacket;
+        this.chunk = chunk;
+        int sections = chunk.getSectionsCount();
+        bits = new int[sections];
+        palettes = new Object[sections];
+        indexes = new int[sections];
+        presetValues = new Object[sections][];
+    }
+
+    public ClientboundLevelChunkWithLightPacket getChunkPacket() {
+        return chunkPacket;
+    }
+
+    public LevelChunk getChunk() {
+        return chunk;
+    }
+
+    public byte[] getBuffer() {
+        return buffer;
+    }
+
+    public void setBuffer(byte[] buffer) {
+        this.buffer = buffer;
+    }
+
+    public int getBits(int chunkSectionIndex) {
+        return bits[chunkSectionIndex];
+    }
+
+    public void setBits(int chunkSectionIndex, int bits) {
+        this.bits[chunkSectionIndex] = bits;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Palette<T> getPalette(int chunkSectionIndex) {
+        return (Palette<T>) palettes[chunkSectionIndex];
+    }
+
+    public void setPalette(int chunkSectionIndex, Palette<T> palette) {
+        palettes[chunkSectionIndex] = palette;
+    }
+
+    public int getIndex(int chunkSectionIndex) {
+        return indexes[chunkSectionIndex];
+    }
+
+    public void setIndex(int chunkSectionIndex, int index) {
+        indexes[chunkSectionIndex] = index;
+    }
+
+    @SuppressWarnings("unchecked")
+    public T[] getPresetValues(int chunkSectionIndex) {
+        return (T[]) presetValues[chunkSectionIndex];
+    }
+
+    public void setPresetValues(int chunkSectionIndex, T[] presetValues) {
+        this.presetValues[chunkSectionIndex] = presetValues;
+    }
+
+    public boolean isWritten(int chunkSectionIndex) {
+        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
new file mode 100644
index 0000000000000000000000000000000000000000..80a2dfb266ae1221680a7b24fee2f7e2a8330b7d
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
@@ -0,0 +1,29 @@
+package com.destroystokyo.paper.antixray;
+
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.LevelChunk;
+
+public final class ChunkPacketInfoAntiXray extends ChunkPacketInfo<BlockState> implements Runnable {
+
+    private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray;
+    private LevelChunk[] nearbyChunks;
+
+    public ChunkPacketInfoAntiXray(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
+        super(chunkPacket, chunk);
+        this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray;
+    }
+
+    public LevelChunk[] getNearbyChunks() {
+        return nearbyChunks;
+    }
+
+    public void setNearbyChunks(LevelChunk... nearbyChunks) {
+        this.nearbyChunks = nearbyChunks;
+    }
+
+    @Override
+    public void run() {
+        chunkPacketBlockControllerAntiXray.obfuscate(this);
+    }
+}
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
index 0ef3e9b472e35bd2572b04722781abf7d4a1094b..40a42a632540d497c1393b112731c41c6e448228 100644
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
@@ -33,7 +33,10 @@ public class ClientboundLevelChunkPacketData {
     }
     // Paper end
 
-    public ClientboundLevelChunkPacketData(LevelChunk chunk) {
+    // Paper start - Anti-Xray - Add chunk packet info
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); }
+    public ClientboundLevelChunkPacketData(LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo) {
+        // Paper end
         this.heightmaps = new CompoundTag();
 
         for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
@@ -43,7 +46,14 @@ public class ClientboundLevelChunkPacketData {
         }
 
         this.buffer = new byte[calculateChunkSize(chunk)];
-        extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk);
+
+        // Paper start - Anti-Xray - Add chunk packet info
+        if (chunkPacketInfo != null) {
+            chunkPacketInfo.setBuffer(this.buffer);
+        }
+
+        extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo);
+        // Paper end
         this.blockEntitiesData = Lists.newArrayList();
         int totalTileEntities = 0; // Paper
 
@@ -103,9 +113,12 @@ public class ClientboundLevelChunkPacketData {
         return byteBuf;
     }
 
-    public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) {
+    // Paper start - Anti-Xray - Add chunk packet info
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) { ClientboundLevelChunkPacketData.extractChunkData(buf, chunk, null); }
+    public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo) {
         for(LevelChunkSection levelChunkSection : chunk.getSections()) {
-            levelChunkSection.write(buf);
+            levelChunkSection.write(buf, chunkPacketInfo);
+            // Paper end
         }
 
     }
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
index 7825d6f0fdcfda6212cff8033ec55fb7db236154..000853110c7a89f2d0403a7a2737025a5ac28240 100644
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java
@@ -13,13 +13,30 @@ public class ClientboundLevelChunkWithLightPacket implements Packet<ClientGamePa
     private final int z;
     private final ClientboundLevelChunkPacketData chunkData;
     private final ClientboundLightUpdatePacketData lightData;
+    // Paper start - Async-Anti-Xray - Ready flag for the connection
+    private volatile boolean ready;
 
-    public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightProvider, @Nullable BitSet skyBits, @Nullable BitSet blockBits, boolean nonEdge) {
+    @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 @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightProvider, @Nullable BitSet skyBits, @Nullable BitSet blockBits, boolean nonEdge) { this(chunk, lightProvider, skyBits, blockBits, nonEdge, true); }
+    public ClientboundLevelChunkWithLightPacket(LevelChunk chunk, LevelLightEngine lightProvider, @Nullable BitSet skyBits, @Nullable BitSet blockBits, boolean nonEdge, boolean modifyBlocks) {
         ChunkPos chunkPos = chunk.getPos();
         this.x = chunkPos.x;
         this.z = chunkPos.z;
-        this.chunkData = new ClientboundLevelChunkPacketData(chunk);
+        com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo = modifyBlocks ? chunk.getLevel().chunkPacketBlockController.getChunkPacketInfo(this, chunk) : null;
+        this.chunkData = new ClientboundLevelChunkPacketData(chunk, chunkPacketInfo);
+        // Paper end
         this.lightData = new ClientboundLightUpdatePacketData(chunkPos, lightProvider, skyBits, blockBits, nonEdge);
+        chunk.getLevel().chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks
     }
 
     public ClientboundLevelChunkWithLightPacket(FriendlyByteBuf buf) {
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 4b05139db6628808128337dbf817712e339c17d0..8371aac0302c8f7c327e0665d6c44d6482519522 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -630,7 +630,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
 
     }
 
-    public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject<ClientboundLevelChunkWithLightPacket> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public
+    public void updateChunkTracking(ServerPlayer player, ChunkPos pos, MutableObject<java.util.Map<Object, ClientboundLevelChunkWithLightPacket>> packet, boolean oldWithinViewDistance, boolean newWithinViewDistance) { // Paper - public // Paper - Anti-Xray - Bypass
         if (player.level == this.level) {
             if (newWithinViewDistance && !oldWithinViewDistance) {
                 ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.toLong());
@@ -1126,18 +1126,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         for (Iterator iterator = this.getPlayers(chunkcoordintpair, false).iterator(); iterator.hasNext(); entityplayer.trackChunk(chunkcoordintpair, (Packet) mutableobject.getValue())) {
             entityplayer = (ServerPlayer) iterator.next();
             if (mutableobject.getValue() == null) {
-                mutableobject.setValue(new ClientboundLevelChunkWithLightPacket(chunk1, this.lightEngine, (BitSet) null, (BitSet) null, true));
+                mutableobject.setValue(new ClientboundLevelChunkWithLightPacket(chunk1, this.lightEngine, (BitSet) null, (BitSet) null, true, true)); // Paper - Anti-Xray
             }
         }
 
     }
 
-    private void playerLoadedChunk(ServerPlayer player, MutableObject<ClientboundLevelChunkWithLightPacket> cachedDataPacket, LevelChunk chunk) {
-        if (cachedDataPacket.getValue() == null) {
-            cachedDataPacket.setValue(new ClientboundLevelChunkWithLightPacket(chunk, this.lightEngine, (BitSet) null, (BitSet) null, true));
+    // Paper start - Anti-Xray - Bypass
+    private void playerLoadedChunk(ServerPlayer player, MutableObject<java.util.Map<Object, ClientboundLevelChunkWithLightPacket>> cachedDataPackets, LevelChunk chunk) {
+        if (cachedDataPackets.getValue() == null) {
+            cachedDataPackets.setValue(new java.util.HashMap<>());
         }
 
-        player.trackChunk(chunk.getPos(), (Packet) cachedDataPacket.getValue());
+        Boolean shouldModify = chunk.getLevel().chunkPacketBlockController.shouldModify(player, chunk);
+        player.trackChunk(chunk.getPos(), (Packet) cachedDataPackets.getValue().computeIfAbsent(shouldModify, (s) -> {
+            return new ClientboundLevelChunkWithLightPacket(chunk, this.lightEngine, (BitSet) null, (BitSet) null, true, (Boolean) s);
+        }));
+        // Paper end
         DebugPackets.sendPoiPacketsForChunk(this.level, chunk.getPos());
         List<Entity> list = Lists.newArrayList();
         List<Entity> list1 = Lists.newArrayList();
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 3b528a6adaa431ebdf11ce2ce8ea3c99f3b1dbe3..76c388347ebbff2d50a975b40dbe93cc2760f6bb 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -438,7 +438,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
     public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
         // Holder holder = worlddimension.type(); // CraftBukkit - decompile error
         // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error
-        super(iworlddataserver, resourcekey, worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig))); // Paper
+        super(iworlddataserver, resourcekey, worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig)), executor); // Paper - Async-Anti-Xray - Pass executor
         this.pvpMode = minecraftserver.isPvpAllowed();
         this.convertable = convertable_conversionsession;
         this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile());
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
index 514c045883060e4a22f748176091d3b236c2a7fd..aee5144bdc5bd9f7b07ce3b72331bcfd42663ec9 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
@@ -49,7 +49,7 @@ import org.bukkit.event.player.PlayerInteractEvent;
 public class ServerPlayerGameMode {
 
     private static final Logger LOGGER = LogUtils.getLogger();
-    protected ServerLevel level;
+    public ServerLevel level; // Paper - Anti-Xray - protected -> public
     protected final ServerPlayer player;
     private GameType gameModeForPlayer;
     @Nullable
@@ -318,6 +318,8 @@ public class ServerPlayerGameMode {
             }
 
         }
+
+        this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, action, direction, worldHeight, sequence); // Paper - Anti-Xray
     }
 
     public void destroyAndAck(BlockPos pos, int sequence, String reason) {
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index 6f5fa7a2db798419a60206bc714e72b49b6a79e8..63036914ff4f7b52bb1880cc4514ed4da11ff163 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -172,6 +172,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
     }
     // Paper end
 
+    public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray
     public final co.aikar.timings.WorldTimingsHandler timings; // Paper
     public static BlockPos lastPhysicsProblem; // Spigot
     private org.spigotmc.TickLimiter entityLimiter;
@@ -190,7 +191,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
 
     public abstract ResourceKey<LevelStem> getTypeKey();
 
-    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator) { // Paper
+    protected Level(WritableLevelData worlddatamutable, ResourceKey<Level> resourcekey, Holder<DimensionType> holder, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - Async-Anti-Xray - Pass executor
         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
         this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper
         this.generator = gen;
@@ -274,6 +275,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
         this.keepSpawnInMemory = this.paperConfig().spawn.keepSpawnLoaded; // Paper
         this.entityLimiter = new org.spigotmc.TickLimiter(spigotConfig.entityMaxTickTime);
         this.tileLimiter = new org.spigotmc.TickLimiter(spigotConfig.tileMaxTickTime);
+        this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
     }
 
     // Paper start
@@ -454,6 +456,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
             // CraftBukkit end
 
             BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
+            this.chunkPacketBlockController.onBlockChange(this, pos, state, iblockdata1, flags, maxUpdateDepth); // Paper - Anti-Xray
 
             if (iblockdata1 == null) {
                 // CraftBukkit start - remove blockstate if failed (or the same)
diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
index 508c2fff8d8e0c6f37b6c4e3b72ba772c2ab2ee5..e254b2d04e4fc1dc76c26f61ea38aeb27755143f 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
@@ -140,17 +140,19 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
             }
         }
 
-        ChunkAccess.replaceMissingSections(heightLimitView, biome, this.sections);
+        ChunkAccess.replaceMissingSections(heightLimitView, biome, this.sections, pos); // Paper - Anti-Xray - Add parameters
         // CraftBukkit start
         this.biomeRegistry = biome;
     }
     public final Registry<Biome> biomeRegistry;
     // CraftBukkit end
 
-    private static void replaceMissingSections(LevelHeightAccessor world, Registry<Biome> biome, LevelChunkSection[] sectionArray) {
+    // Paper start - Anti-Xray - Add parameters
+    private static void replaceMissingSections(LevelHeightAccessor world, Registry<Biome> biome, LevelChunkSection[] sectionArray, ChunkPos pos) {
         for (int i = 0; i < sectionArray.length; ++i) {
             if (sectionArray[i] == null) {
-                sectionArray[i] = new LevelChunkSection(world.getSectionYFromSectionIndex(i), biome);
+                sectionArray[i] = new LevelChunkSection(world.getSectionYFromSectionIndex(i), biome, pos, world instanceof net.minecraft.world.level.Level ? (net.minecraft.world.level.Level) world : null);
+                // Paper end
             }
         }
 
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
index b4c76e53eef8dc0efec98848352a243931ffc1f3..7b28537fda4187036b15ecc04f68ac1c9bf5f67f 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -93,7 +93,7 @@ public class LevelChunk extends ChunkAccess {
     }
 
     public LevelChunk(Level world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks<Block> blockTickScheduler, LevelChunkTicks<Fluid> fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) {
-        super(pos, upgradeData, world, world.registryAccess().registryOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData);
+        super(pos, upgradeData, world, net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData); // Paper - Anti-Xray - The world isn't ready yet, use server singleton for registry
         // Paper start - rewrite light engine
         this.setBlockNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world));
         this.setSkyNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world));
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 ae37e97e52557b48f129cc02eeea395378a48444..785fbcf9bafcdec1c5be213de3d8512690023415 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -36,10 +36,13 @@ public class LevelChunkSection {
         this.recalcBlockCounts();
     }
 
-    public LevelChunkSection(int chunkPos, Registry<Biome> biomeRegistry) {
+    // Paper start - Anti-Xray - Add parameters
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public LevelChunkSection(int chunkPos, Registry<Biome> biomeRegistry) { this(chunkPos, biomeRegistry, null, null); }
+    public LevelChunkSection(int chunkPos, Registry<Biome> biomeRegistry, net.minecraft.world.level.ChunkPos pos, net.minecraft.world.level.Level level) {
+        // Paper end
         this.bottomBlockY = LevelChunkSection.getBottomBlockY(chunkPos);
-        this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
-        this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES);
+        this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, level == null || level.chunkPacketBlockController == null ? null : level.chunkPacketBlockController.getPresetBlockStates(level, pos, this.bottomBlockY())); // Paper - Anti-Xray - Add preset block states
+        this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes
     }
 
     public static int getBottomBlockY(int chunkPos) {
@@ -177,10 +180,13 @@ public class LevelChunkSection {
         this.biomes = datapaletteblock;
     }
 
-    public void write(FriendlyByteBuf buf) {
+    // Paper start - Anti-Xray - Add chunk packet info
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null); }
+    public void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo<BlockState> chunkPacketInfo) {
         buf.writeShort(this.nonEmptyBlockCount);
-        this.states.write(buf);
-        this.biomes.write(buf);
+        this.states.write(buf, chunkPacketInfo, this.bottomBlockY());
+        this.biomes.write(buf, null, this.bottomBlockY());
+        // Paper end
     }
 
     public int getSerializedSize() {
diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
index 4843bd864deba357e0a4b2fd844324218af9774f..02b7e3261f689b9d30b87661db23425f741b0fec 100644
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
@@ -29,6 +29,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
         return 0;
     };
     public final IdMap<T> registry;
+    private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values
     private volatile PalettedContainer.Data<T> data;
     private final PalettedContainer.Strategy strategy;
     private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer");
@@ -41,14 +42,19 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
         this.threadingDetector.checkAndUnlock();
     }
 
-    public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) {
-        PalettedContainerRO.Unpacker<T, PalettedContainer<T>> unpacker = PalettedContainer::unpack;
+    // Paper start - Anti-Xray - Add preset values
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { return PalettedContainer.codecRW(idList, entryCodec, paletteProvider, defaultValue, null); }
+    public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) {
+        PalettedContainerRO.Unpacker<T, PalettedContainer<T>> unpacker = (idListx, paletteProviderx, serialized) -> {
+            return unpack(idListx, paletteProviderx, serialized, defaultValue, presetValues);
+        };
+        // Paper end
         return codec(idList, entryCodec, paletteProvider, defaultValue, unpacker);
     }
 
     public static <T> Codec<PalettedContainerRO<T>> codecRO(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) {
         PalettedContainerRO.Unpacker<T, PalettedContainerRO<T>> unpacker = (idListx, paletteProviderx, serialized) -> {
-            return unpack(idListx, paletteProviderx, serialized).map((result) -> {
+            return unpack(idListx, paletteProviderx, serialized, defaultValue, null).map((result) -> { // Paper - Anti-Xray - Add preset values
                 return result;
             });
         };
@@ -65,19 +71,52 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
         });
     }
 
-    public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries) {
+    // Paper start - Anti-Xray - Add preset values
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); }
+    public PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration<T> dataProvider, BitStorage storage, List<T> paletteEntries, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) {
+        this.presetValues = presetValues;
         this.registry = idList;
         this.strategy = paletteProvider;
         this.data = new PalettedContainer.Data<>(dataProvider, storage, dataProvider.factory().create(dataProvider.bits(), idList, this, paletteEntries));
+
+        if (presetValues != null && (dataProvider.factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY ? this.data.palette.valueFor(0) != defaultValue : dataProvider.factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY)) {
+            // In 1.18 Mojang unfortunately removed code that already handled possible resize operations on read from disk for us
+            // We readd this here but in a smarter way than it was before
+            int maxSize = 1 << dataProvider.bits();
+
+            for (T presetValue : presetValues) {
+                if (this.data.palette.getSize() >= maxSize) {
+                    java.util.Set<T> allValues = new java.util.HashSet<>(paletteEntries);
+                    allValues.addAll(Arrays.asList(presetValues));
+                    int newBits = Mth.ceillog2(allValues.size());
+
+                    if (newBits > dataProvider.bits()) {
+                        this.onResize(newBits, null);
+                    }
+
+                    break;
+                }
+
+                this.data.palette.idFor(presetValue);
+            }
+        }
+        // Paper end
     }
 
-    private PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data<T> data) {
+    // Paper start - Anti-Xray - Add preset values
+    private PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data<T> data, T @org.jetbrains.annotations.Nullable [] presetValues) {
+        this.presetValues = presetValues;
+        // Paper end
         this.registry = idList;
         this.strategy = paletteProvider;
         this.data = data;
     }
 
-    public PalettedContainer(IdMap<T> idList, T object, PalettedContainer.Strategy paletteProvider) {
+    // Paper start - Anti-Xray - Add preset values
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap<T> idList, T object, PalettedContainer.Strategy paletteProvider) { this(idList, object, paletteProvider, null); }
+    public PalettedContainer(IdMap<T> idList, T object, PalettedContainer.Strategy paletteProvider, T @org.jetbrains.annotations.Nullable [] presetValues) {
+        this.presetValues = presetValues;
+        // Paper end
         this.strategy = paletteProvider;
         this.registry = idList;
         this.data = this.createOrReuseData((PalettedContainer.Data<T>)null, 0);
@@ -92,11 +131,33 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
     @Override
     public int onResize(int newBits, T object) {
         PalettedContainer.Data<T> data = this.data;
+
+        // Paper start - Anti-Xray - Add preset values
+        if (this.presetValues != null && object != null && data.configuration().factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY) {
+            int duplicates = 0;
+            List<T> presetValues = Arrays.asList(this.presetValues);
+            duplicates += presetValues.contains(object) ? 1 : 0;
+            duplicates += presetValues.contains(data.palette.valueFor(0)) ? 1 : 0;
+            newBits = Mth.ceillog2((1 << this.strategy.calculateBitsForSerialization(this.registry, 1 << newBits)) + presetValues.size() - duplicates);
+        }
+
         PalettedContainer.Data<T> data2 = this.createOrReuseData(data, newBits);
         data2.copyFrom(data.palette, data.storage);
         this.data = data2;
-        return data2.palette.idFor(object);
+        this.addPresetValues();
+        return object == null ? -1 : data2.palette.idFor(object);
+        // Paper end
+    }
+
+    // Paper start - Anti-Xray - Add preset values
+    private void addPresetValues() {
+        if (this.presetValues != null && this.data.configuration().factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY) {
+            for (T presetValue : this.presetValues) {
+                this.data.palette.idFor(presetValue);
+            }
+        }
     }
+    // Paper end
 
     public T getAndSet(int x, int y, int z, T value) {
         this.acquire();
@@ -166,25 +227,36 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
             data.palette.read(buf);
             buf.readLongArray(data.storage.getRaw());
             this.data = data;
+            this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server)
         } finally {
             this.release();
         }
 
     }
 
+    // Paper start - Anti-Xray - Add chunk packet info
+    @Override
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null, 0); }
     @Override
-    public void write(FriendlyByteBuf buf) {
+    public void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int bottomBlockY) {
         this.acquire();
 
         try {
-            this.data.write(buf);
+            this.data.write(buf, chunkPacketInfo, bottomBlockY);
+
+            if (chunkPacketInfo != null) {
+                // Bottom block to 0 based chunk section index
+                int chunkSectionIndex = (bottomBlockY >> 4) - chunkPacketInfo.getChunk().getMinSection();
+                chunkPacketInfo.setPresetValues(chunkSectionIndex, this.presetValues);
+            }
+            // Paper end
         } finally {
             this.release();
         }
 
     }
 
-    private static <T> DataResult<PalettedContainer<T>> unpack(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData<T> serialized) {
+    private static <T> DataResult<PalettedContainer<T>> unpack(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData<T> serialized, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) { // Paper - Anti-Xray - Add preset values
         List<T> list = serialized.paletteEntries();
         int i = paletteProvider.size();
         int j = paletteProvider.calculateBitsForSerialization(idList, list.size());
@@ -220,7 +292,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
             }
         }
 
-        return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list));
+        return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list, defaultValue, presetValues)); // Paper - Anti-Xray - Add preset values
     }
 
     @Override
@@ -280,12 +352,12 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
     }
 
     public PalettedContainer<T> copy() {
-        return new PalettedContainer<>(this.registry, this.strategy, this.data.copy());
+        return new PalettedContainer<>(this.registry, this.strategy, this.data.copy(), this.presetValues); // Paper - Anti-Xray - Add preset values
     }
 
     @Override
     public PalettedContainer<T> recreate() {
-        return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy);
+        return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy, this.presetValues); // Paper - Anti-Xray - Add preset values
     }
 
     @Override
@@ -329,9 +401,20 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
             return 1 + this.palette.getSerializedSize() + FriendlyByteBuf.getVarIntSize(this.storage.getSize()) + this.storage.getRaw().length * 8;
         }
 
-        public void write(FriendlyByteBuf buf) {
+        // Paper start - Anti-Xray - Add chunk packet info
+        public void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int bottomBlockY) {
             buf.writeByte(this.storage.getBits());
             this.palette.write(buf);
+
+            if (chunkPacketInfo != null) {
+                // Bottom block to 0 based chunk section index
+                int chunkSectionIndex = (bottomBlockY >> 4) - chunkPacketInfo.getChunk().getMinSection();
+                chunkPacketInfo.setBits(chunkSectionIndex, this.configuration.bits());
+                chunkPacketInfo.setPalette(chunkSectionIndex, this.palette);
+                chunkPacketInfo.setIndex(chunkSectionIndex, buf.writerIndex() + FriendlyByteBuf.getVarIntSize(this.storage.getRaw().length));
+            }
+            // Paper end
+
             buf.writeLongArray(this.storage.getRaw());
         }
 
diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java
index 9a2bf744abd8916d492e901be889223591bac3fd..a27fce0f1af9776a713bf1b5277869ed5d3e0c8e 100644
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java
@@ -14,7 +14,10 @@ public interface PalettedContainerRO<T> {
 
     void getAll(Consumer<T> action);
 
-    void write(FriendlyByteBuf buf);
+    // Paper start - Anti-Xray - Add chunk packet info
+    @Deprecated @io.papermc.paper.annotation.DoNotUse void write(FriendlyByteBuf buf);
+    void write(FriendlyByteBuf buf, @javax.annotation.Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo<T> chunkPacketInfo, int bottomBlockY);
+    // Paper end
 
     int getSerializedSize();
 
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 b8f5ad1130b125f71a1feb1083120fb700b9bea1..f6d1eac46c619831e146c62a9c08d3305c63c7bc 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
@@ -71,7 +71,7 @@ import org.slf4j.Logger;
 
 public class ChunkSerializer {
 
-    public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState());
+    public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null); // Paper - Anti-Xray - Add preset block states
     private static final Logger LOGGER = LogUtils.getLogger();
     private static final String TAG_UPGRADE_DATA = "UpgradeData";
     private static final String BLOCK_TICKS_TAG = "block_ticks";
@@ -166,16 +166,20 @@ public class ChunkSerializer {
             if (k >= 0 && k < achunksection.length) {
                 Logger logger;
                 PalettedContainer datapaletteblock;
+                // Paper start - Anti-Xray - Add preset block states
+                BlockState[] presetBlockStates = world.chunkPacketBlockController.getPresetBlockStates(world, chunkPos, b0 << 4);
 
                 if (nbttagcompound1.contains("block_states", 10)) {
-                    dataresult = ChunkSerializer.BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, nbttagcompound1.getCompound("block_states")).promotePartial((s) -> {
+                    Codec<PalettedContainer<BlockState>> blockStateCodec = presetBlockStates == null ? ChunkSerializer.BLOCK_STATE_CODEC : PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), presetBlockStates);
+                    dataresult = blockStateCodec.parse(NbtOps.INSTANCE, nbttagcompound1.getCompound("block_states")).promotePartial((s) -> {
                         ChunkSerializer.logErrors(chunkPos, b0, s);
                     });
                     logger = ChunkSerializer.LOGGER;
                     Objects.requireNonNull(logger);
                     datapaletteblock = (PalettedContainer) ((DataResult<PalettedContainer<BlockState>>) dataresult).getOrThrow(false, logger::error); // CraftBukkit - decompile error
                 } else {
-                    datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
+                    datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, presetBlockStates);
+                    // Paper end
                 }
 
                 PalettedContainer object; // CraftBukkit - read/write
@@ -188,7 +192,7 @@ public class ChunkSerializer {
                     Objects.requireNonNull(logger);
                     object = ((DataResult<PalettedContainer<Holder<Biome>>>) dataresult).getOrThrow(false, logger::error); // CraftBukkit - decompile error
                 } else {
-                    object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES);
+                    object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes
                 }
 
                 LevelChunkSection chunksection = new LevelChunkSection(b0, datapaletteblock, (PalettedContainer) object); // CraftBukkit - read/write
@@ -446,7 +450,7 @@ public class ChunkSerializer {
 
     // CraftBukkit start - read/write
     private static Codec<PalettedContainer<Holder<Biome>>> makeBiomeCodecRW(Registry<Biome> iregistry) {
-        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getHolderOrThrow(Biomes.PLAINS));
+        return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getHolderOrThrow(Biomes.PLAINS), null); // Paper - Anti-Xray - Add preset biomes
     }
     // CraftBukkit end
 
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
index 6772b7e6ebded67f656dfb1af99e4d516aca67c5..1ac0778fba70ba9f09487c76e086b5a873c6c12a 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -55,7 +55,7 @@ public class CraftChunk implements Chunk {
     private final ServerLevel worldServer;
     private final int x;
     private final int z;
-    private static final PalettedContainer<net.minecraft.world.level.block.state.BlockState> emptyBlockIDs = new PalettedContainer<>(net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
+    private static final PalettedContainer<net.minecraft.world.level.block.state.BlockState> emptyBlockIDs = new PalettedContainer<>(net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null); // Paper - Anti-Xray - Add preset block states
     private static final byte[] emptyLight = new byte[2048];
 
     public CraftChunk(net.minecraft.world.level.chunk.LevelChunk chunk) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 6fc57df47db575f4f3d56784ac0bc8b496d1eba1..9b187eba01689ae640117bd638ffe9f16499d051 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -2259,7 +2259,7 @@ public final class CraftServer implements Server {
     public ChunkGenerator.ChunkData createChunkData(World world) {
         Validate.notNull(world, "World cannot be null");
         ServerLevel handle = ((CraftWorld) world).getHandle();
-        return new OldCraftChunkData(world.getMinHeight(), world.getMaxHeight(), handle.registryAccess().registryOrThrow(Registries.BIOME));
+        return new OldCraftChunkData(world.getMinHeight(), world.getMaxHeight(), handle.registryAccess().registryOrThrow(Registries.BIOME), world); // Paper - Anti-Xray - Add parameters
     }
 
     @Override
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 0c1c0fdfc6b1ab305b560d272efde4565922eac5..caacf6dbe6c4fd461624ea421d08a18222fc2a6e 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -409,11 +409,16 @@ public class CraftWorld extends CraftRegionAccessor implements World {
                 List<ServerPlayer> playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false);
                 if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader
 
-                ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null, true);
+                // Paper start - Anti-Xray - Bypass
+                Map<Object, ClientboundLevelChunkWithLightPacket> refreshPackets = new HashMap<>();
                 for (ServerPlayer player : playersInRange) {
                     if (player.connection == null) continue;
 
-                    player.connection.send(refreshPacket);
+                    Boolean shouldModify = chunk.getLevel().chunkPacketBlockController.shouldModify(player, chunk);
+                    player.connection.send(refreshPackets.computeIfAbsent(shouldModify, s -> { // Use connection to prevent creating firing event
+                        return new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null, true, (Boolean) s);
+                    }));
+                    // Paper end
                 }
         // Paper - rewrite player chunk loader
 
diff --git a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
index 960405935e395a31c0300773c41413801cf0d290..4a23d03757e1735b9ebb8c003adcc0374a7d672d 100644
--- a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
+++ b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
@@ -27,8 +27,13 @@ public final class OldCraftChunkData implements ChunkGenerator.ChunkData {
     private final Registry<net.minecraft.world.level.biome.Biome> biomes;
     private Set<BlockPos> tiles;
     private final Set<BlockPos> lights = new HashSet<>();
+    // Paper start - Anti-Xray - Add parameters
+    private final World world;
 
-    public OldCraftChunkData(int minHeight, int maxHeight, Registry<net.minecraft.world.level.biome.Biome> biomes) {
+    @Deprecated @io.papermc.paper.annotation.DoNotUse public OldCraftChunkData(int minHeight, int maxHeight, Registry<net.minecraft.world.level.biome.Biome> biomes) { this(minHeight, maxHeight, biomes, null); }
+    public OldCraftChunkData(int minHeight, int maxHeight, Registry<net.minecraft.world.level.biome.Biome> biomes, World world) {
+        this.world = world;
+        // Paper end
         this.minHeight = minHeight;
         this.maxHeight = maxHeight;
         this.biomes = biomes;
@@ -176,7 +181,7 @@ public final class OldCraftChunkData implements ChunkGenerator.ChunkData {
         int offset = (y - this.minHeight) >> 4;
         LevelChunkSection section = this.sections[offset];
         if (create && section == null) {
-            this.sections[offset] = section = new LevelChunkSection(offset + (this.minHeight >> 4), this.biomes);
+            this.sections[offset] = section = new LevelChunkSection(offset + (this.minHeight >> 4), this.biomes, null, this.world instanceof org.bukkit.craftbukkit.CraftWorld ? ((org.bukkit.craftbukkit.CraftWorld) this.world).getHandle() : null); // Paper - Anti-Xray - Add parameters
         }
         return section;
     }