diff --git a/connector/pom.xml b/connector/pom.xml
index 0b9913ea2..1b6e9fc4e 100644
--- a/connector/pom.xml
+++ b/connector/pom.xml
@@ -149,7 +149,7 @@
         <dependency>
             <groupId>com.github.steveice10</groupId>
             <artifactId>mcprotocollib</artifactId>
-            <version>1.17.1-3-SNAPSHOT</version>
+            <version>1.18-pre-SNAPSHOT</version>
             <scope>compile</scope>
             <exclusions>
                 <exclusion>
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java
index 6154ff65f..b9f3b4db6 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java
@@ -25,8 +25,7 @@
 
 package org.geysermc.connector.network.session.cache;
 
-import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
-import com.github.steveice10.mc.protocol.data.game.chunk.Column;
+import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette;
 import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
 import lombok.Getter;
@@ -57,14 +56,14 @@ public class ChunkCache {
         chunks = cache ? new Long2ObjectOpenHashMap<>() : null;
     }
 
-    public void addToCache(Column chunk) {
+    public void addToCache(int x, int z, DataPalette[] chunks) {
         if (!cache) {
             return;
         }
 
-        long chunkPosition = MathUtils.chunkPositionToLong(chunk.getX(), chunk.getZ());
-        GeyserColumn geyserColumn = GeyserColumn.from(this, chunk);
-        chunks.put(chunkPosition, geyserColumn);
+        long chunkPosition = MathUtils.chunkPositionToLong(x, z);
+        GeyserColumn geyserColumn = GeyserColumn.from(chunks);
+        this.chunks.put(chunkPosition, geyserColumn);
     }
 
     /**
@@ -90,11 +89,11 @@ public class ChunkCache {
             return;
         }
 
-        Chunk chunk = column.getChunks()[(y - minY) >> 4];
+        DataPalette chunk = column.getChunks()[(y - minY) >> 4];
         if (chunk == null) {
             if (block != BlockStateValues.JAVA_AIR_ID) {
                 // A previously empty chunk, which is no longer empty as a block has been added to it
-                chunk = new Chunk();
+                chunk = DataPalette.createForChunk();
                 // Fixes the chunk assuming that all blocks is the `block` variable we are updating. /shrug
                 chunk.getPalette().stateToId(BlockStateValues.JAVA_AIR_ID);
                 column.getChunks()[(y - minY) >> 4] = chunk;
@@ -122,7 +121,7 @@ public class ChunkCache {
             return BlockStateValues.JAVA_AIR_ID;
         }
 
-        Chunk chunk = column.getChunks()[(y - minY) >> 4];
+        DataPalette chunk = column.getChunks()[(y - minY) >> 4];
         if (chunk != null) {
             return chunk.get(x & 0xF, y & 0xF, z & 0xF);
         }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java
index 9384a3515..cbcbe9f1d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java
@@ -106,7 +106,8 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
 
         // We need to send our skin parts to the server otherwise java sees us with no hat, jacket etc
         String locale = session.getLocale();
-        ServerboundClientInformationPacket infoPacket = new ServerboundClientInformationPacket(locale, (byte) session.getRenderDistance(), ChatVisibility.FULL, true, SKIN_PART_VALUES, HandPreference.RIGHT_HAND, false);
+        // TODO customize
+        ServerboundClientInformationPacket infoPacket = new ServerboundClientInformationPacket(locale, (byte) session.getRenderDistance(), ChatVisibility.FULL, true, SKIN_PART_VALUES, HandPreference.RIGHT_HAND, false, true);
         session.sendDownstreamPacket(infoPacket);
 
         session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaBlockEntityDataTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaBlockEntityDataTranslator.java
index 5ef32f151..7067b73a2 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaBlockEntityDataTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaBlockEntityDataTranslator.java
@@ -25,6 +25,7 @@
 
 package org.geysermc.connector.network.translators.java.level;
 
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
 import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
 import com.github.steveice10.mc.protocol.data.game.level.block.UpdatedTileType;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundBlockEntityDataPacket;
@@ -61,7 +62,9 @@ public class JavaBlockEntityDataTranslator extends PacketTranslator<ClientboundB
         } else {
             blockState = BlockStateValues.JAVA_AIR_ID;
         }
-        BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(id, packet.getNbt(), blockState), packet.getPosition());
+        Position position = packet.getPosition();
+        BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(id, position.getX(), position.getY(), position.getZ(),
+                packet.getNbt(), blockState), packet.getPosition());
         // Check for custom skulls.
         if (session.getPreferencesCache().showCustomSkulls() && packet.getNbt().contains("SkullOwner")) {
             SkullBlockEntityTranslator.spawnPlayer(session, packet.getNbt(), blockState);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkTranslator.java
deleted file mode 100644
index 7a13198a2..000000000
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkTranslator.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.connector.network.translators.java.level;
-
-import com.github.steveice10.mc.protocol.data.game.chunk.Column;
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkPacket;
-import com.nukkitx.nbt.NBTOutputStream;
-import com.nukkitx.nbt.NbtMap;
-import com.nukkitx.nbt.NbtUtils;
-import com.nukkitx.network.VarInts;
-import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket;
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufAllocator;
-import io.netty.buffer.ByteBufOutputStream;
-import org.geysermc.connector.GeyserConnector;
-import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.connector.network.translators.PacketTranslator;
-import org.geysermc.connector.network.translators.Translator;
-import org.geysermc.connector.network.translators.world.BiomeTranslator;
-import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
-import org.geysermc.connector.utils.ChunkUtils;
-
-import java.io.IOException;
-
-import static org.geysermc.connector.utils.ChunkUtils.MINIMUM_ACCEPTED_HEIGHT;
-import static org.geysermc.connector.utils.ChunkUtils.MINIMUM_ACCEPTED_HEIGHT_OVERWORLD;
-
-@Translator(packet = ClientboundLevelChunkPacket.class)
-public class JavaLevelChunkTranslator extends PacketTranslator<ClientboundLevelChunkPacket> {
-
-    @Override
-    public void translate(GeyserSession session, ClientboundLevelChunkPacket packet) {
-        if (session.isSpawned()) {
-            ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt());
-        }
-
-        session.getChunkCache().addToCache(packet.getColumn());
-        Column column = packet.getColumn();
-
-        // Ensure that, if the player is using lower world heights, the position is not offset
-        int yOffset = session.getChunkCache().getChunkMinY();
-
-        ChunkUtils.ChunkData chunkData = ChunkUtils.translateToBedrock(session, column, yOffset);
-        ChunkSection[] sections = chunkData.sections();
-
-        // Find highest section
-        int sectionCount = sections.length - 1;
-        while (sectionCount >= 0 && sections[sectionCount] == null) {
-            sectionCount--;
-        }
-        sectionCount++;
-
-        // Estimate chunk size
-        int size = 0;
-        for (int i = 0; i < sectionCount; i++) {
-            ChunkSection section = sections[i];
-            size += (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).estimateNetworkSize();
-        }
-        size += ChunkUtils.EMPTY_CHUNK_DATA.length; // Consists only of biome data
-        size += 1; // Border blocks
-        size += 1; // Extra data length (always 0)
-        size += chunkData.blockEntities().length * 64; // Conservative estimate of 64 bytes per tile entity
-
-        // Allocate output buffer
-        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(size);
-        byte[] payload;
-        try {
-            for (int i = 0; i < sectionCount; i++) {
-                ChunkSection section = sections[i];
-                (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).writeToNetwork(byteBuf);
-            }
-
-            // At this point we're dealing with Bedrock chunk sections
-            boolean overworld = session.getChunkCache().isExtendedHeight();
-            int dimensionOffset = (overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4;
-            for (int i = 0; i < sectionCount; i++) {
-                int biomeYOffset = dimensionOffset + i;
-                if (biomeYOffset < yOffset) {
-                    // Ignore this biome section since it goes below the height of the Java world
-                    byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
-                    continue;
-                }
-                BiomeTranslator.toNewBedrockBiome(session, column.getBiomeData(), i + (dimensionOffset - yOffset)).writeToNetwork(byteBuf);
-            }
-
-            // As of 1.17.10, Bedrock hardcodes to always read 32 biome sections
-            int remainingEmptyBiomes = 32 - sectionCount;
-            for (int i = 0; i < remainingEmptyBiomes; i++) {
-                byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
-            }
-
-            byteBuf.writeByte(0); // Border blocks - Edu edition only
-            VarInts.writeUnsignedInt(byteBuf, 0); // extra data length, 0 for now
-
-            // Encode tile entities into buffer
-            NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf));
-            for (NbtMap blockEntity : chunkData.blockEntities()) {
-                nbtStream.writeTag(blockEntity);
-            }
-
-            // Copy data into byte[], because the protocol lib really likes things that are s l o w
-            byteBuf.readBytes(payload = new byte[byteBuf.readableBytes()]);
-        } catch (IOException e) {
-            session.getConnector().getLogger().error("IO error while encoding chunk", e);
-            return;
-        } finally {
-            byteBuf.release(); // Release buffer to allow buffer pooling to be useful
-        }
-
-        LevelChunkPacket levelChunkPacket = new LevelChunkPacket();
-        levelChunkPacket.setSubChunksLength(sectionCount);
-        levelChunkPacket.setCachingEnabled(false);
-        levelChunkPacket.setChunkX(column.getX());
-        levelChunkPacket.setChunkZ(column.getZ());
-        levelChunkPacket.setData(payload);
-        session.sendUpstreamPacket(levelChunkPacket);
-    }
-}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkWithLightTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkWithLightTranslator.java
new file mode 100644
index 000000000..b6e7880f0
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkWithLightTranslator.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.network.translators.java.level;
+
+import com.github.steveice10.mc.protocol.data.game.chunk.BitStorage;
+import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
+import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette;
+import com.github.steveice10.mc.protocol.data.game.chunk.palette.GlobalPalette;
+import com.github.steveice10.mc.protocol.data.game.chunk.palette.Palette;
+import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
+import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkWithLightPacket;
+import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.github.steveice10.opennbt.tag.builtin.StringTag;
+import com.github.steveice10.opennbt.tag.builtin.Tag;
+import com.github.steveice10.packetlib.io.NetInput;
+import com.github.steveice10.packetlib.io.stream.StreamNetInput;
+import com.nukkitx.math.vector.Vector3i;
+import com.nukkitx.nbt.NBTOutputStream;
+import com.nukkitx.nbt.NbtMap;
+import com.nukkitx.nbt.NbtUtils;
+import com.nukkitx.network.VarInts;
+import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.ByteBufOutputStream;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.PacketTranslator;
+import org.geysermc.connector.network.translators.Translator;
+import org.geysermc.connector.network.translators.world.BiomeTranslator;
+import org.geysermc.connector.network.translators.world.block.BlockStateValues;
+import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity;
+import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
+import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
+import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
+import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
+import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
+import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion;
+import org.geysermc.connector.registry.BlockRegistries;
+import org.geysermc.connector.utils.BlockEntityUtils;
+import org.geysermc.connector.utils.ChunkUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+
+import static org.geysermc.connector.utils.ChunkUtils.*;
+
+@Translator(packet = ClientboundLevelChunkWithLightPacket.class)
+public class JavaLevelChunkWithLightTranslator extends PacketTranslator<ClientboundLevelChunkWithLightPacket> {
+
+    @Override
+    public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacket packet) {
+        if (session.isSpawned()) {
+            ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt());
+        }
+
+        //todo session.getChunkCache().addToCache(packet.getColumn());
+
+        // Ensure that, if the player is using lower world heights, the position is not offset
+        int yOffset = session.getChunkCache().getChunkMinY();
+
+        // Temporarily stores compound tags of Bedrock-only block entities
+        List<NbtMap> bedrockOnlyBlockEntities = new ArrayList<>();
+        DataPalette[] javaChunks = new DataPalette[session.getChunkCache().getChunkHeightY()];
+        DataPalette[] javaBiomes = new DataPalette[session.getChunkCache().getChunkHeightY()];
+
+        BitSet waterloggedPaletteIds = new BitSet();
+        BitSet pistonOrFlowerPaletteIds = new BitSet();
+
+        boolean overworld = session.getChunkCache().isExtendedHeight();
+        int maxBedrockSectionY = ((overworld ? MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD : MAXIMUM_ACCEPTED_HEIGHT) >> 4) - 1;
+
+        int sectionCount;
+        byte[] payload;
+        ByteBuf byteBuf = null;
+        ChunkSection[] sections = new ChunkSection[javaChunks.length - yOffset];
+
+        try {
+            NetInput in = new StreamNetInput(new ByteArrayInputStream(packet.getChunkData()));
+            for (int sectionY = 0; sectionY < session.getChunkCache().getChunkHeightY(); sectionY++) {
+                int bedrockSectionY = sectionY + (yOffset - ((overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4));
+                if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) {
+                    // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client
+                    continue;
+                }
+
+                Chunk javaSection = Chunk.read(in);
+                javaChunks[sectionY] = javaSection.getChunkData();
+                javaBiomes[sectionY] = javaSection.getBiomeData();
+
+                // No need to encode an empty section...
+                if (javaSection.isBlockCountEmpty()) {
+                    continue;
+                }
+
+                Palette javaPalette = javaSection.getChunkData().getPalette();
+                BitStorage javaData = javaSection.getChunkData().getStorage();
+
+                if (javaPalette instanceof GlobalPalette) {
+                    // As this is the global palette, simply iterate through the whole chunk section once
+                    ChunkSection section = new ChunkSection(session.getBlockMappings().getBedrockAirId());
+                    for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
+                        int javaId = javaData.get(yzx);
+                        int bedrockId = session.getBlockMappings().getBedrockBlockId(javaId);
+                        int xzy = indexYZXtoXZY(yzx);
+                        section.getBlockStorageArray()[0].setFullBlock(xzy, bedrockId);
+
+                        if (BlockRegistries.WATERLOGGED.get().contains(javaId)) {
+                            section.getBlockStorageArray()[1].setFullBlock(xzy, session.getBlockMappings().getBedrockWaterId());
+                        }
+
+                        // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
+                        if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId)) {
+                            bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
+                                    Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
+                                    javaId
+                            ));
+                        }
+                    }
+                    sections[bedrockSectionY] = section;
+                    continue;
+                }
+
+                IntList bedrockPalette = new IntArrayList(javaPalette.size());
+                waterloggedPaletteIds.clear();
+                pistonOrFlowerPaletteIds.clear();
+
+                // Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go
+                for (int i = 0; i < javaPalette.size(); i++) {
+                    int javaId = javaPalette.idToState(i);
+                    bedrockPalette.add(session.getBlockMappings().getBedrockBlockId(javaId));
+
+                    if (BlockRegistries.WATERLOGGED.get().contains(javaId)) {
+                        waterloggedPaletteIds.set(i);
+                    }
+
+                    // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
+                    if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId)) {
+                        pistonOrFlowerPaletteIds.set(i);
+                    }
+                }
+
+                // Add Bedrock-exclusive block entities
+                // We only if the palette contained any blocks that are Bedrock-exclusive block entities to avoid iterating through the whole block data
+                // for no reason, as most sections will not contain any pistons or flower pots
+                if (!pistonOrFlowerPaletteIds.isEmpty()) {
+                    for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
+                        int paletteId = javaData.get(yzx);
+                        if (pistonOrFlowerPaletteIds.get(paletteId)) {
+                            bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
+                                    Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
+                                    javaPalette.idToState(paletteId)
+                            ));
+                        }
+                    }
+                }
+
+                BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE);
+                BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette);
+                BlockStorage[] layers;
+
+                // Convert data array from YZX to XZY coordinate order
+                if (waterloggedPaletteIds.isEmpty()) {
+                    // No blocks are waterlogged, simply convert coordinate order
+                    // This could probably be optimized further...
+                    for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
+                        bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx));
+                    }
+
+                    layers = new BlockStorage[]{ layer0 };
+                } else {
+                    // The section contains waterlogged blocks, we need to convert coordinate order AND generate a V1 block storage for
+                    // layer 1 with palette ID 1 indicating water
+                    int[] layer1Data = new int[BlockStorage.SIZE >> 5];
+                    for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
+                        int paletteId = javaData.get(yzx);
+                        int xzy = indexYZXtoXZY(yzx);
+                        bedrockData.set(xzy, paletteId);
+
+                        if (waterloggedPaletteIds.get(paletteId)) {
+                            layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
+                        }
+                    }
+
+                    // V1 palette
+                    IntList layer1Palette = new IntArrayList(2);
+                    layer1Palette.add(session.getBlockMappings().getBedrockAirId()); // Air - see BlockStorage's constructor for more information
+                    layer1Palette.add(session.getBlockMappings().getBedrockWaterId());
+
+                    layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
+                }
+
+                sections[bedrockSectionY] = new ChunkSection(layers);
+            }
+
+            session.getChunkCache().addToCache(packet.getX(), packet.getZ(), javaChunks);
+
+            BlockEntityInfo[] blockEntities = packet.getBlockEntities();
+            NbtMap[] bedrockBlockEntities = new NbtMap[blockEntities.length + bedrockOnlyBlockEntities.size()];
+            int blockEntityCount = 0;
+            while (blockEntityCount < blockEntities.length) {
+                BlockEntityInfo blockEntity = blockEntities[blockEntityCount];
+                CompoundTag tag = blockEntity.getNbt();
+                // TODO use the actual name
+                String tagName;
+                if (tag != null) {
+                    Tag idTag = tag.get("id");
+                    if (idTag != null) {
+                        tagName = (String) idTag.getValue();
+                    } else {
+                        tagName = "Empty";
+                        // Sometimes legacy tags have their ID be a StringTag with empty value
+                        for (Tag subTag : tag) {
+                            if (subTag instanceof StringTag stringTag) {
+                                if (stringTag.getValue().isEmpty()) {
+                                    tagName = stringTag.getName();
+                                    break;
+                                }
+                            }
+                        }
+                        if (tagName.equals("Empty")) {
+                            GeyserConnector.getInstance().getLogger().debug("Got tag with no id: " + tag.getValue());
+                        }
+                    }
+                } else {
+                    tagName = "Empty";
+                }
+
+                String id = BlockEntityUtils.getBedrockBlockEntityId(tagName);
+                int x = blockEntity.getX();
+                int y = blockEntity.getY();
+                int z = blockEntity.getZ();
+
+                // Get the Java block state ID from block entity position
+                DataPalette section = javaChunks[(blockEntity.getY() >> 4) - yOffset];
+                int blockState = section.get(x & 0xF, y & 0xF, z & 0xF);
+
+                if (tagName.equals("minecraft:lectern") && BlockStateValues.getLecternBookStates().get(blockState)) {
+                    // If getLecternBookStates is false, let's just treat it like a normal block entity
+                    bedrockBlockEntities[blockEntityCount] = session.getConnector().getWorldManager().getLecternDataAt(
+                            session, blockEntity.getX(), blockEntity.getY(), blockEntity.getZ(), true);
+                    blockEntityCount++;
+                    continue;
+                }
+
+                BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id);
+                bedrockBlockEntities[blockEntityCount] = blockEntityTranslator.getBlockEntityTag(tagName, x, y, z, tag, blockState);
+
+                // Check for custom skulls
+                if (session.getPreferencesCache().showCustomSkulls() && tag != null && tag.contains("SkullOwner")) {
+                    SkullBlockEntityTranslator.spawnPlayer(session, tag, blockState);
+                }
+                blockEntityCount++;
+            }
+
+            // Append Bedrock-exclusive block entities to output array
+            for (NbtMap tag : bedrockOnlyBlockEntities) {
+                bedrockBlockEntities[blockEntityCount] = tag;
+                blockEntityCount++;
+            }
+
+            // Find highest section
+            sectionCount = sections.length - 1;
+            while (sectionCount >= 0 && sections[sectionCount] == null) {
+                sectionCount--;
+            }
+            sectionCount++;
+
+            // Estimate chunk size
+            int size = 0;
+            for (int i = 0; i < sectionCount; i++) {
+                ChunkSection section = sections[i];
+                size += (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).estimateNetworkSize();
+            }
+            size += ChunkUtils.EMPTY_CHUNK_DATA.length; // Consists only of biome data
+            size += 1; // Border blocks
+            size += 1; // Extra data length (always 0)
+            size += bedrockBlockEntities.length * 64; // Conservative estimate of 64 bytes per tile entity
+
+            // Allocate output buffer
+            byteBuf = ByteBufAllocator.DEFAULT.buffer(size);
+            for (int i = 0; i < sectionCount; i++) {
+                ChunkSection section = sections[i];
+                (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).writeToNetwork(byteBuf);
+            }
+
+            // At this point we're dealing with Bedrock chunk sections
+            int dimensionOffset = (overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4;
+            for (int i = 0; i < sectionCount; i++) {
+                int biomeYOffset = dimensionOffset + i;
+                if (biomeYOffset < yOffset) {
+                    // Ignore this biome section since it goes below the height of the Java world
+                    byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
+                    continue;
+                }
+                BiomeTranslator.toNewBedrockBiome(session, javaBiomes[i]).writeToNetwork(byteBuf);
+            }
+
+            // As of 1.17.10, Bedrock hardcodes to always read 32 biome sections
+            int remainingEmptyBiomes = 32 - sectionCount;
+            for (int i = 0; i < remainingEmptyBiomes; i++) {
+                byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
+            }
+
+            byteBuf.writeByte(0); // Border blocks - Edu edition only
+            VarInts.writeUnsignedInt(byteBuf, 0); // extra data length, 0 for now
+
+            // Encode tile entities into buffer
+            NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf));
+            for (NbtMap blockEntity : bedrockBlockEntities) {
+                nbtStream.writeTag(blockEntity);
+            }
+
+            // Copy data into byte[], because the protocol lib really likes things that are s l o w
+            byteBuf.readBytes(payload = new byte[byteBuf.readableBytes()]);
+        } catch (IOException e) {
+            session.getConnector().getLogger().error("IO error while encoding chunk", e);
+            return;
+        } finally {
+            if (byteBuf != null) {
+                byteBuf.release(); // Release buffer to allow buffer pooling to be useful
+            }
+        }
+
+        LevelChunkPacket levelChunkPacket = new LevelChunkPacket();
+        levelChunkPacket.setSubChunksLength(sectionCount);
+        levelChunkPacket.setCachingEnabled(false);
+        levelChunkPacket.setChunkX(packet.getX());
+        levelChunkPacket.setChunkZ(packet.getZ());
+        levelChunkPacket.setData(payload);
+        session.sendUpstreamPacket(levelChunkPacket);
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java
index baac1abc1..542b3002e 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java
@@ -25,6 +25,8 @@
 
 package org.geysermc.connector.network.translators.world;
 
+import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette;
+import com.github.steveice10.mc.protocol.data.game.chunk.palette.SingletonPalette;
 import com.github.steveice10.opennbt.tag.builtin.*;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import org.geysermc.connector.network.session.GeyserSession;
@@ -98,33 +100,37 @@ public class BiomeTranslator {
         return bedrockData;
     }
 
-    public static BlockStorage toNewBedrockBiome(GeyserSession session, int[] biomeData, int ySection) {
+    public static BlockStorage toNewBedrockBiome(GeyserSession session, DataPalette biomeData) {
         Int2IntMap biomeTranslations = session.getBiomeTranslations();
         // As of 1.17.10: the client expects the same format as a chunk but filled with biomes
-        BlockStorage storage = new BlockStorage(0);
+        // As of 1.18 this is the same as Java Edition
 
-        int biomeY = ySection << 2;
-        int javaOffsetY = biomeY << 4;
-        // Each section of biome corresponding to a chunk section contains 4 * 4 * 4 entries
-        for (int i = 0; i < 64; i++) {
-            int javaId = biomeData[javaOffsetY | i];
-            int x = i & 3;
-            int y = (i >> 4) & 3;
-            int z = (i >> 2) & 3;
-            // Get the Bedrock biome ID override
-            int biomeId = biomeTranslations.get(javaId);
-            int idx = storage.idFor(biomeId);
-            // Convert biome coordinates into block coordinates
-            // Bedrock expects a full 4096 blocks
-            for (int blockX = x << 2; blockX < (x << 2) + 4; blockX++) {
-                for (int blockZ = z << 2; blockZ < (z << 2) + 4; blockZ++) {
-                    for (int blockY = y << 2; blockY < (y << 2) + 4; blockY++) {
-                        storage.getBitArray().set(ChunkSection.blockPosition(blockX, blockY, blockZ), idx);
+        if (biomeData.getPalette() instanceof SingletonPalette palette) {
+            int biomeId = biomeTranslations.get(palette.idToState(0));
+            return new BlockStorage(biomeId);
+        } else {
+            BlockStorage storage = new BlockStorage(0);
+
+            // Each section of biome corresponding to a chunk section contains 4 * 4 * 4 entries
+            for (int i = 0; i < 64; i++) {
+                int javaId = biomeData.getPalette().idToState(biomeData.getStorage().get(i));
+                int x = i & 3;
+                int y = (i >> 4) & 3;
+                int z = (i >> 2) & 3;
+                // Get the Bedrock biome ID override
+                int biomeId = biomeTranslations.get(javaId);
+                int idx = storage.idFor(biomeId);
+                // Convert biome coordinates into block coordinates
+                // Bedrock expects a full 4096 blocks
+                for (int blockX = x << 2; blockX < (x << 2) + 4; blockX++) {
+                    for (int blockZ = z << 2; blockZ < (z << 2) + 4; blockZ++) {
+                        for (int blockY = y << 2; blockY < (y << 2) + 4; blockY++) {
+                            storage.getBitArray().set(ChunkSection.blockPosition(blockX, blockY, blockZ), idx);
+                        }
                     }
                 }
             }
+            return storage;
         }
-
-        return storage;
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java
index 41843e96a..a9edb95be 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java
@@ -42,11 +42,7 @@ public abstract class BlockEntityTranslator {
 
     public abstract void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState);
 
-    public NbtMap getBlockEntityTag(String id, CompoundTag tag, int blockState) {
-        int x = ((IntTag) tag.getValue().get("x")).getValue();
-        int y = ((IntTag) tag.getValue().get("y")).getValue();
-        int z = ((IntTag) tag.getValue().get("z")).getValue();
-
+    public NbtMap getBlockEntityTag(String id, int x, int y, int z, CompoundTag tag, int blockState) {
         NbtMapBuilder tagBuilder = getConstantBedrockTag(BlockEntityUtils.getBedrockBlockEntityId(id), x, y, z);
         translateTag(tagBuilder, tag, blockState);
         return tagBuilder.build();
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java
index 3da5787a5..785616f44 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java
@@ -25,33 +25,21 @@
 
 package org.geysermc.connector.network.translators.world.chunk;
 
-import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
-import com.github.steveice10.mc.protocol.data.game.chunk.Column;
+import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette;
 import lombok.Getter;
-import org.geysermc.connector.network.session.cache.ChunkCache;
 
 /**
- * Acts as a lightweight version of {@link Column} that doesn't store
- * biomes or heightmaps.
+ * Acts as a lightweight chunk class that doesn't store biomes, heightmaps or block entities.
  */
 public class GeyserColumn {
     @Getter
-    private final Chunk[] chunks;
+    private final DataPalette[] chunks;
 
-    private GeyserColumn(Chunk[] chunks) {
+    private GeyserColumn(DataPalette[] chunks) {
         this.chunks = chunks;
     }
 
-    public static GeyserColumn from(ChunkCache chunkCache, Column column) {
-        int chunkHeightY = chunkCache.getChunkHeightY();
-        Chunk[] chunks;
-        if (chunkHeightY < column.getChunks().length) {
-            chunks = new Chunk[chunkHeightY];
-            // TODO addresses https://github.com/Steveice10/MCProtocolLib/pull/598#issuecomment-862782392
-            System.arraycopy(column.getChunks(), 0, chunks, 0, chunks.length);
-        } else {
-            chunks = column.getChunks();
-        }
+    public static GeyserColumn from(DataPalette[] chunks) {
         return new GeyserColumn(chunks);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
index a8fc5f7f8..1a6952453 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
@@ -25,13 +25,10 @@
 
 package org.geysermc.connector.utils;
 
-import com.github.steveice10.mc.protocol.data.game.chunk.BitStorage;
-import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
-import com.github.steveice10.mc.protocol.data.game.chunk.Column;
-import com.github.steveice10.mc.protocol.data.game.chunk.palette.GlobalPalette;
-import com.github.steveice10.mc.protocol.data.game.chunk.palette.Palette;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
-import com.github.steveice10.opennbt.tag.builtin.*;
+import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.github.steveice10.opennbt.tag.builtin.DoubleTag;
+import com.github.steveice10.opennbt.tag.builtin.IntTag;
 import com.nukkitx.math.vector.Vector2i;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.nbt.NbtMap;
@@ -40,27 +37,16 @@ import com.nukkitx.protocol.bedrock.packet.NetworkChunkPublisherUpdatePacket;
 import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
-import it.unimi.dsi.fastutil.ints.IntArrayList;
-import it.unimi.dsi.fastutil.ints.IntList;
 import lombok.experimental.UtilityClass;
-import org.geysermc.connector.GeyserConnector;
 import org.geysermc.connector.entity.ItemFrameEntity;
 import org.geysermc.connector.entity.player.SkullPlayerEntity;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.world.block.BlockStateValues;
 import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity;
-import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
-import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
 import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
 import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
-import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray;
-import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion;
 import org.geysermc.connector.registry.BlockRegistries;
 
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.List;
-
 import static org.geysermc.connector.network.translators.world.block.BlockStateValues.JAVA_AIR_ID;
 
 @UtilityClass
@@ -73,8 +59,8 @@ public class ChunkUtils {
     /**
      * The maximum chunk height Bedrock Edition will accept, from the lowest point to the highest.
      */
-    private static final int MAXIMUM_ACCEPTED_HEIGHT = 256;
-    private static final int MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD = 384;
+    public static final int MAXIMUM_ACCEPTED_HEIGHT = 256;
+    public static final int MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD = 384;
 
     public static final byte[] EMPTY_CHUNK_DATA;
     public static final byte[] EMPTY_BIOME_DATA;
@@ -106,200 +92,10 @@ public class ChunkUtils {
         }
     }
 
-    private static int indexYZXtoXZY(int yzx) {
+    public static int indexYZXtoXZY(int yzx) {
         return (yzx >> 8) | (yzx & 0x0F0) | ((yzx & 0x00F) << 8);
     }
 
-    public static ChunkData translateToBedrock(GeyserSession session, Column column, int yOffset) {
-        Chunk[] javaSections = column.getChunks();
-        ChunkSection[] sections = new ChunkSection[javaSections.length - yOffset];
-
-        // Temporarily stores compound tags of Bedrock-only block entities
-        List<NbtMap> bedrockOnlyBlockEntities = new ArrayList<>();
-
-        BitSet waterloggedPaletteIds = new BitSet();
-        BitSet pistonOrFlowerPaletteIds = new BitSet();
-
-        boolean overworld = session.getChunkCache().isExtendedHeight();
-        int maxBedrockSectionY = ((overworld ? MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD : MAXIMUM_ACCEPTED_HEIGHT) >> 4) - 1;
-
-        for (int sectionY = 0; sectionY < javaSections.length; sectionY++) {
-            int bedrockSectionY = sectionY + (yOffset - ((overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4));
-            if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) {
-                // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client
-                continue;
-            }
-
-            Chunk javaSection = javaSections[sectionY];
-
-            // No need to encode an empty section...
-            if (javaSection == null || javaSection.isEmpty()) {
-                continue;
-            }
-
-            Palette javaPalette = javaSection.getPalette();
-            BitStorage javaData = javaSection.getStorage();
-
-            if (javaPalette instanceof GlobalPalette) {
-                // As this is the global palette, simply iterate through the whole chunk section once
-                ChunkSection section = new ChunkSection(session.getBlockMappings().getBedrockAirId());
-                for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
-                    int javaId = javaData.get(yzx);
-                    int bedrockId = session.getBlockMappings().getBedrockBlockId(javaId);
-                    int xzy = indexYZXtoXZY(yzx);
-                    section.getBlockStorageArray()[0].setFullBlock(xzy, bedrockId);
-
-                    if (BlockRegistries.WATERLOGGED.get().contains(javaId)) {
-                        section.getBlockStorageArray()[1].setFullBlock(xzy, session.getBlockMappings().getBedrockWaterId());
-                    }
-
-                    // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
-                    if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId)) {
-                        bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
-                                Vector3i.from((column.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (column.getZ() << 4) + ((yzx >> 4) & 0xF)),
-                                javaId
-                        ));
-                    }
-                }
-                sections[bedrockSectionY] = section;
-                continue;
-            }
-
-            IntList bedrockPalette = new IntArrayList(javaPalette.size());
-            waterloggedPaletteIds.clear();
-            pistonOrFlowerPaletteIds.clear();
-
-            // Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go
-            for (int i = 0; i < javaPalette.size(); i++) {
-                int javaId = javaPalette.idToState(i);
-                bedrockPalette.add(session.getBlockMappings().getBedrockBlockId(javaId));
-
-                if (BlockRegistries.WATERLOGGED.get().contains(javaId)) {
-                    waterloggedPaletteIds.set(i);
-                }
-
-                // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
-                if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId)) {
-                    pistonOrFlowerPaletteIds.set(i);
-                }
-            }
-
-            // Add Bedrock-exclusive block entities
-            // We only if the palette contained any blocks that are Bedrock-exclusive block entities to avoid iterating through the whole block data
-            // for no reason, as most sections will not contain any pistons or flower pots
-            if (!pistonOrFlowerPaletteIds.isEmpty()) {
-                for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
-                    int paletteId = javaData.get(yzx);
-                    if (pistonOrFlowerPaletteIds.get(paletteId)) {
-                        bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
-                                Vector3i.from((column.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (column.getZ() << 4) + ((yzx >> 4) & 0xF)),
-                                javaPalette.idToState(paletteId)
-                        ));
-                    }
-                }
-            }
-
-            BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE);
-            BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette);
-            BlockStorage[] layers;
-
-            // Convert data array from YZX to XZY coordinate order
-            if (waterloggedPaletteIds.isEmpty()) {
-                // No blocks are waterlogged, simply convert coordinate order
-                // This could probably be optimized further...
-                for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
-                    bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx));
-                }
-
-                layers = new BlockStorage[]{ layer0 };
-            } else {
-                // The section contains waterlogged blocks, we need to convert coordinate order AND generate a V1 block storage for
-                // layer 1 with palette ID 1 indicating water
-                int[] layer1Data = new int[BlockStorage.SIZE >> 5];
-                for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
-                    int paletteId = javaData.get(yzx);
-                    int xzy = indexYZXtoXZY(yzx);
-                    bedrockData.set(xzy, paletteId);
-
-                    if (waterloggedPaletteIds.get(paletteId)) {
-                        layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
-                    }
-                }
-
-                // V1 palette
-                IntList layer1Palette = new IntArrayList(2);
-                layer1Palette.add(session.getBlockMappings().getBedrockAirId()); // Air - see BlockStorage's constructor for more information
-                layer1Palette.add(session.getBlockMappings().getBedrockWaterId());
-
-                layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
-            }
-
-            sections[bedrockSectionY] = new ChunkSection(layers);
-        }
-
-        CompoundTag[] blockEntities = column.getTileEntities();
-        NbtMap[] bedrockBlockEntities = new NbtMap[blockEntities.length + bedrockOnlyBlockEntities.size()];
-        int i = 0;
-        while (i < blockEntities.length) {
-            CompoundTag tag = blockEntities[i];
-            String tagName;
-            Tag idTag = tag.get("id");
-            if (idTag != null) {
-                tagName = (String) idTag.getValue();
-            } else {
-                tagName = "Empty";
-                // Sometimes legacy tags have their ID be a StringTag with empty value
-                for (Tag subTag : tag) {
-                    if (subTag instanceof StringTag stringTag) {
-                        if (stringTag.getValue().isEmpty()) {
-                            tagName = stringTag.getName();
-                            break;
-                        }
-                    }
-                }
-                if (tagName.equals("Empty")) {
-                    GeyserConnector.getInstance().getLogger().debug("Got tag with no id: " + tag.getValue());
-                }
-            }
-
-            String id = BlockEntityUtils.getBedrockBlockEntityId(tagName);
-            int x = (int) tag.get("x").getValue();
-            int y = (int) tag.get("y").getValue();
-            int z = (int) tag.get("z").getValue();
-
-            // Get Java blockstate ID from block entity position
-            int blockState = 0;
-            Chunk section = column.getChunks()[(y >> 4) - yOffset];
-            if (section != null) {
-                blockState = section.get(x & 0xF, y & 0xF, z & 0xF);
-            }
-
-            if (tagName.equals("minecraft:lectern") && BlockStateValues.getLecternBookStates().get(blockState)) {
-                // If getLecternBookStates is false, let's just treat it like a normal block entity
-                bedrockBlockEntities[i] = session.getConnector().getWorldManager().getLecternDataAt(session, x, y, z, true);
-                i++;
-                continue;
-            }
-
-            BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id);
-            bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState);
-
-            // Check for custom skulls
-            if (session.getPreferencesCache().showCustomSkulls() && tag.contains("SkullOwner")) {
-                SkullBlockEntityTranslator.spawnPlayer(session, tag, blockState);
-            }
-            i++;
-        }
-
-        // Append Bedrock-exclusive block entities to output array
-        for (NbtMap tag : bedrockOnlyBlockEntities) {
-            bedrockBlockEntities[i] = tag;
-            i++;
-        }
-
-        return new ChunkData(sections, bedrockBlockEntities);
-    }
-
     public static void updateChunkPosition(GeyserSession session, Vector3i position) {
         Vector2i chunkPos = session.getLastChunkPosition();
         Vector2i newChunkPos = Vector2i.from(position.getX() >> 4, position.getZ() >> 4);