From deb49130a0248ad17fd28a1963c89e984fcdb72a 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 4867615215..df24e3297b 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -1,7 +1,11 @@
 package com.destroystokyo.paper;
 
+import java.util.Arrays;
 import java.util.List;
 
+import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray.ChunkEdgeMode;
+import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray.EngineMode;
+import net.minecraft.server.MinecraftServer;
 import org.bukkit.Bukkit;
 import org.bukkit.configuration.file.YamlConfiguration;
 import org.spigotmc.SpigotWorldConfig;
@@ -502,4 +506,43 @@ public class PaperWorldConfig {
     private void maxAutoSaveChunksPerTick() {
         maxAutoSaveChunksPerTick = getInt("max-auto-save-chunks-per-tick", 24);
     }
+
+    public boolean antiXray;
+    public boolean asynchronous;
+    public EngineMode engineMode;
+    public ChunkEdgeMode chunkEdgeMode;
+    public int maxChunkSectionIndex;
+    public int updateRadius;
+    public List<String> hiddenBlocks;
+    public List<String> replacementBlocks;
+    private void antiXray() {
+        antiXray = getBoolean("anti-xray.enabled", false);
+        asynchronous = true;
+        engineMode = EngineMode.getById(getInt("anti-xray.engine-mode", EngineMode.HIDE.getId()));
+        engineMode = engineMode == null ? EngineMode.HIDE : engineMode;
+        chunkEdgeMode = ChunkEdgeMode.getById(getInt("anti-xray.chunk-edge-mode", ChunkEdgeMode.WAIT.getId()));
+        chunkEdgeMode = chunkEdgeMode == null ? ChunkEdgeMode.DEFAULT : chunkEdgeMode;
+
+        if (chunkEdgeMode != ChunkEdgeMode.WAIT) {
+            log("Migrating anti-xray chunk edge mode to " + ChunkEdgeMode.WAIT + " (" + ChunkEdgeMode.WAIT.getId() + ")");
+            chunkEdgeMode = ChunkEdgeMode.WAIT;
+            set("anti-xray.chunk-edge-mode", ChunkEdgeMode.WAIT.getId());
+        }
+
+        maxChunkSectionIndex = getInt("anti-xray.max-chunk-section-index", 3);
+        maxChunkSectionIndex = maxChunkSectionIndex > 15 ? 15 : maxChunkSectionIndex;
+        updateRadius = getInt("anti-xray.update-radius", 2);
+        hiddenBlocks = getList("anti-xray.hidden-blocks", Arrays.asList("gold_ore", "iron_ore", "coal_ore", "lapis_ore", "mossy_cobblestone", "obsidian", "chest", "diamond_ore", "redstone_ore", "clay", "emerald_ore", "ender_chest"));
+        replacementBlocks = getList("anti-xray.replacement-blocks", Arrays.asList("stone", "oak_planks"));
+        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() + " / Chunk Edge Mode: " + chunkEdgeMode.getDescription() + " / Up to " + ((maxChunkSectionIndex + 1) * 16) + " blocks / Update Radius: " + updateRadius);
+    }
 }
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 0000000000..f7e376ce6a
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java
@@ -0,0 +1,46 @@
+package com.destroystokyo.paper.antixray;
+
+import net.minecraft.server.BlockPosition;
+import net.minecraft.server.Chunk;
+import net.minecraft.server.ChunkSection;
+import net.minecraft.server.EnumDirection;
+import net.minecraft.server.IBlockData;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.IWorldReader;
+import net.minecraft.server.PacketPlayOutMapChunk;
+import net.minecraft.server.PlayerInteractManager;
+import net.minecraft.server.World;
+
+public class ChunkPacketBlockController {
+
+    public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController();
+
+    protected ChunkPacketBlockController() {
+
+    }
+
+    public IBlockData[] getPredefinedBlockData(IWorldReader world, IChunkAccess chunk, ChunkSection chunkSection, boolean initializeBlocks) {
+        return null;
+    }
+
+    public boolean onChunkPacketCreate(Chunk chunk, int chunkSectionSelector, boolean force) {
+        return true;
+    }
+
+    public ChunkPacketInfo<IBlockData> getChunkPacketInfo(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk,
+                                                          int chunkSectionSelector, boolean forceLoad) {
+        return null;
+    }
+
+    public void modifyBlocks(PacketPlayOutMapChunk packetPlayOutMapChunk, ChunkPacketInfo<IBlockData> chunkPacketInfo, boolean loadChunks, Integer ticketHold) {
+        packetPlayOutMapChunk.setReady(true);
+    }
+
+    public void onBlockChange(World world, BlockPosition blockPosition, IBlockData newBlockData, IBlockData oldBlockData, int flag) {
+
+    }
+
+    public void onPlayerLeftClickBlock(PlayerInteractManager playerInteractManager, BlockPosition blockPosition, EnumDirection enumDirection) {
+
+    }
+}
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 0000000000..23626bef3a
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
@@ -0,0 +1,782 @@
+package com.destroystokyo.paper.antixray;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import net.minecraft.server.*;
+import org.bukkit.Bukkit;
+import org.bukkit.World.Environment;
+
+import com.destroystokyo.paper.PaperWorldConfig;
+
+public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController {
+
+    private static ExecutorService executorServiceInstance = null;
+    private final ExecutorService executorService;
+    private final boolean asynchronous;
+    private final EngineMode engineMode;
+    private final ChunkEdgeMode chunkEdgeMode;
+    private final int maxChunkSectionIndex;
+    private final int updateRadius;
+    private final IBlockData[] predefinedBlockData;
+    private final IBlockData[] predefinedBlockDataStone;
+    private final IBlockData[] predefinedBlockDataNetherrack;
+    private final IBlockData[] predefinedBlockDataEndStone;
+    private final int[] predefinedBlockDataBitsGlobal;
+    private final int[] predefinedBlockDataBitsStoneGlobal;
+    private final int[] predefinedBlockDataBitsNetherrackGlobal;
+    private final int[] predefinedBlockDataBitsEndStoneGlobal;
+    private final boolean[] solidGlobal = new boolean[Block.REGISTRY_ID.size()];
+    private final boolean[] obfuscateGlobal = new boolean[Block.REGISTRY_ID.size()];
+    private final ChunkSection[] emptyNearbyChunkSections = {Chunk.EMPTY_CHUNK_SECTION, Chunk.EMPTY_CHUNK_SECTION, Chunk.EMPTY_CHUNK_SECTION, Chunk.EMPTY_CHUNK_SECTION};
+    private final int maxBlockYUpdatePosition;
+
+    public ChunkPacketBlockControllerAntiXray(PaperWorldConfig paperWorldConfig) {
+        asynchronous = paperWorldConfig.asynchronous;
+        engineMode = paperWorldConfig.engineMode;
+        chunkEdgeMode = paperWorldConfig.chunkEdgeMode;
+        maxChunkSectionIndex = paperWorldConfig.maxChunkSectionIndex;
+        updateRadius = paperWorldConfig.updateRadius;
+
+        if (asynchronous) {
+            executorService = getExecutorServiceInstance();
+        } else {
+            executorService = null;
+        }
+
+        List<String> toObfuscate;
+
+        if (engineMode == EngineMode.HIDE) {
+            toObfuscate = paperWorldConfig.hiddenBlocks;
+            predefinedBlockData = null;
+            predefinedBlockDataStone = new IBlockData[] {Blocks.STONE.getBlockData()};
+            predefinedBlockDataNetherrack = new IBlockData[] {Blocks.NETHERRACK.getBlockData()};
+            predefinedBlockDataEndStone = new IBlockData[] {Blocks.END_STONE.getBlockData()};
+            predefinedBlockDataBitsGlobal = null;
+            predefinedBlockDataBitsStoneGlobal = new int[] {ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(Blocks.STONE.getBlockData())};
+            predefinedBlockDataBitsNetherrackGlobal = new int[] {ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(Blocks.NETHERRACK.getBlockData())};
+            predefinedBlockDataBitsEndStoneGlobal = new int[] {ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(Blocks.END_STONE.getBlockData())};
+        } else {
+            toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks);
+            Set<IBlockData> predefinedBlockDataSet = new HashSet<IBlockData>();
+
+            for (String id : paperWorldConfig.hiddenBlocks) {
+                Block block = IRegistry.BLOCK.getOptional(new MinecraftKey(id)).orElse(null);
+
+                if (block != null && !block.isTileEntity()) {
+                    toObfuscate.add(id);
+                    predefinedBlockDataSet.add(block.getBlockData());
+                }
+            }
+
+            predefinedBlockData = predefinedBlockDataSet.size() == 0 ? new IBlockData[] {Blocks.DIAMOND_ORE.getBlockData()} : predefinedBlockDataSet.toArray(new IBlockData[0]);
+            predefinedBlockDataStone = null;
+            predefinedBlockDataNetherrack = null;
+            predefinedBlockDataEndStone = null;
+            predefinedBlockDataBitsGlobal = new int[predefinedBlockData.length];
+
+            for (int i = 0; i < predefinedBlockData.length; i++) {
+                predefinedBlockDataBitsGlobal[i] = ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(predefinedBlockData[i]);
+            }
+
+            predefinedBlockDataBitsStoneGlobal = null;
+            predefinedBlockDataBitsNetherrackGlobal = null;
+            predefinedBlockDataBitsEndStoneGlobal = null;
+        }
+
+        for (String id : toObfuscate) {
+            Block block = IRegistry.BLOCK.getOptional(new MinecraftKey(id)).orElse(null);
+
+            if (block != null) {
+                obfuscateGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(block.getBlockData())] = true;
+            }
+        }
+
+        ChunkEmpty emptyChunk = new ChunkEmpty(null, new ChunkCoordIntPair(0, 0));
+        BlockPosition zeroPos = new BlockPosition(0, 0, 0);
+
+        for (int i = 0; i < solidGlobal.length; i++) {
+            IBlockData blockData = ChunkSection.GLOBAL_PALETTE.getObject(i);
+
+            if (blockData != null) {
+                solidGlobal[i] = blockData.getBlock().isOccluding(blockData, emptyChunk, zeroPos)
+                    && blockData.getBlock() != Blocks.SPAWNER && blockData.getBlock() != Blocks.BARRIER && blockData.getBlock() != Blocks.SHULKER_BOX;
+                // shulker box checks TE.
+            }
+        }
+
+        this.maxBlockYUpdatePosition = (maxChunkSectionIndex + 1) * 16 + updateRadius - 1;
+    }
+
+    private static ExecutorService getExecutorServiceInstance() {
+        if (executorServiceInstance == null) {
+            executorServiceInstance = Executors.newSingleThreadExecutor();
+        }
+
+        return executorServiceInstance;
+    }
+
+    @Override
+    public IBlockData[] getPredefinedBlockData(IWorldReader world, IChunkAccess chunk, ChunkSection chunkSection, boolean initializeBlocks) {
+        //Return the block data which should be added to the data palettes so that they can be used for the obfuscation
+        if (chunkSection.getYPosition() >> 4 <= maxChunkSectionIndex) {
+            switch (engineMode) {
+                case HIDE:
+                    if (world instanceof GeneratorAccess) {
+                        switch (((GeneratorAccess) world).getMinecraftWorld().getWorld().getEnvironment()) {
+                            case NETHER:
+                                return predefinedBlockDataNetherrack;
+                            case THE_END:
+                                return predefinedBlockDataEndStone;
+                            default:
+                                return predefinedBlockDataStone;
+                        }
+                    }
+
+                    return null;
+                default:
+                    return predefinedBlockData;
+            }
+        }
+
+        return null;
+    }
+
+    private final AtomicInteger xrayRequests = new AtomicInteger();
+
+    private Integer addXrayTickets(final int x, final int z, final ChunkProviderServer chunkProvider) {
+        final Integer hold = Integer.valueOf(this.xrayRequests.getAndIncrement());
+
+        // Add at ticket level 33, which is just enough to keep chunks loaded
+        chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z), 0, hold);
+        chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x - 1, z), 0, hold);
+        chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x + 1, z), 0, hold);
+        chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z - 1), 0, hold);
+        chunkProvider.addTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z + 1), 0, hold);
+
+        return hold;
+    }
+
+    private void removeXrayTickets(final int x, final int z, final ChunkProviderServer chunkProvider, final Integer hold) {
+        // Remove at ticket level 33 (same one we added as), which is just enough to keep chunks loaded
+        chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z), 0, hold);
+        chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x - 1, z), 0, hold);
+        chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x + 1, z), 0, hold);
+        chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z - 1), 0, hold);
+        chunkProvider.removeTicket(TicketType.ANTIXRAY, new ChunkCoordIntPair(x, z + 1), 0, hold);
+    }
+
+    private void loadNeighbours(Chunk chunk) {
+        int locX = chunk.getPos().x;
+        int locZ = chunk.getPos().z;
+        chunk.world.getChunkAt(locX - 1, locZ);
+        chunk.world.getChunkAt(locX + 1, locZ);
+        chunk.world.getChunkAt(locX, locZ - 1);
+        chunk.world.getChunkAt(locX, locZ + 1);
+    }
+
+    @Override
+    public boolean onChunkPacketCreate(Chunk chunk, int chunkSectionSelector, boolean force) {
+        int locX = chunk.getPos().x;
+        int locZ = chunk.getPos().z;
+        WorldServer world = (WorldServer)chunk.world;
+        ChunkProviderServer chunkProvider = world.getChunkProvider();
+
+        //Load nearby chunks if necessary
+        if (force || chunkEdgeMode == ChunkEdgeMode.LOAD) { // TODO temporary
+            // if forced, load NOW;
+            this.loadNeighbours(chunk);
+        } else if (chunkEdgeMode == ChunkEdgeMode.WAIT) {
+            if (chunkProvider.getChunkAtIfCachedImmediately(locX - 1, locZ) == null ||
+                chunkProvider.getChunkAtIfCachedImmediately(locX + 1, locZ) == null ||
+                chunkProvider.getChunkAtIfCachedImmediately(locX, locZ - 1) == null ||
+                chunkProvider.getChunkAtIfCachedImmediately(locX, locZ + 1) == null) {
+                //Don't create the chunk packet now, wait until nearby chunks are loaded and create it later
+                return false;
+            }
+        } else if (false && chunkEdgeMode == ChunkEdgeMode.LOAD) {
+            // TODO Note: These should be asynchronous loads; however we have no such thing in 1.14.
+            boolean missingChunk = false;
+            //noinspection ConstantConditions
+            /*
+            missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX - 1, chunk.locZ, true, true, c -> {}) == null;
+            missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX + 1, chunk.locZ, true, true, c -> {}) == null;
+            missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX, chunk.locZ - 1, true, true, c -> {}) == null;
+            missingChunk |= ((WorldServer)chunk.world).getChunkProvider().getChunkAt(chunk.locX, chunk.locZ + 1, true, true, c -> {}) == null;
+             */
+            if (missingChunk) {
+                return false;
+            }
+        }
+
+        //Create the chunk packet now
+        return true;
+    }
+
+    @Override
+    public ChunkPacketInfoAntiXray getChunkPacketInfo(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk,
+                                                      int chunkSectionSelector, boolean forceLoad) {
+        // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later
+        // Note: As of 1.14 this has to be moved later due to the chunk system.
+
+        ChunkPacketInfoAntiXray chunkPacketInfoAntiXray = new ChunkPacketInfoAntiXray(packetPlayOutMapChunk, chunk, chunkSectionSelector, this);
+        return chunkPacketInfoAntiXray;
+    }
+
+    @Override
+    public void modifyBlocks(PacketPlayOutMapChunk packetPlayOutMapChunk, ChunkPacketInfo<IBlockData> chunkPacketInfo, boolean loadChunks, Integer hold) {
+        if (!Bukkit.isPrimaryThread()) {
+            // plugins?
+            final Integer finalHold = hold;
+            MinecraftServer.getServer().scheduleOnMain(() -> {
+                this.modifyBlocks(packetPlayOutMapChunk, chunkPacketInfo, loadChunks, finalHold);
+            });
+            return;
+        }
+        Chunk chunk = chunkPacketInfo.getChunk();
+        int locX = chunk.getPos().x;
+        int locZ = chunk.getPos().z;
+        WorldServer world = (WorldServer)chunk.world;
+
+        Chunk[] chunks = new Chunk[] {
+            (Chunk)world.getChunkIfLoadedImmediately(locX - 1, locZ),
+            (Chunk)world.getChunkIfLoadedImmediately(locX + 1, locZ),
+            (Chunk)world.getChunkIfLoadedImmediately(locX, locZ - 1),
+            (Chunk)world.getChunkIfLoadedImmediately(locX, locZ + 1)
+        };
+
+        if (loadChunks) {
+            // Note: This ugly hack is to get us out of the general chunk load/unload queue to prevent deadlock
+
+            if (chunks[0] == null || chunks[1] == null || chunks[2] == null || chunks[3] == null) {
+                // we need to load
+                MinecraftServer.getServer().scheduleOnMain(() -> {
+                    Integer ticketHold = this.addXrayTickets(locX, locZ, world.getChunkProvider());
+                    this.loadNeighbours(chunk);
+                    this.modifyBlocks(packetPlayOutMapChunk, chunkPacketInfo, false, ticketHold);
+                });
+                return;
+            }
+
+            hold = this.addXrayTickets(locX, locZ, world.getChunkProvider());
+            // fall through to normal behavior, our chunks are now loaded & have a ticket
+        }
+
+        ((ChunkPacketInfoAntiXray)chunkPacketInfo).setNearbyChunks(chunks);
+        ((ChunkPacketInfoAntiXray)chunkPacketInfo).ticketHold = hold;
+
+        if (asynchronous) {
+            executorService.submit((ChunkPacketInfoAntiXray) chunkPacketInfo);
+        } else {
+            obfuscate((ChunkPacketInfoAntiXray) chunkPacketInfo);
+        }
+    }
+
+    //Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay
+    private int[] predefinedBlockDataBits;
+    private final boolean[] solid = new boolean[Block.REGISTRY_ID.size()];
+    private final boolean[] obfuscate = new boolean[Block.REGISTRY_ID.size()];
+    //These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate
+    private boolean[][] current = new boolean[16][16];
+    private boolean[][] next = new boolean[16][16];
+    private boolean[][] nextNext = new boolean[16][16];
+    private final DataBitsReader dataBitsReader = new DataBitsReader();
+    private final DataBitsWriter dataBitsWriter = new DataBitsWriter();
+    private final ChunkSection[] nearbyChunkSections = new ChunkSection[4];
+
+    public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
+        try {
+            boolean[] solidTemp = null;
+            boolean[] obfuscateTemp = null;
+            dataBitsReader.setDataBits(chunkPacketInfoAntiXray.getData());
+            dataBitsWriter.setDataBits(chunkPacketInfoAntiXray.getData());
+            int counter = 0;
+
+            for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) {
+                if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex) != null) {
+                    int[] predefinedBlockDataBitsTemp;
+
+                    if (chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex) == ChunkSection.GLOBAL_PALETTE) {
+                        predefinedBlockDataBitsTemp = engineMode == EngineMode.HIDE ? chunkPacketInfoAntiXray.getChunk().world.getWorld().getEnvironment() == Environment.NETHER ? predefinedBlockDataBitsNetherrackGlobal : chunkPacketInfoAntiXray.getChunk().world.getWorld().getEnvironment() == Environment.THE_END ? predefinedBlockDataBitsEndStoneGlobal : predefinedBlockDataBitsStoneGlobal : predefinedBlockDataBitsGlobal;
+                    } else {
+                        predefinedBlockDataBitsTemp = predefinedBlockDataBits == null ? predefinedBlockDataBits = engineMode == EngineMode.HIDE ? new int[1] : new int[predefinedBlockData.length] : predefinedBlockDataBits;
+
+                        for (int i = 0; i < predefinedBlockDataBitsTemp.length; i++) {
+                            predefinedBlockDataBitsTemp[i] = chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex).getOrCreateIdFor(chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex)[i]);
+                        }
+                    }
+
+                    dataBitsWriter.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex));
+
+                    //Check if the chunk section below was not obfuscated
+                    if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPredefinedObjects(chunkSectionIndex - 1) == null) {
+                        //If so, initialize some stuff
+                        dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
+                        dataBitsReader.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex));
+                        solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), solid, solidGlobal);
+                        obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex), obfuscate, obfuscateGlobal);
+                        //Read the blocks of the upper layer of the chunk section below if it exists
+                        ChunkSection belowChunkSection = null;
+                        boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex - 1]) == Chunk.EMPTY_CHUNK_SECTION;
+
+                        for (int z = 0; z < 16; z++) {
+                            for (int x = 0; x < 16; x++) {
+                                current[z][x] = true;
+                                next[z][x] = skipFirstLayer || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(belowChunkSection.getType(x, 15, z))];
+                            }
+                        }
+
+                        //Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
+                        dataBitsWriter.setBitsPerObject(0);
+                        obfuscateLayer(-1, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, emptyNearbyChunkSections, counter);
+                    }
+
+                    dataBitsWriter.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex));
+                    nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex];
+                    nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex];
+                    nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? Chunk.EMPTY_CHUNK_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex];
+                    nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? Chunk.EMPTY_CHUNK_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;
+                        counter = obfuscateLayer(y, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
+                    }
+
+                    //Check if the chunk section above doesn't need obfuscation
+                    if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPredefinedObjects(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
+                        ChunkSection aboveChunkSection;
+
+                        if (chunkSectionIndex != 15 && (aboveChunkSection = chunkPacketInfoAntiXray.getChunk().getSections()[chunkSectionIndex + 1]) != Chunk.EMPTY_CHUNK_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[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(aboveChunkSection.getType(x, 0, z))]) {
+                                        current[z][x] = true;
+                                    }
+                                }
+                            }
+
+                            //There is nothing to read anymore
+                            dataBitsReader.setBitsPerObject(0);
+                            solid[0] = true;
+                            counter = obfuscateLayer(15, dataBitsReader, dataBitsWriter, solid, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
+                        }
+                    } else {
+                        //If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section
+                        dataBitsReader.setBitsPerObject(chunkPacketInfoAntiXray.getBitsPerObject(chunkSectionIndex + 1));
+                        dataBitsReader.setIndex(chunkPacketInfoAntiXray.getOrCreateIdForIndex(chunkSectionIndex + 1));
+                        solidTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), solid, solidGlobal);
+                        obfuscateTemp = readDataPalette(chunkPacketInfoAntiXray.getDataPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal);
+                        boolean[][] temp = current;
+                        current = next;
+                        next = nextNext;
+                        nextNext = temp;
+                        counter = obfuscateLayer(15, dataBitsReader, dataBitsWriter, solidTemp, obfuscateTemp, predefinedBlockDataBitsTemp, current, next, nextNext, nearbyChunkSections, counter);
+                    }
+
+                    dataBitsWriter.finish();
+                }
+            }
+
+            chunkPacketInfoAntiXray.getPacketPlayOutMapChunk().setReady(true);
+
+        } finally {
+            if (chunkPacketInfoAntiXray.ticketHold != null) {
+                Runnable runnable = () -> {
+                    Chunk chunk = chunkPacketInfoAntiXray.getChunk();
+                    ChunkCoordIntPair chunkPos = chunk.getPos();
+
+                    ChunkPacketBlockControllerAntiXray.this.removeXrayTickets(chunkPos.x, chunkPos.z, (ChunkProviderServer) chunk.world.getChunkProvider(),
+                        chunkPacketInfoAntiXray.ticketHold);
+                };
+                if (MinecraftServer.getServer().isMainThread()) {
+                    runnable.run();
+                } else {
+                    MinecraftServer.getServer().scheduleOnMain(runnable);
+                }
+            }
+        }
+    }
+
+    private int obfuscateLayer(int y, DataBitsReader dataBitsReader, DataBitsWriter dataBitsWriter, boolean[] solid, boolean[] obfuscate, int[] predefinedBlockDataBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, ChunkSection[] nearbyChunkSections, int counter) {
+        //First block of first line
+        int dataBits = dataBitsReader.read();
+
+        if (nextNext[0][0] = !solid[dataBits]) {
+            dataBitsWriter.skip();
+            next[0][1] = true;
+            next[1][0] = true;
+        } else {
+            if (nearbyChunkSections[2] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[2].getType(0, y, 15))] || nearbyChunkSections[0] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[0].getType(15, y, 0))] || current[0][0]) {
+                dataBitsWriter.skip();
+            } else {
+                if (counter >= predefinedBlockDataBits.length) {
+                    counter = 0;
+                }
+
+                dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+            }
+        }
+
+        if (!obfuscate[dataBits]) {
+            next[0][0] = true;
+        }
+
+        //First line
+        for (int x = 1; x < 15; x++) {
+            dataBits = dataBitsReader.read();
+
+            if (nextNext[0][x] = !solid[dataBits]) {
+                dataBitsWriter.skip();
+                next[0][x - 1] = true;
+                next[0][x + 1] = true;
+                next[1][x] = true;
+            } else {
+                if (nearbyChunkSections[2] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[2].getType(x, y, 15))] || current[0][x]) {
+                    dataBitsWriter.skip();
+                } else {
+                    if (counter >= predefinedBlockDataBits.length) {
+                        counter = 0;
+                    }
+
+                    dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+                }
+            }
+
+            if (!obfuscate[dataBits]) {
+                next[0][x] = true;
+            }
+        }
+
+        //Last block of first line
+        dataBits = dataBitsReader.read();
+
+        if (nextNext[0][15] = !solid[dataBits]) {
+            dataBitsWriter.skip();
+            next[0][14] = true;
+            next[1][15] = true;
+        } else {
+            if (nearbyChunkSections[2] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[2].getType(15, y, 15))] || nearbyChunkSections[1] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[1].getType(0, y, 0))] || current[0][15]) {
+                dataBitsWriter.skip();
+            } else {
+                if (counter >= predefinedBlockDataBits.length) {
+                    counter = 0;
+                }
+
+                dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+            }
+        }
+
+        if (!obfuscate[dataBits]) {
+            next[0][15] = true;
+        }
+
+        //All inner lines
+        for (int z = 1; z < 15; z++) {
+            //First block
+            dataBits = dataBitsReader.read();
+
+            if (nextNext[z][0] = !solid[dataBits]) {
+                dataBitsWriter.skip();
+                next[z][1] = true;
+                next[z - 1][0] = true;
+                next[z + 1][0] = true;
+            } else {
+                if (nearbyChunkSections[0] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[0].getType(15, y, z))] || current[z][0]) {
+                    dataBitsWriter.skip();
+                } else {
+                    if (counter >= predefinedBlockDataBits.length) {
+                        counter = 0;
+                    }
+
+                    dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+                }
+            }
+
+            if (!obfuscate[dataBits]) {
+                next[z][0] = true;
+            }
+
+            //All inner blocks
+            for (int x = 1; x < 15; x++) {
+                dataBits = dataBitsReader.read();
+
+                if (nextNext[z][x] = !solid[dataBits]) {
+                    dataBitsWriter.skip();
+                    next[z][x - 1] = true;
+                    next[z][x + 1] = true;
+                    next[z - 1][x] = true;
+                    next[z + 1][x] = true;
+                } else {
+                    if (current[z][x]) {
+                        dataBitsWriter.skip();
+                    } else {
+                        if (counter >= predefinedBlockDataBits.length) {
+                            counter = 0;
+                        }
+
+                        dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+                    }
+                }
+
+                if (!obfuscate[dataBits]) {
+                    next[z][x] = true;
+                }
+            }
+
+            //Last block
+            dataBits = dataBitsReader.read();
+
+            if (nextNext[z][15] = !solid[dataBits]) {
+                dataBitsWriter.skip();
+                next[z][14] = true;
+                next[z - 1][15] = true;
+                next[z + 1][15] = true;
+            } else {
+                if (nearbyChunkSections[1] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[1].getType(0, y, z))] || current[z][15]) {
+                    dataBitsWriter.skip();
+                } else {
+                    if (counter >= predefinedBlockDataBits.length) {
+                        counter = 0;
+                    }
+
+                    dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+                }
+            }
+
+            if (!obfuscate[dataBits]) {
+                next[z][15] = true;
+            }
+        }
+
+        //First block of last line
+        dataBits = dataBitsReader.read();
+
+        if (nextNext[15][0] = !solid[dataBits]) {
+            dataBitsWriter.skip();
+            next[15][1] = true;
+            next[14][0] = true;
+        } else {
+            if (nearbyChunkSections[3] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[3].getType(0, y, 0))] || nearbyChunkSections[0] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[0].getType(15, y, 15))] || current[15][0]) {
+                dataBitsWriter.skip();
+            } else {
+                if (counter >= predefinedBlockDataBits.length) {
+                    counter = 0;
+                }
+
+                dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+            }
+        }
+
+        if (!obfuscate[dataBits]) {
+            next[15][0] = true;
+        }
+
+        //Last line
+        for (int x = 1; x < 15; x++) {
+            dataBits = dataBitsReader.read();
+
+            if (nextNext[15][x] = !solid[dataBits]) {
+                dataBitsWriter.skip();
+                next[15][x - 1] = true;
+                next[15][x + 1] = true;
+                next[14][x] = true;
+            } else {
+                if (nearbyChunkSections[3] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[3].getType(x, y, 0))] || current[15][x]) {
+                    dataBitsWriter.skip();
+                } else {
+                    if (counter >= predefinedBlockDataBits.length) {
+                        counter = 0;
+                    }
+
+                    dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+                }
+            }
+
+            if (!obfuscate[dataBits]) {
+                next[15][x] = true;
+            }
+        }
+
+        //Last block of last line
+        dataBits = dataBitsReader.read();
+
+        if (nextNext[15][15] = !solid[dataBits]) {
+            dataBitsWriter.skip();
+            next[15][14] = true;
+            next[14][15] = true;
+        } else {
+            if (nearbyChunkSections[3] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[3].getType(15, y, 0))] || nearbyChunkSections[1] == Chunk.EMPTY_CHUNK_SECTION || !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(nearbyChunkSections[1].getType(0, y, 15))] || current[15][15]) {
+                dataBitsWriter.skip();
+            } else {
+                if (counter >= predefinedBlockDataBits.length) {
+                    counter = 0;
+                }
+
+                dataBitsWriter.write(predefinedBlockDataBits[counter++]);
+            }
+        }
+
+        if (!obfuscate[dataBits]) {
+            next[15][15] = true;
+        }
+
+        return counter;
+    }
+
+    private boolean[] readDataPalette(DataPalette<IBlockData> dataPalette, boolean[] temp, boolean[] global) {
+        if (dataPalette == ChunkSection.GLOBAL_PALETTE) {
+            return global;
+        }
+
+        IBlockData blockData;
+
+        for (int i = 0; (blockData = dataPalette.getObject(i)) != null; i++) {
+            temp[i] = global[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(blockData)];
+        }
+
+        return temp;
+    }
+
+    @Override
+    public void onBlockChange(World world, BlockPosition blockPosition, IBlockData newBlockData, IBlockData oldBlockData, int flag) {
+        if (oldBlockData != null && solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(oldBlockData)] && !solidGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(newBlockData)] && blockPosition.getY() <= maxBlockYUpdatePosition) {
+            updateNearbyBlocks(world, blockPosition);
+        }
+    }
+
+    @Override
+    public void onPlayerLeftClickBlock(PlayerInteractManager playerInteractManager, BlockPosition blockPosition, EnumDirection enumDirection) {
+        if (blockPosition.getY() <= maxBlockYUpdatePosition) {
+            updateNearbyBlocks(playerInteractManager.world, blockPosition);
+        }
+    }
+
+    private void updateNearbyBlocks(World world, BlockPosition blockPosition) {
+        if (updateRadius >= 2) {
+            BlockPosition temp = blockPosition.west();
+            updateBlock(world, temp);
+            updateBlock(world, temp.west());
+            updateBlock(world, temp.down());
+            updateBlock(world, temp.up());
+            updateBlock(world, temp.north());
+            updateBlock(world, temp.south());
+            updateBlock(world, temp = blockPosition.east());
+            updateBlock(world, temp.east());
+            updateBlock(world, temp.down());
+            updateBlock(world, temp.up());
+            updateBlock(world, temp.north());
+            updateBlock(world, temp.south());
+            updateBlock(world, temp = blockPosition.down());
+            updateBlock(world, temp.down());
+            updateBlock(world, temp.north());
+            updateBlock(world, temp.south());
+            updateBlock(world, temp = blockPosition.up());
+            updateBlock(world, temp.up());
+            updateBlock(world, temp.north());
+            updateBlock(world, temp.south());
+            updateBlock(world, temp = blockPosition.north());
+            updateBlock(world, temp.north());
+            updateBlock(world, temp = blockPosition.south());
+            updateBlock(world, temp.south());
+        } else if (updateRadius == 1) {
+            updateBlock(world, blockPosition.west());
+            updateBlock(world, blockPosition.east());
+            updateBlock(world, blockPosition.down());
+            updateBlock(world, blockPosition.up());
+            updateBlock(world, blockPosition.north());
+            updateBlock(world, blockPosition.south());
+        } else {
+            //Do nothing if updateRadius <= 0 (test mode)
+        }
+    }
+
+    private void updateBlock(World world, BlockPosition blockPosition) {
+        IBlockData blockData = world.getTypeIfLoaded(blockPosition);
+
+        if (blockData != null && obfuscateGlobal[ChunkSection.GLOBAL_PALETTE.getOrCreateIdFor(blockData)]) {
+            //world.notify(blockPosition, blockData, blockData, 3);
+            ((WorldServer)world).getChunkProvider().flagDirty(blockPosition); // We only need to re-send to client
+        }
+    }
+
+    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;
+        }
+    }
+
+    public enum ChunkEdgeMode {
+
+        DEFAULT(1, "default"),
+        WAIT(2, "wait until nearby chunks are loaded"),
+        LOAD(3, "load nearby chunks");
+
+        private final int id;
+        private final String description;
+
+        ChunkEdgeMode(int id, String description) {
+            this.id = id;
+            this.description = description;
+        }
+
+        public static ChunkEdgeMode getById(int id) {
+            for (ChunkEdgeMode chunkEdgeMode : values()) {
+                if (chunkEdgeMode.id == id) {
+                    return chunkEdgeMode;
+                }
+            }
+
+            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 0000000000..a68bace353
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java
@@ -0,0 +1,81 @@
+package com.destroystokyo.paper.antixray;
+
+import net.minecraft.server.Chunk;
+import net.minecraft.server.DataPalette;
+import net.minecraft.server.PacketPlayOutMapChunk;
+
+public class ChunkPacketInfo<T> {
+
+    private final PacketPlayOutMapChunk packetPlayOutMapChunk;
+    private final Chunk chunk;
+    private final int chunkSectionSelector;
+    private byte[] data;
+    private final int[] bitsPerObject = new int[16];
+    private final Object[] dataPalettes = new Object[16];
+    private final int[] dataBitsIndexes = new int[16];
+    private final Object[][] predefinedObjects = new Object[16][];
+
+    public ChunkPacketInfo(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk, int chunkSectionSelector) {
+        this.packetPlayOutMapChunk = packetPlayOutMapChunk;
+        this.chunk = chunk;
+        this.chunkSectionSelector = chunkSectionSelector;
+    }
+
+    public PacketPlayOutMapChunk getPacketPlayOutMapChunk() {
+        return packetPlayOutMapChunk;
+    }
+
+    public Chunk getChunk() {
+        return chunk;
+    }
+
+    public int getChunkSectionSelector() {
+        return chunkSectionSelector;
+    }
+
+    public byte[] getData() {
+        return data;
+    }
+
+    public void setData(byte[] data) {
+        this.data = data;
+    }
+
+    public int getBitsPerObject(int chunkSectionIndex) {
+        return bitsPerObject[chunkSectionIndex];
+    }
+
+    public void setBitsPerObject(int chunkSectionIndex, int bitsPerObject) {
+        this.bitsPerObject[chunkSectionIndex] = bitsPerObject;
+    }
+
+    @SuppressWarnings("unchecked")
+    public DataPalette<T> getDataPalette(int chunkSectionIndex) {
+        return (DataPalette<T>) dataPalettes[chunkSectionIndex];
+    }
+
+    public void setDataPalette(int chunkSectionIndex, DataPalette<T> dataPalette) {
+        dataPalettes[chunkSectionIndex] = dataPalette;
+    }
+
+    public int getOrCreateIdForIndex(int chunkSectionIndex) {
+        return dataBitsIndexes[chunkSectionIndex];
+    }
+
+    public void setDataBitsIndex(int chunkSectionIndex, int dataBitsIndex) {
+        dataBitsIndexes[chunkSectionIndex] = dataBitsIndex;
+    }
+
+    @SuppressWarnings("unchecked")
+    public T[] getPredefinedObjects(int chunkSectionIndex) {
+        return (T[]) predefinedObjects[chunkSectionIndex];
+    }
+
+    public void setPredefinedObjects(int chunkSectionIndex, T[] predefinedObjects) {
+        this.predefinedObjects[chunkSectionIndex] = predefinedObjects;
+    }
+
+    public boolean isWritten(int chunkSectionIndex) {
+        return bitsPerObject[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 0000000000..067dfb2f14
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java
@@ -0,0 +1,31 @@
+package com.destroystokyo.paper.antixray;
+
+import net.minecraft.server.Chunk;
+import net.minecraft.server.IBlockData;
+import net.minecraft.server.PacketPlayOutMapChunk;
+
+public class ChunkPacketInfoAntiXray extends ChunkPacketInfo<IBlockData> implements Runnable {
+
+    private Chunk[] nearbyChunks;
+    private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray;
+    public Integer ticketHold;
+
+    public ChunkPacketInfoAntiXray(PacketPlayOutMapChunk packetPlayOutMapChunk, Chunk chunk, int chunkSectionSelector,
+                                   ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) {
+        super(packetPlayOutMapChunk, chunk, chunkSectionSelector);
+        this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray;
+    }
+
+    public Chunk[] getNearbyChunks() {
+        return nearbyChunks;
+    }
+
+    public void setNearbyChunks(Chunk... nearbyChunks) {
+        this.nearbyChunks = nearbyChunks;
+    }
+
+    @Override
+    public void run() {
+        chunkPacketBlockControllerAntiXray.obfuscate(this);
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/antixray/DataBitsReader.java b/src/main/java/com/destroystokyo/paper/antixray/DataBitsReader.java
new file mode 100644
index 0000000000..cc586827aa
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/DataBitsReader.java
@@ -0,0 +1,56 @@
+package com.destroystokyo.paper.antixray;
+
+public class DataBitsReader {
+
+    private byte[] dataBits;
+    private int bitsPerObject;
+    private int mask;
+    private int longInDataBitsIndex;
+    private int bitInLongIndex;
+    private long current;
+
+    public void setDataBits(byte[] dataBits) {
+        this.dataBits = dataBits;
+    }
+
+    public void setBitsPerObject(int bitsPerObject) {
+        this.bitsPerObject = bitsPerObject;
+        mask = (1 << bitsPerObject) - 1;
+    }
+
+    public void setIndex(int index) {
+        this.longInDataBitsIndex = index;
+        bitInLongIndex = 0;
+        init();
+    }
+
+    private void init() {
+        if (dataBits.length > longInDataBitsIndex + 7) {
+            current = ((((long) dataBits[longInDataBitsIndex]) << 56)
+                    | (((long) dataBits[longInDataBitsIndex + 1] & 0xff) << 48)
+                    | (((long) dataBits[longInDataBitsIndex + 2] & 0xff) << 40)
+                    | (((long) dataBits[longInDataBitsIndex + 3] & 0xff) << 32)
+                    | (((long) dataBits[longInDataBitsIndex + 4] & 0xff) << 24)
+                    | (((long) dataBits[longInDataBitsIndex + 5] & 0xff) << 16)
+                    | (((long) dataBits[longInDataBitsIndex + 6] & 0xff) << 8)
+                    | (((long) dataBits[longInDataBitsIndex + 7] & 0xff)));
+        }
+    }
+
+    public int read() {
+        int value = (int) (current >>> bitInLongIndex) & mask;
+        bitInLongIndex += bitsPerObject;
+
+        if (bitInLongIndex > 63) {
+            bitInLongIndex -= 64;
+            longInDataBitsIndex += 8;
+            init();
+
+            if (bitInLongIndex > 0) {
+                value |= current << bitsPerObject - bitInLongIndex & mask;
+            }
+        }
+
+        return value;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/antixray/DataBitsWriter.java b/src/main/java/com/destroystokyo/paper/antixray/DataBitsWriter.java
new file mode 100644
index 0000000000..37093419cf
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/antixray/DataBitsWriter.java
@@ -0,0 +1,84 @@
+package com.destroystokyo.paper.antixray;
+
+public class DataBitsWriter {
+
+    private byte[] dataBits;
+    private int bitsPerObject;
+    private long mask;
+    private int longInDataBitsIndex;
+    private int bitInLongIndex;
+    private long current;
+    private boolean dirty;
+
+    public void setDataBits(byte[] dataBits) {
+        this.dataBits = dataBits;
+    }
+
+    public void setBitsPerObject(int bitsPerObject) {
+        this.bitsPerObject = bitsPerObject;
+        mask = (1 << bitsPerObject) - 1;
+    }
+
+    public void setIndex(int index) {
+        this.longInDataBitsIndex = index;
+        bitInLongIndex = 0;
+        init();
+    }
+
+    private void init() {
+        if (dataBits.length > longInDataBitsIndex + 7) {
+            current = ((((long) dataBits[longInDataBitsIndex]) << 56)
+                    | (((long) dataBits[longInDataBitsIndex + 1] & 0xff) << 48)
+                    | (((long) dataBits[longInDataBitsIndex + 2] & 0xff) << 40)
+                    | (((long) dataBits[longInDataBitsIndex + 3] & 0xff) << 32)
+                    | (((long) dataBits[longInDataBitsIndex + 4] & 0xff) << 24)
+                    | (((long) dataBits[longInDataBitsIndex + 5] & 0xff) << 16)
+                    | (((long) dataBits[longInDataBitsIndex + 6] & 0xff) << 8)
+                    | (((long) dataBits[longInDataBitsIndex + 7] & 0xff)));
+        }
+
+        dirty = false;
+    }
+
+    public void finish() {
+        if (dirty && dataBits.length > longInDataBitsIndex + 7) {
+            dataBits[longInDataBitsIndex] = (byte) (current >> 56 & 0xff);
+            dataBits[longInDataBitsIndex + 1] = (byte) (current >> 48 & 0xff);
+            dataBits[longInDataBitsIndex + 2] = (byte) (current >> 40 & 0xff);
+            dataBits[longInDataBitsIndex + 3] = (byte) (current >> 32 & 0xff);
+            dataBits[longInDataBitsIndex + 4] = (byte) (current >> 24 & 0xff);
+            dataBits[longInDataBitsIndex + 5] = (byte) (current >> 16 & 0xff);
+            dataBits[longInDataBitsIndex + 6] = (byte) (current >> 8 & 0xff);
+            dataBits[longInDataBitsIndex + 7] = (byte) (current & 0xff);
+        }
+    }
+
+    public void write(int value) {
+        current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex;
+        dirty = true;
+        bitInLongIndex += bitsPerObject;
+
+        if (bitInLongIndex > 63) {
+            finish();
+            bitInLongIndex -= 64;
+            longInDataBitsIndex += 8;
+            init();
+
+            if (bitInLongIndex > 0) {
+                current = current & ~(mask >>> bitsPerObject - bitInLongIndex) | (value & mask) >>> bitsPerObject - bitInLongIndex;
+                dirty = true;
+            }
+        }
+    }
+
+    public void skip() {
+        bitInLongIndex += bitsPerObject;
+
+        if (bitInLongIndex > 63) {
+            finish();
+            bitInLongIndex -= 64;
+            longInDataBitsIndex += 8;
+            init();
+        }
+    }
+}
diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java
index af0d6aff4d..472d3a4c03 100644
--- a/src/main/java/net/minecraft/server/Chunk.java
+++ b/src/main/java/net/minecraft/server/Chunk.java
@@ -416,7 +416,7 @@ public class Chunk implements IChunkAccess {
                 return null;
             }
 
-            chunksection = new ChunkSection(j >> 4 << 4);
+            chunksection = new ChunkSection(j >> 4 << 4, this, this.world, true); // Paper - Anti-Xray
             this.sections[j >> 4] = chunksection;
         }
 
diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
index 8e4b3e52cb..79e85520f3 100644
--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java
+++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
@@ -57,7 +57,7 @@ public class ChunkRegionLoader {
             byte b0 = nbttagcompound2.getByte("Y");
 
             if (nbttagcompound2.hasKeyOfType("Palette", 9) && nbttagcompound2.hasKeyOfType("BlockStates", 12)) {
-                ChunkSection chunksection = new ChunkSection(b0 << 4);
+                ChunkSection chunksection = new ChunkSection(b0 << 4, null, worldserver, false); // Paper - Anti-Xray
 
                 chunksection.getBlocks().a(nbttagcompound2.getList("Palette", 10), nbttagcompound2.getLongArray("BlockStates"));
                 chunksection.recalcBlockCounts();
@@ -115,7 +115,7 @@ public class ChunkRegionLoader {
                 loadEntities(nbttagcompound1, chunk);
             });
         } else {
-            ProtoChunk protochunk = new ProtoChunk(chunkcoordintpair, chunkconverter, achunksection, protochunkticklist, protochunkticklist1);
+            ProtoChunk protochunk = new ProtoChunk(chunkcoordintpair, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, worldserver); // Paper - Anti-Xray
 
             protochunk.a(biomestorage);
             object = protochunk;
diff --git a/src/main/java/net/minecraft/server/ChunkSection.java b/src/main/java/net/minecraft/server/ChunkSection.java
index 0d5deee365..4526527aca 100644
--- a/src/main/java/net/minecraft/server/ChunkSection.java
+++ b/src/main/java/net/minecraft/server/ChunkSection.java
@@ -6,21 +6,31 @@ public class ChunkSection {
 
     public static final DataPalette<IBlockData> GLOBAL_PALETTE = new DataPaletteGlobal<>(Block.REGISTRY_ID, Blocks.AIR.getBlockData());
     private final int yPos;
-    private short nonEmptyBlockCount;
+    short nonEmptyBlockCount; // Paper - private -> package-private
     private short tickingBlockCount;
     private short e;
     final DataPaletteBlock<IBlockData> blockIds;
 
     public ChunkSection(int i) {
-        this(i, (short) 0, (short) 0, (short) 0);
+        // Paper start - add parameters
+        this(i, (IChunkAccess)null, (IWorldReader)null, true);
+    }
+    public ChunkSection(int i, IChunkAccess chunk, IWorldReader world, boolean initializeBlocks) {
+        this(i, (short) 0, (short) 0, (short) 0, chunk, world, initializeBlocks);
+        // Paper end
     }
 
     public ChunkSection(int i, short short0, short short1, short short2) {
+        // Paper start - add parameters
+        this(i, short0, short1, short2, (IChunkAccess)null, (IWorldReader)null, true);
+    }
+    public ChunkSection(int i, short short0, short short1, short short2, IChunkAccess chunk, IWorldReader world, boolean initializeBlocks) {
+        // Paper end
         this.yPos = i;
         this.nonEmptyBlockCount = short0;
         this.tickingBlockCount = short1;
         this.e = short2;
-        this.blockIds = new DataPaletteBlock<>(ChunkSection.GLOBAL_PALETTE, Block.REGISTRY_ID, GameProfileSerializer::d, GameProfileSerializer::a, Blocks.AIR.getBlockData());
+        this.blockIds = new DataPaletteBlock<>(ChunkSection.GLOBAL_PALETTE, Block.REGISTRY_ID, GameProfileSerializer::d, GameProfileSerializer::a, Blocks.AIR.getBlockData(), world instanceof GeneratorAccess ? ((GeneratorAccess) world).getMinecraftWorld().chunkPacketBlockController.getPredefinedBlockData(world, chunk, this, initializeBlocks) : null, initializeBlocks); // Paper - Anti-Xray - Add predefined block data
     }
 
     public IBlockData getType(int i, int j, int k) {
diff --git a/src/main/java/net/minecraft/server/DataPaletteBlock.java b/src/main/java/net/minecraft/server/DataPaletteBlock.java
index 2c1d1b1a55..44aed67274 100644
--- a/src/main/java/net/minecraft/server/DataPaletteBlock.java
+++ b/src/main/java/net/minecraft/server/DataPaletteBlock.java
@@ -3,6 +3,7 @@ package net.minecraft.server;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2IntMap.Entry;
+import com.destroystokyo.paper.antixray.ChunkPacketInfo; // Paper - Anti-Xray
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.concurrent.locks.ReentrantLock;
@@ -19,6 +20,7 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
     private final Function<NBTTagCompound, T> e;
     private final Function<T, NBTTagCompound> f;
     private final T g;
+    private final T[] predefinedObjects; // Paper - Anti-Xray - Add predefined objects
     protected DataBits a; protected DataBits getDataBits() { return this.a; } // Paper - OBFHELPER
     private DataPalette<T> h; private DataPalette<T> getDataPalette() { return this.h; } // Paper - OBFHELPER
     private int i; private int getBitsPerObject() { return this.i; } // Paper - OBFHELPER
@@ -47,14 +49,50 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
     }
 
     public DataPaletteBlock(DataPalette<T> datapalette, RegistryBlockID<T> registryblockid, Function<NBTTagCompound, T> function, Function<T, NBTTagCompound> function1, T t0) {
+        // Paper start - Anti-Xray - Support default constructor
+        this(datapalette, registryblockid, function, function1, t0, null, true);
+    }
+
+    public DataPaletteBlock(DataPalette<T> datapalette, RegistryBlockID<T> registryblockid, Function<NBTTagCompound, T> function, Function<T, NBTTagCompound> function1, T t0, T[] predefinedObjects, boolean initialize) {
+        // Paper end - Anti-Xray - Add predefined objects
         this.b = datapalette;
         this.d = registryblockid;
         this.e = function;
         this.f = function1;
         this.g = t0;
-        this.b(4);
+        // Paper start - Anti-Xray - Add predefined objects
+        this.predefinedObjects = predefinedObjects;
+
+        if (initialize) {
+            if (predefinedObjects == null) {
+                // Default
+                this.initialize(4);
+            } else {
+                // MathHelper.d() is trailingBits(roundCeilPow2(n)), alternatively; (int)ceil(log2(n)); however it's trash, use numberOfLeadingZeros instead
+                // Count the bits of the maximum array index to initialize a data palette with enough space from the beginning
+                // The length of the array is used because air is also added to the data palette from the beginning
+                // Start with at least 4
+                int maxIndex = predefinedObjects.length >> 4;
+                int bitCount = (32 - Integer.numberOfLeadingZeros(Math.max(16, maxIndex) - 1));
+
+                // Initialize with at least 15 free indixes
+                this.initialize((1 << bitCount) - predefinedObjects.length < 16 ? bitCount + 1 : bitCount);
+                this.addPredefinedObjects();
+            }
+        }
+        // Paper end
     }
 
+    // Paper start - Anti-Xray - Add predefined objects
+    private void addPredefinedObjects() {
+        if (this.predefinedObjects != null && this.getDataPalette() != this.getDataPaletteGlobal()) {
+            for (int i = 0; i < this.predefinedObjects.length; i++) {
+                this.getDataPalette().getOrCreateIdFor(this.predefinedObjects[i]);
+            }
+        }
+    }
+    // Paper end
+
     private static int b(int i, int j, int k) {
         return j << 8 | k << 4 | i;
     }
@@ -88,6 +126,7 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
 
         int j;
 
+        this.addPredefinedObjects(); // Paper - Anti-Xray - Add predefined objects
         for (j = 0; j < databits.b(); ++j) {
             T t1 = datapalette.a(databits.a(j));
 
@@ -139,22 +178,39 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
 
     public void writeDataPaletteBlock(PacketDataSerializer packetDataSerializer) { this.b(packetDataSerializer); } // Paper - OBFHELPER
     public void b(PacketDataSerializer packetdataserializer) {
+        // Paper start - add parameters
+        this.writeDataPaletteBlock(packetdataserializer, null, 0);
+    }
+    public void writeDataPaletteBlock(PacketDataSerializer packetdataserializer, ChunkPacketInfo<T> chunkPacketInfo, int chunkSectionIndex) {
+        // Paper end
         this.a();
         packetdataserializer.writeByte(this.i);
         this.h.b(packetdataserializer);
+
+        // Paper start - Anti-Xray - Add chunk packet info
+        if (chunkPacketInfo != null) {
+            chunkPacketInfo.setBitsPerObject(chunkSectionIndex, this.getBitsPerObject());
+            chunkPacketInfo.setDataPalette(chunkSectionIndex, this.getDataPalette());
+            chunkPacketInfo.setDataBitsIndex(chunkSectionIndex, packetdataserializer.writerIndex() + PacketDataSerializer.countBytes(this.getDataBits().getDataBits().length));
+            chunkPacketInfo.setPredefinedObjects(chunkSectionIndex, this.predefinedObjects);
+        }
+        // Paper end
+
         packetdataserializer.a(this.a.a());
         this.b();
     }
 
     public void a(NBTTagList nbttaglist, long[] along) {
         this.a();
-        int i = Math.max(4, MathHelper.d(nbttaglist.size()));
+        // Paper - Anti-Xray - TODO: Should this.predefinedObjects.length just be added here (faster) or should the contents be compared to calculate the size (less RAM)?
+        int i = Math.max(4, MathHelper.d(nbttaglist.size() + (this.predefinedObjects == null ? 0 : this.predefinedObjects.length))); // Paper - Anti-Xray - Calculate the size with predefined objects
 
-        if (i != this.i) {
+        if (true || i != this.i) { // Paper - Anti-Xray - Not initialized yet
             this.b(i);
         }
 
         this.h.a(nbttaglist);
+        this.addPredefinedObjects(); // Paper - Anti-Xray - Add predefined objects
         int j = along.length * 64 / 4096;
 
         if (this.h == this.b) {
diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java
index 02a9f3d5fa..55441e1002 100644
--- a/src/main/java/net/minecraft/server/NetworkManager.java
+++ b/src/main/java/net/minecraft/server/NetworkManager.java
@@ -188,6 +188,11 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
 
     public void sendPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericfuturelistener) {
         // Paper start - handle oversized packets better
+        // Special case keepalive, allow it to go out of queue order
+        if (packet instanceof PacketPlayOutKeepAlive && this.isConnected()) {
+            this.dispatchPacket(packet, genericfuturelistener);
+            return;
+        }
         // write the packets to the queue, then flush - antixray hooks there already
         java.util.List<Packet> extraPackets = InnerUtil.buildExtraPackets(packet);
         boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty();
@@ -250,21 +255,32 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
 
     }
 
-    private void sendPacketQueue() { this.o(); } // Paper - OBFHELPER
-    private void o() {
+    // Paper start - Async-Anti-Xray - Stop dispatching further packets and return false if the peeked packet is a chunk packet which is not ready
+    private boolean sendPacketQueue() { return this.o(); } // OBFHELPER // void -> boolean
+    private boolean o() { // void -> boolean
         if (this.channel != null && this.channel.isOpen()) {
             Queue queue = this.packetQueue;
 
             synchronized (this.packetQueue) {
-                NetworkManager.QueuedPacket networkmanager_queuedpacket;
-
-                while ((networkmanager_queuedpacket = (NetworkManager.QueuedPacket) this.packetQueue.poll()) != null) {
-                    this.b(networkmanager_queuedpacket.a, networkmanager_queuedpacket.b);
+                while (!this.packetQueue.isEmpty()) {
+                    NetworkManager.QueuedPacket networkmanager_queuedpacket = (NetworkManager.QueuedPacket) queue.peek(); // poll -> peek
+
+                    if (networkmanager_queuedpacket != null) { // Fix NPE (Spigot bug caused by handleDisconnection())
+                        if (networkmanager_queuedpacket.getPacket() instanceof PacketPlayOutMapChunk && !((PacketPlayOutMapChunk) networkmanager_queuedpacket.getPacket()).isReady()) { // Check if the peeked packet is a chunk packet which is not ready
+                            return false; // Return false if the peeked packet is a chunk packet which is not ready
+                        } else {
+                            queue.poll(); // poll here
+                            this.dispatchPacket(networkmanager_queuedpacket.getPacket(), networkmanager_queuedpacket.getGenericFutureListener()); // dispatch the packet
+                        }
+                    }
                 }
 
             }
         }
+
+        return true; // Return true if all packets were dispatched
     }
+    // Paper end
 
     public void a() {
         this.o();
diff --git a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
index 23223f3f45..e54663c214 100644
--- a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
+++ b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
@@ -1,5 +1,6 @@
 package net.minecraft.server;
 
+import com.destroystokyo.paper.antixray.ChunkPacketInfo; // Paper - Anti-Xray
 import com.google.common.collect.Lists;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
@@ -20,8 +21,11 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
     private byte[] f; private byte[] getData() { return this.f; } // Paper - OBFHELPER
     private List<NBTTagCompound> g;
     private boolean h;
+    private volatile boolean ready; // Paper - Async-Anti-Xray - Ready flag for the network manager
 
-    public PacketPlayOutMapChunk() {}
+    public PacketPlayOutMapChunk() {
+        this.ready = true; // Paper - Async-Anti-Xray - Set the ready flag to true
+    }
 
     // Paper start
     private final java.util.List<Packet> extraPackets = new java.util.ArrayList<>();
@@ -33,6 +37,12 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
     }
     // Paper end
     public PacketPlayOutMapChunk(Chunk chunk, int i) {
+        // Paper start - add forceLoad param
+        this(chunk, i, false);
+    }
+    public PacketPlayOutMapChunk(Chunk chunk, int i, boolean forceLoad) {
+        // Paper end
+        ChunkPacketInfo<IBlockData> chunkPacketInfo = chunk.world.chunkPacketBlockController.getChunkPacketInfo(this, chunk, i, forceLoad); // Paper - Anti-Xray - Add chunk packet info
         ChunkCoordIntPair chunkcoordintpair = chunk.getPos();
 
         this.a = chunkcoordintpair.x;
@@ -55,7 +65,12 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
         }
 
         this.f = new byte[this.a(chunk, i)];
-        this.c = this.a(new PacketDataSerializer(this.j()), chunk, i);
+        // Paper start - Anti-Xray - Add chunk packet info
+        if (chunkPacketInfo != null) {
+            chunkPacketInfo.setData(this.getData());
+        }
+        // Paper end
+        this.c = this.writeChunk(new PacketDataSerializer(this.j()), chunk, i, chunkPacketInfo); // Paper - Anti-Xray - Add chunk packet info
         this.g = Lists.newArrayList();
         iterator = chunk.getTileEntities().entrySet().iterator();
         int totalTileEntities = 0; // Paper
@@ -82,9 +97,19 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
                 this.g.add(nbttagcompound);
             }
         }
+        chunk.world.chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo, forceLoad, null); // Paper - Anti-Xray - Modify blocks
+    }
 
+    // Paper start - Async-Anti-Xray - Getter and Setter for the ready flag
+    public boolean isReady() {
+        return this.ready;
     }
 
+    public void setReady(boolean ready) {
+        this.ready = ready;
+    }
+    // Paper end
+
     @Override
     public void a(PacketDataSerializer packetdataserializer) throws IOException {
         this.a = packetdataserializer.readInt();
@@ -151,6 +176,11 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
 
     public int writeChunk(PacketDataSerializer packetDataSerializer, Chunk chunk, int chunkSectionSelector) { return this.a(packetDataSerializer, chunk, chunkSectionSelector); } // Paper - OBFHELPER
     public int a(PacketDataSerializer packetdataserializer, Chunk chunk, int i) {
+        // Paper start - Add parameter
+        return this.writeChunk(packetdataserializer, chunk, i, null);
+    }
+    public int writeChunk(PacketDataSerializer packetdataserializer, Chunk chunk, int i, ChunkPacketInfo<IBlockData> chunkPacketInfo) {
+        // Paper end
         int j = 0;
         ChunkSection[] achunksection = chunk.getSections();
         int k = 0;
@@ -160,7 +190,8 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
 
             if (chunksection != Chunk.a && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) {
                 j |= 1 << k;
-                chunksection.b(packetdataserializer);
+                packetdataserializer.writeShort(chunksection.nonEmptyBlockCount); // Paper - Anti-Xray - Add chunk packet info
+                chunksection.getBlocks().writeDataPaletteBlock(packetdataserializer, chunkPacketInfo, k); // Paper - Anti-Xray - Add chunk packet info
             }
         }
 
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
index 040d4b41ea..f1620ba80e 100644
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
@@ -223,6 +223,11 @@ public class PlayerChunk {
             World world = chunk.getWorld();
 
             if (this.dirtyCount == 64) {
+                // Paper start - Anti-Xray - Load nearby chunks if necessary
+                if (!chunk.world.chunkPacketBlockController.onChunkPacketCreate(chunk, '\uffff', false)) {
+                    return;
+                }
+                // Paper end
                 this.s = -1;
             }
 
@@ -255,7 +260,7 @@ public class PlayerChunk {
                     this.a(world, blockposition);
                 }
             } else if (this.dirtyCount == 64) {
-                this.a(new PacketPlayOutMapChunk(chunk, this.r), false);
+                this.a(new PacketPlayOutMapChunk(chunk, this.r, true), false); // Paper - Anti-Xray
             } else if (this.dirtyCount != 0) {
                 this.a(new PacketPlayOutMultiBlockChange(this.dirtyCount, this.dirtyBlocks, chunk), false);
 
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
index 9171785ad5..eb29d0e956 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -604,7 +604,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
                 PlayerChunkMap.LOGGER.error("Couldn't load chunk {}", chunkcoordintpair, exception);
             }
 
-            return Either.left(new ProtoChunk(chunkcoordintpair, ChunkConverter.a));
+            return Either.left(new ProtoChunk(chunkcoordintpair, ChunkConverter.a, this.world)); // Paper - Anti-Xray
         }, this.executor);
     }
 
@@ -1325,7 +1325,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
 
     private void a(EntityPlayer entityplayer, Packet<?>[] apacket, Chunk chunk) {
         if (apacket[0] == null) {
-            apacket[0] = new PacketPlayOutMapChunk(chunk, 65535);
+            apacket[0] = new PacketPlayOutMapChunk(chunk, 65535, true); // Paper - Anti-Xray
             apacket[1] = new PacketPlayOutLightUpdate(chunk.getPos(), this.lightEngine);
         }
 
diff --git a/src/main/java/net/minecraft/server/PlayerInteractManager.java b/src/main/java/net/minecraft/server/PlayerInteractManager.java
index 17b7eddac4..ce66090b8d 100644
--- a/src/main/java/net/minecraft/server/PlayerInteractManager.java
+++ b/src/main/java/net/minecraft/server/PlayerInteractManager.java
@@ -266,6 +266,8 @@ public class PlayerInteractManager {
             }
 
         }
+
+        this.world.chunkPacketBlockController.onPlayerLeftClickBlock(this, blockposition, enumdirection); // Paper - Anti-Xray
     }
 
     public void a(BlockPosition blockposition, PacketPlayInBlockDig.EnumPlayerDigType packetplayinblockdig_enumplayerdigtype, String s) {
diff --git a/src/main/java/net/minecraft/server/ProtoChunk.java b/src/main/java/net/minecraft/server/ProtoChunk.java
index 39339fa275..f376e21068 100644
--- a/src/main/java/net/minecraft/server/ProtoChunk.java
+++ b/src/main/java/net/minecraft/server/ProtoChunk.java
@@ -45,16 +45,28 @@ public class ProtoChunk implements IChunkAccess {
     private long s;
     private final Map<WorldGenStage.Features, BitSet> t;
     private volatile boolean u;
+    private final GeneratorAccess world; // Paper - Anti-Xray
 
     public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter) {
+        // Paper start - add world parameter
+        this(chunkcoordintpair, chunkconverter, (GeneratorAccess)null);
+    }
+    public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter, GeneratorAccess world) {
+        // Paper end
         this(chunkcoordintpair, chunkconverter, (ChunkSection[]) null, new ProtoChunkTickList<>((block) -> {
             return block == null || block.getBlockData().isAir();
         }, chunkcoordintpair), new ProtoChunkTickList<>((fluidtype) -> {
             return fluidtype == null || fluidtype == FluidTypes.EMPTY;
-        }, chunkcoordintpair));
+        }, chunkcoordintpair), world); // Paper - add world parameter
     }
 
     public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter, @Nullable ChunkSection[] achunksection, ProtoChunkTickList<Block> protochunkticklist, ProtoChunkTickList<FluidType> protochunkticklist1) {
+        // Paper start - add world parameter
+        this(chunkcoordintpair, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, (GeneratorAccess)null);
+    }
+    public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter, @Nullable ChunkSection[] achunksection, ProtoChunkTickList<Block> protochunkticklist, ProtoChunkTickList<FluidType> protochunkticklist1, GeneratorAccess world) {
+        this.world = world;
+        // Paper end
         this.f = Maps.newEnumMap(HeightMap.Type.class);
         this.g = ChunkStatus.EMPTY;
         this.h = Maps.newHashMap();
@@ -207,7 +219,7 @@ public class ProtoChunk implements IChunkAccess {
 
     public ChunkSection a(int i) {
         if (this.j[i] == Chunk.a) {
-            this.j[i] = new ChunkSection(i << 4);
+            this.j[i] = new ChunkSection(i << 4, this, this.world, true); // Paper - Anti-Xray
         }
 
         return this.j[i];
diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
index 75ab9f185b..4cf28bc2df 100644
--- a/src/main/java/net/minecraft/server/TicketType.java
+++ b/src/main/java/net/minecraft/server/TicketType.java
@@ -22,6 +22,7 @@ public class TicketType<T> {
     public static final TicketType<Unit> PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit
     public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
     public static final TicketType<Long> FUTURE_AWAIT = a("future_await", Long::compareTo); // Paper
+    public static final TicketType<Integer> ANTIXRAY = a("antixray", Integer::compareTo); // Paper - Anti-Xray
 
     public static <T> TicketType<T> a(String s, Comparator<T> comparator) {
         return new TicketType<>(s, comparator, 0L);
diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java
index 8cf3c10274..0bde171743 100644
--- a/src/main/java/net/minecraft/server/World.java
+++ b/src/main/java/net/minecraft/server/World.java
@@ -2,6 +2,8 @@ package net.minecraft.server;
 
 import co.aikar.timings.Timing;
 import co.aikar.timings.Timings;
+import com.destroystokyo.paper.antixray.ChunkPacketBlockController; // Paper - Anti-Xray
+import com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray; // Paper - Anti-Xray
 import com.destroystokyo.paper.event.server.ServerExceptionEvent;
 import com.destroystokyo.paper.exception.ServerInternalException;
 import com.google.common.base.MoreObjects;
@@ -78,6 +80,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
     public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
 
     public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper
+    public final ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray
 
     public final co.aikar.timings.WorldTimingsHandler timings; // Paper
     public static BlockPosition lastPhysicsProblem; // Spigot
@@ -119,6 +122,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
     protected World(WorldData worlddata, DimensionManager dimensionmanager, BiFunction<World, WorldProvider, IChunkProvider> bifunction, GameProfilerFiller gameprofilerfiller, boolean flag, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env) {
         this.spigotConfig = new org.spigotmc.SpigotWorldConfig( worlddata.getName() ); // Spigot
         this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(worlddata.getName(), this.spigotConfig); // Paper
+        this.chunkPacketBlockController = this.paperConfig.antiXray ? new ChunkPacketBlockControllerAntiXray(this.paperConfig) : ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
         this.generator = gen;
         this.world = new CraftWorld((WorldServer) this, gen, env);
         this.ticksPerAnimalSpawns = this.getServer().getTicksPerAnimalSpawns(); // CraftBukkit
@@ -342,6 +346,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
             // CraftBukkit end
 
             IBlockData iblockdata1 = chunk.setType(blockposition, iblockdata, (i & 64) != 0, (i & 1024) == 0); // CraftBukkit custom NO_PLACE flag
+            this.chunkPacketBlockController.onBlockChange(this, blockposition, iblockdata, iblockdata1, i); // Paper - Anti-Xray
 
             if (iblockdata1 == null) {
                 // CraftBukkit start - remove blockstate if failed (or the same)
diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CraftChunkData.java b/src/main/java/org/bukkit/craftbukkit/generator/CraftChunkData.java
index 8191e7c348..969d548de2 100644
--- a/src/main/java/org/bukkit/craftbukkit/generator/CraftChunkData.java
+++ b/src/main/java/org/bukkit/craftbukkit/generator/CraftChunkData.java
@@ -21,9 +21,11 @@ public final class CraftChunkData implements ChunkGenerator.ChunkData {
     private final int maxHeight;
     private final ChunkSection[] sections;
     private Set<BlockPosition> tiles;
+    private World world; // Paper - Anti-Xray
 
     public CraftChunkData(World world) {
         this(world.getMaxHeight());
+        this.world = world; // Paper - Anti-Xray
     }
 
     /* pp for tests */ CraftChunkData(int maxHeight) {
@@ -157,7 +159,7 @@ public final class CraftChunkData implements ChunkGenerator.ChunkData {
     private ChunkSection getChunkSection(int y, boolean create) {
         ChunkSection section = sections[y >> 4];
         if (create && section == null) {
-            sections[y >> 4] = section = new ChunkSection(y >> 4 << 4);
+            sections[y >> 4] = section = new ChunkSection(y >> 4 << 4, null, world instanceof org.bukkit.craftbukkit.CraftWorld ? ((org.bukkit.craftbukkit.CraftWorld) world).getHandle() : null, true); // Paper - Anti-Xray
         }
         return section;
     }
-- 
2.26.0