0001-Add-custom-per-block-replacement.patch
@@ -0,0 +1,721 @@
+From 9ef96155077f5830f6f2e3ea3605a6795438418f Mon Sep 17 00:00:00 2001
+From: stonar96 <[email protected]>
+Date: Thu, 2 Feb 2023 15:35:24 +0100
+Subject: [PATCH] Add custom per block replacement
+
+
+diff --git a/RayTraceAntiXray/src/main/java/com/vanillage/raytraceantixray/antixray/ChunkPacketBlockControllerAntiXray.java b/RayTraceAntiXray/src/main/java/com/vanillage/raytraceantixray/antixray/ChunkPacketBlockControllerAntiXray.java
+index b3cd0ff..bc7d9f1 100644
+--- a/RayTraceAntiXray/src/main/java/com/vanillage/raytraceantixray/antixray/ChunkPacketBlockControllerAntiXray.java
++++ b/RayTraceAntiXray/src/main/java/com/vanillage/raytraceantixray/antixray/ChunkPacketBlockControllerAntiXray.java
+@@ -58,8 +58,8 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ private final int[] presetBlockStateBitsNetherrackGlobal;
+ private final int[] presetBlockStateBitsEndStoneGlobal;
+ public final boolean[] solidGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
+- private final boolean[] obfuscateGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
+- private final boolean[] traceGlobal;
++ private final int[] obfuscateGlobal = new int[Block.BLOCK_STATE_REGISTRY.size()];
++ private final int[] traceGlobal;
+ private final LevelChunkSection[] emptyNearbyChunkSections = {EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION};
+ public final boolean rayTraceThirdPerson;
+ public final double rayTraceDistance;
+@@ -76,9 +76,9 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ this.rayTraceThirdPerson = rayTraceThirdPerson;
+ this.rayTraceDistance = rayTraceDistance;
+ this.maxRayTraceBlockCountPerChunk = maxRayTraceBlockCountPerChunk;
+- List<String> toObfuscate;
++ List<String> toObfuscate = paperWorldConfig.hiddenBlocks;
+
+- if (engineMode == EngineMode.HIDE) {
++ /*if (engineMode == EngineMode.HIDE) {
+ toObfuscate = paperWorldConfig.hiddenBlocks;
+ presetBlockStates = null;
+ presetBlockStatesFull = null;
+@@ -124,16 +124,57 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ presetBlockStateBitsDeepslateGlobal = null;
+ presetBlockStateBitsNetherrackGlobal = null;
+ presetBlockStateBitsEndStoneGlobal = null;
++ }*/
++
++ List<BlockState> presetBlockStateList = new LinkedList<>();
++ BlockState defaultBlockState;
++
++ switch (level.getWorld().getEnvironment()) {
++ case NETHER:
++ defaultBlockState = Blocks.NETHERRACK.defaultBlockState();
++ break;
++ case THE_END:
++ defaultBlockState = Blocks.END_STONE.defaultBlockState();
++ break;
++ default:
++ defaultBlockState = Blocks.STONE.defaultBlockState();
+ }
+
++ // -1 means false (don't obfuscate).
++ Arrays.fill(obfuscateGlobal, -1);
++
+ for (String id : toObfuscate) {
+- Block block = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(id)).orElse(null);
++ String fromToIdSeparator = "->";
++ int fromToIdSeparatorIndex = id.lastIndexOf(fromToIdSeparator);
++ String fromId = fromToIdSeparatorIndex >= 0 ? id.substring(0, fromToIdSeparatorIndex) : id;
++ Block block = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(fromId)).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()) {
++ BlockState toBlockState = null;
++
++ if (fromToIdSeparatorIndex >= 0) {
++ Block toBlock = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(id.substring(fromToIdSeparatorIndex + fromToIdSeparator.length()))).orElse(null);
++
++ if (toBlock != null && !(toBlock instanceof EntityBlock)) {
++ toBlockState = toBlock.defaultBlockState();
++ }
++ }
++
++ if (toBlockState == null) {
++ toBlockState = defaultBlockState;
++ }
++
++ int presetBlockStateListIndex = presetBlockStateList.indexOf(toBlockState);
++
++ if (presetBlockStateListIndex < 0) {
++ presetBlockStateListIndex = presetBlockStateList.size();
++ presetBlockStateList.add(toBlockState);
++ }
++
+ // Replace all block states of a specified block
+ for (BlockState blockState : block.getStateDefinition().getPossibleStates()) {
+- obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
++ obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = presetBlockStateListIndex;
+ }
+ }
+ }
+@@ -141,22 +182,69 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ if (toTrace == null) {
+ traceGlobal = obfuscateGlobal;
+ } else {
+- traceGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()];
++ traceGlobal = new int[Block.BLOCK_STATE_REGISTRY.size()];
++ // -1 means false (don't obfuscate).
++ Arrays.fill(traceGlobal, -1);
+
+ for (String id : toTrace) {
+- Block block = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(id)).orElse(null);
++ String fromToIdSeparator = "->";
++ int fromToIdSeparatorIndex = id.lastIndexOf(fromToIdSeparator);
++ String fromId = fromToIdSeparatorIndex >= 0 ? id.substring(0, fromToIdSeparatorIndex) : id;
++ Block block = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(fromId)).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()) {
++ BlockState toBlockState = null;
++
++ if (fromToIdSeparatorIndex >= 0) {
++ Block toBlock = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(id.substring(fromToIdSeparatorIndex + fromToIdSeparator.length()))).orElse(null);
++
++ if (toBlock != null && !(toBlock instanceof EntityBlock)) {
++ toBlockState = toBlock.defaultBlockState();
++ }
++ }
++
++ if (toBlockState == null) {
++ toBlockState = defaultBlockState;
++ }
++
++ int presetBlockStateListIndex = presetBlockStateList.indexOf(toBlockState);
++
++ if (presetBlockStateListIndex < 0) {
++ presetBlockStateListIndex = presetBlockStateList.size();
++ presetBlockStateList.add(toBlockState);
++ }
++
+ // Replace all block states of a specified block
+ for (BlockState blockState : block.getStateDefinition().getPossibleStates()) {
+- traceGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
+- obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true;
++ traceGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = presetBlockStateListIndex;
++ obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = presetBlockStateListIndex;
+ }
+ }
+ }
+ }
+
++ if (presetBlockStateList.isEmpty()) {
++ presetBlockStateList.add(defaultBlockState);
++ }
++
++ presetBlockStates = presetBlockStateList.toArray(new BlockState[0]);
++ presetBlockStatesFull = presetBlockStates;
++ presetBlockStatesStone = null;
++ presetBlockStatesDeepslate = null;
++ presetBlockStatesNetherrack = null;
++ presetBlockStatesEndStone = null;
++ presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length];
++
++ for (int i = 0; i < presetBlockStatesFull.length; i++) {
++ presetBlockStateBitsGlobal[i] = GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]);
++ }
++
++ presetBlockStateBitsStoneGlobal = null;
++ presetBlockStateBitsDeepslateGlobal = null;
++ presetBlockStateBitsNetherrackGlobal = null;
++ presetBlockStateBitsEndStoneGlobal = null;
++
+ EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS));
+ BlockPos zeroPos = new BlockPos(0, 0, 0);
+
+@@ -175,14 +263,14 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ }
+
+ private int getPresetBlockStatesFullLength() {
+- return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length;
++ return presetBlockStatesFull.length;
+ }
+
+ @Override
+ public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int bottomBlockY) {
+ // Return the block states to be added to the paletted containers so that they can be used for obfuscation
+ if (bottomBlockY < maxBlockHeight) {
+- if (engineMode == EngineMode.HIDE) {
++ /*if (engineMode == EngineMode.HIDE) {
+ switch (level.getWorld().getEnvironment()) {
+ case NETHER:
+ return presetBlockStatesNetherrack;
+@@ -191,7 +279,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ default:
+ return bottomBlockY < 0 ? presetBlockStatesDeepslate : presetBlockStatesStone;
+ }
+- }
++ }*/
+
+ return presetBlockStates;
+ }
+@@ -235,22 +323,24 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ // 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()]);
+- private static final ThreadLocal<boolean[]> TRACE = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]);
++ private static final ThreadLocal<int[]> OBFUSCATE = ThreadLocal.withInitial(() -> new int[Block.BLOCK_STATE_REGISTRY.size()]);
++ private static final ThreadLocal<int[]> TRACE = ThreadLocal.withInitial(() -> new int[Block.BLOCK_STATE_REGISTRY.size()]);
+ // These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate
+ private static final ThreadLocal<boolean[][]> CURRENT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+ private static final ThreadLocal<boolean[][]> NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
+ private static final ThreadLocal<boolean[][]> NEXT_NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]);
++ private static final ThreadLocal<int[][]> OBFUSCATE_CACHE = ThreadLocal.withInitial(() -> new int[16][16]);
+ private static final ThreadLocal<boolean[][]> TRACE_CACHE = ThreadLocal.withInitial(() -> new boolean[16][16]);
+
+ public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) {
+ int[] presetBlockStateBits = this.presetBlockStateBits.get();
+ boolean[] solid = SOLID.get();
+- boolean[] obfuscate = OBFUSCATE.get();
+- boolean[] trace = traceGlobal == obfuscateGlobal ? obfuscate : TRACE.get();
++ int[] obfuscate = OBFUSCATE.get();
++ int[] trace = traceGlobal == obfuscateGlobal ? obfuscate : TRACE.get();
+ boolean[][] current = CURRENT.get();
+ boolean[][] next = NEXT.get();
+ boolean[][] nextNext = NEXT_NEXT.get();
++ int[][] obfuscateCache = OBFUSCATE_CACHE.get();
+ boolean[][] traceCache = TRACE_CACHE.get();
+ // bitStorageReader, bitStorageWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it
+ BitStorageReader bitStorageReader = new BitStorageReader();
+@@ -260,8 +350,8 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ Level level = chunk.getLevel();
+ int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSection(), chunk.getSectionsCount()) - 1;
+ boolean[] solidTemp = null;
+- boolean[] obfuscateTemp = null;
+- boolean[] traceTemp = null;
++ int[] obfuscateTemp = null;
++ int[] traceTemp = null;
+ bitStorageReader.setBuffer(chunkPacketInfoAntiXray.getBuffer());
+ bitStorageWriter.setBuffer(chunkPacketInfoAntiXray.getBuffer());
+ int numberOfBlocks = presetBlockStateBits.length;
+@@ -290,7 +380,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ int[] presetBlockStateBitsTemp;
+
+ if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) instanceof GlobalPalette) {
+- if (engineMode == EngineMode.HIDE) {
++ /*if (engineMode == EngineMode.HIDE) {
+ switch (level.getWorld().getEnvironment()) {
+ case NETHER:
+ presetBlockStateBitsTemp = presetBlockStateBitsNetherrackGlobal;
+@@ -301,9 +391,9 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ default:
+ presetBlockStateBitsTemp = chunkSectionIndex + chunk.getMinSection() < 0 ? presetBlockStateBitsDeepslateGlobal : presetBlockStateBitsStoneGlobal;
+ }
+- } else {
++ } else {*/
+ presetBlockStateBitsTemp = presetBlockStateBitsGlobal;
+- }
++ //}
+ } else {
+ // If it's presetBlockStates, use this.presetBlockStatesFull instead
+ BlockState[] presetBlockStatesFull = chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) == presetBlockStates ? this.presetBlockStatesFull : chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex);
+@@ -340,7 +430,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section
+ bitStorageWriter.setBits(0);
+- obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, -1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, traceCache, emptyNearbyChunkSections, random, blocks);
++ obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, -1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, obfuscateCache, traceCache, emptyNearbyChunkSections, random, blocks);
+ }
+
+ bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex));
+@@ -355,7 +445,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ current = next;
+ next = nextNext;
+ nextNext = temp;
+- obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, traceCache, nearbyChunkSections, random, blocks);
++ obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, obfuscateCache, traceCache, nearbyChunkSections, random, blocks);
+ }
+
+ // Check if the chunk section above doesn't need obfuscation
+@@ -378,7 +468,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ // There is nothing to read anymore
+ bitStorageReader.setBits(0);
+ solid[0] = true;
+- obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, 15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, traceCache, nearbyChunkSections, random, blocks);
++ obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, 15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, obfuscateCache, traceCache, nearbyChunkSections, random, blocks);
+ } 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));
+@@ -390,7 +480,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ current = next;
+ next = nextNext;
+ nextNext = temp;
+- obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, 15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, traceCache, nearbyChunkSections, random, blocks);
++ obfuscateLayer(chunk.getPos(), chunk.getMinSection(), chunkSectionIndex, 15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, traceTemp, presetBlockStateBitsTemp, current, next, nextNext, obfuscateCache, traceCache, nearbyChunkSections, random, blocks);
+ }
+
+ bitStorageWriter.flush();
+@@ -404,7 +494,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ chunkPacketInfoAntiXray.getChunkPacket().setReady(true);
+ }
+
+- private void obfuscateLayer(ChunkPos chunkPos, int minSection, int chunkSectionIndex, int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, boolean[] obfuscate, boolean[] trace, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, boolean[][] traceCache, LevelChunkSection[] nearbyChunkSections, IntSupplier random, Collection<? super BlockPos> blocks) {
++ private void obfuscateLayer(ChunkPos chunkPos, int minSection, int chunkSectionIndex, int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, int[] obfuscate, int[] trace, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, int[][] obfuscateCache, boolean[][] traceCache, LevelChunkSection[] nearbyChunkSections, IntSupplier random, Collection<? super BlockPos> blocks) {
+ int minX = chunkPos.getMinBlockX();
+ int minZ = chunkPos.getMinBlockZ();
+ int realY = (chunkSectionIndex + minSection << 4) + y;
+@@ -413,7 +503,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[0][0] = !solid[bits]) {
+ if (traceCache[0][0] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][0]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 0, realY, minZ + 0));
+ } else {
+ bitStorageWriter.skip();
+@@ -424,23 +514,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[0][0] || isTransparent(nearbyChunkSections[2], 0, y, 15) || isTransparent(nearbyChunkSections[0], 15, y, 0)) {
+ if (traceCache[0][0] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][0]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 0, realY, minZ + 0));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][0]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ int obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[0][0] = obfuscateValue;
+ traceCache[0][0] = true;
+ } else {
+ traceCache[0][0] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[0][0] = true;
++ } else {
++ obfuscateCache[0][0] = obfuscateValue;
+ }
+ }
+
+@@ -450,7 +546,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[0][x] = !solid[bits]) {
+ if (traceCache[0][x] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][x]]); // Exposed to air
+ blocks.add(new BlockPos(minX + x, realY, minZ + 0));
+ } else {
+ bitStorageWriter.skip();
+@@ -462,23 +558,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[0][x] || isTransparent(nearbyChunkSections[2], x, y, 15)) {
+ if (traceCache[0][x] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][x]]); // Exposed to air
+ blocks.add(new BlockPos(minX + x, realY, minZ + 0));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][x]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[0][x] = obfuscateValue;
+ traceCache[0][x] = true;
+ } else {
+ traceCache[0][x] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[0][x] = true;
++ } else {
++ obfuscateCache[0][x] = obfuscateValue;
+ }
+ }
+ }
+@@ -488,7 +590,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[0][15] = !solid[bits]) {
+ if (traceCache[0][15] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][15]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 15, realY, minZ + 0));
+ } else {
+ bitStorageWriter.skip();
+@@ -499,23 +601,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[0][15] || isTransparent(nearbyChunkSections[2], 15, y, 15) || isTransparent(nearbyChunkSections[1], 0, y, 0)) {
+ if (traceCache[0][15] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][15]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 15, realY, minZ + 0));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[0][15]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[0][15] = obfuscateValue;
+ traceCache[0][15] = true;
+ } else {
+ traceCache[0][15] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[0][15] = true;
++ } else {
++ obfuscateCache[0][15] = obfuscateValue;
+ }
+ }
+
+@@ -526,7 +634,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[z][0] = !solid[bits]) {
+ if (traceCache[z][0] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][0]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 0, realY, minZ + z));
+ } else {
+ bitStorageWriter.skip();
+@@ -538,23 +646,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[z][0] || isTransparent(nearbyChunkSections[0], 15, y, z)) {
+ if (traceCache[z][0] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][0]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 0, realY, minZ + z));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][0]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[z][0] = obfuscateValue;
+ traceCache[z][0] = true;
+ } else {
+ traceCache[z][0] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[z][0] = true;
++ } else {
++ obfuscateCache[z][0] = obfuscateValue;
+ }
+ }
+
+@@ -564,7 +678,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[z][x] = !solid[bits]) {
+ if (traceCache[z][x] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][x]]); // Exposed to air
+ blocks.add(new BlockPos(minX + x, realY, minZ + z));
+ } else {
+ bitStorageWriter.skip();
+@@ -577,23 +691,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[z][x]) {
+ if (traceCache[z][x] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][x]]); // Exposed to air
+ blocks.add(new BlockPos(minX + x, realY, minZ + z));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][x]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[z][x] = obfuscateValue;
+ traceCache[z][x] = true;
+ } else {
+ traceCache[z][x] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[z][x] = true;
++ } else {
++ obfuscateCache[z][x] = obfuscateValue;
+ }
+ }
+ }
+@@ -603,7 +723,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[z][15] = !solid[bits]) {
+ if (traceCache[z][15] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][15]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 15, realY, minZ + z));
+ } else {
+ bitStorageWriter.skip();
+@@ -615,23 +735,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[z][15] || isTransparent(nearbyChunkSections[1], 0, y, z)) {
+ if (traceCache[z][15] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][15]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 15, realY, minZ + z));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[z][15]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[z][15] = obfuscateValue;
+ traceCache[z][15] = true;
+ } else {
+ traceCache[z][15] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[z][15] = true;
++ } else {
++ obfuscateCache[z][15] = obfuscateValue;
+ }
+ }
+ }
+@@ -641,7 +767,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[15][0] = !solid[bits]) {
+ if (traceCache[15][0] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][0]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 0, realY, minZ + 15));
+ } else {
+ bitStorageWriter.skip();
+@@ -652,23 +778,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[15][0] || isTransparent(nearbyChunkSections[3], 0, y, 0) || isTransparent(nearbyChunkSections[0], 15, y, 15)) {
+ if (traceCache[15][0] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][0]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 0, realY, minZ + 15));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][0]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[15][0] = obfuscateValue;
+ traceCache[15][0] = true;
+ } else {
+ traceCache[15][0] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[15][0] = true;
++ } else {
++ obfuscateCache[15][0] = obfuscateValue;
+ }
+ }
+
+@@ -678,7 +810,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[15][x] = !solid[bits]) {
+ if (traceCache[15][x] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][x]]); // Exposed to air
+ blocks.add(new BlockPos(minX + x, realY, minZ + 15));
+ } else {
+ bitStorageWriter.skip();
+@@ -690,23 +822,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[15][x] || isTransparent(nearbyChunkSections[3], x, y, 0)) {
+ if (traceCache[15][x] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][x]]); // Exposed to air
+ blocks.add(new BlockPos(minX + x, realY, minZ + 15));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][x]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[15][x] = obfuscateValue;
+ traceCache[15][x] = true;
+ } else {
+ traceCache[15][x] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[15][x] = true;
++ } else {
++ obfuscateCache[15][x] = obfuscateValue;
+ }
+ }
+ }
+@@ -716,7 +854,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+
+ if (nextNext[15][15] = !solid[bits]) {
+ if (traceCache[15][15] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][15]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 15, realY, minZ + 15));
+ } else {
+ bitStorageWriter.skip();
+@@ -727,23 +865,29 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ } else {
+ if (current[15][15] || isTransparent(nearbyChunkSections[3], 15, y, 0) || isTransparent(nearbyChunkSections[1], 0, y, 15)) {
+ if (traceCache[15][15] && blocks.size() < maxRayTraceBlockCountPerChunk) {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][15]]); // Exposed to air
+ blocks.add(new BlockPos(minX + 15, realY, minZ + 15));
+ } else {
+ bitStorageWriter.skip();
+ }
+ } else {
+- bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); // Not exposed to air
++ bitStorageWriter.write(presetBlockStateBits[obfuscateCache[15][15]]); // Not exposed to air
+ }
+ }
+
+- if (trace[bits]) {
++ obfuscateValue = trace[bits];
++
++ if (obfuscateValue != -1) {
++ obfuscateCache[15][15] = obfuscateValue;
+ traceCache[15][15] = true;
+ } else {
+ traceCache[15][15] = false;
++ obfuscateValue = obfuscate[bits];
+
+- if (!obfuscate[bits]) {
++ if (obfuscateValue == -1) {
+ next[15][15] = true;
++ } else {
++ obfuscateCache[15][15] = obfuscateValue;
+ }
+ }
+ }
+@@ -782,6 +926,25 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ return temp;
+ }
+
++ private int[] readPalette(Palette<BlockState> palette, int[] temp, int[] global) {
++ if (palette instanceof GlobalPalette) {
++ return global;
++ }
++
++ try {
++ for (int i = 0; i < palette.getSize(); i++) {
++ temp[i] = global[GLOBAL_BLOCKSTATE_PALETTE.idFor(palette.valueFor(i))];
++ }
++ } catch (MissingPaletteEntryException e) {
++ // Race condition / visibility issue / no happens-before relationship
++ // We don't care because we at least see the state as it was when the chunk packet was created
++ // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur until we have all the data that we need here
++ // Since all palettes have a fixed initial maximum size and there is no internal restructuring and no values are removed from palettes, we are also guaranteed to see the data
++ }
++
++ return temp;
++ }
++
+ @Override
+ public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) {
+ if (oldBlockState != null && solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) {
+@@ -838,7 +1001,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
+ public void updateBlock(Level level, BlockPos blockPos) {
+ BlockState blockState = level.getBlockStateIfLoaded(blockPos);
+
+- if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) {
++ if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] != -1) {
+ ((ServerLevel) level).getChunkSource().blockChanged(blockPos);
+ }
+ }