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 teleportMap = new Int2ObjectOpenHashMap<>(); private final PlayerInventory playerInventory; @@ -188,6 +187,13 @@ public class GeyserSession implements CommandSender { private final Map skullCache = new ConcurrentHashMap<>(); private final Long2ObjectMap 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> 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 BIOMES = SimpleRegistry.create("bedrock/biome_definitions.dat", RegistryLoaders.NBT); + public static final SimpleRegistry 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> 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 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> { + + @Override + public Object2IntMap 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> biomeEntriesType = new TypeReference>() { }; + Map 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 biomes = new Object2IntOpenHashMap<>(); + for (Map.Entry 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":