diff --git a/.gitignore b/.gitignore index c4c878af4..69c07e500 100644 --- a/.gitignore +++ b/.gitignore @@ -241,4 +241,4 @@ config.yml logs/ public-key.pem locales/ -cache/ \ No newline at end of file +/cache/ \ No newline at end of file diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index 194ec6e5a..714c0852f 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -20,7 +20,7 @@ org.spigotmc spigot-api - 1.14-R0.1-SNAPSHOT + 1.15.2-R0.1-SNAPSHOT provided diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java index d51cf21a5..9cc0bc064 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java @@ -56,7 +56,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { private GeyserSpigotConfiguration geyserConfig; private GeyserSpigotLogger geyserLogger; private IGeyserPingPassthrough geyserSpigotPingPassthrough; - private GeyserSpigotBlockPlaceListener blockPlaceListener; private GeyserSpigotWorldManager geyserWorldManager; private GeyserConnector connector; @@ -124,10 +123,15 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { // Used to determine if Block.getBlockData() is present. boolean isLegacy = !isCompatible(Bukkit.getServer().getVersion(), "1.13.0"); if (isLegacy) - geyserLogger.debug("Legacy version of Minecraft (1.12.2 or older) detected."); + geyserLogger.debug("Legacy version of Minecraft (1.12.2 or older) detected; falling back to ViaVersion for block state retrieval."); - this.geyserWorldManager = new GeyserSpigotWorldManager(isLegacy, isViaVersion); - this.blockPlaceListener = new GeyserSpigotBlockPlaceListener(connector, isLegacy, isViaVersion); + boolean use3dBiomes = isCompatible(Bukkit.getServer().getVersion(), "1.16.0"); + if (!use3dBiomes) { + geyserLogger.debug("Legacy version of Minecraft (1.15.2 or older) detected; not using 3D biomes."); + } + + this.geyserWorldManager = new GeyserSpigotWorldManager(isLegacy, use3dBiomes, isViaVersion); + GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(connector, isLegacy, isViaVersion); Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java index 4873a1750..c6443bd05 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotWorldManager.java @@ -25,23 +25,71 @@ package org.geysermc.platform.spigot.world; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.databind.JsonNode; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.block.Biome; import org.bukkit.block.Block; +import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.GeyserWorldManager; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.GameRule; +import org.geysermc.connector.utils.LanguageUtils; import us.myles.ViaVersion.protocols.protocol1_13_1to1_13.Protocol1_13_1To1_13; import us.myles.ViaVersion.protocols.protocol1_16_2to1_16_1.data.MappingData; -@AllArgsConstructor +import java.io.InputStream; + public class GeyserSpigotWorldManager extends GeyserWorldManager { private final boolean isLegacy; - // You need ViaVersion to connect to an older server with Geyser. - // However, we still check for ViaVersion in case there's some other way that gets Geyser on a pre-1.13 Bukkit server + private final boolean use3dBiomes; + /** + * You need ViaVersion to connect to an older server with Geyser. + * However, we still check for ViaVersion in case there's some other way that gets Geyser on a pre-1.13 Bukkit server + */ private final boolean isViaVersion; + /** + * Stores a list of {@link Biome} ordinal numbers to Minecraft biome numeric IDs. + * + * Working with the Biome enum in Spigot poses two problems: + * 1: The Biome enum values change in both order and names over the years. + * 2: There is no way to get the Minecraft biome ID from the name itself with Spigot. + * To solve both of these problems, we store a JSON file of every Biome enum that has existed, + * along with its 1.16 biome number. + * + * The key is the Spigot Biome ordinal; the value is the Minecraft Java biome numerical ID + */ + private final Int2IntMap biomeToIdMap = new Int2IntOpenHashMap(Biome.values().length); + + public GeyserSpigotWorldManager(boolean isLegacy, boolean use3dBiomes, boolean isViaVersion) { + this.isLegacy = isLegacy; + this.use3dBiomes = use3dBiomes; + this.isViaVersion = isViaVersion; + + // Load the values into the biome-to-ID map + InputStream biomeStream = FileUtils.getResource("biomes.json"); + JsonNode biomes; + try { + biomes = GeyserConnector.JSON_MAPPER.readTree(biomeStream); + } catch (Exception e) { + throw new AssertionError(LanguageUtils.getLocaleStringLog("geyser.toolbox.fail.runtime_java"), e); + } + // Only load in the biomes that are present in this version of Minecraft + for (Biome enumBiome : Biome.values()) { + if (biomes.has(enumBiome.toString())) { + biomeToIdMap.put(enumBiome.ordinal(), biomes.get(enumBiome.toString()).intValue()); + } else { + GeyserConnector.getInstance().getLogger().debug("No biome mapping found for " + enumBiome.toString() + + ", defaulting to 0"); + biomeToIdMap.put(enumBiome.ordinal(), 0); + } + } + } @Override public int getBlockAt(GeyserSession session, int x, int y, int z) { @@ -77,6 +125,43 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager { } @Override + @SuppressWarnings("deprecation") + public int[] getBiomeDataAt(GeyserSession session, int x, int z) { + if (session.getPlayerEntity() == null) { + return new int[1024]; + } + int[] biomeData = new int[1024]; + World world = Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld(); + int chunkX = x << 4; + int chunkZ = z << 4; + int chunkXmax = chunkX + 16; + int chunkZmax = chunkZ + 16; + // 3D biomes didn't exist until 1.15 + if (use3dBiomes) { + for (int localX = chunkX; localX < chunkXmax; localX += 4) { + for (int localY = 0; localY < 255; localY += + 4) { + for (int localZ = chunkZ; localZ < chunkZmax; localZ += 4) { + // Index is based on wiki.vg's index requirements + final int i = ((localY >> 2) & 63) << 4 | ((localZ >> 2) & 3) << 2 | ((localX >> 2) & 3); + biomeData[i] = biomeToIdMap.getOrDefault(world.getBiome(localX, localY, localZ).ordinal(), 0); + } + } + } + } else { + // Looks like the same code, but we're not checking the Y coordinate here + for (int localX = chunkX; localX < chunkXmax; localX += 4) { + for (int localY = 0; localY < 255; localY += + 4) { + for (int localZ = chunkZ; localZ < chunkZmax; localZ += 4) { + // Index is based on wiki.vg's index requirements + final int i = ((localY >> 2) & 63) << 4 | ((localZ >> 2) & 3) << 2 | ((localX >> 2) & 3); + biomeData[i] = biomeToIdMap.getOrDefault(world.getBiome(localX, localZ).ordinal(), 0); + } + } + } + } + return biomeData; + } + public Boolean getGameRuleBool(GeyserSession session, GameRule gameRule) { return Boolean.parseBoolean(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getGameRuleValue(gameRule.getJavaID())); } diff --git a/bootstrap/spigot/src/main/resources/biomes.json b/bootstrap/spigot/src/main/resources/biomes.json new file mode 100644 index 000000000..56520e914 --- /dev/null +++ b/bootstrap/spigot/src/main/resources/biomes.json @@ -0,0 +1,155 @@ +{ + "MUTATED_ICE_FLATS" : 140, + "MUTATED_TAIGA" : 133, + "SAVANNA_PLATEAU_MOUNTAINS" : 164, + "DEEP_WARM_OCEAN" : 47, + "REDWOOD_TAIGA_HILLS" : 33, + "THE_VOID" : 127, + "COLD_TAIGA_MOUNTAINS" : 158, + "BAMBOO_JUNGLE_HILLS" : 169, + "MOUNTAINS" : 3, + "MESA_PLATEAU" : 39, + "SNOWY_TAIGA_HILLS" : 31, + "DEEP_FROZEN_OCEAN" : 50, + "EXTREME_HILLS" : 3, + "BIRCH_FOREST_MOUNTAINS" : 155, + "FOREST" : 4, + "BIRCH_FOREST" : 27, + "SNOWY_TUNDRA" : 12, + "ICE_SPIKES" : 140, + "FROZEN_OCEAN" : 10, + "WARPED_FOREST" : 172, + "WOODED_BADLANDS_PLATEAU" : 38, + "BADLANDS_PLATEAU" : 39, + "ICE_PLAINS_SPIKES" : 140, + "MEGA_TAIGA" : 32, + "MUTATED_SAVANNA_ROCK" : 164, + "SAVANNA_PLATEAU" : 36, + "DARK_FOREST_HILLS" : 157, + "END_MIDLANDS" : 41, + "SHATTERED_SAVANNA_PLATEAU" : 164, + "SAVANNA" : 35, + "MUSHROOM_ISLAND_SHORE" : 15, + "SWAMP" : 6, + "ICE_MOUNTAINS" : 13, + "BEACH" : 16, + "MUTATED_MESA_CLEAR_ROCK" : 167, + "END_HIGHLANDS" : 42, + "COLD_BEACH" : 26, + "JUNGLE" : 21, + "MUTATED_TAIGA_COLD" : 158, + "TALL_BIRCH_HILLS" : 156, + "DARK_FOREST" : 29, + "WOODED_HILLS" : 18, + "HELL" : 8, + "MUTATED_REDWOOD_TAIGA" : 160, + "MESA_PLATEAU_FOREST" : 38, + "MUSHROOM_ISLAND" : 14, + "BADLANDS" : 37, + "END_BARRENS" : 43, + "MUTATED_EXTREME_HILLS_WITH_TREES" : 162, + "MUTATED_JUNGLE_EDGE" : 151, + "MODIFIED_BADLANDS_PLATEAU" : 167, + "ROOFED_FOREST_MOUNTAINS" : 157, + "SOUL_SAND_VALLEY" : 170, + "DESERT" : 2, + "MUTATED_PLAINS" : 129, + "MUTATED_BIRCH_FOREST" : 155, + "WOODED_MOUNTAINS" : 34, + "TAIGA_HILLS" : 19, + "BAMBOO_JUNGLE" : 168, + "SWAMPLAND_MOUNTAINS" : 134, + "DESERT_MOUNTAINS" : 130, + "REDWOOD_TAIGA" : 32, + "MUSHROOM_FIELDS" : 14, + "GIANT_TREE_TAIGA_HILLS" : 33, + "PLAINS" : 1, + "JUNGLE_EDGE" : 23, + "SAVANNA_MOUNTAINS" : 163, + "DEEP_COLD_OCEAN" : 49, + "DESERT_LAKES" : 130, + "MOUNTAIN_EDGE" : 20, + "SNOWY_MOUNTAINS" : 13, + "MESA_PLATEAU_MOUNTAINS" : 167, + "JUNGLE_MOUNTAINS" : 149, + "SMALLER_EXTREME_HILLS" : 20, + "MESA_PLATEAU_FOREST_MOUNTAINS" : 166, + "NETHER_WASTES" : 8, + "BIRCH_FOREST_HILLS_MOUNTAINS" : 156, + "MUTATED_JUNGLE" : 149, + "WARM_OCEAN" : 44, + "DEEP_OCEAN" : 24, + "STONE_BEACH" : 25, + "MODIFIED_JUNGLE" : 149, + "MUTATED_SAVANNA" : 163, + "TAIGA_COLD_HILLS" : 31, + "OCEAN" : 0, + "SMALL_END_ISLANDS" : 40, + "MUSHROOM_FIELD_SHORE" : 15, + "GRAVELLY_MOUNTAINS" : 131, + "FROZEN_RIVER" : 11, + "TAIGA_COLD" : 30, + "BASALT_DELTAS" : 173, + "EXTREME_HILLS_WITH_TREES" : 34, + "MEGA_TAIGA_HILLS" : 33, + "MUTATED_FOREST" : 132, + "MUTATED_BIRCH_FOREST_HILLS" : 156, + "SKY" : 9, + "LUKEWARM_OCEAN" : 45, + "EXTREME_HILLS_MOUNTAINS" : 131, + "COLD_TAIGA_HILLS" : 31, + "THE_END" : 9, + "SUNFLOWER_PLAINS" : 129, + "SAVANNA_ROCK" : 36, + "ERODED_BADLANDS" : 165, + "STONE_SHORE" : 25, + "EXTREME_HILLS_PLUS_MOUNTAINS" : 162, + "CRIMSON_FOREST" : 171, + "VOID" : 127, + "SNOWY_TAIGA" : 30, + "SNOWY_TAIGA_MOUNTAINS" : 158, + "FLOWER_FOREST" : 132, + "COLD_OCEAN" : 46, + "BEACHES" : 16, + "MESA" : 37, + "MUSHROOM_SHORE" : 15, + "MESA_CLEAR_ROCK" : 39, + "NETHER" : 8, + "ICE_PLAINS" : 12, + "SHATTERED_SAVANNA" : 163, + "ROOFED_FOREST" : 29, + "GIANT_SPRUCE_TAIGA_HILLS" : 161, + "SNOWY_BEACH" : 26, + "MESA_BRYCE" : 165, + "JUNGLE_EDGE_MOUNTAINS" : 151, + "MUTATED_DESERT" : 130, + "MODIFIED_GRAVELLY_MOUNTAINS" : 158, + "MEGA_SPRUCE_TAIGA" : 160, + "TAIGA_MOUNTAINS" : 133, + "SMALL_MOUNTAINS" : 20, + "EXTREME_HILLS_PLUS" : 34, + "GIANT_SPRUCE_TAIGA" : 160, + "FOREST_HILLS" : 18, + "DESERT_HILLS" : 17, + "MUTATED_REDWOOD_TAIGA_HILLS" : 161, + "MEGA_SPRUCE_TAIGA_HILLS" : 161, + "RIVER" : 7, + "GIANT_TREE_TAIGA" : 32, + "SWAMPLAND" : 6, + "JUNGLE_HILLS" : 22, + "TALL_BIRCH_FOREST" : 155, + "DEEP_LUKEWARM_OCEAN" : 48, + "MESA_ROCK" : 38, + "SWAMP_HILLS" : 134, + "MODIFIED_WOODED_BADLANDS_PLATEAU" : 166, + "MODIFIED_JUNGLE_EDGE" : 151, + "BIRCH_FOREST_HILLS" : 28, + "COLD_TAIGA" : 30, + "TAIGA" : 5, + "MUTATED_MESA_ROCK" : 166, + "MUTATED_SWAMPLAND" : 134, + "ICE_FLATS" : 12, + "MUTATED_ROOFED_FOREST" : 157, + "MUTATED_MESA" : 165, + "MUTATED_EXTREME_HILLS" : 131 +} 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 9601a2981..2cc9ea134 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 @@ -42,7 +42,7 @@ public class ChunkCache { private final boolean cache; @Getter - private Map chunks = new HashMap<>(); + private final Map chunks = new HashMap<>(); public ChunkCache(GeyserSession session) { if (session.getConnector().getWorldManager().getClass() == GeyserBootstrap.DEFAULT_CHUNK_MANAGER.getClass()) { @@ -57,6 +57,15 @@ public class ChunkCache { return; } ChunkPosition position = new ChunkPosition(chunk.getX(), chunk.getZ()); + if (chunk.getBiomeData() == null && chunks.containsKey(position)) { + Column newColumn = chunk; + chunk = chunks.get(position); + for (int i = 0; i < newColumn.getChunks().length; i++) { + if (newColumn.getChunks()[i] != null) { + chunk.getChunks()[i] = newColumn.getChunks()[i]; + } + } + } chunks.put(position, chunk); } 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 5170929a9..6dae9b4d6 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 @@ -45,18 +45,33 @@ import org.geysermc.connector.utils.ChunkUtils; @Translator(packet = ServerChunkDataPacket.class) public class JavaChunkDataTranslator extends PacketTranslator { + /** + * Determines if we should process non-full chunks + */ + private final boolean isCacheChunks; + + public JavaChunkDataTranslator() { + isCacheChunks = GeyserConnector.getInstance().getConfig().isCacheChunks(); + } + @Override public void translate(ServerChunkDataPacket packet, GeyserSession session) { if (session.isSpawned()) { ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt()); } - if (packet.getColumn().getBiomeData() == null) //Non-full chunk + if (packet.getColumn().getBiomeData() == null && !isCacheChunks) { + // Non-full chunk without chunk caching + session.getConnector().getLogger().debug("Not sending non-full chunk because chunk caching is off."); return; + } + + // Non-full chunks don't have all the chunk data, and Bedrock won't accept that + final boolean isNonFullChunk = (packet.getColumn().getBiomeData() == null); GeyserConnector.getInstance().getGeneralThreadPool().execute(() -> { try { - ChunkUtils.ChunkData chunkData = ChunkUtils.translateToBedrock(packet.getColumn()); + ChunkUtils.ChunkData chunkData = ChunkUtils.translateToBedrock(session, packet.getColumn(), isNonFullChunk); ByteBuf byteBuf = Unpooled.buffer(32); ChunkSection[] sections = chunkData.sections; @@ -71,7 +86,12 @@ public class JavaChunkDataTranslator extends PacketTranslator