From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: stonar96 <minecraft.stonar96@gmail.com>
Date: Mon, 20 Aug 2018 03:03:58 +0200
Subject: [PATCH] Anti-Xray


diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
index a91a7d8f56a068b18d50a8b987b71510b0a19d5b..9e16c9e6b6e84d84028821f2ade103dbced66470 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -1,7 +1,9 @@
 package com.destroystokyo.paper;
 
+import java.util.Arrays;
 import java.util.List;
 
+import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray.EngineMode;
 import org.bukkit.Bukkit;
 import org.bukkit.configuration.file.YamlConfiguration;
 import org.spigotmc.SpigotWorldConfig;
@@ -484,5 +486,40 @@ public class PaperWorldConfig {
     private void lightQueueSize() {
         lightQueueSize = getInt("light-queue-size", lightQueueSize);
     }
+
+    public boolean antiXray;
+    public EngineMode engineMode;
+    public int maxBlockHeight;
+    public int updateRadius;
+    public boolean lavaObscures;
+    public boolean usePermission;
+    public List<String> hiddenBlocks;
+    public List<String> replacementBlocks;
+    private void antiXray() {
+        antiXray = getBoolean("anti-xray.enabled", false);
+        engineMode = EngineMode.getById(getInt("anti-xray.engine-mode", EngineMode.HIDE.getId()));
+        engineMode = engineMode == null ? EngineMode.HIDE : engineMode;
+        maxBlockHeight = getInt("anti-xray.max-block-height", 64);
+        updateRadius = getInt("anti-xray.update-radius", 2);
+        lavaObscures = getBoolean("anti-xray.lava-obscures", false);
+        usePermission = getBoolean("anti-xray.use-permission", false);
+        hiddenBlocks = getList("anti-xray.hidden-blocks", Arrays.asList("copper_ore", "deepslate_copper_ore", "gold_ore", "deepslate_gold_ore", "iron_ore", "deepslate_iron_ore",
+            "coal_ore", "deepslate_coal_ore", "lapis_ore", "deepslate_lapis_ore", "mossy_cobblestone", "obsidian", "chest", "diamond_ore", "deepslate_diamond_ore",
+            "redstone_ore", "deepslate_redstone_ore", "clay", "emerald_ore", "deepslate_emerald_ore", "ender_chest"));
+        replacementBlocks = getList("anti-xray.replacement-blocks", Arrays.asList("stone", "oak_planks", "deepslate"));
+        if (PaperConfig.version < 19) {
+            hiddenBlocks.remove("lit_redstone_ore");
+            int index = replacementBlocks.indexOf("planks");
+            if (index != -1) {
+                replacementBlocks.set(index, "oak_planks");
+            }
+            set("anti-xray.hidden-blocks", hiddenBlocks);
+            set("anti-xray.replacement-blocks", replacementBlocks);
+        }
+        log("Anti-Xray: " + (antiXray ? "enabled" : "disabled") + " / Engine Mode: " + engineMode.getDescription() + " / Up to " + ((maxBlockHeight >> 4) << 4) + " blocks / Update Radius: " + updateRadius);
+        if (antiXray && usePermission) {
+            Bukkit.getLogger().warning("You have enabled permission-based Anti-Xray checking - depending on your permission plugin, this may cause performance issues");
+        }
+    }
 }
 
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..280ece653cdda74e9c8fab4e9e5b3a952901cb01
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
@@ -0,0 +1,46 @@
+package com.destroystokyo.paper.antixray;
+
+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;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+
+public class ChunkPacketBlockController {
+
+    public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController();
+
+    protected ChunkPacketBlockController() {
+
+    }
+
+    public BlockState[] getPresetBlockStates(Level level, ChunkAccess chunk, LevelChunkSection chunkSection, boolean initializeBlocks) {
+        return null;
+    }
+
+    public boolean shouldModify(ServerPlayer player, LevelChunk chunk) {
+        return false;
+    }
+
+    public ChunkPacketInfo<BlockState> getChunkPacketInfo(ClientboundLevelChunkPacket chunkPacket, LevelChunk chunk) {
+        return null;
+    }
+
+    public void modifyBlocks(ClientboundLevelChunkPacket 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) {
+
+    }
+}
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..58052a5f773b5882acd90bc0214a36e8a512817c
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
@@ -0,0 +1,635 @@
+package com.destroystokyo.paper.antixray;
+
+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;
+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.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 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[] 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 maxBlockHeightUpdatePosition;
+
+    public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) {
+        this.executor = executor;
+        PaperWorldConfig paperWorldConfig = level.paperConfig;
+        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()};
+            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> presetBlockStateList = new LinkedList<>();
+
+            for (String id : paperWorldConfig.hiddenBlocks) {
+                Block block = Registry.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;
+            presetBlockStatesNetherrack = null;
+            presetBlockStatesEndStone = null;
+            presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length];
+
+            for (int i = 0; i < presetBlockStatesFull.length; i++) {
+                presetBlockStateBitsGlobal[i] = LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]);
+            }
+
+            presetBlockStateBitsStoneGlobal = null;
+            presetBlockStateBitsNetherrackGlobal = null;
+            presetBlockStateBitsEndStoneGlobal = null;
+        }
+
+        for (String id : toObfuscate) {
+            Block block = Registry.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[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
+                }
+            }
+        }
+
+        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 blockState = LevelChunkSection.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 || 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, 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 (level.getWorld().getEnvironment()) {
+                    case NETHER -> presetBlockStatesNetherrack;
+                    case THE_END -> presetBlockStatesEndStone;
+                    default -> 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(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
+        return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this);
+    }
+
+    @Override
+    public void modifyBlocks(ClientboundLevelChunkPacket 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.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[]> 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[][]> nextNext = ThreadLocal.withInitial(() -> new boolean[16][16]);
+
+    public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
+        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();
+        // 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;
+        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) == 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 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++) {
+                        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]) == LevelChunk.EMPTY_SECTION;
+
+                    for (int z = 0; z < 16; z++) {
+                        for (int x = 0; x < 16; x++) {
+                            current[z][x] = true;
+                            next[z][x] = skipFirstLayer || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(belowChunkSection.getBlockState(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 ? 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];
+                nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? LevelChunk.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]) != LevelChunk.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 (!solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(aboveChunkSection.getBlockState(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] || 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))]) {
+                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] || nearbyChunkSections[2] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[2].getBlockState(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] || 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))]) {
+                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] || nearbyChunkSections[0] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[0].getBlockState(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] || nearbyChunkSections[1] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[1].getBlockState(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] || 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))]) {
+                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] || nearbyChunkSections[3] == LevelChunk.EMPTY_SECTION || !solidGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(nearbyChunkSections[3].getBlockState(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] || 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))]) {
+                bitStorageWriter.skip();
+            } else {
+                bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]);
+            }
+        }
+
+        if (!obfuscate[bits]) {
+            next[15][15] = true;
+        }
+    }
+
+    private boolean[] readPalette(Palette<BlockState> palette, boolean[] temp, boolean[] global) {
+        if (palette == LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE) {
+            return global;
+        }
+
+        BlockState blockState;
+
+        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 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 serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight) {
+        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.getTypeIfLoaded(blockPos);
+
+        if (blockState != null && obfuscateGlobal[LevelChunkSection.GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) {
+            ((ServerLevel) level).getChunkSource().blockChanged(blockPos);
+        }
+    }
+
+    public enum EngineMode {
+
+        HIDE(1, "hide ores"),
+        OBFUSCATE(2, "obfuscate");
+
+        private final int id;
+        private final String description;
+
+        EngineMode(int id, String description) {
+            this.id = id;
+            this.description = description;
+        }
+
+        public static EngineMode getById(int id) {
+            for (EngineMode engineMode : values()) {
+                if (engineMode.id == id) {
+                    return engineMode;
+                }
+            }
+
+            return null;
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+    }
+}
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..7bc2d4daffa8e9e71c3bf496d2cf1a2b7f3c6a4b
--- /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.ClientboundLevelChunkPacket;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.Palette;
+
+public class ChunkPacketInfo<T> {
+
+    private final ClientboundLevelChunkPacket 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(ClientboundLevelChunkPacket 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 ClientboundLevelChunkPacket 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..02324a59ac21db5349fe2a74248b2c6f92fa8233
--- /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.ClientboundLevelChunkPacket;
+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(ClientboundLevelChunkPacket 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/ClientboundLevelChunkPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacket.java
index c28879f32b004f36ff746ea2274f91ddd9501e71..60d72e488bc77cd913328be400ca374a873b4561 100644
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacket.java
@@ -37,7 +37,24 @@ public class ClientboundLevelChunkPacket implements Packet<ClientGamePacketListe
     }
     // Paper end
 
-    public ClientboundLevelChunkPacket(LevelChunk chunk) {
+    // Paper start - Async-Anti-Xray - Ready flag for the connection
+    private volatile boolean ready;
+
+    @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;
+        // Paper end
         ChunkPos chunkPos = chunk.getPos();
         this.x = chunkPos.x;
         this.z = chunkPos.z;
@@ -51,7 +68,14 @@ public class ClientboundLevelChunkPacket implements Packet<ClientGamePacketListe
 
         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.setBuffer(this.buffer);
+        }
+
+        this.availableSections = this.extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo);
+        // Paper end
         this.blockEntitiesTags = Lists.newArrayList();
         int totalTileEntities = 0; // Paper
 
@@ -71,6 +95,7 @@ public class ClientboundLevelChunkPacket implements Packet<ClientGamePacketListe
             this.blockEntitiesTags.add(compoundTag);
         }
 
+        chunk.level.chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks
     }
 
     public ClientboundLevelChunkPacket(FriendlyByteBuf buf) {
@@ -120,7 +145,10 @@ public class ClientboundLevelChunkPacket implements Packet<ClientGamePacketListe
         return byteBuf;
     }
 
-    public BitSet extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) {
+    // Paper start - Anti-Xray - Add chunk packet info
+    @Deprecated public BitSet extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) { return extractChunkData(buf, chunk, null); } // Notice for updates: Please make sure this method isn't used anywhere
+    public BitSet extractChunkData(FriendlyByteBuf buf, LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo<net.minecraft.world.level.block.state.BlockState> chunkPacketInfo) {
+        // Paper end
         BitSet bitSet = new BitSet();
         LevelChunkSection[] levelChunkSections = chunk.getSections();
         int i = 0;
@@ -129,7 +157,7 @@ public class ClientboundLevelChunkPacket implements Packet<ClientGamePacketListe
             LevelChunkSection levelChunkSection = levelChunkSections[i];
             if (levelChunkSection != LevelChunk.EMPTY_SECTION && !levelChunkSection.isEmpty()) {
                 bitSet.set(i);
-                levelChunkSection.write(buf);
+                levelChunkSection.write(buf, chunkPacketInfo); // Paper - Anti-Xray - Add chunk packet info
             }
         }
 
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 6ddb24ae627b9bfdfd82b19b1a746f32bb8d0532..5e7fe43d1b27bed51f8a7c0fcddb8604d40f6954 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1486,7 +1486,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
 
     public void playerLoadedChunk(ServerPlayer player, Packet<?>[] packets, LevelChunk chunk) {
         if (packets[0] == null) {
-            packets[0] = new ClientboundLevelChunkPacket(chunk);
+            packets[0] = new ClientboundLevelChunkPacket(chunk, chunk.level.chunkPacketBlockController.shouldModify(player, chunk)); // Paper - Ani-Xray - Bypass
             packets[1] = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, (BitSet) null, (BitSet) null, true);
         }
 
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 7eec9afc5782d5d588f029cf7451c354a52f176a..afb62f6d7d0c4dc80e9ceea6c6fe420f551b40b6 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -288,7 +288,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
     // Add env and gen to constructor, WorldData -> WorldDataServer
     public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, ServerLevelData iworlddataserver, ResourceKey<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, org.bukkit.generator.BiomeProvider biomeProvider) {
         // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error
-        super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getProfiler, false, flag, i, gen, biomeProvider, env);
+        super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getProfiler, false, flag, i, gen, biomeProvider, env, executor); // Paper - Anti-Xray - Pass executor
         this.pvpMode = minecraftserver.isPvpAllowed();
         this.convertable = convertable_conversionsession;
         this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelPath.toFile());
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
index 8659df9d2e9d8b3b0cd992efc9a1f3b2d958e9c8..3f7919f4e8f5a5b79bf4e541ea5f4ce482965fa1 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 = LogManager.getLogger();
-    protected ServerLevel level;
+    public ServerLevel level; // Paper - Anti-Xray - protected -> public
     protected final ServerPlayer player;
     private GameType gameModeForPlayer;
     @Nullable
@@ -315,6 +315,8 @@ public class ServerPlayerGameMode {
             }
 
         }
+
+        this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, action, direction, worldHeight); // Paper - Anti-Xray
     }
 
     public void destroyAndAck(BlockPos pos, ServerboundPlayerActionPacket.Action action, 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 fac722d2f5bbd86cbeacc7da4bb45155257ae97f..d050d60900a6829a9409ef944d7e4fceb5248dc1 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -165,6 +165,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
     public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
 
     public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper
+    public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray
 
     public final co.aikar.timings.WorldTimingsHandler timings; // Paper
     public static BlockPos lastPhysicsProblem; // Spigot
@@ -186,7 +187,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
         return this.typeKey;
     }
 
-    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.generator.BiomeProvider biomeProvider, 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.generator.BiomeProvider biomeProvider, 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;
@@ -259,6 +260,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
         this.keepSpawnInMemory = this.paperConfig.keepSpawnInMemory; // Paper
         this.entityLimiter = new org.spigotmc.TickLimiter(spigotConfig.entityMaxTickTime);
         this.tileLimiter = new org.spigotmc.TickLimiter(spigotConfig.tileMaxTickTime);
+        this.chunkPacketBlockController = this.paperConfig.antiXray ?
+            new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor)
+            : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
     }
 
     // Paper start
@@ -449,6 +453,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
             // 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, 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 149ac5ec368b53a9a5e9208bd49a3c9453625d9c..12d11a249c759e99568a76c791cc0d65adfcfe94 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
@@ -74,7 +74,7 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess {
     default LevelChunkSection getOrCreateSection(int yIndex) {
         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, this.getLevel(), true); // Paper - Anti-Xray - Add parameters
         }
 
         return levelChunkSections[yIndex];
diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
index 69c2454533e6f21c70792b555ec02c6bc6d169b3..f9e0e109497d685a9d88d2fa8892287b9fa97443 100644
--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
@@ -86,7 +86,7 @@ public class EmptyLevelChunk extends LevelChunk {
         private static final Biome[] EMPTY_BIOMES = new Biome[0];
 
         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 - Anti-Xray - The world isnt ready yet, use server singleton for registry
         }
 
         @Override
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 8141935e2ee58bbb58c6b5cfdef5a9a88d7658ec..515e28eea8cbab261320352ee0db9b877807f3ed 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -456,7 +456,7 @@ public class LevelChunk implements ChunkAccess {
                 return null;
             }
 
-            chunksection = new LevelChunkSection(SectionPos.blockToSectionCoord(i));
+            chunksection = new LevelChunkSection(SectionPos.blockToSectionCoord(i), this, this.level, true); // Paper - Anti-Xray - Add parameters
             this.sections[j] = chunksection;
         }
 
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 5fd66020a937b641e2a060cf38df731a43f3bf55..c9fefeef19bd46ade51b23eadb5eef3a88024ea1 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -20,16 +20,25 @@ public class LevelChunkSection {
     private short tickingFluidCount;
     final PalettedContainer<BlockState> states; // Paper - package-private
 
-    public LevelChunkSection(int yOffset) {
-        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.world.level.Level level, boolean initializeBlocks) {
+        this(yOffset, (short) 0, (short) 0, (short) 0, chunk, level, initializeBlocks);
+        // Paper end
     }
 
-    public LevelChunkSection(int yOffset, short nonEmptyBlockCount, short randomTickableBlockCount, short nonEmptyFluidCount) {
+    // Paper start - Anti-Xray - Add parameters
+    @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.world.level.Level level, boolean initializeBlocks) {
+        // Paper end
         this.bottomBlockY = getBottomBlockY(yOffset);
         this.nonEmptyBlockCount = nonEmptyBlockCount;
         this.tickingBlockCount = randomTickableBlockCount;
         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(),
+            level == null ? null : level.chunkPacketBlockController.getPresetBlockStates(level, chunk, this, initializeBlocks), initializeBlocks); // Paper - Anti-Xray - Add preset block states
     }
 
     public static int getBottomBlockY(int chunkPos) {
@@ -147,9 +156,12 @@ public class LevelChunkSection {
         this.states.read(buf);
     }
 
-    public void write(FriendlyByteBuf buf) {
+    // 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
         buf.writeShort(this.nonEmptyBlockCount);
-        this.states.write(buf);
+        this.states.write(buf, chunkPacketInfo, this.bottomBlockY); // Paper - Anti-Xray - Add chunk packet info
     }
 
     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 bb8fd88aebb550edec8c679622a02a595cbc6694..ac51089aae57a5f1d2411367ff177e058702894c 100644
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
@@ -28,6 +28,7 @@ public class PalettedContainer<T> implements PaletteResize<T> {
     private final Function<CompoundTag, T> reader;
     private final Function<T, CompoundTag> writer;
     private final T defaultValue;
+    private final T[] presetValues; // Paper - Anti-Xray - Add preset values
     protected BitStorage storage;
     private Palette<T> palette;
     private int bits;
@@ -48,14 +49,46 @@ public class PalettedContainer<T> implements PaletteResize<T> {
         this.lock.release();
     }
 
-    public PalettedContainer(Palette<T> fallbackPalette, IdMapper<T> idList, Function<CompoundTag, T> elementDeserializer, Function<T, CompoundTag> elementSerializer, T defaultElement) {
+    // 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[] 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 preset values
+        this.presetValues = presetValues;
+
+        if (initialize) {
+            if (presetValues == null) {
+                // Default
+                this.setBits(4);
+            } else {
+                // 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 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;
@@ -84,6 +117,7 @@ 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
 
         for(int i = 0; i < bitStorage.getSize(); ++i) {
             T object = palette.valueFor(bitStorage.get(i));
@@ -153,17 +187,38 @@ 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); } // 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 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();
@@ -174,12 +229,14 @@ public class PalettedContainer<T> implements PaletteResize<T> {
     public void read(ListTag paletteNbt, long[] data) {
         try {
             this.acquire();
-            int i = Math.max(4, Mth.ceillog2(paletteNbt.size()));
-            if (i != this.bits) {
+            // 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.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);
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 79a7221b0462931e94f31ccd7c6dc2720ddfb752..afbb42595afeb151208880dcf48b94d7c00a8733 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
@@ -136,7 +136,7 @@ public class ChunkSerializer {
             byte b0 = nbttagcompound2.getByte("Y");
 
             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 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();
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
index 17c2f2dca587b350dc3e48f01dc62025a12d83c0..1ba393e2b47cca45bfa8e4ff4ef2438f273fd467 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java
@@ -46,7 +46,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 LevelChunkSection(0).getStates();
+    private static final PalettedContainer<net.minecraft.world.level.block.state.BlockState> emptyBlockIDs = new LevelChunkSection(0, null, null, true).getStates(); // Paper - Anti-Xray - Add parameters
     private static final byte[] emptyLight = new byte[2048];
 
     public CraftChunk(net.minecraft.world.level.chunk.LevelChunk chunk) {
@@ -275,7 +275,7 @@ public class CraftChunk implements Chunk {
                 CompoundTag data = new CompoundTag();
                 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 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;
diff --git a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
index 7bfd803d11976dba9ace79edc76bb083a2eccf38..a48c659c02c6c33a8efdac6daf9c9a0708f05071 100644
--- a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
+++ b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java
@@ -25,9 +25,11 @@ public final class OldCraftChunkData implements ChunkGenerator.ChunkData {
     private final LevelChunkSection[] sections;
     private Set<BlockPos> tiles;
     private final Set<BlockPos> lights = new HashSet<>();
+    private World world; // Paper - Anti-Xray - Add parameters
 
     public OldCraftChunkData(World world) {
         this(world.getMinHeight(), world.getMaxHeight());
+        this.world = world; // Paper - Anti-Xray - Add parameters
     }
 
     /* pp for tests */ OldCraftChunkData(int minHeight, int maxHeight) {
@@ -177,7 +179,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.sections[offset] = section = new LevelChunkSection(offset + (this.minHeight >> 4), null, world instanceof org.bukkit.craftbukkit.CraftWorld ? ((org.bukkit.craftbukkit.CraftWorld) this.world).getHandle() : null, true); // Paper - Anti-Xray - Add parameters
         }
         return section;
     }
