diff --git a/.gitignore b/.gitignore
index c4c878af..69c07e50 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 194ec6e5..714c0852 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 d51cf21a..9cc0bc06 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 4873a175..c6443bd0 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 00000000..56520e91
--- /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 9601a298..2cc9ea13 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 5170929a..6dae9b4d 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