diff --git a/README.md b/README.md
index bbb9532a5..3e247f4b5 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
 
 Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
 
-### Currently supporting Minecraft Bedrock 1.18.0 - 1.18.30 and Minecraft Java 1.18.2.
+### Currently supporting Minecraft Bedrock 1.18.0 - 1.18.31 and Minecraft Java 1.18.2.
 
 ## Setting Up
 Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.
diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
index 4c2f5d651..9ff4c9ebc 100644
--- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
+++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
@@ -66,6 +66,8 @@ public class DumpInfo {
 
     private final DumpInfo.VersionInfo versionInfo;
     private final int cpuCount;
+    private final Locale systemLocale;
+    private final String systemEncoding;
     private Properties gitInfo;
     private final GeyserConfiguration config;
     private final Floodgate floodgate;
@@ -81,6 +83,8 @@ public class DumpInfo {
         this.versionInfo = new VersionInfo();
 
         this.cpuCount = Runtime.getRuntime().availableProcessors();
+        this.systemLocale = Locale.getDefault();
+        this.systemEncoding = System.getProperty("file.encoding");
 
         try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource("git.properties")) {
             this.gitInfo = new Properties();
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
index 48d0e80e0..a9b3ffedc 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
@@ -51,6 +51,7 @@ public final class BlockStateValues {
     private static final Int2ObjectMap<String> FLOWER_POT_VALUES = new Int2ObjectOpenHashMap<>();
     private static final IntSet HORIZONTAL_FACING_JIGSAWS = new IntOpenHashSet();
     private static final LecternHasBookMap LECTERN_BOOK_STATES = new LecternHasBookMap();
+    private static final IntSet NON_WATER_CAULDRONS = new IntOpenHashSet();
     private static final Int2IntMap NOTEBLOCK_PITCHES = new FixedInt2IntMap();
     private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap();
     private static final IntSet STICKY_PISTONS = new IntOpenHashSet();
@@ -176,7 +177,7 @@ public final class BlockStateValues {
             return;
         }
 
-        if (javaId.startsWith("minecraft:water")) {
+        if (javaId.startsWith("minecraft:water") && !javaId.contains("cauldron")) {
             String strLevel = javaId.substring(javaId.lastIndexOf("level=") + 6, javaId.length() - 1);
             int level = Integer.parseInt(strLevel);
             WATER_LEVEL.put(javaBlockState, level);
@@ -189,6 +190,11 @@ public final class BlockStateValues {
             if (direction.isHorizontal()) {
                 HORIZONTAL_FACING_JIGSAWS.add(javaBlockState);
             }
+            return;
+        }
+
+        if (javaId.contains("_cauldron") && !javaId.contains("water_")) {
+             NON_WATER_CAULDRONS.add(javaBlockState);
         }
     }
 
@@ -214,6 +220,15 @@ public final class BlockStateValues {
         return BED_COLORS.getOrDefault(state, (byte) -1);
     }
 
+    /**
+     * Non-water cauldrons (since Bedrock 1.18.30) must have a block entity packet sent on chunk load to fix rendering issues.
+     *
+     * @return if this Java block state is a non-empty non-water cauldron
+     */
+    public static boolean isCauldron(int state) {
+        return NON_WATER_CAULDRONS.contains(state);
+    }
+
     /**
      * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags
      * in Bedrock need the conditional information.
diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
index 1f28a0264..cb9be9459 100644
--- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
+++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
@@ -62,7 +62,9 @@ public final class GameProtocol {
         SUPPORTED_BEDROCK_CODECS.add(Bedrock_v486.V486_CODEC.toBuilder()
                 .minecraftVersion("1.18.10/1.18.12") // 1.18.11 is also supported, but was only on Switch and since that auto-updates it's not needed
                 .build());
-        SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
+        SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
+                .minecraftVersion("1.18.30/1.18.31")
+                .build());
     }
 
     /**
diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java
index d8aa6a456..412d7d779 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java
@@ -132,7 +132,6 @@ public class BlockRegistryPopulator {
             } catch (Exception e) {
                 throw new AssertionError("Unable to get blocks from runtime block states", e);
             }
-            Map<String, NbtMap> javaIdentifierToBedrockTag = new Object2ObjectOpenHashMap<>(blocksTag.size());
             // New since 1.16.100 - find the block runtime ID by the order given to us in the block palette,
             // as we no longer send a block palette
             Object2IntMap<NbtMap> blockStateOrderedMap = new Object2IntOpenHashMap<>(blocksTag.size());
@@ -202,10 +201,6 @@ public class BlockRegistryPopulator {
                     flowerPotBlocks.put(cleanJavaIdentifier.intern(), blocksTag.get(bedrockRuntimeId));
                 }
 
-                if (!cleanJavaIdentifier.equals(entry.getValue().get("bedrock_identifier").asText())) {
-                    javaIdentifierToBedrockTag.put(cleanJavaIdentifier.intern(), blocksTag.get(bedrockRuntimeId));
-                }
-
                 javaToBedrockBlocks[javaRuntimeId] = bedrockRuntimeId;
             }
 
@@ -240,7 +235,6 @@ public class BlockRegistryPopulator {
 
             BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion)
                     .javaToBedrockBlocks(javaToBedrockBlocks)
-                    .javaIdentifierToBedrockTag(javaIdentifierToBedrockTag)
                     .itemFrames(itemFrames)
                     .flowerPotBlocks(flowerPotBlocks)
                     .jigsawStateIds(jigsawStateIds)
diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java
index a105682a6..41318ee64 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java
@@ -47,12 +47,6 @@ public class BlockMappings {
 
     NbtList<NbtMap> bedrockBlockStates;
 
-    /**
-     * Contains a map of Java blocks to their respective Bedrock block tag, if the Java identifier is different from Bedrock.
-     * Required to fix villager trades with these blocks.
-     */
-    Map<String, NbtMap> javaIdentifierToBedrockTag;
-
     int commandBlockRuntimeId;
 
     Object2IntMap<NbtMap> itemFrames;
@@ -74,13 +68,4 @@ public class BlockMappings {
     public boolean isItemFrame(int bedrockBlockRuntimeId) {
         return this.itemFrames.values().contains(bedrockBlockRuntimeId);
     }
-
-    /**
-     * @param cleanJavaIdentifier the clean Java identifier of the block to look up
-     *
-     * @return the block tag of the block name mapped from Java to Bedrock.
-     */
-    public NbtMap getBedrockBlockNbt(String cleanJavaIdentifier) {
-        return this.javaIdentifierToBedrockTag.get(cleanJavaIdentifier);
-    }
 }
\ No newline at end of file
diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/BiomeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/BiomeTranslator.java
index a202c3f81..ac9a0517a 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/level/BiomeTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/level/BiomeTranslator.java
@@ -126,11 +126,10 @@ public class BiomeTranslator {
                 storage = new BlockStorage(bitArray, bedrockPalette);
             } else {
                 storage = new BlockStorage(0);
-                BitArray bitArray = storage.getBitArray();
 
                 // 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 javaId = palette.idToState(biomeData.getStorage().get(i));
                     int x = i & 3;
                     int y = (i >> 4) & 3;
                     int z = (i >> 2) & 3;
@@ -139,7 +138,9 @@ public class BiomeTranslator {
                     int idx = storage.idFor(biomeId);
                     // Convert biome coordinates into block coordinates
                     // Bedrock expects a full 4096 blocks
-                    multiplyIdToStorage(bitArray, idx, x, y, z);
+                    // Implementation note: storage.getBitArray() must be called and not stored - if the palette
+                    // grows, then the instance can change
+                    multiplyIdToStorage(storage.getBitArray(), idx, x, y, z);
                 }
             }
             return storage;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java
index 0ec7219c3..94760b66c 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java
@@ -26,7 +26,10 @@
 package org.geysermc.geyser.translator.level.block.entity;
 
 import com.nukkitx.math.vector.Vector3i;
+import com.nukkitx.nbt.NbtList;
 import com.nukkitx.nbt.NbtMap;
+import com.nukkitx.nbt.NbtType;
+import org.geysermc.geyser.level.block.BlockStateValues;
 import org.geysermc.geyser.session.GeyserSession;
 
 /**
@@ -59,6 +62,18 @@ public interface BedrockOnlyBlockEntity extends RequiresBlockState {
             return FlowerPotBlockEntityTranslator.getTag(session, blockState, position);
         } else if (PistonBlockEntityTranslator.isBlock(blockState)) {
             return PistonBlockEntityTranslator.getTag(blockState, position);
+        } else if (BlockStateValues.isCauldron(blockState)) {
+            // As of 1.18.30: this is required to make rendering not look weird on chunk load (lava and snow cauldrons look dim)
+            return NbtMap.builder()
+                    .putString("id", "Cauldron")
+                    .putByte("isMovable", (byte) 0)
+                    .putShort("PotionId", (short) -1)
+                    .putShort("PotionType", (short) -1)
+                    .putList("Items", NbtType.END, NbtList.EMPTY)
+                    .putInt("x", position.getX())
+                    .putInt("y", position.getY())
+                    .putInt("z", position.getZ())
+                    .build();
         }
         return null;
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java
index 8af5c8af1..1c9ded0c1 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java
@@ -178,12 +178,8 @@ public class JavaMerchantOffersTranslator extends PacketTranslator<ClientboundMe
             builder.put("tag", tag);
         }
 
-        NbtMap blockTag = session.getBlockMappings().getBedrockBlockNbt(mapping.getJavaIdentifier());
-        if (blockTag != null) {
-            // This fixes certain blocks being unable to stack after grabbing one
-            builder.putCompound("Block", blockTag);
-            builder.putShort("Damage", (short) 0);
-        }
+        // Implementation note: previously we added a block tag to fix some blocks (black concrete?) that wouldn't stack
+        // after buying. This no longer seems to be an issue as of Bedrock 1.18.30, and including it breaks sugar canes.
 
         return builder.build();
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java
index 165d90b36..3855b1139 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java
@@ -99,7 +99,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
         final List<NbtMap> bedrockBlockEntities = new ObjectArrayList<>(blockEntities.length);
 
         BitSet waterloggedPaletteIds = new BitSet();
-        BitSet pistonOrFlowerPaletteIds = new BitSet();
+        BitSet bedrockOnlyBlockEntityIds = new BitSet();
 
         BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
         int maxBedrockSectionY = (bedrockDimension.height() >> 4) - 1;
@@ -144,7 +144,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
                         }
 
                         // 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)) {
+                        if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) {
                             bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
                                     Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
                                     javaId
@@ -173,7 +173,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
 
                 IntList bedrockPalette = new IntArrayList(javaPalette.size());
                 waterloggedPaletteIds.clear();
-                pistonOrFlowerPaletteIds.clear();
+                bedrockOnlyBlockEntityIds.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++) {
@@ -184,19 +184,19 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
                         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);
+                    // Check if block is piston, flower or cauldron 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) || BlockStateValues.isCauldron(javaId)) {
+                        bedrockOnlyBlockEntityIds.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()) {
+                if (!bedrockOnlyBlockEntityIds.isEmpty()) {
                     for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
                         int paletteId = javaData.get(yzx);
-                        if (pistonOrFlowerPaletteIds.get(paletteId)) {
+                        if (bedrockOnlyBlockEntityIds.get(paletteId)) {
                             bedrockBlockEntities.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)
@@ -233,9 +233,9 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
                     }
 
                     // 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());
+                    IntList layer1Palette = IntList.of(
+                            session.getBlockMappings().getBedrockAirId(), // Air - see BlockStorage's constructor for more information
+                            session.getBlockMappings().getBedrockWaterId());
 
                     layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
                 }
diff --git a/core/src/main/java/org/geysermc/geyser/util/BlockEntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/BlockEntityUtils.java
index 2548ae0dc..ff283d08f 100644
--- a/core/src/main/java/org/geysermc/geyser/util/BlockEntityUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/BlockEntityUtils.java
@@ -30,15 +30,15 @@ import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.nbt.NbtMap;
 import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
-import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import org.geysermc.geyser.registry.Registries;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
 import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
 import org.geysermc.geyser.translator.level.block.entity.FlowerPotBlockEntityTranslator;
-import org.geysermc.geyser.registry.Registries;
 
 import javax.annotation.Nonnull;
-import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 public class BlockEntityUtils {
@@ -46,27 +46,22 @@ public class BlockEntityUtils {
      * A list of all block entities that require the Java block state in order to fill out their block entity information.
      * This list will be smaller with cache sections on as we don't need to double-cache data
      */
-    public static final ObjectArrayList<BedrockOnlyBlockEntity> BEDROCK_ONLY_BLOCK_ENTITIES = new ObjectArrayList<>();
+    public static final List<BedrockOnlyBlockEntity> BEDROCK_ONLY_BLOCK_ENTITIES = List.of(
+            (BedrockOnlyBlockEntity) Registries.BLOCK_ENTITIES.get().get(BlockEntityType.CHEST),
+            new FlowerPotBlockEntityTranslator()
+    );
 
     /**
      * Contains a list of irregular block entity name translations that can't be fit into the regex
      */
-    public static final Map<BlockEntityType, String> BLOCK_ENTITY_TRANSLATIONS = new HashMap<>() {
-        {
+    public static final Map<BlockEntityType, String> BLOCK_ENTITY_TRANSLATIONS = Map.of(
             // Bedrock/Java differences
-            put(BlockEntityType.ENCHANTING_TABLE, "EnchantTable");
-            put(BlockEntityType.JIGSAW, "JigsawBlock");
-            put(BlockEntityType.PISTON, "PistonArm");
-            put(BlockEntityType.TRAPPED_CHEST, "Chest");
+            BlockEntityType.ENCHANTING_TABLE, "EnchantTable",
+            BlockEntityType.JIGSAW, "JigsawBlock",
+            BlockEntityType.PISTON, "PistonArm",
+            BlockEntityType.TRAPPED_CHEST, "Chest"
             // There are some legacy IDs sent but as far as I can tell they are not needed for things to work properly
-        }
-    };
-
-    static {
-        // Seeing as there are only two - and, hopefully, will only ever be two - we can hardcode this
-        BEDROCK_ONLY_BLOCK_ENTITIES.add((BedrockOnlyBlockEntity) Registries.BLOCK_ENTITIES.get().get(BlockEntityType.CHEST));
-        BEDROCK_ONLY_BLOCK_ENTITIES.add(new FlowerPotBlockEntityTranslator());
-    }
+    );
 
     public static String getBedrockBlockEntityId(BlockEntityType type) {
         // These are the only exceptions when it comes to block entity ids
@@ -77,9 +72,9 @@ public class BlockEntityUtils {
 
         String id = type.name();
         // Split at every space or capital letter - for the latter, some legacy Java block entity tags are the correct format already
-        String[] words = id.split("_");;
+        String[] words = id.split("_");
         for (int i = 0; i < words.length; i++) {
-            words[i] = words[i].substring(0, 1).toUpperCase() + words[i].substring(1).toLowerCase();
+            words[i] = words[i].substring(0, 1).toUpperCase(Locale.ROOT) + words[i].substring(1).toLowerCase(Locale.ROOT);
         }
 
         return String.join("", words);