From 8c96c3b11d0c00c76039782740a73f588860ed15 Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Tue, 27 Jul 2021 20:29:27 -0400
Subject: [PATCH 1/3] Biome reworkings

- Introduce biome mappings for having a constant reference between Java biome identifier and their Bedrock equivalents
- Don't assume biome IDs and instead listen to the server for biome IDs
- Ensure that only valid Bedrock biomes are sent. With the caves and cliffs experimental toggle, Bedrock will crash if an invalid biome ID is sent its way.
---
 .../network/session/GeyserSession.java        |  14 ++-
 .../java/JavaJoinGameTranslator.java          |   2 +
 .../java/world/JavaChunkDataTranslator.java   |   6 +-
 .../translators/world/BiomeTranslator.java    | 112 ++++++++++++++++++
 .../translators/world/chunk/BlockStorage.java |   2 +-
 .../connector/registry/Registries.java        |   8 +-
 .../geysermc/connector/registry/Registry.java |   2 +-
 .../loader/BiomeIdentifierRegistryLoader.java |  73 ++++++++++++
 .../geysermc/connector/utils/BiomeUtils.java  |  85 -------------
 connector/src/main/resources/mappings         |   2 +-
 10 files changed, 210 insertions(+), 96 deletions(-)
 create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java
 create mode 100644 connector/src/main/java/org/geysermc/connector/registry/loader/BiomeIdentifierRegistryLoader.java
 delete mode 100644 connector/src/main/java/org/geysermc/connector/utils/BiomeUtils.java

diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
index 8f71e6fa8..355dcedcb 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
@@ -58,9 +58,7 @@ import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.*;
-import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
-import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
-import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.ints.*;
 import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
 import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
@@ -143,6 +141,7 @@ public class GeyserSession implements CommandSender {
     private final PreferencesCache preferencesCache;
     private final TagCache tagCache;
     private WorldCache worldCache;
+
     private final Int2ObjectMap<TeleportCache> teleportMap = new Int2ObjectOpenHashMap<>();
 
     private final PlayerInventory playerInventory;
@@ -188,6 +187,13 @@ public class GeyserSession implements CommandSender {
     private final Map<Vector3i, SkullPlayerEntity> skullCache = new ConcurrentHashMap<>();
     private final Long2ObjectMap<ClientboundMapItemDataPacket> storedMaps = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
 
+    /**
+     * Stores the differences between Java and Bedrock biome network IDs.
+     * If Java's ocean biome is 0, and Bedrock's is 0, it will not be in the list.
+     * If Java's bamboo biome is 42, and Bedrock's is 48, it will be in this list.
+     */
+    private final Int2IntMap biomeTranslations = new Int2IntOpenHashMap();
+
     /**
      * A map of Vector3i positions to Java entities.
      * Used for translating Bedrock block actions to Java entity actions.
@@ -503,7 +509,7 @@ public class GeyserSession implements CommandSender {
         ChunkUtils.sendEmptyChunks(this, playerEntity.getPosition().toInt(), 0, false);
 
         BiomeDefinitionListPacket biomeDefinitionListPacket = new BiomeDefinitionListPacket();
-        biomeDefinitionListPacket.setDefinitions(Registries.BIOMES.get());
+        biomeDefinitionListPacket.setDefinitions(Registries.BIOMES_NBT.get());
         upstream.sendPacket(biomeDefinitionListPacket);
 
         AvailableEntityIdentifiersPacket entityPacket = new AvailableEntityIdentifiersPacket();
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java
index 6b5f63438..e46a1a89c 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java
@@ -39,6 +39,7 @@ import org.geysermc.connector.entity.player.PlayerEntity;
 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.utils.ChunkUtils;
 import org.geysermc.connector.utils.DimensionUtils;
 import org.geysermc.connector.utils.PluginMessageUtils;
@@ -66,6 +67,7 @@ public class JavaJoinGameTranslator extends PacketTranslator<ServerJoinGamePacke
         }
         session.setWorldName(packet.getWorldName());
 
+        BiomeTranslator.loadServerBiomes(session, packet.getDimensionCodec());
         session.getTagCache().clear();
 
         AdventureSettingsPacket bedrockPacket = new AdventureSettingsPacket();
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaChunkDataTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaChunkDataTranslator.java
index 24b5a8a06..45a9e45f5 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaChunkDataTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaChunkDataTranslator.java
@@ -40,7 +40,7 @@ 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.chunk.ChunkSection;
-import org.geysermc.connector.utils.BiomeUtils;
+import org.geysermc.connector.network.translators.world.BiomeTranslator;
 import org.geysermc.connector.utils.ChunkUtils;
 
 @Translator(packet = ServerChunkDataPacket.class)
@@ -101,7 +101,7 @@ public class JavaChunkDataTranslator extends PacketTranslator<ServerChunkDataPac
 
                     if (NEW_BIOME_WRITE) {
                         for (int i = 0; i < sectionCount; i++) {
-                            BiomeUtils.toNewBedrockBiome(column.getBiomeData(), i).writeToNetwork(byteBuf);
+                            BiomeTranslator.toNewBedrockBiome(session, column.getBiomeData(), i).writeToNetwork(byteBuf);
                         }
 
                         // As of 1.17.10, Bedrock hardcodes to always read 32 biome sections
@@ -110,7 +110,7 @@ public class JavaChunkDataTranslator extends PacketTranslator<ServerChunkDataPac
                             byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
                         }
                     } else {
-                        byteBuf.writeBytes(BiomeUtils.toBedrockBiome(column.getBiomeData())); // Biomes - 256 bytes
+                        byteBuf.writeBytes(BiomeTranslator.toBedrockBiome(session, column.getBiomeData())); // Biomes - 256 bytes
                     }
                     byteBuf.writeByte(0); // Border blocks - Edu edition only
                     VarInts.writeUnsignedInt(byteBuf, 0); // extra data length, 0 for now
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
new file mode 100644
index 000000000..326c9a6f7
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java
@@ -0,0 +1,112 @@
+/*
+ * 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.world;
+
+import com.github.steveice10.opennbt.tag.builtin.*;
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
+import org.geysermc.connector.network.translators.world.chunk.ChunkSection;
+import org.geysermc.connector.registry.Registries;
+
+import java.util.Arrays;
+
+// Based off of ProtocolSupport's LegacyBiomeData.java:
+// https://github.com/ProtocolSupport/ProtocolSupport/blob/b2cad35977f3fcb65bee57b9e14fc9c975f71d32/src/protocolsupport/protocol/typeremapper/legacy/LegacyBiomeData.java
+// Array index formula by https://wiki.vg/Chunk_Format
+public class BiomeTranslator {
+
+    public static void loadServerBiomes(GeyserSession session, CompoundTag codec) {
+        Int2IntMap biomeTranslations = session.getBiomeTranslations();
+        biomeTranslations.clear();
+
+        CompoundTag worldGen = codec.get("minecraft:worldgen/biome");
+        ListTag serverBiomes = worldGen.get("value");
+
+        for (Tag tag : serverBiomes) {
+            CompoundTag biomeTag = (CompoundTag) tag;
+
+            String javaIdentifier = ((StringTag) biomeTag.get("name")).getValue();
+            int bedrockId = Registries.BIOME_IDENTIFIERS.get().getOrDefault(javaIdentifier, 0);
+            int javaId = ((IntTag) biomeTag.get("id")).getValue();
+
+            if (javaId != bedrockId) {
+                // When we see the Java ID, we should instead apply the Bedrock ID
+                biomeTranslations.put(javaId, bedrockId);
+            }
+        }
+    }
+
+    public static byte[] toBedrockBiome(GeyserSession session, int[] biomeData) {
+        byte[] bedrockData = new byte[256];
+        if (biomeData == null) {
+            return bedrockData;
+        }
+        Int2IntMap biomeTranslations = session.getBiomeTranslations();
+
+        for (int y = 0; y < 16; y += 4) {
+            for (int z = 0; z < 16; z += 4) {
+                for (int x = 0; x < 16; x += 4) {
+                    int javaId = biomeData[((y >> 2) & 63) << 4 | ((z >> 2) & 3) << 2 | ((x >> 2) & 3)];
+                    byte biomeId = (byte) biomeTranslations.getOrDefault(javaId, javaId);
+                    int offset = ((z + (y / 4)) << 4) | x;
+                    Arrays.fill(bedrockData, offset, offset + 4, biomeId);
+                }
+            }
+        }
+        return bedrockData;
+    }
+
+    public static BlockStorage toNewBedrockBiome(GeyserSession session, int[] biomeData, int ySection) {
+        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);
+
+        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, or this ID if it's the same
+            int biomeId = biomeTranslations.getOrDefault(javaId, 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;
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java
index faf8d6dc8..2d027faba 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/BlockStorage.java
@@ -101,7 +101,7 @@ public class BlockStorage {
         this.bitArray = newBitArray;
     }
 
-    private int idFor(int runtimeId) {
+    public int idFor(int runtimeId) { // Set to public so we can reuse the palette ID for biomes
         int index = this.palette.indexOf(runtimeId);
         if (index != -1) {
             return index;
diff --git a/connector/src/main/java/org/geysermc/connector/registry/Registries.java b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
index 5cd2e4806..3447bdfc8 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/Registries.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
@@ -36,6 +36,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.PotionMixData;
 import it.unimi.dsi.fastutil.Pair;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
 import org.geysermc.connector.network.translators.effect.Effect;
 import org.geysermc.connector.network.translators.sound.SoundHandler;
@@ -59,7 +60,12 @@ public class Registries {
     /**
      * A registry holding a CompoundTag of all the known biomes.
      */
-    public static final SimpleRegistry<NbtMap> BIOMES = SimpleRegistry.create("bedrock/biome_definitions.dat", RegistryLoaders.NBT);
+    public static final SimpleRegistry<NbtMap> BIOMES_NBT = SimpleRegistry.create("bedrock/biome_definitions.dat", RegistryLoaders.NBT);
+
+    /**
+     * A mapped registry which stores Java biome identifiers and their Bedrock biome identifier.
+     */
+    public static final SimpleRegistry<Object2IntMap<String>> BIOME_IDENTIFIERS = SimpleRegistry.create("mappings/biomes.json", BiomeIdentifierRegistryLoader::new);
 
     /**
      * A mapped registry which stores a block entity identifier to its {@link BlockEntityTranslator}.
diff --git a/connector/src/main/java/org/geysermc/connector/registry/Registry.java b/connector/src/main/java/org/geysermc/connector/registry/Registry.java
index 0e999442f..135e94342 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/Registry.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/Registry.java
@@ -58,7 +58,7 @@ import java.util.function.Consumer;
  * however it demonstrates a fairly basic use case of how this system works. Typically
  * though, the first parameter would be a location of some sort, such as a file path
  * where the loader will load the mappings from. The NBT registry is a good reference
- * point for something both simple and practical. See {@link Registries#BIOMES} and
+ * point for something both simple and practical. See {@link Registries#BIOMES_NBT} and
  * {@link org.geysermc.connector.registry.loader.NbtRegistryLoader}.
  *
  * @param <M> the value being held by the registry
diff --git a/connector/src/main/java/org/geysermc/connector/registry/loader/BiomeIdentifierRegistryLoader.java b/connector/src/main/java/org/geysermc/connector/registry/loader/BiomeIdentifierRegistryLoader.java
new file mode 100644
index 000000000..c38e3efa9
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/registry/loader/BiomeIdentifierRegistryLoader.java
@@ -0,0 +1,73 @@
+/*
+ * 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.registry.loader;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.type.TypeReference;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.utils.FileUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+public class BiomeIdentifierRegistryLoader implements RegistryLoader<String, Object2IntMap<String>> {
+
+    @Override
+    public Object2IntMap<String> load(String input) {
+        // As of Bedrock Edition 1.17.10 with the experimental toggle, any unmapped biome identifier sent to the client
+        // crashes the client. Therefore, we need to have a list of all valid Bedrock biome IDs with which we can use from.
+        // The server sends the corresponding Java network IDs, so we don't need to worry about that now.
+
+        // Reference variable for Jackson to read off of
+        TypeReference<Map<String, BiomeEntry>> biomeEntriesType = new TypeReference<Map<String, BiomeEntry>>() { };
+        Map<String, BiomeEntry> biomeEntries;
+
+        try (InputStream stream = FileUtils.getResource("mappings/biomes.json")) {
+            biomeEntries = GeyserConnector.JSON_MAPPER.readValue(stream, biomeEntriesType);
+        } catch (IOException e) {
+            throw new AssertionError("Unable to load Bedrock runtime biomes", e);
+        }
+
+        Object2IntMap<String> biomes = new Object2IntOpenHashMap<>();
+        for (Map.Entry<String, BiomeEntry> biome : biomeEntries.entrySet()) {
+            // Java Edition identifier -> Bedrock integer ID
+            biomes.put(biome.getKey(), biome.getValue().bedrockId);
+        }
+
+        return biomes;
+    }
+
+    private static class BiomeEntry {
+        /**
+         * The Bedrock network ID for this biome.
+         */
+        @JsonProperty("bedrock_id")
+        private int bedrockId;
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/utils/BiomeUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BiomeUtils.java
deleted file mode 100644
index da557ea57..000000000
--- a/connector/src/main/java/org/geysermc/connector/utils/BiomeUtils.java
+++ /dev/null
@@ -1,85 +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.utils;
-
-import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
-
-import java.util.Arrays;
-
-// Based off of ProtocolSupport's LegacyBiomeData.java:
-// https://github.com/ProtocolSupport/ProtocolSupport/blob/b2cad35977f3fcb65bee57b9e14fc9c975f71d32/src/protocolsupport/protocol/typeremapper/legacy/LegacyBiomeData.java
-// Array index formula by https://wiki.vg/Chunk_Format
-public class BiomeUtils {
-    public static byte[] toBedrockBiome(int[] biomeData) {
-        byte[] bedrockData = new byte[256];
-        if (biomeData == null) {
-            return bedrockData;
-        }
-
-        for (int y = 0; y < 16; y += 4) {
-            for (int z = 0; z < 16; z += 4) {
-                for (int x = 0; x < 16; x += 4) {
-                    byte biomeId = (byte) biomeID(biomeData, x, y, z);
-                    int offset = ((z + (y / 4)) << 4) | x;
-                    Arrays.fill(bedrockData, offset, offset + 4, biomeId);
-                }
-            }
-        }
-        return bedrockData;
-    }
-
-    public static BlockStorage toNewBedrockBiome(int[] biomeData, int ySection) {
-        BlockStorage storage = new BlockStorage(0);
-        int blockY = ySection << 4;
-        int i = 0;
-        // Iterate over biomes like a chunk, grab the biome from Java, and add it to Bedrock's biome palette
-        // Might be able to be optimized by iterating over Java's biome data section?? Unsure.
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = blockY; y < (blockY + 16); y++) {
-                    int biomeId = biomeID(biomeData, x, y, z);
-                    storage.setFullBlock(i, biomeId);
-                    i++;
-                }
-            }
-        }
-        return storage;
-    }
-
-    private static int biomeID(int[] biomeData, int x, int y, int z) {
-        int biomeId = biomeData[((y >> 2) & 63) << 4 | ((z >> 2) & 3) << 2 | ((x >> 2) & 3)];
-        if (biomeId >= 40 && biomeId <= 43) { // Java has multiple End dimensions that Bedrock doesn't recognize
-            biomeId = 9;
-        } else if (biomeId >= 170 && biomeId <= 173) { // 1.16 nether biomes. Dunno why it's like this :microjang:
-            biomeId += 8;
-        } else if (biomeId == 168) { // Bamboo jungle
-            biomeId = 48;
-        } else if (biomeId == 169) { // Bamboo jungle hills
-            biomeId = 49;
-        }
-        return biomeId;
-    }
-}
diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings
index 8351b0f5b..f109d34a3 160000
--- a/connector/src/main/resources/mappings
+++ b/connector/src/main/resources/mappings
@@ -1 +1 @@
-Subproject commit 8351b0f5bb6e9a1d614f84e18c91e82288c34bf6
+Subproject commit f109d34a343da0ade6132661839b893859680d91

From a5beebdffab4e3238e13854b1d5fd0a7ece1f066 Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Tue, 27 Jul 2021 22:52:07 -0400
Subject: [PATCH 2/3] Add best-fit replacement biomes for custom biomes

---
 .../translators/world/BiomeTranslator.java    | 39 ++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

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 326c9a6f7..3f22f8bc9 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
@@ -50,9 +50,46 @@ public class BiomeTranslator {
             CompoundTag biomeTag = (CompoundTag) tag;
 
             String javaIdentifier = ((StringTag) biomeTag.get("name")).getValue();
-            int bedrockId = Registries.BIOME_IDENTIFIERS.get().getOrDefault(javaIdentifier, 0);
+            int bedrockId = Registries.BIOME_IDENTIFIERS.get().getOrDefault(javaIdentifier, -1);
             int javaId = ((IntTag) biomeTag.get("id")).getValue();
 
+            if (bedrockId == -1) {
+                // There is no matching Bedrock variation for this biome; let's set the closest match based on biome category
+                String category = ((StringTag) ((CompoundTag) biomeTag.get("element")).get("category")).getValue();
+                String replacementBiome;
+                switch (category) {
+                    case "extreme_hills":
+                        replacementBiome = "minecraft:mountains";
+                        break;
+                    case "icy":
+                        replacementBiome = "minecraft:ice_spikes";
+                        break;
+                    case "mushroom":
+                        replacementBiome = "minecraft:mushroom_fields";
+                        break;
+                    case "nether":
+                        replacementBiome = "minecraft:nether_wastes";
+                        break;
+                    default:
+                        replacementBiome = "minecraft:ocean"; // Typically ID 0 so a good default
+                        break;
+                    case "taiga":
+                    case "jungle":
+                    case "mesa":
+                    case "plains":
+                    case "savanna":
+                    case "the_end":
+                    case "beach":
+                    case "ocean":
+                    case "desert":
+                    case "river":
+                    case "swamp":
+                        replacementBiome = "minecraft:" + category;
+                        break;
+                }
+                bedrockId = Registries.BIOME_IDENTIFIERS.get().getInt(replacementBiome);
+            }
+
             if (javaId != bedrockId) {
                 // When we see the Java ID, we should instead apply the Bedrock ID
                 biomeTranslations.put(javaId, bedrockId);

From 4241b5463f158e473b38807908b724845db5bb42 Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Tue, 27 Jul 2021 23:01:38 -0400
Subject: [PATCH 3/3] Fix mesa replacement biome

---
 .../connector/network/translators/world/BiomeTranslator.java  | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

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 3f22f8bc9..e76d179bf 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
@@ -64,6 +64,9 @@ public class BiomeTranslator {
                     case "icy":
                         replacementBiome = "minecraft:ice_spikes";
                         break;
+                    case "mesa":
+                        replacementBiome = "minecraft:badlands";
+                        break;
                     case "mushroom":
                         replacementBiome = "minecraft:mushroom_fields";
                         break;
@@ -75,7 +78,6 @@ public class BiomeTranslator {
                         break;
                     case "taiga":
                     case "jungle":
-                    case "mesa":
                     case "plains":
                     case "savanna":
                     case "the_end":