diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 75a76cd05..46653e624 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -11,4 +11,4 @@ assignees: '' Add a description **Alternatives?** -Any alternatives you have tryed +List any alternatives you might have tried diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 9cb0726ca..78a3ce299 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -22,7 +22,7 @@ jobs: - name: submodules-init uses: snickerbockers/submodules-init@v4 - name: Build with Maven - run: mvn -B package + run: mvn -B package -T 2C - name: Archive artifacts (Geyser Standalone) uses: actions/upload-artifact@v2 if: success() diff --git a/.gitignore b/.gitignore index c4c878af4..e3c3b0a56 100644 --- a/.gitignore +++ b/.gitignore @@ -241,4 +241,5 @@ config.yml logs/ public-key.pem locales/ -cache/ \ No newline at end of file +/cache/ +/packs/ \ No newline at end of file diff --git a/README.md b/README.md index 64ff22522..ba3e6fc2d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,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 now joined us here! -### Currently supporting Minecraft Bedrock v1.16.x and Minecraft Java v1.16.2. +### Currently supporting Minecraft Bedrock v1.16.x and Minecraft Java v1.16.3. ## Setting Up Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set up Geyser. @@ -35,13 +35,14 @@ Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set ## What's Left to be Added/Fixed - The Following Inventories - - [ ] Enchantment Table + - [ ] Enchantment Table (as a proper GUI) - [ ] Beacon - [ ] Cartography Table - [ ] Stonecutter - - [ ] Command Block - [ ] Structure Block - [ ] Horse Inventory + - [ ] Loom + - [ ] Smithing Table - Some Entity Flags ## Compiling diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 8497b9684..44b28e931 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -61,14 +61,34 @@ net.md_5.bungee.jni org.geysermc.platform.bungeecord.shaded.jni + + com.fasterxml.jackson + org.geysermc.platform.bungeecord.shaded.jackson + io.netty org.geysermc.platform.bungeecord.shaded.netty - org.reflections.reflections + org.reflections org.geysermc.platform.bungeecord.shaded.reflections + + com.google.common + org.geysermc.platform.bungeecord.shaded.google.common + + + com.google.guava + org.geysermc.platform.bungeecord.shaded.google.guava + + + org.dom4j + org.geysermc.platform.bungeecord.shaded.dom4j + + + net.kyori.adventure + org.geysermc.platform.bungeecord.shaded.adventure + diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java index 500f342a6..0d7d6e06b 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java @@ -29,27 +29,22 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Getter; import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.config.Configuration; import org.geysermc.connector.FloodgateKeyLoader; import org.geysermc.connector.configuration.GeyserJacksonConfiguration; import java.nio.file.Path; -import java.nio.file.Paths; @Getter @JsonIgnoreProperties(ignoreUnknown = true) -public class GeyserBungeeConfiguration extends GeyserJacksonConfiguration { - +public final class GeyserBungeeConfiguration extends GeyserJacksonConfiguration { @JsonIgnore - private Path floodgateKey; + private Path floodgateKeyPath; - public void loadFloodgate(GeyserBungeePlugin plugin, Configuration configuration) { + public void loadFloodgate(GeyserBungeePlugin plugin) { Plugin floodgate = plugin.getProxy().getPluginManager().getPlugin("floodgate"); - floodgateKey = FloodgateKeyLoader.getKey(plugin.getGeyserLogger(), this, Paths.get(plugin.getDataFolder().toString(), configuration.getString("floodgate-key-file"), "public-key.pem"), floodgate, floodgate != null ? floodgate.getDataFolder().toPath() : null); - } + Path geyserDataFolder = plugin.getDataFolder().toPath(); + Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; - @Override - public Path getFloodgateKeyFile() { - return floodgateKey; + floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgate, floodgateDataFolder, geyserDataFolder, plugin.getGeyserLogger()); } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeLogger.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeLogger.java index cd07b333d..e40f404c1 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeLogger.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeLogger.java @@ -25,19 +25,21 @@ package org.geysermc.platform.bungeecord; +import lombok.Getter; +import lombok.Setter; import org.geysermc.connector.GeyserLogger; import java.util.logging.Level; import java.util.logging.Logger; public class GeyserBungeeLogger implements GeyserLogger { + private final Logger logger; + @Getter @Setter + private boolean debug; - private Logger logger; - private boolean debugMode; - - public GeyserBungeeLogger(Logger logger, boolean debugMode) { + public GeyserBungeeLogger(Logger logger, boolean debug) { this.logger = logger; - this.debugMode = debugMode; + this.debug = debug; } @Override @@ -72,12 +74,8 @@ public class GeyserBungeeLogger implements GeyserLogger { @Override public void debug(String message) { - if (debugMode) + if (debug) { info(message); - } - - @Override - public void setDebug(boolean debug) { - debugMode = debug; + } } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java index a600ebcd3..9d8e05ac7 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java @@ -27,13 +27,10 @@ package org.geysermc.platform.bungeecord; import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.config.Configuration; -import net.md_5.bungee.config.ConfigurationProvider; -import net.md_5.bungee.config.YamlConfiguration; -import org.geysermc.connector.common.PlatformType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.command.CommandManager; +import org.geysermc.connector.common.PlatformType; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.dump.BootstrapDumpInfo; import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; @@ -64,13 +61,11 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { if (!getDataFolder().exists()) getDataFolder().mkdir(); - Configuration configuration = null; try { if (!getDataFolder().exists()) getDataFolder().mkdir(); File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"), "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString())); this.geyserConfig = FileUtils.loadConfig(configFile, GeyserBungeeConfiguration.class); - configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(new File(getDataFolder(), "config.yml")); } catch (IOException ex) { getLogger().log(Level.WARNING, LanguageUtils.getLocaleStringLog("geyser.config.failed"), ex); ex.printStackTrace(); @@ -83,6 +78,11 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { // By default this should be localhost but may need to be changed in some circumstances if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) { + this.geyserConfig.setAutoconfiguredRemote(true); + // Don't use localhost if not listening on all interfaces + if (!javaAddr.getHostString().equals("0.0.0.0") && !javaAddr.getHostString().equals("")) { + this.geyserConfig.getRemote().setAddress(javaAddr.getHostString()); + } this.geyserConfig.getRemote().setPort(javaAddr.getPort()); } @@ -97,9 +97,13 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { if (geyserConfig.getRemote().getAuthType().equals("floodgate") && getProxy().getPluginManager().getPlugin("floodgate") == null) { geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); return; + } else if (geyserConfig.isAutoconfiguredRemote() && getProxy().getPluginManager().getPlugin("floodgate-bungee") != null) { + // Floodgate installed means that the user wants Floodgate authentication + geyserLogger.debug("Auto-setting to Floodgate authentication."); + geyserConfig.getRemote().setAuthType("floodgate"); } - geyserConfig.loadFloodgate(this, configuration); + geyserConfig.loadFloodgate(this); this.connector = GeyserConnector.start(PlatformType.BUNGEECORD, this); diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index 422b2769b..d4dc33260 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -20,13 +20,13 @@ org.spigotmc spigot-api - 1.14-R0.1-SNAPSHOT + 1.15.2-R0.1-SNAPSHOT provided us.myles viaversion - 3.1.0 + 3.1.1 provided @@ -76,9 +76,25 @@ org.geysermc.platform.spigot.shaded.jackson - org.reflections.reflections + org.reflections org.geysermc.platform.spigot.shaded.reflections + + com.google.common + org.geysermc.platform.spigot.shaded.google.common + + + com.google.guava + org.geysermc.platform.spigot.shaded.google.guava + + + org.dom4j + org.geysermc.platform.spigot.shaded.dom4j + + + net.kyori.adventure + org.geysermc.platform.spigot.shaded.adventure + diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java index de4e58c3b..209a21061 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java @@ -27,7 +27,6 @@ package org.geysermc.platform.spigot; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; @@ -35,26 +34,19 @@ import org.geysermc.connector.FloodgateKeyLoader; import org.geysermc.connector.configuration.GeyserJacksonConfiguration; import java.nio.file.Path; -import java.nio.file.Paths; @Getter @JsonIgnoreProperties(ignoreUnknown = true) -public class GeyserSpigotConfiguration extends GeyserJacksonConfiguration { - - @JsonProperty("floodgate-key-file") - private String floodgateKeyFile; - +public final class GeyserSpigotConfiguration extends GeyserJacksonConfiguration { @JsonIgnore - private Path floodgateKey; + private Path floodgateKeyPath; public void loadFloodgate(GeyserSpigotPlugin plugin) { Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); - floodgateKey = FloodgateKeyLoader.getKey(plugin.getGeyserLogger(), this, Paths.get(plugin.getDataFolder().toString(), plugin.getConfig().getString("floodgate-key-file", "public-key.pem")), floodgate, floodgate != null ? floodgate.getDataFolder().toPath() : null); - } + Path geyserDataFolder = plugin.getDataFolder().toPath(); + Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; - @Override - public Path getFloodgateKeyFile() { - return floodgateKey; + floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgate, floodgateDataFolder, geyserDataFolder, plugin.getGeyserLogger()); } @Override diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotLogger.java index 252d6bbed..b462f1f1c 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotLogger.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotLogger.java @@ -26,7 +26,8 @@ package org.geysermc.platform.spigot; import lombok.AllArgsConstructor; - +import lombok.Getter; +import lombok.Setter; import org.geysermc.connector.GeyserLogger; import java.util.logging.Level; @@ -34,9 +35,9 @@ import java.util.logging.Logger; @AllArgsConstructor public class GeyserSpigotLogger implements GeyserLogger { - - private Logger logger; - private boolean debugMode; + private final Logger logger; + @Getter @Setter + private boolean debug; @Override public void severe(String message) { @@ -70,12 +71,8 @@ public class GeyserSpigotLogger implements GeyserLogger { @Override public void debug(String message) { - if (debugMode) + if (debug) { info(message); - } - - @Override - public void setDebug(boolean debug) { - debugMode = debug; + } } } 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 dab63c4ea..a8ae50d69 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 @@ -27,10 +27,10 @@ package org.geysermc.platform.spigot; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -import org.geysermc.connector.common.PlatformType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.command.CommandManager; +import org.geysermc.connector.common.PlatformType; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.dump.BootstrapDumpInfo; import org.geysermc.connector.network.translators.world.WorldManager; @@ -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; @@ -83,6 +82,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { // By default this should be localhost but may need to be changed in some circumstances if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) { + geyserConfig.setAutoconfiguredRemote(true); + // Don't use localhost if not listening on all interfaces + if (!Bukkit.getIp().equals("0.0.0.0") && !Bukkit.getIp().equals("")) { + geyserConfig.getRemote().setAddress(Bukkit.getIp()); + } geyserConfig.getRemote().setPort(Bukkit.getPort()); } @@ -97,6 +101,10 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); this.getPluginLoader().disablePlugin(this); return; + } else if (geyserConfig.isAutoconfiguredRemote() && Bukkit.getPluginManager().getPlugin("floodgate-bukkit") != null) { + // Floodgate installed means that the user wants Floodgate authentication + geyserLogger.debug("Auto-setting to Floodgate authentication."); + geyserConfig.getRemote().setAuthType("floodgate"); } geyserConfig.loadFloodgate(this); @@ -115,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/GeyserSpigotBlockPlaceListener.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotBlockPlaceListener.java index 63cb3b4ab..cb59e202b 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotBlockPlaceListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotBlockPlaceListener.java @@ -59,7 +59,7 @@ public class GeyserSpigotBlockPlaceListener implements Listener { } else { javaBlockId = event.getBlockPlaced().getBlockData().getAsString(); } - placeBlockSoundPacket.setExtraData(BlockTranslator.getBedrockBlockId(BlockTranslator.getJavaIdBlockMap().get(javaBlockId))); + placeBlockSoundPacket.setExtraData(BlockTranslator.getBedrockBlockId(BlockTranslator.getJavaIdBlockMap().getOrDefault(javaBlockId, 0))); placeBlockSoundPacket.setIdentifier(":"); session.sendUpstreamPacket(placeBlockSoundPacket); session.setLastBlockPlacePosition(null); 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 c43d9eaba..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,34 +25,85 @@ package org.geysermc.platform.spigot.world; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -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) { if (session.getPlayerEntity() == null) { return BlockTranslator.AIR; } + if (Bukkit.getPlayer(session.getPlayerEntity().getUsername()) == null) { + return BlockTranslator.AIR; + } if (isLegacy) { return getLegacyBlock(session, x, y, z, isViaVersion); } - return BlockTranslator.getJavaIdBlockMap().get(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getBlockAt(x, y, z).getBlockData().getAsString()); + //TODO possibly: detect server version for all versions and use ViaVersion for block state mappings like below + return BlockTranslator.getJavaIdBlockMap().getOrDefault(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getBlockAt(x, y, z).getBlockData().getAsString(), 0); } @SuppressWarnings("deprecation") @@ -74,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/bootstrap/sponge/pom.xml b/bootstrap/sponge/pom.xml index f3c898084..132f38173 100644 --- a/bootstrap/sponge/pom.xml +++ b/bootstrap/sponge/pom.xml @@ -70,9 +70,25 @@ org.geysermc.platform.sponge.shaded.fastutil - org.reflections.reflections + org.reflections org.geysermc.platform.sponge.shaded.reflections + + com.google.common + org.geysermc.platform.sponge.shaded.google.common + + + com.google.guava + org.geysermc.platform.sponge.shaded.google.guava + + + org.dom4j + org.geysermc.platform.sponge.shaded.dom4j + + + net.kyori.adventure + org.geysermc.platform.sponge.shaded.adventure + diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java index 7f08a0950..2d5eefebd 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeConfiguration.java @@ -25,256 +25,13 @@ package org.geysermc.platform.sponge; -import lombok.AllArgsConstructor; +import org.geysermc.connector.configuration.GeyserJacksonConfiguration; -import ninja.leaping.configurate.ConfigurationNode; - -import org.geysermc.connector.configuration.GeyserConfiguration; - -import java.io.File; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; - -public class GeyserSpongeConfiguration implements GeyserConfiguration { - - private File dataFolder; - private ConfigurationNode node; - - private SpongeBedrockConfiguration bedrockConfig; - private SpongeRemoteConfiguration remoteConfig; - private SpongeMetricsInfo metricsInfo; - - private Map userAuthInfo = new HashMap<>(); - - public GeyserSpongeConfiguration(File dataFolder, ConfigurationNode node) { - this.dataFolder = dataFolder; - this.node = node; - - this.bedrockConfig = new SpongeBedrockConfiguration(node.getNode("bedrock")); - this.remoteConfig = new SpongeRemoteConfiguration(node.getNode("remote")); - this.metricsInfo = new SpongeMetricsInfo(); - - if (node.getNode("userAuths").getValue() == null) - return; - - List userAuths = new ArrayList(((LinkedHashMap)node.getNode("userAuths").getValue()).keySet()); - for (String key : userAuths) { - userAuthInfo.put(key, new SpongeUserAuthenticationInfo(key)); - } - } +public final class GeyserSpongeConfiguration extends GeyserJacksonConfiguration { @Override - public SpongeBedrockConfiguration getBedrock() { - return bedrockConfig; - } - - @Override - public SpongeRemoteConfiguration getRemote() { - return remoteConfig; - } - - @Override - public Map getUserAuths() { - return userAuthInfo; - } - - @Override - public boolean isCommandSuggestions() { - return node.getNode("command-suggestions").getBoolean(true); - } - - @Override - public boolean isPassthroughMotd() { - return node.getNode("passthrough-motd").getBoolean(false); - } - - @Override - public boolean isPassthroughProtocolName() { - return node.getNode("passthrough-protocol-name").getBoolean(false); - } - - @Override - public boolean isPassthroughPlayerCounts() { - return node.getNode("passthrough-player-counts").getBoolean(false); - } - - @Override - public boolean isLegacyPingPassthrough() { - return node.getNode("legacy-ping-passthrough").getBoolean(false); - } - - @Override - public int getPingPassthroughInterval() { - return node.getNode("ping-passthrough-interval").getInt(3); - } - - @Override - public int getMaxPlayers() { - return node.getNode("max-players").getInt(100); - } - - @Override - public boolean isDebugMode() { - return node.getNode("debug-mode").getBoolean(false); - } - - @Override - public int getGeneralThreadPool() { - return node.getNode("genereal-thread-pool").getInt(32); - } - - @Override - public boolean isAllowThirdPartyCapes() { - return node.getNode("allow-third-party-capes").getBoolean(true); - } - - @Override - public boolean isAllowThirdPartyEars() { - return node.getNode("allow-third-party-ears").getBoolean(false); - } - - @Override - public boolean isShowCooldown() { - return node.getNode("show-cooldown").getBoolean(true); - } - - @Override - public String getDefaultLocale() { - return node.getNode("default-locale").getString("en_us"); - } - - @Override - public Path getFloodgateKeyFile() { - return Paths.get(dataFolder.toString(), node.getNode("floodgate-key-file").getString("public-key.pem")); - } - - @Override - public boolean isCacheChunks() { - return node.getNode("cache-chunks").getBoolean(false); - } - - @Override - public int getCacheImages() { - return node.getNode("cache-skins").getInt(0); - } - - @Override - public boolean isAboveBedrockNetherBuilding() { - return node.getNode("above-bedrock-nether-building").getBoolean(false); - } - - @Override - public SpongeMetricsInfo getMetrics() { - return metricsInfo; - } - - @AllArgsConstructor - public class SpongeBedrockConfiguration implements IBedrockConfiguration { - - private ConfigurationNode node; - - @Override - public String getAddress() { - return node.getNode("address").getString("0.0.0.0"); - } - - @Override - public int getPort() { - return node.getNode("port").getInt(19132); - } - - @Override - public boolean isCloneRemotePort() { - return node.getNode("clone-remote-port").getBoolean(false); - } - - @Override - public String getMotd1() { - return node.getNode("motd1").getString("GeyserMC"); - } - - @Override - public String getMotd2() { - return node.getNode("motd2").getString("GeyserMC"); - } - } - - @AllArgsConstructor - public class SpongeRemoteConfiguration implements IRemoteConfiguration { - - private ConfigurationNode node; - - @Override - public String getAddress() { - return node.getNode("address").getString("127.0.0.1"); - } - - @Override - public void setAddress(String address) { - node.getNode("address").setValue(address); - } - - @Override - public int getPort() { - return node.getNode("port").getInt(25565); - } - - @Override - public void setPort(int port) { - node.getNode("port").setValue(port); - } - - @Override - public String getAuthType() { - return node.getNode("auth-type").getString("online"); - } - } - - public class SpongeUserAuthenticationInfo implements IUserAuthenticationInfo { - - private String key; - - public SpongeUserAuthenticationInfo(String key) { - this.key = key; - } - - @Override - public String getEmail() { - return node.getNode("userAuths").getNode(key).getNode("email").getString(); - } - - @Override - public String getPassword() { - return node.getNode("userAuths").getNode(key).getNode("password").getString(); - } - } - - public class SpongeMetricsInfo implements IMetricsInfo { - - @Override - public boolean isEnabled() { - return node.getNode("metrics").getNode("enabled").getBoolean(true); - } - - @Override - public String getUniqueId() { - return node.getNode("metrics").getNode("uuid").getString("generateduuid"); - } - } - - @Override - public boolean isEnableProxyConnections() { - return node.getNode("enable-proxy-connections").getBoolean(false); - } - - @Override - public int getMtu() { - return node.getNode("mtu").getInt(1400); - } - - @Override - public int getConfigVersion() { - return node.getNode("config-version").getInt(0); + public Path getFloodgateKeyPath() { + return null; //floodgate isn't available for Sponge } } diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeLogger.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeLogger.java index fb7cb54bb..bdbd25311 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeLogger.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongeLogger.java @@ -26,15 +26,16 @@ package org.geysermc.platform.sponge; import lombok.AllArgsConstructor; - +import lombok.Getter; +import lombok.Setter; import org.geysermc.connector.GeyserLogger; import org.slf4j.Logger; @AllArgsConstructor public class GeyserSpongeLogger implements GeyserLogger { - - private Logger logger; - private boolean debugMode; + private final Logger logger; + @Getter @Setter + private boolean debug; @Override public void severe(String message) { @@ -68,12 +69,8 @@ public class GeyserSpongeLogger implements GeyserLogger { @Override public void debug(String message) { - if (debugMode) + if (debug) { info(message); - } - - @Override - public void setDebug(boolean debugMode) { - this.debugMode = debugMode; + } } } diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java index e57e383b2..106d2b155 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java @@ -26,14 +26,11 @@ package org.geysermc.platform.sponge; import com.google.inject.Inject; -import ninja.leaping.configurate.ConfigurationNode; -import ninja.leaping.configurate.loader.ConfigurationLoader; -import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; -import org.geysermc.connector.common.PlatformType; -import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.command.CommandManager; +import org.geysermc.connector.common.PlatformType; +import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.dump.BootstrapDumpInfo; import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; import org.geysermc.connector.ping.IGeyserPingPassthrough; @@ -85,33 +82,27 @@ public class GeyserSpongePlugin implements GeyserBootstrap { ex.printStackTrace(); } - ConfigurationLoader loader = YAMLConfigurationLoader.builder().setPath(configFile.toPath()).build(); - ConfigurationNode config; try { - config = loader.load(); - this.geyserConfig = new GeyserSpongeConfiguration(configDir, config); + this.geyserConfig = FileUtils.loadConfig(configFile, GeyserSpongeConfiguration.class); } catch (IOException ex) { logger.warn(LanguageUtils.getLocaleStringLog("geyser.config.failed")); ex.printStackTrace(); return; } - ConfigurationNode serverIP = config.getNode("remote").getNode("address"); - ConfigurationNode serverPort = config.getNode("remote").getNode("port"); - if (Sponge.getServer().getBoundAddress().isPresent()) { InetSocketAddress javaAddr = Sponge.getServer().getBoundAddress().get(); // Don't change the ip if its listening on all interfaces // By default this should be 127.0.0.1 but may need to be changed in some circumstances if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) { - serverPort.setValue(javaAddr.getPort()); + this.geyserConfig.setAutoconfiguredRemote(true); + geyserConfig.getRemote().setPort(javaAddr.getPort()); } } - ConfigurationNode bedrockPort = config.getNode("bedrock").getNode("port"); if (geyserConfig.getBedrock().isCloneRemotePort()){ - bedrockPort.setValue(serverPort.getValue()); + geyserConfig.getBedrock().setPort(geyserConfig.getRemote().getPort()); } this.geyserLogger = new GeyserSpongeLogger(logger, geyserConfig.isDebugMode()); diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java index e4b70133f..123a9a600 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java @@ -33,8 +33,8 @@ import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.core.appender.ConsoleAppender; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; -import org.geysermc.connector.common.PlatformType; import org.geysermc.connector.command.CommandManager; +import org.geysermc.connector.common.PlatformType; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.dump.BootstrapDumpInfo; import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; @@ -49,6 +49,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Path; import java.nio.file.Paths; +import java.text.MessageFormat; import java.util.UUID; public class GeyserStandaloneBootstrap implements GeyserBootstrap { @@ -62,22 +63,61 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { @Getter private boolean useGui = System.console() == null && !isHeadless(); + private String configFilename = "config.yml"; private GeyserConnector connector; + public static void main(String[] args) { - for (String arg : args) { + GeyserStandaloneBootstrap bootstrap = new GeyserStandaloneBootstrap(); + // Set defaults + boolean useGuiOpts = bootstrap.useGui; + String configFilenameOpt = bootstrap.configFilename; + + for (int i = 0; i < args.length; i++) { // By default, standalone Geyser will check if it should open the GUI based on if the GUI is null // Optionally, you can force the use of a GUI or no GUI by specifying args - if (arg.equals("gui")) { - new GeyserStandaloneBootstrap().onEnable(true); - return; - } else if (arg.equals("nogui")) { - new GeyserStandaloneBootstrap().onEnable(false); - return; + // Allows gui and nogui without options, for backwards compatibility + String arg = args[i]; + switch (arg) { + case "--gui": + case "gui": + useGuiOpts = true; + break; + case "--nogui": + case "nogui": + useGuiOpts = false; + break; + case "--config": + case "-c": + if (i >= args.length - 1) { + System.err.println(MessageFormat.format(LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.confignotspecified"), "-c")); + return; + } + configFilenameOpt = args[i+1]; i++; + System.out.println(MessageFormat.format(LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.configspecified"), configFilenameOpt)); + break; + case "--help": + case "-h": + System.out.println(MessageFormat.format(LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.usage"), "[java -jar] Geyser.jar [opts]")); + System.out.println(" " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.options")); + System.out.println(" -c, --config [file] " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.config")); + System.out.println(" -h, --help " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.help")); + System.out.println(" --gui, --nogui " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.gui")); + return; + default: + String badArgMsg = LanguageUtils.getLocaleStringLog("geyser.bootstrap.args.unrecognised"); + System.err.println(MessageFormat.format(badArgMsg, arg)); + return; } } - new GeyserStandaloneBootstrap().onEnable(); + bootstrap.onEnable(useGuiOpts, configFilenameOpt); + } + + public void onEnable(boolean useGui, String configFilename) { + this.configFilename = configFilename; + this.useGui = useGui; + this.onEnable(); } public void onEnable(boolean useGui) { @@ -106,9 +146,10 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { LoopbackUtil.checkLoopback(geyserLogger); try { - File configFile = FileUtils.fileOrCopiedFromResource("config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString())); + File configFile = FileUtils.fileOrCopiedFromResource(new File(configFilename), "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString())); geyserConfig = FileUtils.loadConfig(configFile, GeyserStandaloneConfiguration.class); if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) { + geyserConfig.setAutoconfiguredRemote(true); // Doesn't really need to be set but /shrug geyserConfig.getRemote().setAddress("127.0.0.1"); } } catch (IOException ex) { diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java index 29e18d08f..5707863c2 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneConfiguration.java @@ -26,7 +26,6 @@ package org.geysermc.platform.standalone; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import org.geysermc.connector.configuration.GeyserJacksonConfiguration; @@ -35,13 +34,9 @@ import java.nio.file.Paths; @Getter @JsonIgnoreProperties(ignoreUnknown = true) -public class GeyserStandaloneConfiguration extends GeyserJacksonConfiguration { - - @JsonProperty("floodgate-key-file") - private String floodgateKeyFile; - +public final class GeyserStandaloneConfiguration extends GeyserJacksonConfiguration { @Override - public Path getFloodgateKeyFile() { - return Paths.get(floodgateKeyFile); + public Path getFloodgateKeyPath() { + return Paths.get(getFloodgateKeyFile()); } } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneLogger.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneLogger.java index 0d9ced951..f0f7156f9 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneLogger.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneLogger.java @@ -26,18 +26,16 @@ package org.geysermc.platform.standalone; import lombok.extern.log4j.Log4j2; - import net.minecrell.terminalconsole.SimpleTerminalConsole; - import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; -import org.geysermc.connector.common.ChatColor; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.GeyserLogger; import org.geysermc.connector.command.CommandSender; +import org.geysermc.connector.common.ChatColor; @Log4j2 -public class GeyserStandaloneLogger extends SimpleTerminalConsole implements org.geysermc.connector.GeyserLogger, CommandSender { - +public class GeyserStandaloneLogger extends SimpleTerminalConsole implements GeyserLogger, CommandSender { private boolean colored = true; @Override @@ -99,10 +97,6 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements org Configurator.setLevel(log.getName(), debug ? Level.DEBUG : Level.INFO); } - /** - * Used for setting debug mode in GUI mode - * @return if debug is enabled - */ public boolean isDebug() { return log.isDebugEnabled(); } diff --git a/bootstrap/velocity/pom.xml b/bootstrap/velocity/pom.xml index b08e5cbc5..ee445b6e7 100644 --- a/bootstrap/velocity/pom.xml +++ b/bootstrap/velocity/pom.xml @@ -57,14 +57,34 @@ + + com.fasterxml.jackson + org.geysermc.platform.velocity.shaded.jackson + it.unimi.dsi.fastutil org.geysermc.platform.velocity.shaded.fastutil - org.reflections.reflections + org.reflections org.geysermc.platform.velocity.shaded.reflections + + com.google.common + org.geysermc.platform.velocity.shaded.google.common + + + com.google.guava + org.geysermc.platform.velocity.shaded.google.guava + + + org.dom4j + org.geysermc.platform.velocity.shaded.dom4j + + + net.kyori.adventure + org.geysermc.platform.velocity.shaded.adventure + diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java index 075804437..7dc6746b0 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityConfiguration.java @@ -27,7 +27,6 @@ package org.geysermc.platform.velocity; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.ProxyServer; import lombok.Getter; @@ -37,25 +36,15 @@ import org.geysermc.connector.configuration.GeyserJacksonConfiguration; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Optional; @Getter @JsonIgnoreProperties(ignoreUnknown = true) -public class GeyserVelocityConfiguration extends GeyserJacksonConfiguration { - - @JsonProperty("floodgate-key-file") - private String floodgateKeyFile; - +public final class GeyserVelocityConfiguration extends GeyserJacksonConfiguration { @JsonIgnore - private Path floodgateKey; - - @Override - public Path getFloodgateKeyFile() { - return floodgateKey; - } + private Path floodgateKeyPath; public void loadFloodgate(GeyserVelocityPlugin plugin, ProxyServer proxyServer, File dataFolder) { - Optional floodgate = proxyServer.getPluginManager().getPlugin("floodgate"); - floodgate.ifPresent(it -> floodgateKey = FloodgateKeyLoader.getKey(plugin.getGeyserLogger(), this, Paths.get(dataFolder.toString(), floodgateKeyFile.isEmpty() ? floodgateKeyFile : "public-key.pem"), it, Paths.get("plugins/floodgate/"))); + PluginContainer floodgate = proxyServer.getPluginManager().getPlugin("floodgate").orElse(null); + floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgate, Paths.get("plugins/floodgate/"), dataFolder.toPath(), plugin.getGeyserLogger()); } } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityLogger.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityLogger.java index a935d786c..cb98411c8 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityLogger.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityLogger.java @@ -26,15 +26,16 @@ package org.geysermc.platform.velocity; import lombok.AllArgsConstructor; - +import lombok.Getter; +import lombok.Setter; import org.geysermc.connector.GeyserLogger; import org.slf4j.Logger; @AllArgsConstructor public class GeyserVelocityLogger implements GeyserLogger { - - private Logger logger; - private boolean debugMode; + private final Logger logger; + @Getter @Setter + private boolean debug; @Override public void severe(String message) { @@ -68,12 +69,8 @@ public class GeyserVelocityLogger implements GeyserLogger { @Override public void debug(String message) { - if (debugMode) + if (debug) { info(message); - } - - @Override - public void setDebug(boolean debugMode) { - this.debugMode = debugMode; + } } } \ No newline at end of file diff --git a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java index 6d8032343..291d7a001 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/platform/velocity/GeyserVelocityPlugin.java @@ -26,19 +26,17 @@ package org.geysermc.platform.velocity; import com.google.inject.Inject; - import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Plugin; - import com.velocitypowered.api.proxy.ProxyServer; import lombok.Getter; -import org.geysermc.connector.common.PlatformType; -import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; +import org.geysermc.connector.common.PlatformType; +import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.dump.BootstrapDumpInfo; import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; import org.geysermc.connector.ping.IGeyserPingPassthrough; @@ -94,6 +92,11 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { // By default this should be localhost but may need to be changed in some circumstances if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) { + this.geyserConfig.setAutoconfiguredRemote(true); + // Don't use localhost if not listening on all interfaces + if (!javaAddr.getHostString().equals("0.0.0.0") && !javaAddr.getHostString().equals("")) { + this.geyserConfig.getRemote().setAddress(javaAddr.getHostString()); + } geyserConfig.getRemote().setPort(javaAddr.getPort()); } @@ -107,6 +110,10 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { if (geyserConfig.getRemote().getAuthType().equals("floodgate") && !proxyServer.getPluginManager().getPlugin("floodgate").isPresent()) { geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); return; + } else if (geyserConfig.isAutoconfiguredRemote() && proxyServer.getPluginManager().getPlugin("floodgate").isPresent()) { + // Floodgate installed means that the user wants Floodgate authentication + geyserLogger.debug("Auto-setting to Floodgate authentication."); + geyserConfig.getRemote().setAuthType("floodgate"); } geyserConfig.loadFloodgate(this, proxyServer, configFolder.toFile()); diff --git a/connector/pom.xml b/connector/pom.xml index a3748044a..f52713cea 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -31,9 +31,9 @@ compile - com.nukkitx.protocol - bedrock-v409 - 2.6.0-SNAPSHOT + com.github.CloudburstMC.Protocol + bedrock-v408 + 250beb2a94 compile @@ -110,8 +110,8 @@ com.github.GeyserMC - MCProtocolLib - f37c98dc70 + mcprotocollib + e4a3aa636a compile @@ -129,24 +129,29 @@ org.reflections reflections - 0.9.12 + 0.9.11 - net.kyori + org.dom4j + dom4j + 2.1.3 + + + com.github.kyoripowered.adventure adventure-api - 4.0.0-SNAPSHOT + 557865caef compile - net.kyori + com.github.kyoripowered.adventure adventure-text-serializer-gson - 4.0.0-SNAPSHOT + 557865caef compile - net.kyori + com.github.kyoripowered.adventure adventure-text-serializer-legacy - 4.0.0-SNAPSHOT + 557865caef compile @@ -232,6 +237,52 @@ + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.9.1 + + + process-classes + + execute + + + + + + + + + + + org.reflections + reflections + 0.9.11 + + + org.dom4j + dom4j + 2.1.3 + + + org.codehaus.groovy + groovy-all + 3.0.5 + runtime + pom + + + diff --git a/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java b/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java index a30a5f4d4..ec5dd349a 100644 --- a/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java +++ b/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java @@ -25,17 +25,19 @@ package org.geysermc.connector; +import org.geysermc.connector.configuration.GeyserJacksonConfiguration; import org.geysermc.connector.utils.LanguageUtils; -import org.geysermc.connector.configuration.GeyserConfiguration; import java.nio.file.Files; import java.nio.file.Path; public class FloodgateKeyLoader { - public static Path getKey(GeyserLogger logger, GeyserConfiguration config, Path floodgateKey, Object floodgate, Path floodgateFolder) { + public static Path getKeyPath(GeyserJacksonConfiguration config, Object floodgate, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) { + Path floodgateKey = geyserDataFolder.resolve(config.getFloodgateKeyFile()); + if (!Files.exists(floodgateKey) && config.getRemote().getAuthType().equals("floodgate")) { if (floodgate != null) { - Path autoKey = floodgateFolder.resolve("public-key.pem"); + Path autoKey = floodgateDataFolder.resolve("public-key.pem"); if (Files.exists(autoKey)) { logger.info(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.auto_loaded")); floodgateKey = autoKey; diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 9b6045be2..de4d2c2bf 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -25,6 +25,7 @@ package org.geysermc.connector; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.nukkitx.network.raknet.RakNetConstants; @@ -47,6 +48,7 @@ import org.geysermc.connector.network.translators.effect.EffectRegistry; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.item.ItemTranslator; import org.geysermc.connector.network.translators.item.PotionMixRegistry; +import org.geysermc.connector.network.translators.item.RecipeRegistry; import org.geysermc.connector.network.translators.sound.SoundHandlerRegistry; import org.geysermc.connector.network.translators.sound.SoundRegistry; import org.geysermc.connector.network.translators.world.WorldManager; @@ -55,6 +57,7 @@ import org.geysermc.connector.network.translators.world.block.entity.BlockEntity import org.geysermc.connector.utils.DimensionUtils; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LocaleUtils; +import org.geysermc.connector.utils.ResourcePack; import org.geysermc.floodgate.crypto.AesCipher; import org.geysermc.floodgate.crypto.AesKeyProducer; import org.geysermc.floodgate.crypto.Base64Topping; @@ -77,7 +80,7 @@ import java.util.concurrent.TimeUnit; @Getter public class GeyserConnector { - public static final ObjectMapper JSON_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES); + public static final ObjectMapper JSON_MAPPER = new ObjectMapper().enable(JsonParser.Feature.IGNORE_UNDEFINED).enable(JsonParser.Feature.ALLOW_COMMENTS).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); public static final String NAME = "Geyser"; public static final String VERSION = "DEV"; // A fallback for running in IDEs @@ -138,9 +141,12 @@ public class GeyserConnector { ItemTranslator.init(); LocaleUtils.init(); PotionMixRegistry.init(); + RecipeRegistry.init(); SoundRegistry.init(); SoundHandlerRegistry.init(); + ResourcePack.loadPacks(); + if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) { // Set the remote address to localhost since that is where we are always connecting try { @@ -168,7 +174,7 @@ public class GeyserConnector { config.getRemote().setPort(remotePort = Integer.parseInt(record[2])); logger.debug("Found SRV record \"" + remoteAddress + ":" + remotePort + "\""); } - } catch (Exception ex) { + } catch (Exception | NoClassDefFoundError ex) { // Check for a NoClassDefFoundError to prevent Android crashes logger.debug("Exception while trying to find an SRV record for the remote host."); if (config.isDebugMode()) ex.printStackTrace(); // Otherwise we can get a stack trace for any domain that doesn't have an SRV record @@ -202,7 +208,7 @@ public class GeyserConnector { if (throwable == null) { logger.info(LanguageUtils.getLocaleStringLog("geyser.core.start", config.getBedrock().getAddress(), String.valueOf(config.getBedrock().getPort()))); } else { - logger.severe(LanguageUtils.getLocaleStringLog("geyser.core.fail", config.getBedrock().getAddress(), config.getBedrock().getPort())); + logger.severe(LanguageUtils.getLocaleStringLog("geyser.core.fail", config.getBedrock().getAddress(), String.valueOf(config.getBedrock().getPort()))); throwable.printStackTrace(); } }).join(); @@ -234,6 +240,10 @@ public class GeyserConnector { message += LanguageUtils.getLocaleStringLog("geyser.core.finish.console"); } logger.info(message); + + if (platformType == PlatformType.STANDALONE) { + logger.warning(LanguageUtils.getLocaleStringLog("geyser.core.movement_warn")); + } } public void shutdown() { @@ -319,6 +329,18 @@ public class GeyserConnector { return bootstrap.getWorldManager(); } + /** + * Get the production status of the current runtime. + * Will return true if the version number is not 'DEV'. + * Should only happen in compiled jars. + * + * @return If we are in a production build/environment + */ + public boolean isProduction() { + //noinspection ConstantConditions + return !"DEV".equals(GeyserConnector.VERSION); + } + public static GeyserConnector getInstance() { return instance; } diff --git a/connector/src/main/java/org/geysermc/connector/GeyserLogger.java b/connector/src/main/java/org/geysermc/connector/GeyserLogger.java index 0ba2a689f..138285eb1 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserLogger.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserLogger.java @@ -84,4 +84,9 @@ public interface GeyserLogger { * @param debug if the logger should print debug messages */ void setDebug(boolean debug); + + /** + * If debug is enabled for this logger + */ + boolean isDebug(); } diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java index 636058a02..c69a8705f 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/StopCommand.java @@ -49,10 +49,6 @@ public class StopCommand extends GeyserCommand { return; } - connector.shutdown(); - - if (connector.getPlatformType() == PlatformType.STANDALONE) { - System.exit(0); - } + connector.getBootstrap().onDisable(); } } diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java index 681474a98..f7f62e597 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/VersionCommand.java @@ -26,6 +26,7 @@ package org.geysermc.connector.command.defaults; import com.github.steveice10.mc.protocol.MinecraftConstants; +import com.nukkitx.protocol.bedrock.BedrockPacketCodec; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.GeyserCommand; @@ -38,6 +39,7 @@ import org.geysermc.connector.utils.WebUtils; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Properties; public class VersionCommand extends GeyserCommand { @@ -51,7 +53,15 @@ public class VersionCommand extends GeyserCommand { @Override public void execute(CommandSender sender, String[] args) { - sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.version.version", GeyserConnector.NAME, GeyserConnector.VERSION, MinecraftConstants.GAME_VERSION, BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion())); + String bedrockVersions; + List supportedCodecs = BedrockProtocol.SUPPORTED_BEDROCK_CODECS; + if (supportedCodecs.size() > 1) { + bedrockVersions = supportedCodecs.get(0).getMinecraftVersion() + " - " + supportedCodecs.get(supportedCodecs.size() - 1).getMinecraftVersion(); + } else { + bedrockVersions = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion(); + } + + sender.sendMessage(LanguageUtils.getLocaleStringLog("geyser.commands.version.version", GeyserConnector.NAME, GeyserConnector.VERSION, MinecraftConstants.GAME_VERSION, bedrockVersions)); // Disable update checking in dev mode //noinspection ConstantConditions - changes in production diff --git a/connector/src/main/java/org/geysermc/connector/common/PlatformType.java b/connector/src/main/java/org/geysermc/connector/common/PlatformType.java index 7c245c9bf..4daa5d37d 100644 --- a/connector/src/main/java/org/geysermc/connector/common/PlatformType.java +++ b/connector/src/main/java/org/geysermc/connector/common/PlatformType.java @@ -32,6 +32,7 @@ import lombok.Getter; @AllArgsConstructor public enum PlatformType { + ANDROID("Android"), BUNGEECORD("BungeeCord"), SPIGOT("Spigot"), SPONGE("Sponge"), diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index 2cf9a181e..c1cc4d036 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -27,7 +27,6 @@ package org.geysermc.connector.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import org.geysermc.connector.GeyserLogger; - import org.geysermc.connector.utils.LanguageUtils; import java.nio.file.Path; @@ -74,12 +73,14 @@ public interface GeyserConfiguration { String getDefaultLocale(); - Path getFloodgateKeyFile(); + Path getFloodgateKeyPath(); boolean isAboveBedrockNetherBuilding(); boolean isCacheChunks(); + boolean isForceResourcePacks(); + int getCacheImages(); IMetricsInfo getMetrics(); @@ -95,6 +96,8 @@ public interface GeyserConfiguration { String getMotd1(); String getMotd2(); + + String getServerName(); } interface IRemoteConfiguration { @@ -123,6 +126,8 @@ public interface GeyserConfiguration { String getUniqueId(); } + int getScoreboardPacketThreshold(); + // if u have offline mode enabled pls be safe boolean isEnableProxyConnections(); diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index f322b4363..99a3a7a5a 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.Setter; +import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.serializer.AsteriskSerializer; import java.nio.file.Path; @@ -38,94 +39,105 @@ import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) public abstract class GeyserJacksonConfiguration implements GeyserConfiguration { + /** + * If the config was originally 'auto' before the values changed + */ + @Setter + private boolean autoconfiguredRemote = false; + private BedrockConfiguration bedrock; private RemoteConfiguration remote; @JsonProperty("floodgate-key-file") - private String floodgateKeyFile; + private String floodgateKeyFile = "public-key.pem"; - public abstract Path getFloodgateKeyFile(); + public abstract Path getFloodgateKeyPath(); private Map userAuths; @JsonProperty("command-suggestions") - private boolean commandSuggestions; + private boolean commandSuggestions = true; @JsonProperty("passthrough-motd") - private boolean isPassthroughMotd; + private boolean isPassthroughMotd = false; @JsonProperty("passthrough-player-counts") - private boolean isPassthroughPlayerCounts; + private boolean isPassthroughPlayerCounts = false; @JsonProperty("passthrough-protocol-name") - private boolean isPassthroughProtocolName; + private boolean isPassthroughProtocolName = false; @JsonProperty("legacy-ping-passthrough") - private boolean isLegacyPingPassthrough; + private boolean isLegacyPingPassthrough = false; @JsonProperty("ping-passthrough-interval") - private int pingPassthroughInterval; + private int pingPassthroughInterval = 3; @JsonProperty("max-players") - private int maxPlayers; + private int maxPlayers = 100; @JsonProperty("debug-mode") - private boolean debugMode; + private boolean debugMode = false; @JsonProperty("general-thread-pool") - private int generalThreadPool; + private int generalThreadPool = 32; @JsonProperty("allow-third-party-capes") - private boolean allowThirdPartyCapes; + private boolean allowThirdPartyCapes = true; @JsonProperty("show-cooldown") private boolean showCooldown = true; @JsonProperty("allow-third-party-ears") - private boolean allowThirdPartyEars; + private boolean allowThirdPartyEars = false; @JsonProperty("default-locale") - private String defaultLocale; + private String defaultLocale = null; // is null by default so system language takes priority @JsonProperty("cache-chunks") - private boolean cacheChunks; + private boolean cacheChunks = false; @JsonProperty("cache-images") private int cacheImages = 0; @JsonProperty("above-bedrock-nether-building") - private boolean aboveBedrockNetherBuilding; + private boolean aboveBedrockNetherBuilding = false; + + @JsonProperty("force-resource-packs") + private boolean forceResourcePacks = true; private MetricsInfo metrics; @Getter public static class BedrockConfiguration implements IBedrockConfiguration { - @AsteriskSerializer.Asterisk(sensitive = true) - private String address; + private String address = "0.0.0.0"; @Setter - private int port; + private int port = 19132; @JsonProperty("clone-remote-port") - private boolean cloneRemotePort; + private boolean cloneRemotePort = false; - private String motd1; - private String motd2; + private String motd1 = "GeyserMC"; + private String motd2 = "Geyser"; + + @JsonProperty("server-name") + private String serverName = GeyserConnector.NAME; } @Getter public static class RemoteConfiguration implements IRemoteConfiguration { - @Setter @AsteriskSerializer.Asterisk(sensitive = true) - private String address; + private String address = "auto"; @Setter - private int port; + private int port = 25565; + @Setter @JsonProperty("auth-type") - private String authType; + private String authType = "online"; } @Getter @@ -139,13 +151,15 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @Getter public static class MetricsInfo implements IMetricsInfo { - - private boolean enabled; + private boolean enabled = true; @JsonProperty("uuid") - private String uniqueId; + private String uniqueId = "generateuuid"; } + @JsonProperty("scoreboard-packet-threshold") + private int scoreboardPacketThreshold = 10; + @JsonProperty("enable-proxy-connections") private boolean enableProxyConnections = false; @@ -153,5 +167,5 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private int mtu = 1400; @JsonProperty("config-version") - private int configVersion; + private int configVersion = 0; } diff --git a/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java b/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java index 5aed562ec..b229e1670 100644 --- a/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java +++ b/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java @@ -31,6 +31,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.Getter; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.serializer.AsteriskSerializer; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.BedrockProtocol; import org.geysermc.connector.network.session.GeyserSession; @@ -112,16 +113,21 @@ public class DumpInfo { private final boolean dockerCheck; NetworkInfo() { - try { - // This is the most reliable for getting the main local IP - Socket socket = new Socket(); - socket.connect(new InetSocketAddress("geysermc.org", 80)); - this.internalIP = socket.getLocalAddress().getHostAddress(); - } catch (IOException e1) { + if (AsteriskSerializer.showSensitive) { try { - // Fallback to the normal way of getting the local IP - this.internalIP = InetAddress.getLocalHost().getHostAddress(); - } catch (UnknownHostException ignored) { } + // This is the most reliable for getting the main local IP + Socket socket = new Socket(); + socket.connect(new InetSocketAddress("geysermc.org", 80)); + this.internalIP = socket.getLocalAddress().getHostAddress(); + } catch (IOException e1) { + try { + // Fallback to the normal way of getting the local IP + this.internalIP = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException ignored) { } + } + } else { + // Sometimes the internal IP is the external IP... + this.internalIP = "***"; } this.dockerCheck = DockerCheck.checkBasic(); diff --git a/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java new file mode 100644 index 000000000..8cabba645 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2020 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.entity; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; +import com.github.steveice10.mc.protocol.data.message.Message; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import org.geysermc.connector.entity.type.EntityType; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.utils.MessageUtils; + +public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity { + + public CommandBlockMinecartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { + super(entityId, geyserId, entityType, position, motion, rotation); + // Required, or else the GUI will not open + metadata.put(EntityData.CONTAINER_TYPE, (byte) 16); + metadata.put(EntityData.CONTAINER_BASE_SIZE, 1); + // Required, or else the client does not bother to send a packet back with the new information + metadata.put(EntityData.COMMAND_BLOCK_ENABLED, (byte) 1); + } + + @Override + public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { + if (entityMetadata.getId() == 13) { + metadata.put(EntityData.COMMAND_BLOCK_COMMAND, entityMetadata.getValue()); + } + if (entityMetadata.getId() == 14) { + metadata.put(EntityData.COMMAND_BLOCK_LAST_OUTPUT, MessageUtils.getBedrockMessage((Message) entityMetadata.getValue())); + } + super.updateBedrockMetadata(entityMetadata, session); + } + + /** + * By default, the command block shown is purple on Bedrock, which does not match Java Edition's orange. + */ + @Override + public void updateDefaultBlockMetadata() { + metadata.put(EntityData.DISPLAY_ITEM, BlockTranslator.BEDROCK_RUNTIME_COMMAND_BLOCK_ID); + metadata.put(EntityData.DISPLAY_OFFSET, 6); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java index 3ca170436..ede3b78bb 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java @@ -72,6 +72,10 @@ public class FireworkEntity extends Entity { } CompoundTag fireworks = tag.get("Fireworks"); + if (fireworks == null) { + // Thank you Mineplex very cool + return; + } NbtMapBuilder fireworksBuilder = NbtMap.builder(); if (fireworks.get("Flight") != null) { diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java index 392cec24c..f9d2ace46 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java @@ -34,7 +34,6 @@ import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; -import com.nukkitx.protocol.bedrock.packet.StartGamePacket; import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; @@ -102,13 +101,7 @@ public class ItemFrameEntity extends Entity { ItemEntry itemEntry = ItemRegistry.getItem((ItemStack) entityMetadata.getValue()); NbtMapBuilder builder = NbtMap.builder(); - String blockName = ""; - for (StartGamePacket.ItemEntry startGamePacketItemEntry : ItemRegistry.ITEMS) { - if (startGamePacketItemEntry.getId() == (short) itemEntry.getBedrockId()) { - blockName = startGamePacketItemEntry.getIdentifier(); - break; - } - } + String blockName = ItemRegistry.getBedrockIdentifer(itemEntry); builder.putByte("Count", (byte) itemData.getCount()); if (itemData.getTag() != null) { diff --git a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java index 52b273513..390110d18 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/PlayerEntity.java @@ -27,20 +27,24 @@ package org.geysermc.connector.entity; import com.github.steveice10.mc.auth.data.GameProfile; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; +import com.github.steveice10.mc.protocol.data.game.scoreboard.NameTagVisibility; import com.github.steveice10.mc.protocol.data.message.TextMessage; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3f; -import com.nukkitx.protocol.bedrock.data.AdventureSetting; import com.nukkitx.protocol.bedrock.data.AttributeData; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData; -import com.nukkitx.protocol.bedrock.packet.*; +import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket; +import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; +import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket; +import com.nukkitx.protocol.bedrock.packet.UpdateAttributesPacket; import lombok.Getter; import lombok.Setter; import org.geysermc.connector.entity.attribute.Attribute; import org.geysermc.connector.entity.attribute.AttributeType; +import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.cache.EntityEffectCache; @@ -48,7 +52,10 @@ import org.geysermc.connector.scoreboard.Team; import org.geysermc.connector.utils.AttributeUtils; import org.geysermc.connector.utils.MessageUtils; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; @Getter @Setter @@ -60,8 +67,14 @@ public class PlayerEntity extends LivingEntity { private boolean playerList = true; // Player is in the player list private final EntityEffectCache effectCache; - private Entity leftParrot; - private Entity rightParrot; + /** + * Saves the parrot currently on the player's left shoulder; otherwise null + */ + private ParrotEntity leftParrot; + /** + * Saves the parrot currently on the player's right shoulder; otherwise null + */ + private ParrotEntity rightParrot; public PlayerEntity(GameProfile gameProfile, long entityId, long geyserId, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, EntityType.PLAYER, position, motion, rotation); @@ -73,12 +86,6 @@ public class PlayerEntity extends LivingEntity { if (geyserId == 1) valid = true; } - @Override - public boolean despawnEntity(GeyserSession session) { - super.despawnEntity(session); - return !playerList; // don't remove from cache when still on playerlist - } - @Override public void spawnEntity(GeyserSession session) { if (geyserId == 1) return; @@ -111,7 +118,7 @@ public class PlayerEntity extends LivingEntity { } public void sendPlayer(GeyserSession session) { - if(session.getEntityCache().getPlayerEntity(uuid) == null) + if (session.getEntityCache().getPlayerEntity(uuid) == null) return; if (session.getUpstream().isInitialized() && session.getEntityCache().getEntityByGeyserId(geyserId) == null) { @@ -184,6 +191,12 @@ public class PlayerEntity extends LivingEntity { @Override public void updatePositionAndRotation(GeyserSession session, double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) { moveRelative(session, moveX, moveY, moveZ, yaw, pitch, isOnGround); + if (leftParrot != null) { + leftParrot.moveRelative(session, moveX, moveY, moveZ, yaw, pitch, isOnGround); + } + if (rightParrot != null) { + rightParrot.moveRelative(session, moveX, moveY, moveZ, yaw, pitch, isOnGround); + } } @Override @@ -197,6 +210,12 @@ public class PlayerEntity extends LivingEntity { movePlayerPacket.setOnGround(isOnGround); movePlayerPacket.setMode(MovePlayerPacket.Mode.HEAD_ROTATION); session.sendUpstreamPacket(movePlayerPacket); + if (leftParrot != null) { + leftParrot.updateRotation(session, yaw, pitch, isOnGround); + } + if (rightParrot != null) { + rightParrot.updateRotation(session, yaw, pitch, isOnGround); + } } @Override @@ -209,11 +228,6 @@ public class PlayerEntity extends LivingEntity { super.updateBedrockMetadata(entityMetadata, session); if (entityMetadata.getId() == 2) { - // System.out.println(session.getScoreboardCache().getScoreboard().getObjectives().keySet()); - for (Team team : session.getWorldCache().getScoreboard().getTeams().values()) { - // session.getConnector().getLogger().info("team name " + team.getName()); - // session.getConnector().getLogger().info("team entities " + team.getEntities()); - } String username = this.username; TextMessage name = (TextMessage) entityMetadata.getValue(); if (name != null) { @@ -221,8 +235,18 @@ public class PlayerEntity extends LivingEntity { } Team team = session.getWorldCache().getScoreboard().getTeamFor(username); if (team != null) { - // session.getConnector().getLogger().info("team name es " + team.getName() + " with prefix " + team.getPrefix() + " and suffix " + team.getSuffix()); - metadata.put(EntityData.NAMETAG, team.getPrefix() + MessageUtils.toChatColor(team.getColor()) + username + team.getSuffix()); + // Cover different visibility settings + if (team.getNameTagVisibility() == NameTagVisibility.NEVER) { + metadata.put(EntityData.NAMETAG, ""); + } else if (team.getNameTagVisibility() == NameTagVisibility.HIDE_FOR_OTHER_TEAMS && + !team.getEntities().contains(session.getPlayerEntity().getUsername())) { + metadata.put(EntityData.NAMETAG, ""); + } else if (team.getNameTagVisibility() == NameTagVisibility.HIDE_FOR_OWN_TEAM && + team.getEntities().contains(session.getPlayerEntity().getUsername())) { + metadata.put(EntityData.NAMETAG, ""); + } else { + metadata.put(EntityData.NAMETAG, team.getPrefix() + MessageUtils.toChatColor(team.getColor()) + username + team.getSuffix()); + } } } @@ -238,11 +262,15 @@ public class PlayerEntity extends LivingEntity { } // Parrot occupying shoulder - if ((entityMetadata.getId() == 18 && leftParrot == null) || (entityMetadata.getId() == 19 && rightParrot == null)) { // null check since this code just creates the parrot + if (entityMetadata.getId() == 18 || entityMetadata.getId() == 19) { CompoundTag tag = (CompoundTag) entityMetadata.getValue(); if (tag != null && !tag.isEmpty()) { + if ((entityMetadata.getId() == 18 && leftParrot != null) || (entityMetadata.getId() == 19 && rightParrot != null)) { + // No need to update a parrot's data when it already exists + return; + } // The parrot is a separate entity in Bedrock, but part of the player entity in Java - Entity parrot = new Entity(0, session.getEntityCache().getNextEntityId().incrementAndGet(), + ParrotEntity parrot = new ParrotEntity(0, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.PARROT, position, motion, rotation); parrot.spawnEntity(session); parrot.getMetadata().put(EntityData.VARIANT, tag.get("Variant").getValue()); diff --git a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java index 87f4c8b50..500e135ed 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java +++ b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java @@ -135,7 +135,7 @@ public enum EntityType { MINECART_CHEST(MinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:chest_minecart"), MINECART_FURNACE(FurnaceMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"), MINECART_SPAWNER(SpawnerMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"), - MINECART_COMMAND_BLOCK(MinecartEntity.class, 100, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:command_block_minecart"), + MINECART_COMMAND_BLOCK(CommandBlockMinecartEntity.class, 100, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:command_block_minecart"), LINGERING_POTION(ThrowableEntity.class, 101, 0f), LLAMA_SPIT(Entity.class, 102, 0.25f), EVOKER_FANGS(Entity.class, 103, 0.8f, 0.5f, 0.5f, 0f, "minecraft:evocation_fang"), diff --git a/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java b/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java index 432ca8270..225335a97 100644 --- a/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java @@ -31,6 +31,10 @@ import lombok.Setter; public class PlayerInventory extends Inventory { + /** + * Stores the held item slot, starting at index 0. + * Add 36 in order to get the network item slot. + */ @Getter @Setter private int heldItemSlot; diff --git a/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java b/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java index 904f70a0a..5d4462b45 100644 --- a/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java +++ b/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java @@ -28,28 +28,27 @@ package org.geysermc.connector.network; import com.nukkitx.protocol.bedrock.BedrockPacketCodec; import com.nukkitx.protocol.bedrock.v407.Bedrock_v407; import com.nukkitx.protocol.bedrock.v408.Bedrock_v408; -import com.nukkitx.protocol.bedrock.v409.Bedrock_v409; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.ArrayList; +import java.util.List; /** * Contains information about the supported Bedrock protocols in Geyser. */ public class BedrockProtocol { /** - * Default Bedrock codec that should act as a fallback and as the version shown in /geyser version + * Default Bedrock codec that should act as a fallback. Should represent the latest available + * release of the game that Geyser supports. */ public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v408.V408_CODEC; /** * A list of all supported Bedrock versions that can join Geyser */ - public static final Set SUPPORTED_BEDROCK_CODECS = ConcurrentHashMap.newKeySet(); + public static final List SUPPORTED_BEDROCK_CODECS = new ArrayList<>(); static { - SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); SUPPORTED_BEDROCK_CODECS.add(Bedrock_v407.V407_CODEC); - SUPPORTED_BEDROCK_CODECS.add(Bedrock_v409.V409_CODEC); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } /** diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index f65b3ef1a..7e97d4298 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -26,17 +26,24 @@ package org.geysermc.connector.network; import com.nukkitx.protocol.bedrock.BedrockPacket; +import com.nukkitx.protocol.bedrock.data.ResourcePackType; import com.nukkitx.protocol.bedrock.BedrockPacketCodec; import com.nukkitx.protocol.bedrock.packet.*; +import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.configuration.GeyserConfiguration; -import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; -import org.geysermc.connector.utils.LoginEncryptionUtils; import org.geysermc.connector.utils.LanguageUtils; +import org.geysermc.connector.utils.LoginEncryptionUtils; +import org.geysermc.connector.utils.MathUtils; +import org.geysermc.connector.utils.ResourcePack; +import org.geysermc.connector.utils.ResourcePackManifest; import org.geysermc.connector.utils.SettingsUtils; +import java.io.FileInputStream; +import java.io.InputStream; + public class UpstreamPacketHandler extends LoggingPacketHandler { public UpstreamPacketHandler(GeyserConnector connector, GeyserSession session) { @@ -70,6 +77,11 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { session.sendUpstreamPacket(playStatus); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); + for(ResourcePack resourcePack : ResourcePack.PACKS.values()) { + ResourcePackManifest.Header header = resourcePack.getManifest().getHeader(); + resourcePacksInfo.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry(header.getUuid().toString(), header.getVersionString(), resourcePack.getFile().length(), "", "", "", false)); + } + resourcePacksInfo.setForcedToAccept(GeyserConnector.getInstance().getConfig().isForceResourcePacks()); session.sendUpstreamPacket(resourcePacksInfo); return true; } @@ -81,13 +93,42 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { session.connect(connector.getRemoteServer()); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.connect", session.getAuthData().getName())); break; - case HAVE_ALL_PACKS: - ResourcePackStackPacket stack = new ResourcePackStackPacket(); - stack.setExperimental(false); - stack.setForcedToAccept(false); - stack.setGameVersion("*"); - session.sendUpstreamPacket(stack); + + case SEND_PACKS: + for(String id : packet.getPackIds()) { + ResourcePackDataInfoPacket data = new ResourcePackDataInfoPacket(); + String[] packID = id.split("_"); + ResourcePack pack = ResourcePack.PACKS.get(packID[0]); + ResourcePackManifest.Header header = pack.getManifest().getHeader(); + + data.setPackId(header.getUuid()); + int chunkCount = (int) Math.ceil((int) pack.getFile().length() / (double) ResourcePack.CHUNK_SIZE); + data.setChunkCount(chunkCount); + data.setCompressedPackSize(pack.getFile().length()); + data.setMaxChunkSize(ResourcePack.CHUNK_SIZE); + data.setHash(pack.getSha256()); + data.setPackVersion(packID[1]); + data.setPremium(false); + data.setType(ResourcePackType.RESOURCE); + + session.sendUpstreamPacket(data); + } break; + + case HAVE_ALL_PACKS: + ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); + stackPacket.setExperimental(false); + stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not + stackPacket.setGameVersion(session.getClientData().getGameVersion()); + + for (ResourcePack pack : ResourcePack.PACKS.values()) { + ResourcePackManifest.Header header = pack.getManifest().getHeader(); + stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.getUuid().toString(), header.getVersionString(), "")); + } + + session.sendUpstreamPacket(stackPacket); + break; + default: session.disconnect("disconnectionScreen.resourcePack"); break; @@ -149,4 +190,30 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { boolean defaultHandler(BedrockPacket packet) { return translateAndDefault(packet); } + + @Override + public boolean handle(ResourcePackChunkRequestPacket packet) { + ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket(); + ResourcePack pack = ResourcePack.PACKS.get(packet.getPackId().toString()); + + data.setChunkIndex(packet.getChunkIndex()); + data.setProgress(packet.getChunkIndex() * ResourcePack.CHUNK_SIZE); + data.setPackVersion(packet.getPackVersion()); + data.setPackId(packet.getPackId()); + + int offset = packet.getChunkIndex() * ResourcePack.CHUNK_SIZE; + byte[] packData = new byte[(int) MathUtils.constrain(pack.getFile().length() - offset, 0, ResourcePack.CHUNK_SIZE)]; + + try (InputStream inputStream = new FileInputStream(pack.getFile())) { + inputStream.skip(offset); + inputStream.read(packData, 0, packData.length); + } catch (Exception e) { + e.printStackTrace(); + } + + data.setData(packData); + + session.sendUpstreamPacket(data); + return true; + } } 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 9a2ebe56d..42c2f2437 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 @@ -70,6 +70,7 @@ import org.geysermc.connector.network.session.cache.*; import org.geysermc.connector.network.translators.BiomeTranslator; import org.geysermc.connector.network.translators.EntityIdentifierRegistry; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; +import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.*; @@ -155,8 +156,6 @@ public class GeyserSession implements CommandSender { @Setter private Vector3i lastInteractionPosition; - @Setter - private boolean switchingDimension = false; private boolean manyDimPackets = false; private ServerRespawnPacket lastDimPacket = null; @@ -174,6 +173,11 @@ public class GeyserSession implements CommandSender { @Setter private long lastInteractedVillagerEid; + /** + * Stores the enchantment information the client has received if they are in an enchantment table GUI + */ + private final EnchantmentInventoryTranslator.EnchantmentSlotData[] enchantmentSlotData = new EnchantmentInventoryTranslator.EnchantmentSlotData[3]; + /** * The current attack speed of the player. Used for sending proper cooldown timings. */ @@ -186,8 +190,6 @@ public class GeyserSession implements CommandSender { @Setter private long lastHitTime; - private MinecraftProtocol protocol; - private boolean reducedDebugInfo = false; @Setter @@ -235,6 +237,17 @@ public class GeyserSession implements CommandSender { @Setter private boolean thunder = false; + /** + * Stores the last text inputted into a sign. + * + * Bedrock sends packets every time you update the sign, Java only wants the final packet. + * Until we determine that the user has finished editing, we save the sign's current status. + */ + @Setter + private String lastSignMessage; + + private MinecraftProtocol protocol; + public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) { this.connector = connector; this.upstream = new UpstreamSession(bedrockServerSession); @@ -294,10 +307,13 @@ public class GeyserSession implements CommandSender { attributesPacket.setAttributes(attributes); upstream.sendPacket(attributesPacket); + GameRulesChangedPacket gamerulePacket = new GameRulesChangedPacket(); // Only allow the server to send health information // Setting this to false allows natural regeneration to work false but doesn't break it being true - GameRulesChangedPacket gamerulePacket = new GameRulesChangedPacket(); gamerulePacket.getGameRules().add(new GameRuleData<>("naturalregeneration", false)); + // Don't let the client modify the inventory on death + // Setting this to true allows keep inventory to work if enabled but doesn't break functionality being false + gamerulePacket.getGameRules().add(new GameRuleData<>("keepinventory", true)); upstream.sendPacket(gamerulePacket); } @@ -578,8 +594,10 @@ public class GeyserSession implements CommandSender { startGamePacket.setFromWorldTemplate(false); startGamePacket.setWorldTemplateOptionLocked(false); - startGamePacket.setLevelId("world"); - startGamePacket.setLevelName("world"); + String serverName = connector.getConfig().getBedrock().getServerName(); + startGamePacket.setLevelId(serverName); + startGamePacket.setLevelName(serverName); + startGamePacket.setPremiumWorldTemplateId("00000000-0000-0000-0000-000000000000"); // startGamePacket.setCurrentTick(0); startGamePacket.setEnchantmentSeed(0); @@ -587,7 +605,7 @@ public class GeyserSession implements CommandSender { startGamePacket.setBlockPalette(BlockTranslator.BLOCKS); startGamePacket.setItemEntries(ItemRegistry.ITEMS); startGamePacket.setVanillaVersion("*"); - // startGamePacket.setMovementServerAuthoritative(true); + startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); upstream.sendPacket(startGamePacket); } @@ -612,7 +630,7 @@ public class GeyserSession implements CommandSender { * @param packet the bedrock packet from the NukkitX protocol lib */ public void sendUpstreamPacket(BedrockPacket packet) { - if (upstream != null && !upstream.isClosed()) { + if (upstream != null) { upstream.sendPacket(packet); } else { connector.getLogger().debug("Tried to send upstream packet " + packet.getClass().getSimpleName() + " but the session was null"); @@ -625,7 +643,7 @@ public class GeyserSession implements CommandSender { * @param packet the bedrock packet from the NukkitX protocol lib */ public void sendUpstreamPacketImmediately(BedrockPacket packet) { - if (upstream != null && !upstream.isClosed()) { + if (upstream != null) { upstream.sendPacketImmediately(packet); } else { connector.getLogger().debug("Tried to send upstream packet " + packet.getClass().getSimpleName() + " immediately but the session was null"); @@ -684,8 +702,12 @@ public class GeyserSession implements CommandSender { public void sendAdventureSettings() { AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket(); adventureSettingsPacket.setUniqueEntityId(playerEntity.getGeyserId()); - adventureSettingsPacket.setCommandPermission(CommandPermission.NORMAL); - adventureSettingsPacket.setPlayerPermission(PlayerPermission.MEMBER); + // Set command permission if OP permission level is high enough + // This allows mobile players access to a GUI for doing commands. The commands there do not change above OPERATOR + // and all commands there are accessible with OP permission level 2 + adventureSettingsPacket.setCommandPermission(opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL); + // Required to make command blocks destroyable + adventureSettingsPacket.setPlayerPermission(opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER); Set flags = new HashSet<>(); if (canFly) { diff --git a/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java index 09870eef0..393ebfa82 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java @@ -41,17 +41,15 @@ public class UpstreamSession { private boolean initialized = false; public void sendPacket(@NonNull BedrockPacket packet) { - if (isClosed()) - return; - - session.sendPacket(packet); + if (!isClosed()) { + session.sendPacket(packet); + } } public void sendPacketImmediately(@NonNull BedrockPacket packet) { - if (isClosed()) - return; - - session.sendPacketImmediately(packet); + if (!isClosed()) { + session.sendPacketImmediately(packet); + } } public void disconnect(String reason) { 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/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java index 0bc51ac7a..4b54c9434 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java @@ -76,9 +76,6 @@ public class EntityCache { if (entity != null && entity.isValid() && (force || entity.despawnEntity(session))) { long geyserId = entityIdTranslations.remove(entity.getEntityId()); entities.remove(geyserId); - if (entity.is(PlayerEntity.class)) { - playerEntities.remove(entity.as(PlayerEntity.class).getUuid()); - } return true; } return false; diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldCache.java index 310e5f9d7..ce49d2a09 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/WorldCache.java @@ -31,37 +31,40 @@ import lombok.Setter; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Scoreboard; - -import java.util.Collection; +import org.geysermc.connector.scoreboard.ScoreboardUpdater; @Getter public class WorldCache { - - private GeyserSession session; - + private final GeyserSession session; @Setter private Difficulty difficulty = Difficulty.EASY; - private boolean showCoordinates = true; private Scoreboard scoreboard; + private final ScoreboardUpdater scoreboardUpdater; public WorldCache(GeyserSession session) { this.session = session; this.scoreboard = new Scoreboard(session); + scoreboardUpdater = new ScoreboardUpdater(this); + scoreboardUpdater.start(); } public void removeScoreboard() { if (scoreboard != null) { - Collection objectives = scoreboard.getObjectives().values(); - scoreboard = new Scoreboard(session); - - for (Objective objective : objectives) { + for (Objective objective : scoreboard.getObjectives().values()) { scoreboard.despawnObjective(objective); } + scoreboard = new Scoreboard(session); } } + public int increaseAndGetScoreboardPacketsPerSecond() { + int pendingPps = scoreboardUpdater.incrementAndGetPacketsPerSecond(); + int pps = scoreboardUpdater.getPacketsPerSecond(); + return Math.max(pps, pendingPps); + } + /** * Tell the client to hide or show the coordinates * diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java index 92d2e9102..c43864816 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java @@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.BedrockPacket; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; import org.reflections.Reflections; @@ -48,7 +49,7 @@ public class PacketTranslatorRegistry { private static final ObjectArrayList> IGNORED_PACKETS = new ObjectArrayList<>(); static { - Reflections ref = new Reflections("org.geysermc.connector.network.translators"); + Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators") : new Reflections("org.geysermc.connector.network.translators"); for (Class clazz : ref.getTypesAnnotatedWith(Translator.class)) { Class packet = clazz.getAnnotation(Translator.class).packet(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockEntityDataTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockEntityDataTranslator.java index 38b940399..3522b4d56 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockEntityDataTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockEntityDataTranslator.java @@ -26,23 +26,18 @@ package org.geysermc.connector.network.translators.bedrock; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientUpdateJigsawBlockPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientUpdateSignPacket; import com.nukkitx.nbt.NbtMap; import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; - -import java.util.HashMap; -import java.util.Map; +import org.geysermc.connector.utils.SignUtils; @Translator(packet = BlockEntityDataPacket.class) public class BedrockBlockEntityDataTranslator extends PacketTranslator { - // In case two people are editing signs at the same time this array holds the temporary messages to be sent - // Position -> Message being held - protected static Map lastMessages = new HashMap<>(); - @Override public void translate(BlockEntityDataPacket packet, GeyserSession session) { NbtMap tag = packet.getData(); @@ -50,9 +45,8 @@ public class BedrockBlockEntityDataTranslator extends PacketTranslator SignUtils.JAVA_CHARACTER_WIDTH_MAX) { + // We need to apply some more logic if we went over the character width max + boolean wentOverMax = widthCount > SignUtils.JAVA_CHARACTER_WIDTH_MAX && character != '\n'; + widthCount = 0; + // Saves if we're moving a word to the next line + String word = null; + if (wentOverMax && iterator < lines.length - 1) { + // If we went over the max, we want to try to wrap properly like Bedrock does. + // So we look for a space in the Bedrock user's text to imply a word. + int index = newMessage.lastIndexOf(" "); + if (index != -1) { + // There is indeed a space in this line; let's get it + word = newMessage.substring(index + 1); + // 'Delete' that word from the string builder + newMessage.delete(index, newMessage.length()); + } + } lines[iterator] = newMessage.toString(); iterator++; - // Bedrock, for whatever reason, can hold a message out of bounds + // Bedrock, for whatever reason, can hold a message out of the bounds of the four lines // We don't care about that so we discard that if (iterator > lines.length - 1) { break; } newMessage = new StringBuilder(); + if (wentOverMax) { + // Apply the wrapped word to the new line + if (word != null) { + newMessage.append(word); + // And apply the width count + for (char wordCharacter : word.toCharArray()) { + widthCount += SignUtils.getCharacterWidth(wordCharacter); + } + } + // If we went over the max, we want to append the character to the new line. + newMessage.append(character); + widthCount += SignUtils.getCharacterWidth(character); + } } else newMessage.append(character); } // Put the final line on since it isn't done in the for loop if (iterator < lines.length) lines[iterator] = newMessage.toString(); + Position pos = new Position(tag.getInt("x"), tag.getInt("y"), tag.getInt("z")); ClientUpdateSignPacket clientUpdateSignPacket = new ClientUpdateSignPacket(pos, lines); session.sendDownstreamPacket(clientUpdateSignPacket); - //TODO (potentially): originally I was going to update the sign blocks so Bedrock and Java users would match visually - // However Java can still store a lot per-line and visuals are still messed up so that doesn't work - // We remove the sign position from map to indicate there is no work-in-progress sign - lastMessages.remove(pos); + // We set the sign text cached in the session to null to indicate there is no work-in-progress sign + session.setLastSignMessage(null); + + } else if (tag.getString("id").equals("JigsawBlock")) { + // Client has just sent a jigsaw block update + Position pos = new Position(tag.getInt("x"), tag.getInt("y"), tag.getInt("z")); + String name = tag.getString("name"); + String target = tag.getString("target"); + String pool = tag.getString("target_pool"); + String finalState = tag.getString("final_state"); + String joint = tag.getString("joint"); + ClientUpdateJigsawBlockPacket jigsawPacket = new ClientUpdateJigsawBlockPacket(pos, name, target, pool, + finalState, joint); + session.sendDownstreamPacket(jigsawPacket); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestPacketTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestPacketTranslator.java deleted file mode 100644 index 39cfceb10..000000000 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestPacketTranslator.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2019-2020 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.bedrock; - -import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientMoveItemToHotbarPacket; -import com.nukkitx.math.vector.Vector3i; -import com.nukkitx.protocol.bedrock.packet.BlockPickRequestPacket; -import com.nukkitx.protocol.bedrock.packet.PlayerHotbarPacket; -import org.geysermc.connector.inventory.Inventory; -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.item.ItemEntry; -import org.geysermc.connector.network.translators.item.ItemRegistry; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; - -@Translator(packet = BlockPickRequestPacket.class) -public class BedrockBlockPickRequestPacketTranslator extends PacketTranslator { - - @Override - public void translate(BlockPickRequestPacket packet, GeyserSession session) { - Vector3i vector = packet.getBlockPosition(); - int blockToPick = session.getConnector().getWorldManager().getBlockAt(session, vector.getX(), vector.getY(), vector.getZ()); - - // Block is air - chunk caching is probably off - if (blockToPick == 0) { - return; - } - - // Get the inventory to choose a slot to pick - Inventory inventory = session.getInventoryCache().getOpenInventory(); - if (inventory == null) { - inventory = session.getInventory(); - } - - String targetIdentifier = BlockTranslator.getJavaIdBlockMap().inverse().get(blockToPick).split("\\[")[0]; - - // Check hotbar for item - for (int i = 36; i < 45; i++) { - if (inventory.getItem(i) == null) { - continue; - } - ItemEntry item = ItemRegistry.getItem(inventory.getItem(i)); - // If this isn't the item we're looking for - if (!item.getJavaIdentifier().equals(targetIdentifier)) { - continue; - } - - PlayerHotbarPacket hotbarPacket = new PlayerHotbarPacket(); - hotbarPacket.setContainerId(0); - // Java inventory slot to hotbar slot ID - hotbarPacket.setSelectedHotbarSlot(i - 36); - hotbarPacket.setSelectHotbarSlot(true); - session.sendUpstreamPacket(hotbarPacket); - session.getInventory().setHeldItemSlot(i - 36); - // Don't check inventory if item was in hotbar - return; - } - - // Check inventory for item - for (int i = 9; i < 36; i++) { - if (inventory.getItem(i) == null) { - continue; - } - ItemEntry item = ItemRegistry.getItem(inventory.getItem(i)); - // If this isn't the item we're looking for - if (!item.getJavaIdentifier().equals(targetIdentifier)) { - continue; - } - - ClientMoveItemToHotbarPacket packetToSend = new ClientMoveItemToHotbarPacket(i); // https://wiki.vg/Protocol#Pick_Item - session.sendDownstreamPacket(packetToSend); - return; - } - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestTranslator.java new file mode 100644 index 000000000..023a83afe --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBlockPickRequestTranslator.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2020 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.bedrock; + +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.protocol.bedrock.packet.BlockPickRequestPacket; +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.block.BlockTranslator; +import org.geysermc.connector.utils.InventoryUtils; + +@Translator(packet = BlockPickRequestPacket.class) +public class BedrockBlockPickRequestTranslator extends PacketTranslator { + + @Override + public void translate(BlockPickRequestPacket packet, GeyserSession session) { + Vector3i vector = packet.getBlockPosition(); + int blockToPick = session.getConnector().getWorldManager().getBlockAt(session, vector.getX(), vector.getY(), vector.getZ()); + + // Block is air - chunk caching is probably off + if (blockToPick == 0) { + return; + } + + String targetIdentifier = BlockTranslator.getJavaIdBlockMap().inverse().get(blockToPick).split("\\[")[0]; + InventoryUtils.findOrCreatePickedBlock(session, targetIdentifier); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandBlockUpdateTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandBlockUpdateTranslator.java new file mode 100644 index 000000000..b1e732ffe --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockCommandBlockUpdateTranslator.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-2020 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.bedrock; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.github.steveice10.mc.protocol.data.game.world.block.CommandBlockMode; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientUpdateCommandBlockMinecartPacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientUpdateCommandBlockPacket; +import com.nukkitx.protocol.bedrock.packet.CommandBlockUpdatePacket; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; + +@Translator(packet = CommandBlockUpdatePacket.class) +public class BedrockCommandBlockUpdateTranslator extends PacketTranslator { + + @Override + public void translate(CommandBlockUpdatePacket packet, GeyserSession session) { + String command = packet.getCommand(); + boolean outputTracked = packet.isOutputTracked(); + if (packet.isBlock()) { + CommandBlockMode mode; + switch (packet.getMode()) { + case CHAIN: // The green one + mode = CommandBlockMode.SEQUENCE; + break; + case REPEATING: // The purple one + mode = CommandBlockMode.AUTO; + break; + default: // NORMAL, the orange one + mode = CommandBlockMode.REDSTONE; + break; + } + boolean isConditional = packet.isConditional(); + boolean automatic = !packet.isRedstoneMode(); // Automatic = Always Active option in Java + ClientUpdateCommandBlockPacket commandBlockPacket = new ClientUpdateCommandBlockPacket( + new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), + command, mode, outputTracked, isConditional, automatic); + session.sendDownstreamPacket(commandBlockPacket); + } else { + ClientUpdateCommandBlockMinecartPacket commandMinecartPacket = new ClientUpdateCommandBlockMinecartPacket( + (int) session.getEntityCache().getEntityByGeyserId(packet.getMinecartRuntimeEntityId()).getEntityId(), + command, outputTracked + ); + session.sendDownstreamPacket(commandMinecartPacket); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityPickRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityPickRequestTranslator.java new file mode 100644 index 000000000..4aa044ee4 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockEntityPickRequestTranslator.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2020 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.bedrock; + +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.packet.EntityPickRequestPacket; +import org.geysermc.connector.entity.Entity; +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.item.ItemEntry; +import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.utils.InventoryUtils; + +/** + * Called when the Bedrock user uses the pick block button on an entity + */ +@Translator(packet = EntityPickRequestPacket.class) +public class BedrockEntityPickRequestTranslator extends PacketTranslator { + + @Override + public void translate(EntityPickRequestPacket packet, GeyserSession session) { + if (session.getGameMode() != GameMode.CREATIVE) return; // Apparently Java behavior + Entity entity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId()); + if (entity == null) return; + + // Get the corresponding item + String itemName; + switch (entity.getEntityType()) { + case BOAT: + // Include type of boat in the name + int variant = entity.getMetadata().getInt(EntityData.VARIANT); + String typeOfBoat; + switch (variant) { + case 1: + typeOfBoat = "spruce"; + break; + case 2: + typeOfBoat = "birch"; + break; + case 3: + typeOfBoat = "jungle"; + break; + case 4: + typeOfBoat = "acacia"; + break; + case 5: + typeOfBoat = "dark_oak"; + break; + default: + typeOfBoat = "oak"; + break; + } + itemName = typeOfBoat + "_boat"; + break; + case LEASH_KNOT: + itemName = "lead"; + break; + case MINECART_CHEST: + case MINECART_COMMAND_BLOCK: + case MINECART_FURNACE: + case MINECART_HOPPER: + case MINECART_TNT: + // Move MINECART to the end of the name + itemName = entity.getEntityType().toString().toLowerCase().replace("minecart_", "") + "_minecart"; + break; + case MINECART_SPAWNER: + // Turns into a normal minecart + itemName = "minecart"; + break; + case ARMOR_STAND: + case END_CRYSTAL: + case ITEM_FRAME: + case MINECART: + case PAINTING: + // No spawn egg, just an item + itemName = entity.getEntityType().toString().toLowerCase(); + break; + default: + itemName = entity.getEntityType().toString().toLowerCase() + "_spawn_egg"; + break; + } + + String fullItemName = "minecraft:" + itemName; + ItemEntry entry = ItemRegistry.getItemEntry(fullItemName); + // Verify it is, indeed, an item + if (entry == null) return; + + InventoryUtils.findOrCreatePickedBlock(session, fullItemName); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index f8e4eb6d0..95e669572 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -39,8 +39,11 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import org.geysermc.connector.entity.CommandBlockMinecartEntity; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity; @@ -98,11 +101,31 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator= 2 && session.getGameMode() == GameMode.CREATIVE) { + // Otherwise insufficient permissions + int blockState = BlockTranslator.getJavaBlockState(packet.getBlockRuntimeId()); + String blockName = BlockTranslator.getJavaIdBlockMap().inverse().getOrDefault(blockState, ""); + // In the future this can be used for structure blocks too, however not all elements + // are available in each GUI + if (blockName.contains("jigsaw")) { + ContainerOpenPacket openPacket = new ContainerOpenPacket(); + openPacket.setBlockPosition(packet.getBlockPosition()); + openPacket.setId((byte) 1); + openPacket.setType(ContainerType.JIGSAW_EDITOR); + openPacket.setUniqueEntityId(-1); + session.sendUpstreamPacket(openPacket); + } + } + } + Vector3i blockPos = packet.getBlockPosition(); // TODO: Find a better way to do this? switch (packet.getBlockFace()) { @@ -195,6 +218,18 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { @Override public void translate(EmotePacket packet, GeyserSession session) { long javaId = session.getPlayerEntity().getEntityId(); - for (GeyserSession otherSession : GeyserConnector.getInstance().getPlayers()) { + for (GeyserSession otherSession : session.getConnector().getPlayers()) { if (otherSession != session) { - packet.setRuntimeEntityId(otherSession.getEntityCache().getEntityByJavaId(javaId).getGeyserId()); + if (otherSession.isClosed()) continue; + Entity otherEntity = otherSession.getEntityCache().getEntityByJavaId(javaId); + if (otherEntity == null) continue; + packet.setRuntimeEntityId(otherEntity.getGeyserId()); otherSession.sendUpstreamPacket(packet); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java index be918ba74..8809941b9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -86,6 +86,13 @@ public class BedrockMovePlayerTranslator extends PacketTranslator actions) { + for (InventoryActionData action : actions) { + if (action.getSource().getContainerId() == inventory.getId()) { + // This is the hopper UI + switch (action.getSlot()) { + case 1: + // Don't allow the slot to be put through if the item isn't lapis + if ((action.getToItem().getId() != DYE_ID + && action.getToItem().getDamage() != LAPIS_DAMAGE) && action.getToItem() != ItemData.AIR) { + updateInventory(session, inventory); + InventoryUtils.updateCursor(session); + return; + } + break; + case 2: + case 3: + case 4: + // The books here act as buttons + ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), action.getSlot() - 2); + session.sendDownstreamPacket(packet); + updateInventory(session, inventory); + InventoryUtils.updateCursor(session); + return; + default: + break; + } + } + } + + super.translateActions(session, inventory, actions); + } + + @Override + public void updateInventory(GeyserSession session, Inventory inventory) { + super.updateInventory(session, inventory); + ItemData[] items = new ItemData[5]; + items[0] = ItemTranslator.translateToBedrock(session, inventory.getItem(0)); + items[1] = ItemTranslator.translateToBedrock(session, inventory.getItem(1)); + for (int i = 0; i < 3; i++) { + items[i + 2] = session.getEnchantmentSlotData()[i].getItem() != null ? session.getEnchantmentSlotData()[i].getItem() : createEnchantmentBook(); + } + + InventoryContentPacket contentPacket = new InventoryContentPacket(); + contentPacket.setContainerId(inventory.getId()); + contentPacket.setContents(items); + session.sendUpstreamPacket(contentPacket); } @Override public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { + int bookSlotToUpdate; + switch (key) { + case 0: + case 1: + case 2: + // Experience required + bookSlotToUpdate = key; + session.getEnchantmentSlotData()[bookSlotToUpdate].setExperienceRequired(value); + break; + case 4: + case 5: + case 6: + // Enchantment name + bookSlotToUpdate = key - 4; + if (value != -1) { + session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentType(EnchantmentTableEnchantments.values()[value - 1]); + } else { + // -1 means no enchantment specified + session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentType(null); + } + break; + case 7: + case 8: + case 9: + // Enchantment level + bookSlotToUpdate = key - 7; + session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentLevel(value); + break; + default: + return; + } + updateEnchantmentBook(session, inventory, bookSlotToUpdate); + } + @Override + public void openInventory(GeyserSession session, Inventory inventory) { + super.openInventory(session, inventory); + for (int i = 0; i < session.getEnchantmentSlotData().length; i++) { + session.getEnchantmentSlotData()[i] = new EnchantmentSlotData(); + } + } + + @Override + public void closeInventory(GeyserSession session, Inventory inventory) { + super.closeInventory(session, inventory); + Arrays.fill(session.getEnchantmentSlotData(), null); + } + + private ItemData createEnchantmentBook() { + NbtMapBuilder root = NbtMap.builder(); + NbtMapBuilder display = NbtMap.builder(); + + display.putString("Name", ChatColor.RESET + "No Enchantment"); + + root.put("display", display.build()); + return ItemData.of(ENCHANTED_BOOK_ID, (short) 0, 1, root.build()); + } + + private void updateEnchantmentBook(GeyserSession session, Inventory inventory, int slot) { + NbtMapBuilder root = NbtMap.builder(); + NbtMapBuilder display = NbtMap.builder(); + EnchantmentSlotData data = session.getEnchantmentSlotData()[slot]; + if (data.getEnchantmentType() != null) { + display.putString("Name", ChatColor.ITALIC + data.getEnchantmentType().toString(session) + + (data.getEnchantmentLevel() != -1 ? " " + toRomanNumeral(session, data.getEnchantmentLevel()) : "") + "?"); + } else { + display.putString("Name", ChatColor.RESET + "No Enchantment"); + } + + display.putList("Lore", NbtType.STRING, Collections.singletonList(ChatColor.DARK_GRAY + data.getExperienceRequired() + "xp")); + root.put("display", display.build()); + ItemData book = ItemData.of(ENCHANTED_BOOK_ID, (short) 0, 1, root.build()); + + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(inventory.getId()); + slotPacket.setSlot(slot + 2); + slotPacket.setItem(book); + session.sendUpstreamPacket(slotPacket); + data.setItem(book); + } + + private String toRomanNumeral(GeyserSession session, int level) { + return LocaleUtils.getLocaleString("enchantment.level." + level, + session.getClientData().getLanguageCode()); + } + + /** + * Stores the data of each slot in an enchantment table + */ + @NoArgsConstructor + @Getter + @Setter + @ToString + public static class EnchantmentSlotData { + private EnchantmentTableEnchantments enchantmentType = null; + private int enchantmentLevel = 0; + private int experienceRequired = 0; + private ItemData item; + } + + /** + * Classifies enchantments by Java order + */ + public enum EnchantmentTableEnchantments { + PROTECTION, + FIRE_PROTECTION, + FEATHER_FALLING, + BLAST_PROTECTION, + PROJECTILE_PROTECTION, + RESPIRATION, + AQUA_AFFINITY, + THORNS, + DEPTH_STRIDER, + FROST_WALKER, + BINDING_CURSE, + SHARPNESS, + SMITE, + BANE_OF_ARTHROPODS, + KNOCKBACK, + FIRE_ASPECT, + LOOTING, + SWEEPING, + EFFICIENCY, + SILK_TOUCH, + UNBREAKING, + FORTUNE, + POWER, + PUNCH, + FLAME, + INFINITY, + LUCK_OF_THE_SEA, + LURE, + LOYALTY, + IMPALING, + RIPTIDE, + CHANNELING, + MENDING, + VANISHING_CURSE, // After this is not documented + MULTISHOT, + PIERCING, + QUICK_CHARGE, + SOUL_SPEED; + + public String toString(GeyserSession session) { + return LocaleUtils.getLocaleString("enchantment.minecraft." + this.toString().toLowerCase(), + session.getClientData().getLanguageCode()); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java index 7d06aed14..e44e4bd0b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java @@ -56,7 +56,6 @@ public abstract class InventoryTranslator { put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator()); put(WindowType.MERCHANT, new MerchantInventoryTranslator()); put(WindowType.SMITHING, new SmithingInventoryTranslator()); - //put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator()); //TODO InventoryTranslator furnace = new FurnaceInventoryTranslator(); put(WindowType.FURNACE, furnace); @@ -64,6 +63,7 @@ public abstract class InventoryTranslator { put(WindowType.SMOKER, furnace); InventoryUpdater containerUpdater = new ContainerInventoryUpdater(); + put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator(containerUpdater)); //TODO put(WindowType.GENERIC_3X3, new BlockInventoryTranslator(9, "minecraft:dispenser[facing=north,triggered=false]", ContainerType.DISPENSER, containerUpdater)); put(WindowType.HOPPER, new BlockInventoryTranslator(5, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER, containerUpdater)); put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(27, "minecraft:shulker_box[facing=north]", ContainerType.CONTAINER, containerUpdater)); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java index 4828fbf27..1f3af019f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java @@ -173,21 +173,8 @@ public class ItemRegistry { int netId = 1; List creativeItems = new ArrayList<>(); for (JsonNode itemNode : creativeItemEntries) { - try { - short damage = 0; - NbtMap tag = null; - if (itemNode.has("damage")) { - damage = itemNode.get("damage").numberValue().shortValue(); - } - if (itemNode.has("nbt_b64")) { - byte[] bytes = Base64.getDecoder().decode(itemNode.get("nbt_b64").asText()); - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - tag = (NbtMap) NbtUtils.createReaderLE(bais).readTag(); - } - creativeItems.add(ItemData.fromNet(netId++, itemNode.get("id").asInt(), damage, 1, tag)); - } catch (IOException e) { - e.printStackTrace(); - } + ItemData item = getBedrockItemFromJson(itemNode); + creativeItems.add(ItemData.fromNet(netId++, item.getId(), item.getDamage(), item.getCount(), item.getTag())); } CREATIVE_ITEMS = creativeItems.toArray(new ItemData[0]); } @@ -233,4 +220,48 @@ public class ItemRegistry { return JAVA_IDENTIFIER_MAP.computeIfAbsent(javaIdentifier, key -> ITEM_ENTRIES.values() .stream().filter(itemEntry -> itemEntry.getJavaIdentifier().equals(key)).findFirst().orElse(null)); } + + /** + * Finds the Bedrock string identifier of an ItemEntry + * + * @param entry the ItemEntry to search for + * @return the Bedrock identifier + */ + public static String getBedrockIdentifer(ItemEntry entry) { + String blockName = ""; + for (StartGamePacket.ItemEntry startGamePacketItemEntry : ItemRegistry.ITEMS) { + if (startGamePacketItemEntry.getId() == (short) entry.getBedrockId()) { + blockName = startGamePacketItemEntry.getIdentifier(); // Find the Bedrock string name + break; + } + } + return blockName; + } + + /** + * Gets a Bedrock {@link ItemData} from a {@link JsonNode} + * @param itemNode the JSON node that contains ProxyPass-compatible Bedrock item data + * @return + */ + public static ItemData getBedrockItemFromJson(JsonNode itemNode) { + int count = 1; + short damage = 0; + NbtMap tag = null; + if (itemNode.has("damage")) { + damage = itemNode.get("damage").numberValue().shortValue(); + } + if (itemNode.has("count")) { + count = itemNode.get("count").asInt(); + } + if (itemNode.has("nbt_b64")) { + byte[] bytes = Base64.getDecoder().decode(itemNode.get("nbt_b64").asText()); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + try { + tag = (NbtMap) NbtUtils.createReaderLE(bais).readTag(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return ItemData.of(itemNode.get("id").asInt(), damage, count, tag); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java index 37dd066ed..864e9fe03 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java @@ -35,12 +35,14 @@ import com.nukkitx.nbt.NbtType; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.MessageUtils; import org.reflections.Reflections; @@ -62,7 +64,7 @@ public abstract class ItemTranslator { static { /* Load item translators */ - Reflections ref = new Reflections("org.geysermc.connector.network.translators.item"); + Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.item") : new Reflections("org.geysermc.connector.network.translators.item"); Map loadedNbtItemTranslators = new HashMap<>(); for (Class clazz : ref.getTypesAnnotatedWith(ItemRemapper.class)) { @@ -138,11 +140,13 @@ public abstract class ItemTranslator { if (nbt != null) { for (NbtItemStackTranslator translator : NBT_TRANSLATORS) { if (translator.acceptItem(bedrockItem)) { - translator.translateToBedrock(nbt, bedrockItem); + translator.translateToBedrock(session, nbt, bedrockItem); } } } + translateDisplayProperties(session, nbt); + ItemData itemData; ItemTranslator itemStackTranslator = ITEM_STACK_TRANSLATORS.get(bedrockItem.getJavaId()); if (itemStackTranslator != null) { @@ -151,42 +155,43 @@ public abstract class ItemTranslator { itemData = DEFAULT_TRANSLATOR.translateToBedrock(itemStack, bedrockItem); } - - // Get the display name of the item - NbtMap tag = itemData.getTag(); - if (tag != null) { - NbtMap display = tag.getCompound("display"); - if (display != null && !display.isEmpty() && display.containsKey("Name")) { - String name = display.getString("Name"); - - // If its not a message convert it - if (!MessageUtils.isMessage(name)) { - TextComponent component = LegacyComponentSerializer.legacySection().deserialize(name); - name = GsonComponentSerializer.gson().serialize(component); - } - - // Check if its a message to translate - if (MessageUtils.isMessage(name)) { - // Get the translated name - name = MessageUtils.getTranslatedBedrockMessage(MessageSerializer.fromString(name), session.getClientData().getLanguageCode()); - - // Build the new display tag - NbtMapBuilder displayBuilder = display.toBuilder(); - displayBuilder.putString("Name", name); - - // Build the new root tag - NbtMapBuilder builder = tag.toBuilder(); - builder.put("display", displayBuilder.build()); - - // Create a new item with the original data + updated name - itemData = ItemData.of(itemData.getId(), itemData.getDamage(), itemData.getCount(), builder.build()); - } - } + if (nbt != null) { + // Translate the canDestroy and canPlaceOn Java NBT + ListTag canDestroy = nbt.get("CanDestroy"); + String[] canBreak = new String[0]; + ListTag canPlaceOn = nbt.get("CanPlaceOn"); + String[] canPlace = new String[0]; + canBreak = getCanModify(canDestroy, canBreak); + canPlace = getCanModify(canPlaceOn, canPlace); + itemData = ItemData.of(itemData.getId(), itemData.getDamage(), itemData.getCount(), itemData.getTag(), canPlace, canBreak); } return itemData; } + /** + * Translates the Java NBT of canDestroy and canPlaceOn to its Bedrock counterparts. + * In Java, this is treated as normal NBT, but in Bedrock, these arguments are extra parts of the item data itself. + * @param canModifyJava the list of items in Java + * @param canModifyBedrock the empty list of items in Bedrock + * @return the new list of items in Bedrock + */ + private static String[] getCanModify(ListTag canModifyJava, String[] canModifyBedrock) { + if (canModifyJava != null && canModifyJava.size() > 0) { + canModifyBedrock = new String[canModifyJava.size()]; + for (int i = 0; i < canModifyBedrock.length; i++) { + // Get the Java identifier of the block that can be placed + String block = ((StringTag) canModifyJava.get(i)).getValue(); + // Sometimes this is done but it's still valid + if (!block.startsWith("minecraft:")) block = "minecraft:" + block; + // Get the Bedrock identifier of the item and replace it. + // This will unfortunately be limited - for example, beds and banners will be translated weirdly + canModifyBedrock[i] = BlockTranslator.getBedrockBlockIdentifier(block).replace("minecraft:", ""); + } + } + return canModifyBedrock; + } + private static final ItemTranslator DEFAULT_TRANSLATOR = new ItemTranslator() { @Override public List getAppliedItems() { @@ -375,6 +380,38 @@ public abstract class ItemTranslator { return null; } + /** + * Translates the display name of the item + * @param session the Bedrock client's session + * @param tag the tag to translate + */ + public static void translateDisplayProperties(GeyserSession session, CompoundTag tag) { + if (tag != null) { + CompoundTag display = tag.get("display"); + if (display != null && !display.isEmpty() && display.contains("Name")) { + String name = ((StringTag) display.get("Name")).getValue(); + + // If its not a message convert it + if (!MessageUtils.isMessage(name)) { + Component component = LegacyComponentSerializer.legacySection().deserialize(name); + name = GsonComponentSerializer.gson().serialize(component); + } + + // Check if its a message to translate + if (MessageUtils.isMessage(name)) { + // Get the translated name + name = MessageUtils.getTranslatedBedrockMessage(MessageSerializer.fromString(name), session.getClientData().getLanguageCode()); + + // Add the new name tag + display.put(new StringTag("Name", name)); + + // Add to the new root tag + tag.put(display); + } + } + } + } + /** * Checks if an {@link ItemStack} is equal to another item stack * diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/NbtItemStackTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/NbtItemStackTranslator.java index 3c0b75b75..89d41e98d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/NbtItemStackTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/NbtItemStackTranslator.java @@ -26,17 +26,33 @@ package org.geysermc.connector.network.translators.item; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import org.geysermc.connector.network.session.GeyserSession; public class NbtItemStackTranslator { - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + /** + * Translate the item NBT to Bedrock + * @param session the client's current session + * @param itemTag the item's CompoundTag + * @param itemEntry Geyser's item entry + */ + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { } + /** + * Translate the item NBT to Java. + * @param itemTag the item's CompoundTag + * @param itemEntry Geyser's item entry + */ public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) { } + /** + * @param itemEntry Geyser's item entry + * @return if the item should be processed under this class + */ public boolean acceptItem(ItemEntry itemEntry) { return true; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/RecipeRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/RecipeRegistry.java new file mode 100644 index 000000000..191b285c6 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/RecipeRegistry.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2019-2020 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.item; + +import com.fasterxml.jackson.databind.JsonNode; +import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.utils.FileUtils; +import org.geysermc.connector.utils.LanguageUtils; + +import java.io.InputStream; +import java.util.List; +import java.util.UUID; + +/** + * Manages any recipe-related storing + */ +public class RecipeRegistry { + + /** + * A list of all possible leather armor dyeing recipes. + * Created manually. + */ + public static List LEATHER_DYEING_RECIPES = new ObjectArrayList<>(); + /** + * A list of all possible firework rocket recipes, including the base rocket. + * Obtained from a ProxyPass dump of protocol v407 + */ + public static List FIREWORK_ROCKET_RECIPES = new ObjectArrayList<>(21); + /** + * A list of all possible firework star recipes. + * Obtained from a ProxyPass dump of protocol v407 + */ + public static List FIREWORK_STAR_RECIPES = new ObjectArrayList<>(40); + /** + * A list of all possible shulker box dyeing options. + * Obtained from a ProxyPass dump of protocol v407 + */ + public static List SHULKER_BOX_DYEING_RECIPES = new ObjectArrayList<>(); + + static { + // Get all recipes that are not directly sent from a Java server + InputStream stream = FileUtils.getResource("mappings/recipes.json"); + + JsonNode items; + try { + items = GeyserConnector.JSON_MAPPER.readTree(stream); + } catch (Exception e) { + throw new AssertionError(LanguageUtils.getLocaleStringLog("geyser.toolbox.fail.runtime_java"), e); + } + + for (JsonNode entry: items.get("leather_armor")) { + // This won't be perfect, as we can't possibly send every leather input for every kind of color + // But it does display the correct output from a base leather armor, and besides visuals everything works fine + LEATHER_DYEING_RECIPES.add(getCraftingDataFromJsonNode(entry)); + } + for (JsonNode entry : items.get("firework_rockets")) { + FIREWORK_ROCKET_RECIPES.add(getCraftingDataFromJsonNode(entry)); + } + for (JsonNode entry : items.get("firework_stars")) { + FIREWORK_STAR_RECIPES.add(getCraftingDataFromJsonNode(entry)); + } + for (JsonNode entry : items.get("shulker_boxes")) { + SHULKER_BOX_DYEING_RECIPES.add(getCraftingDataFromJsonNode(entry)); + } + } + + /** + * Computes a Bedrock crafting recipe from the given JSON data. + * @param node the JSON data to compute + * @return the {@link CraftingData} to send to the Bedrock client. + */ + private static CraftingData getCraftingDataFromJsonNode(JsonNode node) { + ItemData output = ItemRegistry.getBedrockItemFromJson(node.get("output").get(0)); + List inputs = new ObjectArrayList<>(); + for (JsonNode entry : node.get("input")) { + inputs.add(ItemRegistry.getBedrockItemFromJson(entry)); + } + UUID uuid = UUID.randomUUID(); + if (node.get("type").asInt() == 5) { + // Shulker box + return CraftingData.fromShulkerBox(uuid.toString(), + inputs.toArray(new ItemData[0]), new ItemData[]{output}, uuid, "crafting_table", 0); + } + return CraftingData.fromShapeless(uuid.toString(), + inputs.toArray(new ItemData[0]), new ItemData[]{output}, uuid, "crafting_table", 0); + } + + public static void init() { + // no-op + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java index b30031ebc..e45566264 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BasicItemTranslator.java @@ -33,6 +33,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; @@ -45,7 +46,7 @@ import java.util.List; public class BasicItemTranslator extends NbtItemStackTranslator { @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { if (!itemTag.contains("display")) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java index e802f0174..41ee4fbca 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java @@ -29,6 +29,7 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; import org.geysermc.connector.network.translators.item.ItemEntry; @@ -41,7 +42,7 @@ import java.util.List; public class BookPagesTranslator extends NbtItemStackTranslator { @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { if (!itemTag.contains("pages")) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/CrossbowTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/CrossbowTranslator.java index 3c5271ebf..979c5a205 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/CrossbowTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/CrossbowTranslator.java @@ -29,6 +29,7 @@ import com.github.steveice10.opennbt.tag.builtin.ByteTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; import org.geysermc.connector.network.translators.item.ItemEntry; @@ -37,7 +38,7 @@ import org.geysermc.connector.network.translators.item.ItemEntry; public class CrossbowTranslator extends NbtItemStackTranslator { @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { if (itemTag.get("ChargedProjectiles") != null) { ListTag chargedProjectiles = itemTag.get("ChargedProjectiles"); if (!chargedProjectiles.getValue().isEmpty()) { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantedBookTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantedBookTranslator.java index 63ac6c510..990d5a7ad 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantedBookTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantedBookTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.connector.network.translators.item.translators.nbt; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; import org.geysermc.connector.network.translators.item.ItemEntry; @@ -36,7 +37,7 @@ import org.geysermc.connector.network.translators.item.ItemEntry; public class EnchantedBookTranslator extends NbtItemStackTranslator { @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { if (!itemTag.contains("StoredEnchantments")) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantmentTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantmentTranslator.java index 6bd9cc77e..6884c00ba 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantmentTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/EnchantmentTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.connector.network.translators.item.translators.nbt; import com.github.steveice10.opennbt.tag.builtin.*; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; import org.geysermc.connector.network.translators.item.Enchantment; @@ -40,7 +41,7 @@ import java.util.Map; public class EnchantmentTranslator extends NbtItemStackTranslator { @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { List newTags = new ArrayList<>(); if (itemTag.contains("Enchantments")) { ListTag enchantmentTag = itemTag.get("Enchantments"); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkTranslator.java index 0e23169f8..3b453ea18 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkTranslator.java @@ -26,6 +26,7 @@ package org.geysermc.connector.network.translators.item.translators.nbt; import com.github.steveice10.opennbt.tag.builtin.*; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; @@ -36,7 +37,7 @@ import org.geysermc.connector.utils.MathUtils; public class FireworkTranslator extends NbtItemStackTranslator { @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { if (!itemTag.contains("Fireworks")) { return; } @@ -106,6 +107,9 @@ public class FireworkTranslator extends NbtItemStackTranslator { fireworks.put(new ByteTag("Flight", MathUtils.convertByte(fireworks.get("Flight").getValue()))); } + if (!itemTag.contains("Explosions")) { + return; + } ListTag explosions = fireworks.get("Explosions"); for (Tag effect : explosions.getValue()) { CompoundTag effectData = (CompoundTag) effect; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java index 9f864ccf4..93af3e709 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.connector.network.translators.item.translators.nbt; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.IntTag; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; import org.geysermc.connector.network.translators.item.ItemEntry; @@ -37,7 +38,7 @@ public class LeatherArmorTranslator extends NbtItemStackTranslator { private static final String[] ITEMS = new String[]{"minecraft:leather_helmet", "minecraft:leather_chestplate", "minecraft:leather_leggings", "minecraft:leather_boots"}; @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { if (!itemTag.contains("display")) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/MapItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/MapItemTranslator.java index 4a36880c9..c9b49efd4 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/MapItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/MapItemTranslator.java @@ -29,6 +29,7 @@ import com.github.steveice10.opennbt.tag.builtin.ByteTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.IntTag; import com.github.steveice10.opennbt.tag.builtin.LongTag; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; @@ -37,7 +38,7 @@ import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; public class MapItemTranslator extends NbtItemStackTranslator { @Override - public void translateToBedrock(CompoundTag itemTag, ItemEntry itemEntry) { + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { IntTag mapId = itemTag.get("map"); if (mapId != null) { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/ShulkerBoxItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/ShulkerBoxItemTranslator.java new file mode 100644 index 000000000..a9930f698 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/ShulkerBoxItemTranslator.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2020 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.item.translators.nbt; + +import com.github.steveice10.opennbt.tag.builtin.*; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.ItemRemapper; +import org.geysermc.connector.network.translators.item.ItemEntry; +import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.network.translators.item.ItemTranslator; +import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; + +@ItemRemapper +public class ShulkerBoxItemTranslator extends NbtItemStackTranslator { + + @Override + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { + if (!itemTag.contains("BlockEntityTag")) return; // Empty shulker box + + CompoundTag blockEntityTag = itemTag.get("BlockEntityTag"); + if (blockEntityTag.get("Items") == null) return; + ListTag itemsList = new ListTag("Items"); + for (Tag item : (ListTag) blockEntityTag.get("Items")) { + CompoundTag itemData = (CompoundTag) item; // Information about the item + CompoundTag boxItemTag = new CompoundTag(""); // Final item tag to add to the list + boxItemTag.put(new ByteTag("Slot", ((ByteTag) itemData.get("Slot")).getValue())); + boxItemTag.put(new ByteTag("WasPickedUp", (byte) 0)); // ??? + + ItemEntry boxItemEntry = ItemRegistry.getItemEntry(((StringTag) itemData.get("id")).getValue()); + String blockName = ItemRegistry.getBedrockIdentifer(boxItemEntry); + + boxItemTag.put(new StringTag("Name", blockName)); + boxItemTag.put(new ShortTag("Damage", (short) boxItemEntry.getBedrockData())); + boxItemTag.put(new ByteTag("Count", ((ByteTag) itemData.get("Count")).getValue())); + if (itemData.contains("tag")) { + // Only the display name is what we have interest in, so just translate that if relevant + CompoundTag displayTag = itemData.get("tag"); + ItemTranslator.translateDisplayProperties(session, displayTag); + boxItemTag.put(displayTag); + } + + itemsList.add(boxItemTag); + } + itemTag.put(itemsList); + // Don't actually bother with removing the block entity tag. Too risky to translate + // if the user is on creative and messing with a shulker box + //itemTag.remove("BlockEntityTag"); + } + + @Override + public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) { + if (itemTag.contains("Items")) { // Remove any extraneous Bedrock tag and don't touch the Java one + itemTag.remove("Items"); + } + } + + @Override + public boolean acceptItem(ItemEntry itemEntry) { + return itemEntry.getJavaIdentifier().contains("shulker_box"); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java index 75ccc0a5a..9ffb4f0d9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java @@ -41,10 +41,7 @@ import lombok.EqualsAndHashCode; 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.item.ItemEntry; -import org.geysermc.connector.network.translators.item.ItemRegistry; -import org.geysermc.connector.network.translators.item.ItemTranslator; -import org.geysermc.connector.network.translators.item.PotionMixRegistry; +import org.geysermc.connector.network.translators.item.*; import java.util.*; import java.util.stream.Collectors; @@ -83,6 +80,24 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator { + + @Override + public void translate(ServerDisconnectPacket packet, GeyserSession session) { + session.disconnect(MessageUtils.getTranslatedBedrockMessage(packet.getReason(), session.getClientData().getLanguageCode(), true)); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java index 10b2ba9ad..6a9ef4dc6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerListEntryTranslator.java @@ -88,18 +88,13 @@ public class JavaPlayerListEntryTranslator extends PacketTranslator { + private static final GeyserLogger LOGGER = GeyserConnector.getInstance().getLogger(); @Override public void translate(ServerTeamPacket packet, GeyserSession session) { - GeyserConnector.getInstance().getLogger().debug("Team packet " + packet.getTeamName() + " " + packet.getAction() + " " + Arrays.toString(packet.getPlayers())); + if (LOGGER.isDebug()) { + LOGGER.debug("Team packet " + packet.getTeamName() + " " + packet.getAction() + " " + Arrays.toString(packet.getPlayers())); + } + + int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond(); Scoreboard scoreboard = session.getWorldCache().getScoreboard(); Team team = scoreboard.getTeam(packet.getTeamName()); @@ -54,42 +61,59 @@ public class JavaTeamTranslator extends PacketTranslator { scoreboard.registerNewTeam(packet.getTeamName(), toPlayerSet(packet.getPlayers())) .setName(MessageUtils.getBedrockMessage(packet.getDisplayName())) .setColor(packet.getColor()) + .setNameTagVisibility(packet.getNameTagVisibility()) .setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getClientData().getLanguageCode())) .setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode())); break; case UPDATE: - if (team != null) { - team.setName(MessageUtils.getBedrockMessage(packet.getDisplayName())) - .setColor(packet.getColor()) - .setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getClientData().getLanguageCode())) - .setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode())) - .setUpdateType(UpdateType.UPDATE); - } else { - GeyserConnector.getInstance().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); + if (team == null) { + LOGGER.debug(LanguageUtils.getLocaleStringLog( + "geyser.network.translator.team.failed_not_registered", + packet.getAction(), packet.getTeamName() + )); + return; } + + team.setName(MessageUtils.getBedrockMessage(packet.getDisplayName())) + .setColor(packet.getColor()) + .setNameTagVisibility(packet.getNameTagVisibility()) + .setPrefix(MessageUtils.getTranslatedBedrockMessage(packet.getPrefix(), session.getClientData().getLanguageCode())) + .setSuffix(MessageUtils.getTranslatedBedrockMessage(packet.getSuffix(), session.getClientData().getLanguageCode())) + .setUpdateType(UpdateType.UPDATE); break; case ADD_PLAYER: - if (team != null) { - team.addEntities(packet.getPlayers()); - } else { - GeyserConnector.getInstance().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); + if (team == null) { + LOGGER.debug(LanguageUtils.getLocaleStringLog( + "geyser.network.translator.team.failed_not_registered", + packet.getAction(), packet.getTeamName() + )); + return; } + team.addEntities(packet.getPlayers()); break; case REMOVE_PLAYER: - if (team != null) { - team.removeEntities(packet.getPlayers()); - } else { - GeyserConnector.getInstance().getLogger().debug(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_not_registered", packet.getAction(), packet.getTeamName())); + if (team == null) { + LOGGER.debug(LanguageUtils.getLocaleStringLog( + "geyser.network.translator.team.failed_not_registered", + packet.getAction(), packet.getTeamName() + )); + return; } + team.removeEntities(packet.getPlayers()); break; case REMOVE: scoreboard.removeTeam(packet.getTeamName()); break; } - scoreboard.onUpdate(); + + // ScoreboardUpdater will handle it for us if the packets per second + // (for score and team packets) is higher then the first threshold + if (pps < ScoreboardUpdater.FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) { + scoreboard.onUpdate(); + } } private Set toPlayerSet(String[] players) { - return new ObjectOpenHashSet<>(Arrays.asList(players)); + return new ObjectOpenHashSet<>(players); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java index 8d7d59a89..35033ca52 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaUpdateScoreTranslator.java @@ -25,48 +25,58 @@ package org.geysermc.connector.network.translators.java.scoreboard; +import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardAction; +import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerUpdateScorePacket; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.GeyserLogger; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.cache.WorldCache; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Scoreboard; - -import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardAction; -import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerUpdateScorePacket; +import org.geysermc.connector.scoreboard.ScoreboardUpdater; import org.geysermc.connector.utils.LanguageUtils; @Translator(packet = ServerUpdateScorePacket.class) public class JavaUpdateScoreTranslator extends PacketTranslator { + private final GeyserLogger logger; + + public JavaUpdateScoreTranslator() { + logger = GeyserConnector.getInstance().getLogger(); + } @Override public void translate(ServerUpdateScorePacket packet, GeyserSession session) { - try { - Scoreboard scoreboard = session.getWorldCache().getScoreboard(); + WorldCache worldCache = session.getWorldCache(); + Scoreboard scoreboard = worldCache.getScoreboard(); + int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond(); - Objective objective = scoreboard.getObjective(packet.getObjective()); - if (objective == null && packet.getAction() != ScoreboardAction.REMOVE) { - GeyserConnector.getInstance().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.translator.score.failed_objective", packet.getObjective())); - return; - } + Objective objective = scoreboard.getObjective(packet.getObjective()); + if (objective == null && packet.getAction() != ScoreboardAction.REMOVE) { + logger.info(LanguageUtils.getLocaleStringLog("geyser.network.translator.score.failed_objective", packet.getObjective())); + return; + } - switch (packet.getAction()) { - case ADD_OR_UPDATE: - objective.setScore(packet.getEntry(), packet.getValue()); - break; - case REMOVE: - if (objective != null) { - objective.resetScore(packet.getEntry()); - } else { - for (Objective objective1 : scoreboard.getObjectives().values()) { - objective1.resetScore(packet.getEntry()); - } + switch (packet.getAction()) { + case ADD_OR_UPDATE: + objective.setScore(packet.getEntry(), packet.getValue()); + break; + case REMOVE: + if (objective != null) { + objective.removeScore(packet.getEntry()); + } else { + for (Objective objective1 : scoreboard.getObjectives().values()) { + objective1.removeScore(packet.getEntry()); } - break; - } + } + break; + } + + // ScoreboardUpdater will handle it for us if the packets per second + // (for score and team packets) is higher then the first threshold + if (pps < ScoreboardUpdater.FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) { scoreboard.onUpdate(); - } catch (Exception ex) { - ex.printStackTrace(); } } } 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 556d0eab3..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 @@ -25,41 +25,53 @@ package org.geysermc.connector.network.translators.java.world; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerChunkDataPacket; import com.nukkitx.nbt.NBTOutputStream; import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtUtils; import com.nukkitx.network.VarInts; import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; -import it.unimi.dsi.fastutil.objects.Object2IntMap; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.BiomeTranslator; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.utils.ChunkUtils; import org.geysermc.connector.network.translators.world.chunk.ChunkSection; +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; @@ -74,7 +86,12 @@ public class JavaChunkDataTranslator extends PacketTranslator blockEntityEntry : chunkData.getLoadBlockEntitiesLater().object2IntEntrySet()) { - int x = blockEntityEntry.getKey().getInt("x"); - int y = blockEntityEntry.getKey().getInt("y"); - int z = blockEntityEntry.getKey().getInt("z"); - ChunkUtils.updateBlock(session, blockEntityEntry.getIntValue(), new Position(x, y, z)); - } - chunkData.getLoadBlockEntitiesLater().clear(); session.getChunkCache().addToCache(packet.getColumn()); } catch (Exception ex) { ex.printStackTrace(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaCollectItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaCollectItemTranslator.java index 31379bd2d..a90c7016b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaCollectItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaCollectItemTranslator.java @@ -41,6 +41,7 @@ public class JavaCollectItemTranslator extends PacketTranslator { @@ -141,6 +142,11 @@ public class JavaNotifyClientTranslator extends PacketTranslator { @@ -41,6 +42,7 @@ public class JavaSpawnPositionTranslator extends PacketTranslator= 2 && + session.getGameMode() == GameMode.CREATIVE && packet.getNbt().size() > 5) { + ContainerOpenPacket openPacket = new ContainerOpenPacket(); + openPacket.setBlockPosition(Vector3i.from(packet.getPosition().getX(), packet.getPosition().getY(), packet.getPosition().getZ())); + openPacket.setId((byte) 1); + openPacket.setType(ContainerType.COMMAND_BLOCK); + openPacket.setUniqueEntityId(-1); + session.sendUpstreamPacket(openPacket); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/sound/SoundHandlerRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/sound/SoundHandlerRegistry.java index 893975e5e..163c451cb 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/sound/SoundHandlerRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/sound/SoundHandlerRegistry.java @@ -25,6 +25,8 @@ package org.geysermc.connector.network.translators.sound; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.utils.FileUtils; import org.reflections.Reflections; import java.util.HashMap; @@ -38,7 +40,7 @@ public class SoundHandlerRegistry { static final Map> INTERACTION_HANDLERS = new HashMap<>(); static { - Reflections ref = new Reflections("org.geysermc.connector.network.translators.sound"); + Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.sound") : new Reflections("org.geysermc.connector.network.translators.sound"); for (Class clazz : ref.getTypesAnnotatedWith(SoundHandler.class)) { try { SoundInteractionHandler interactionHandler = (SoundInteractionHandler) clazz.newInstance(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java index 83f1a7783..46aa6f7f6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java @@ -32,6 +32,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.ClientChatPacket; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.world.chunk.ChunkPosition; import org.geysermc.connector.utils.GameRule; public class GeyserWorldManager extends WorldManager { @@ -43,6 +44,13 @@ public class GeyserWorldManager extends WorldManager { return session.getChunkCache().getBlockAt(new Position(x, y, z)); } + @Override + public int[] getBiomeDataAt(GeyserSession session, int x, int z) { + if (!session.getConnector().getConfig().isCacheChunks()) + return new int[1024]; + return session.getChunkCache().getChunks().get(new ChunkPosition(x, z)).getBiomeData(); + } + @Override public void setGameRule(GeyserSession session, String name, Object value) { session.sendDownstreamPacket(new ClientChatPacket("/gamerule " + name + " " + value)); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java index 326012277..ba78b8f6f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java @@ -74,6 +74,16 @@ public abstract class WorldManager { */ public abstract int getBlockAt(GeyserSession session, int x, int y, int z); + /** + * Gets the biome data for the specified chunk. + * + * @param session the session of the player + * @param x the chunk's X coordinate + * @param z the chunk's Z coordinate + * @return the biome data for the specified region with a length of 1024. + */ + public abstract int[] getBiomeDataAt(GeyserSession session, int x, int z); + /** * Updates a gamerule value on the Java server * diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java index 53607317a..305118e6f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java @@ -39,6 +39,7 @@ public class BlockStateValues { private static final Int2IntMap BANNER_COLORS = new Int2IntOpenHashMap(); private static final Int2ByteMap BED_COLORS = new Int2ByteOpenHashMap(); + private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap(); private static final Int2ObjectMap DOUBLE_CHEST_VALUES = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap FLOWER_POT_VALUES = new Int2ObjectOpenHashMap<>(); private static final Map FLOWER_POT_BLOCKS = new HashMap<>(); @@ -67,6 +68,11 @@ public class BlockStateValues { return; } + if (entry.getKey().contains("command_block")) { + COMMAND_BLOCK_VALUES.put(javaBlockState, entry.getKey().contains("conditional=true") ? (byte) 1 : (byte) 0); + return; + } + if (entry.getValue().get("double_chest_position") != null) { boolean isX = (entry.getValue().get("x") != null); boolean isDirectionPositive = ((entry.getValue().get("x") != null && entry.getValue().get("x").asBoolean()) || @@ -138,6 +144,16 @@ public class BlockStateValues { return -1; } + /** + * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags + * in Bedrock need the conditional information. + * + * @return the list of all command blocks and if they are conditional (1 or 0) + */ + public static Int2ByteMap getCommandBlockValues() { + return COMMAND_BLOCK_VALUES; + } + /** * All double chest values are part of the block state in Java and part of the block entity tag in Bedrock. * This gives the DoubleChestValue that can be calculated into the final tag. diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java index e627b8454..fc7c852cb 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java @@ -28,15 +28,12 @@ package org.geysermc.connector.network.translators.world.block; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import com.nukkitx.nbt.NBTInputStream; -import com.nukkitx.nbt.NbtList; -import com.nukkitx.nbt.NbtMap; -import com.nukkitx.nbt.NbtMapBuilder; -import com.nukkitx.nbt.NbtType; -import com.nukkitx.nbt.NbtUtils; +import com.nukkitx.nbt.*; import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.translators.world.block.entity.BlockEntity; import org.geysermc.connector.utils.FileUtils; @@ -52,6 +49,11 @@ public class BlockTranslator { private static final Int2IntMap JAVA_TO_BEDROCK_BLOCK_MAP = new Int2IntOpenHashMap(); private static final Int2IntMap BEDROCK_TO_JAVA_BLOCK_MAP = new Int2IntOpenHashMap(); + /** + * Stores a list of differences in block identifiers. + * Items will not be added to this list if the key and value is the same. + */ + private static final Object2ObjectMap JAVA_TO_BEDROCK_IDENTIFIERS = new Object2ObjectOpenHashMap<>(); private static final BiMap JAVA_ID_BLOCK_MAP = HashBiMap.create(); private static final IntSet WATERLOGGED = new IntOpenHashSet(); private static final Object2IntMap ITEM_FRAMES = new Object2IntOpenHashMap<>(); @@ -65,6 +67,11 @@ public class BlockTranslator { public static final Int2BooleanMap JAVA_RUNTIME_ID_TO_CAN_HARVEST_WITH_HAND = new Int2BooleanOpenHashMap(); public static final Int2ObjectMap JAVA_RUNTIME_ID_TO_TOOL_TYPE = new Int2ObjectOpenHashMap<>(); + /** + * Runtime command block ID, used for fixing command block minecart appearances + */ + public static final int BEDROCK_RUNTIME_COMMAND_BLOCK_ID; + // For block breaking animation math public static final IntSet JAVA_RUNTIME_WOOL_IDS = new IntOpenHashSet(); public static final int JAVA_RUNTIME_COBWEB_ID; @@ -106,13 +113,14 @@ public class BlockTranslator { addedStatesMap.defaultReturnValue(-1); List paletteList = new ArrayList<>(); - Reflections ref = new Reflections("org.geysermc.connector.network.translators.world.block.entity"); + Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity"); ref.getTypesAnnotatedWith(BlockEntity.class); int waterRuntimeId = -1; int javaRuntimeId = -1; int bedrockRuntimeId = 0; int cobwebRuntimeId = -1; + int commandBlockRuntimeId = -1; int furnaceRuntimeId = -1; int furnaceLitRuntimeId = -1; int spawnerRuntimeId = -1; @@ -140,23 +148,15 @@ public class BlockTranslator { JAVA_RUNTIME_ID_TO_TOOL_TYPE.put(javaRuntimeId, toolTypeNode.textValue()); } - if (javaId.contains("wool")) { - JAVA_RUNTIME_WOOL_IDS.add(javaRuntimeId); - } - - if (javaId.contains("cobweb")) { - cobwebRuntimeId = javaRuntimeId; - } - JAVA_ID_BLOCK_MAP.put(javaId, javaRuntimeId); // Used for adding all "special" Java block states to block state map String identifier; - String bedrock_identifer = entry.getValue().get("bedrock_identifier").asText(); + String bedrockIdentifier = entry.getValue().get("bedrock_identifier").asText(); for (Class clazz : ref.getTypesAnnotatedWith(BlockEntity.class)) { identifier = clazz.getAnnotation(BlockEntity.class).regex(); // Endswith, or else the block bedrock gets picked up for bed - if (bedrock_identifer.endsWith(identifier) && !identifier.equals("")) { + if (bedrockIdentifier.endsWith(identifier) && !identifier.equals("")) { JAVA_ID_TO_BLOCK_ENTITY_MAP.put(javaRuntimeId, clazz.getAnnotation(BlockEntity.class).name()); break; } @@ -164,9 +164,15 @@ public class BlockTranslator { BlockStateValues.storeBlockStateValues(entry, javaRuntimeId); + String cleanJavaIdentifier = entry.getKey().split("\\[")[0]; + + if (!cleanJavaIdentifier.equals(bedrockIdentifier)) { + JAVA_TO_BEDROCK_IDENTIFIERS.put(cleanJavaIdentifier, bedrockIdentifier); + } + // Get the tag needed for non-empty flower pots if (entry.getValue().get("pottable") != null) { - BlockStateValues.getFlowerPotBlocks().put(entry.getKey().split("\\[")[0], buildBedrockState(entry.getValue())); + BlockStateValues.getFlowerPotBlocks().put(cleanJavaIdentifier, buildBedrockState(entry.getValue())); } if ("minecraft:water[level=0]".equals(javaId)) { @@ -197,15 +203,23 @@ public class BlockTranslator { } JAVA_TO_BEDROCK_BLOCK_MAP.put(javaRuntimeId, bedrockRuntimeId); - if (javaId.startsWith("minecraft:furnace[facing=north")) { + if (javaId.contains("wool")) { + JAVA_RUNTIME_WOOL_IDS.add(javaRuntimeId); + + } else if (javaId.contains("cobweb")) { + cobwebRuntimeId = javaRuntimeId; + + } else if (javaId.equals("minecraft:command_block[conditional=false,facing=north]")) { + commandBlockRuntimeId = bedrockRuntimeId; + + } else if (javaId.startsWith("minecraft:furnace[facing=north")) { if (javaId.contains("lit=true")) { furnaceLitRuntimeId = javaRuntimeId; } else { furnaceRuntimeId = javaRuntimeId; } - } - if (javaId.startsWith("minecraft:spawner")) { + } else if (javaId.startsWith("minecraft:spawner")) { spawnerRuntimeId = javaRuntimeId; } @@ -217,6 +231,11 @@ public class BlockTranslator { } JAVA_RUNTIME_COBWEB_ID = cobwebRuntimeId; + if (commandBlockRuntimeId == -1) { + throw new AssertionError("Unable to find command block in palette"); + } + BEDROCK_RUNTIME_COMMAND_BLOCK_ID = commandBlockRuntimeId; + if (furnaceRuntimeId == -1) { throw new AssertionError("Unable to find furnace in palette"); } @@ -297,6 +316,14 @@ public class BlockTranslator { return BEDROCK_TO_JAVA_BLOCK_MAP.get(bedrockId); } + /** + * @param javaIdentifier the Java identifier of the block to search for + * @return the Bedrock identifier if different, or else the Java identifier + */ + public static String getBedrockBlockIdentifier(String javaIdentifier) { + return JAVA_TO_BEDROCK_IDENTIFIERS.getOrDefault(javaIdentifier, javaIdentifier); + } + public static int getItemFrame(NbtMap tag) { return ITEM_FRAMES.getOrDefault(tag, -1); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java index 9e86cb4cf..57393a6c5 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BannerBlockEntityTranslator.java @@ -27,12 +27,9 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; -import com.nukkitx.nbt.NbtMap; -import com.nukkitx.nbt.NbtType; import org.geysermc.connector.network.translators.item.translators.BannerTranslator; import org.geysermc.connector.network.translators.world.block.BlockStateValues; -import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -65,17 +62,4 @@ public class BannerBlockEntityTranslator extends BlockEntityTranslator implement return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - CompoundTag tag = getConstantJavaTag(javaId, x, y, z); - tag.put(new ListTag("Patterns")); - return tag; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z).toBuilder() - .putList("Patterns", NbtType.COMPOUND, new ArrayList<>()) - .build(); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java index b84aad984..080bdc3b2 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BedBlockEntityTranslator.java @@ -26,7 +26,6 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.nukkitx.nbt.NbtMap; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import java.util.HashMap; @@ -50,15 +49,4 @@ public class BedBlockEntityTranslator extends BlockEntityTranslator implements R return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - return null; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z).toBuilder() - .putByte("color", (byte) 0) - .build(); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java index c4401c4c8..54f593a61 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java @@ -28,12 +28,13 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.IntTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; - import it.unimi.dsi.fastutil.objects.ObjectArrayList; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.utils.BlockEntityUtils; +import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; import org.reflections.Reflections; @@ -52,6 +53,7 @@ public abstract class BlockEntityTranslator { { // Bedrock/Java differences put("minecraft:enchanting_table", "EnchantTable"); + put("minecraft:jigsaw", "JigsawBlock"); put("minecraft:piston_head", "PistonArm"); put("minecraft: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 @@ -66,7 +68,7 @@ public abstract class BlockEntityTranslator { } static { - Reflections ref = new Reflections("org.geysermc.connector.network.translators.world.block.entity"); + Reflections ref = GeyserConnector.getInstance().isProduction() ? FileUtils.getReflections("org.geysermc.connector.network.translators.world.block.entity") : new Reflections("org.geysermc.connector.network.translators.world.block.entity"); for (Class clazz : ref.getTypesAnnotatedWith(BlockEntity.class)) { GeyserConnector.getInstance().getLogger().debug("Found annotated block entity: " + clazz.getCanonicalName()); @@ -89,10 +91,6 @@ public abstract class BlockEntityTranslator { public abstract Map translateTag(CompoundTag tag, int blockState); - public abstract CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z); - - public abstract NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z); - public NbtMap getBlockEntityTag(String id, CompoundTag tag, int blockState) { int x = Integer.parseInt(String.valueOf(tag.getValue().get("x").getValue())); int y = Integer.parseInt(String.valueOf(tag.getValue().get("y").getValue())); @@ -123,7 +121,7 @@ public abstract class BlockEntityTranslator { } @SuppressWarnings("unchecked") - protected T getOrDefault(com.github.steveice10.opennbt.tag.builtin.Tag tag, T defaultValue) { + protected T getOrDefault(Tag tag, T defaultValue) { return (tag != null && tag.getValue() != null) ? (T) tag.getValue() : defaultValue; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java index e3d2c9f5e..d6ac0281b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java @@ -50,18 +50,6 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator { return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - CompoundTag tag = getConstantJavaTag(javaId, x, y, z); - tag.put(new ListTag("Items")); - return tag; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z); - } - protected NbtMap getItem(CompoundTag tag) { ItemEntry entry = ItemRegistry.getItemEntry((String) tag.get("id").getValue()); NbtMapBuilder tagBuilder = NbtMap.builder() diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CommandBlockBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CommandBlockBlockEntityTranslator.java new file mode 100644 index 000000000..6bc940adb --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CommandBlockBlockEntityTranslator.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2020 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.block.entity; + +import com.github.steveice10.opennbt.tag.builtin.*; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; +import org.geysermc.connector.utils.MessageUtils; + +import java.util.HashMap; +import java.util.Map; + +@BlockEntity(name = "CommandBlock", regex = "command_block") +public class CommandBlockBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState { + + @Override + public Map translateTag(CompoundTag tag, int blockState) { + Map map = new HashMap<>(); + if (tag.size() < 5) { + return map; // These values aren't here + } + // Java infers from the block state, but Bedrock needs it in the tag + map.put("conditionalMode", BlockStateValues.getCommandBlockValues().getOrDefault(blockState, (byte) 0)); + // Java and Bedrock values + map.put("conditionMet", ((ByteTag) tag.get("conditionMet")).getValue()); + map.put("auto", ((ByteTag) tag.get("auto")).getValue()); + map.put("CustomName", MessageUtils.getBedrockMessage(((StringTag) tag.get("CustomName")).getValue())); + map.put("powered", ((ByteTag) tag.get("powered")).getValue()); + map.put("Command", ((StringTag) tag.get("Command")).getValue()); + map.put("SuccessCount", ((IntTag) tag.get("SuccessCount")).getValue()); + map.put("TrackOutput", ((ByteTag) tag.get("TrackOutput")).getValue()); + map.put("UpdateLastExecution", ((ByteTag) tag.get("UpdateLastExecution")).getValue()); + if (tag.get("LastExecution") != null) { + map.put("LastExecution", ((LongTag) tag.get("LastExecution")).getValue()); + } else { + map.put("LastExecution", (long) 0); + } + return map; + } + + @Override + public boolean isBlock(int blockState) { + return BlockStateValues.getCommandBlockValues().containsKey(blockState); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java index fa8bab3b0..5b59420e0 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/DoubleChestBlockEntityTranslator.java @@ -27,7 +27,6 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3i; -import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockStateValues; @@ -92,13 +91,4 @@ public class DoubleChestBlockEntityTranslator extends BlockEntityTranslator impl return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - return null; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return null; - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java index 6de136119..e9715bd32 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EmptyBlockEntityTranslator.java @@ -26,7 +26,6 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.nukkitx.nbt.NbtMap; import java.util.HashMap; import java.util.Map; @@ -39,13 +38,4 @@ public class EmptyBlockEntityTranslator extends BlockEntityTranslator { return new HashMap<>(); } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - return getConstantJavaTag(javaId, x, y, z); - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java index 784afed5b..af94c560d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/EndGatewayBlockEntityTranslator.java @@ -26,14 +26,12 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.LongTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; import com.nukkitx.nbt.NbtList; -import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtType; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; -import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -56,25 +54,11 @@ public class EndGatewayBlockEntityTranslator extends BlockEntityTranslator { return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - CompoundTag tag = getConstantJavaTag(javaId, x, y, z); - tag.put(new LongTag("Age")); - return tag; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z).toBuilder() - .putList("ExitPortal", NbtType.INT, Arrays.asList(0, 0, 0)) - .build(); - } - private int getExitPortalCoordinate(CompoundTag tag, String axis) { // Return 0 if it doesn't exist, otherwise give proper value if (tag.get("ExitPortal") != null) { LinkedHashMap compoundTag = (LinkedHashMap) tag.get("ExitPortal").getValue(); - com.github.steveice10.opennbt.tag.builtin.IntTag intTag = (com.github.steveice10.opennbt.tag.builtin.IntTag) compoundTag.get(axis); + IntTag intTag = (IntTag) compoundTag.get(axis); return intTag.getValue(); } return 0; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/JigsawBlockBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/JigsawBlockBlockEntityTranslator.java new file mode 100644 index 000000000..43ac1a96f --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/JigsawBlockBlockEntityTranslator.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2020 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.block.entity; + +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; + +import java.util.HashMap; +import java.util.Map; + +@BlockEntity(name = "JigsawBlock", regex = "jigsaw") +public class JigsawBlockBlockEntityTranslator extends BlockEntityTranslator { + + @Override + public Map translateTag(CompoundTag tag, int blockState) { + Map map = new HashMap<>(); + map.put("joint", ((StringTag) tag.get("joint")).getValue()); + map.put("name", ((StringTag) tag.get("name")).getValue()); + map.put("target_pool", ((StringTag) tag.get("pool")).getValue()); + map.put("final_state", ((StringTag) tag.get("final_state")).getValue()); + map.put("target", ((StringTag) tag.get("target")).getValue()); + return map; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java index 08a7ae187..08e3abaab 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/ShulkerBoxBlockEntityTranslator.java @@ -26,7 +26,6 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.nukkitx.nbt.NbtMap; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import java.util.HashMap; @@ -46,15 +45,4 @@ public class ShulkerBoxBlockEntityTranslator extends BlockEntityTranslator { return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - return null; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z).toBuilder() - .putByte("facing", (byte) 1) - .build(); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java index a95c853e7..b40ed42c9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java @@ -27,8 +27,8 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.mc.protocol.data.message.MessageSerializer; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.nukkitx.nbt.NbtMap; import org.geysermc.connector.utils.MessageUtils; +import org.geysermc.connector.utils.SignUtils; import java.util.HashMap; import java.util.Map; @@ -46,12 +46,25 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator { String signLine = getOrDefault(tag.getValue().get("Text" + currentLine), ""); signLine = MessageUtils.getBedrockMessage(MessageSerializer.fromString(signLine)); - //Java allows up to 16+ characters on certain symbols. - if(signLine.length() >= 15 && (signLine.contains("-") || signLine.contains("="))) { - signLine = signLine.substring(0, 14); + // Check the character width on the sign to ensure there is no overflow that is usually hidden + // to Java Edition clients but will appear to Bedrock clients + int signWidth = 0; + StringBuilder finalSignLine = new StringBuilder(); + for (char c : signLine.toCharArray()) { + signWidth += SignUtils.getCharacterWidth(c); + if (signWidth <= SignUtils.BEDROCK_CHARACTER_WIDTH_MAX) { + finalSignLine.append(c); + } else { + break; + } } - signText.append(signLine); + // Java Edition 1.14 added the ability to change the text color of the whole sign using dye + if (tag.contains("Color")) { + signText.append(getBedrockSignColor(tag.get("Color").getValue().toString())); + } + + signText.append(finalSignLine.toString()); signText.append("\n"); } @@ -59,20 +72,65 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator { return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - CompoundTag tag = getConstantJavaTag(javaId, x, y, z); - tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text1", "{\"text\":\"\"}")); - tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text2", "{\"text\":\"\"}")); - tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text3", "{\"text\":\"\"}")); - tag.put(new com.github.steveice10.opennbt.tag.builtin.StringTag("Text4", "{\"text\":\"\"}")); - return tag; + /** + * Maps a color stored in a sign's Color tag to a Bedrock Edition formatting code. + *
+ * The color names correspond to dye names, because of this we can't use {@link MessageUtils#getColor(String)}. + * + * @param javaColor The dye color stored in the sign's Color tag. + * @return A Bedrock Edition formatting code for valid dye colors, otherwise an empty string. + */ + private static String getBedrockSignColor(String javaColor) { + String base = "\u00a7"; + switch (javaColor) { + case "white": + base += 'f'; + break; + case "orange": + base += '6'; + break; + case "magenta": + case "purple": + base += '5'; + break; + case "light_blue": + base += 'b'; + break; + case "yellow": + base += 'e'; + break; + case "lime": + base += 'a'; + break; + case "pink": + base += 'd'; + break; + case "gray": + base += '8'; + break; + case "light_gray": + base += '7'; + break; + case "cyan": + base += '3'; + break; + case "blue": + base += '9'; + break; + case "brown": // Brown does not have a bedrock counterpart. + case "red": // In Java Edition light red (&c) can only be applied using commands. Red dye gives &4. + base += '4'; + break; + case "green": + base += '2'; + break; + case "black": + base += '0'; + break; + default: + return ""; + } + return base; } - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z).toBuilder() - .putString("Text", "") - .build(); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java index 9547ba2ff..6d350c0cc 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SkullBlockEntityTranslator.java @@ -25,7 +25,6 @@ package org.geysermc.connector.network.translators.world.block.entity; -import com.nukkitx.nbt.NbtMap; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import java.util.HashMap; @@ -51,16 +50,4 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements return tags; } - @Override - public com.github.steveice10.opennbt.tag.builtin.CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - return null; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z).toBuilder() - .putFloat("Rotation", 0f) - .putByte("SkullType", (byte) 0) - .build(); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java index 3c443eeeb..2601e3de9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SpawnerBlockEntityTranslator.java @@ -26,7 +26,6 @@ package org.geysermc.connector.network.translators.world.block.entity; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.nukkitx.nbt.NbtMap; import org.geysermc.connector.entity.type.EntityType; import java.util.HashMap; @@ -86,16 +85,4 @@ public class SpawnerBlockEntityTranslator extends BlockEntityTranslator { return tags; } - @Override - public CompoundTag getDefaultJavaTag(String javaId, int x, int y, int z) { - return null; - } - - @Override - public NbtMap getDefaultBedrockTag(String bedrockId, int x, int y, int z) { - return getConstantBedrockTag(bedrockId, x, y, z).toBuilder() - .putByte("isMovable", (byte) 1) - .putString("id", "MobSpawner") - .build(); - } } diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java index c3e6c863c..92a1add34 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Objective.java @@ -29,23 +29,23 @@ import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition import lombok.Getter; import lombok.Setter; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Getter public class Objective { - private Scoreboard scoreboard; - private long id; - private boolean temp; + private final Scoreboard scoreboard; + private final long id; + private boolean active = true; @Setter private UpdateType updateType = UpdateType.ADD; private String objectiveName; - private String displaySlot; + private String displaySlotName; private String displayName = "unknown"; private int type = 0; // 0 = integer, 1 = heart - private Map scores = new HashMap<>(); + private Map scores = new ConcurrentHashMap<>(); private Objective(Scoreboard scoreboard) { this.id = scoreboard.getNextId().getAndIncrement(); @@ -54,23 +54,20 @@ public class Objective { /** * /!\ This method is made for temporary objectives until the real objective is received - * @param scoreboard the scoreboard + * + * @param scoreboard the scoreboard * @param objectiveName the name of the objective */ public Objective(Scoreboard scoreboard, String objectiveName) { this(scoreboard); this.objectiveName = objectiveName; - this.temp = true; + this.active = false; } public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) { - this(scoreboard, objectiveName, displaySlot.name().toLowerCase(), displayName, type); - } - - public Objective(Scoreboard scoreboard, String objectiveName, String displaySlot, String displayName, int type) { this(scoreboard); this.objectiveName = objectiveName; - this.displaySlot = displaySlot; + this.displaySlotName = translateDisplaySlot(displaySlot); this.displayName = displayName; this.type = type; } @@ -87,21 +84,9 @@ public class Objective { public void setScore(String id, int score) { if (scores.containsKey(id)) { scores.get(id).setScore(score).setUpdateType(UpdateType.ADD); - } else { - registerScore(id, score); + return; } - } - - public void setScoreText(String oldText, String newText) { - if (!scores.containsKey(oldText) || oldText.equals(newText)) return; - Score oldScore = scores.get(oldText); - - Score newScore = new Score(this, newText) - .setScore(oldScore.getScore()) - .setTeam(scoreboard.getTeamFor(newText)); - - scores.put(newText, newScore); - oldScore.setUpdateType(UpdateType.REMOVE); + registerScore(id, score); } public int getScore(String id) { @@ -113,37 +98,61 @@ public class Objective { public Score getScore(int line) { for (Score score : scores.values()) { - if (score.getScore() == line) return score; + if (score.getScore() == line) { + return score; + } } return null; } - public void resetScore(String id) { + public void removeScore(String id) { if (scores.containsKey(id)) { scores.get(id).setUpdateType(UpdateType.REMOVE); } } - public void removeScore(String id) { + /** + * Used internally to remove a score from the score map + */ + public void removeScore0(String id) { scores.remove(id); } public Objective setDisplayName(String displayName) { this.displayName = displayName; - if (updateType == UpdateType.NOTHING) updateType = UpdateType.UPDATE; + if (updateType == UpdateType.NOTHING) { + updateType = UpdateType.UPDATE; + } return this; } public Objective setType(int type) { this.type = type; - if (updateType == UpdateType.NOTHING) updateType = UpdateType.UPDATE; + if (updateType == UpdateType.NOTHING) { + updateType = UpdateType.UPDATE; + } return this; } - public void removeTemp(ScoreboardPosition displaySlot) { - if (temp) { - temp = false; - this.displaySlot = displaySlot.name().toLowerCase(); + public void setActive(ScoreboardPosition displaySlot) { + if (!active) { + active = true; + displaySlotName = translateDisplaySlot(displaySlot); + } + } + + public void removed() { + scores = null; + } + + private static String translateDisplaySlot(ScoreboardPosition displaySlot) { + switch (displaySlot) { + case BELOW_NAME: + return "belowname"; + case PLAYER_LIST: + return "list"; + default: + return "sidebar"; } } } diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java index d5a65e8c0..635bafa3d 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Score.java @@ -25,40 +25,63 @@ package org.geysermc.connector.scoreboard; +import com.nukkitx.protocol.bedrock.data.ScoreInfo; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; -@Getter @Setter +@Getter @Accessors(chain = true) public class Score { - private Objective objective; - private long id; + private final Objective objective; + private ScoreInfo cachedInfo; + private final long id; + @Setter private UpdateType updateType = UpdateType.ADD; - private String name; + private final String name; private Team team; private int score; + @Setter private int oldScore = Integer.MIN_VALUE; public Score(Objective objective, String name) { this.id = objective.getScoreboard().getNextId().getAndIncrement(); this.objective = objective; this.name = name; + update(); } public String getDisplayName() { - if (team != null && team.getUpdateType() != UpdateType.REMOVE) { + if (team != null) { return team.getPrefix() + name + team.getSuffix(); } return name; } public Score setScore(int score) { - if (oldScore == Integer.MIN_VALUE) { - this.oldScore = score; - } this.score = score; + updateType = UpdateType.UPDATE; return this; } + + public Score setTeam(Team team) { + if (this.team != null && team != null) { + if (!this.team.equals(team)) { + this.team = team; + updateType = UpdateType.UPDATE; + } + return this; + } + // simplified from (this.team != null && team == null) || (this.team == null && team != null) + if (this.team != null || team != null) { + this.team = team; + updateType = UpdateType.UPDATE; + } + return this; + } + + public void update() { + cachedInfo = new ScoreInfo(id, objective.getObjectiveName(), score, getDisplayName()); + } } diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java index 5fdda617f..9f89d9d2b 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java @@ -26,73 +26,72 @@ package org.geysermc.connector.scoreboard; import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition; +import com.nukkitx.protocol.bedrock.BedrockPacket; import com.nukkitx.protocol.bedrock.data.ScoreInfo; import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket; import com.nukkitx.protocol.bedrock.packet.SetDisplayObjectivePacket; import com.nukkitx.protocol.bedrock.packet.SetScorePacket; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; - +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.GeyserLogger; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import static org.geysermc.connector.scoreboard.UpdateType.*; @Getter public class Scoreboard { - private GeyserSession session; - private AtomicLong nextId = new AtomicLong(0); + private final GeyserSession session; + private final GeyserLogger logger; + private final AtomicLong nextId = new AtomicLong(0); - private Map objectives = new HashMap<>(); - private Map teams = new HashMap<>(); + private final Map objectives = new ConcurrentHashMap<>(); + private final Map teams = new HashMap<>(); + + private int lastScoreCount = 0; public Scoreboard(GeyserSession session) { this.session = session; + this.logger = GeyserConnector.getInstance().getLogger(); } - public Objective registerNewObjective(String objectiveId, boolean temp) { - if (!temp || objectives.containsKey(objectiveId)) return objectives.get(objectiveId); + public Objective registerNewObjective(String objectiveId, boolean active) { + if (active || objectives.containsKey(objectiveId)) { + return objectives.get(objectiveId); + } Objective objective = new Objective(this, objectiveId); objectives.put(objectiveId, objective); return objective; } public Objective registerNewObjective(String objectiveId, ScoreboardPosition displaySlot) { - Objective objective = null; - if (objectives.containsKey(objectiveId)) { - objective = objectives.get(objectiveId); - if (objective.isTemp()) objective.removeTemp(displaySlot); - else { - despawnObjective(objective); - objective = null; + Objective objective = objectives.get(objectiveId); + if (objective != null) { + if (!objective.isActive()) { + objective.setActive(displaySlot); + return objective; } + despawnObjective(objective); } - if (objective == null) { - objective = new Objective(this, objectiveId, displaySlot, "unknown", 0); - objectives.put(objectiveId, objective); - } + + objective = new Objective(this, objectiveId, displaySlot, "unknown", 0); + objectives.put(objectiveId, objective); return objective; } public Team registerNewTeam(String teamName, Set players) { - if (teams.containsKey(teamName)) { - session.getConnector().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName)); - return getTeam(teamName); + Team team = teams.get(teamName); + if (team != null) { + logger.info(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName)); + return team; } - Team team = new Team(this, teamName).setEntities(players); + team = new Team(this, teamName).setEntities(players); teams.put(teamName, team); - - for (Objective objective : objectives.values()) { - for (Score score : objective.getScores().values()) { - if (players.contains(score.getName())) { - score.setTeam(team); - } - } - } return team; } @@ -106,102 +105,119 @@ public class Scoreboard { public void unregisterObjective(String objectiveName) { Objective objective = getObjective(objectiveName); - if (objective != null) objective.setUpdateType(REMOVE); + if (objective != null) { + objective.setUpdateType(REMOVE); + } } public void removeTeam(String teamName) { Team remove = teams.remove(teamName); - if (remove != null) remove.setUpdateType(REMOVE); + if (remove != null) { + remove.setUpdateType(REMOVE); + } } public void onUpdate() { - Set changedObjectives = new ObjectOpenHashSet<>(); - List addScores = new ArrayList<>(); - List removeScores = new ArrayList<>(); + List addScores = new ArrayList<>(getLastScoreCount()); + List removeScores = new ArrayList<>(getLastScoreCount()); - for (String objectiveId : new ArrayList<>(objectives.keySet())) { - Objective objective = objectives.get(objectiveId); - if (objective.isTemp()) { - session.getConnector().getLogger().debug("Ignoring temp Scoreboard Objective '"+ objectiveId +'\''); + for (Objective objective : objectives.values()) { + if (!objective.isActive()) { + logger.debug("Ignoring non-active Scoreboard Objective '"+ objective.getObjectiveName() +'\''); continue; } - if (objective.getUpdateType() != NOTHING) changedObjectives.add(objective); + // hearts can't hold teams, so we treat them differently + if (objective.getType() == 1) { + for (Score score : objective.getScores().values()) { + if (score.getUpdateType() == NOTHING) { + continue; + } + + boolean update = score.getUpdateType() == UPDATE; + if (update) { + score.update(); + } + + if (score.getUpdateType() == ADD || update) { + addScores.add(score.getCachedInfo()); + } + if (score.getUpdateType() == REMOVE || update) { + removeScores.add(score.getCachedInfo()); + } + } + continue; + } boolean globalUpdate = objective.getUpdateType() == UPDATE; - boolean globalAdd = objective.getUpdateType() == ADD || globalUpdate; - boolean globalRemove = objective.getUpdateType() == REMOVE || globalUpdate; + boolean globalAdd = objective.getUpdateType() == ADD; + boolean globalRemove = objective.getUpdateType() == REMOVE; - boolean hasUpdate = globalUpdate; - - List handledScores = new ArrayList<>(); - for (String identifier : new ObjectOpenHashSet<>(objective.getScores().keySet())) { - Score score = objective.getScores().get(identifier); + for (Score score : objective.getScores().values()) { Team team = score.getTeam(); - boolean inTeam = team != null && team.getEntities().contains(score.getName()); + boolean add = globalAdd || globalUpdate; + boolean remove = globalRemove; + boolean teamChanged = false; + if (team != null) { + if (team.getUpdateType() == REMOVE || !team.hasEntity(score.getName())) { + score.setTeam(null); + teamChanged = true; + } - boolean teamAdd = team != null && (team.getUpdateType() == ADD || team.getUpdateType() == UPDATE); - boolean teamRemove = team != null && (team.getUpdateType() == REMOVE || team.getUpdateType() == UPDATE); + teamChanged |= team.getUpdateType() == UPDATE; - if (team != null && (team.getUpdateType() == REMOVE || !inTeam)) score.setTeam(null); - - boolean add = (hasUpdate || globalAdd || teamAdd || teamRemove || score.getUpdateType() == ADD || score.getUpdateType() == UPDATE) && (score.getUpdateType() != REMOVE); - boolean remove = hasUpdate || globalRemove || teamAdd || teamRemove || score.getUpdateType() == REMOVE || score.getUpdateType() == UPDATE; - - boolean updated = false; - if (!hasUpdate) { - updated = hasUpdate = add; + add |= team.getUpdateType() == ADD || team.getUpdateType() == UPDATE; + remove |= team.getUpdateType() == REMOVE; } - if (updated) { - for (Score score1 : handledScores) { - ScoreInfo scoreInfo = new ScoreInfo(score1.getId(), score1.getObjective().getObjectiveName(), score1.getScore(), score1.getDisplayName()); - addScores.add(scoreInfo); - removeScores.add(scoreInfo); - } + add |= score.getUpdateType() == ADD || score.getUpdateType() == UPDATE; + remove |= score.getUpdateType() == REMOVE; + if (score.getUpdateType() == REMOVE) { + add = false; + } + + if (score.getUpdateType() == UPDATE || teamChanged) { + score.update(); } if (add) { - addScores.add(new ScoreInfo(score.getId(), score.getObjective().getObjectiveName(), score.getScore(), score.getDisplayName())); + addScores.add(score.getCachedInfo()); } if (remove) { - removeScores.add(new ScoreInfo(score.getId(), score.getObjective().getObjectiveName(), score.getOldScore(), score.getDisplayName())); + removeScores.add(score.getCachedInfo()); } + // score is pending to be updated, so we use the current score as the old score score.setOldScore(score.getScore()); + // score is pending to be removed, so we can remove it from the objective if (score.getUpdateType() == REMOVE) { - objective.removeScore(score.getName()); + objective.removeScore0(score.getName()); } - if (add || remove) { - changedObjectives.add(objective); - } else { // stays the same like before - handledScores.add(score); - } score.setUpdateType(NOTHING); } - } - for (Objective objective : changedObjectives) { - boolean update = objective.getUpdateType() == NOTHING || objective.getUpdateType() == UPDATE; - if (objective.getUpdateType() == REMOVE || update) { + if (globalRemove || globalUpdate) { RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket(); removeObjectivePacket.setObjectiveId(objective.getObjectiveName()); session.sendUpstreamPacket(removeObjectivePacket); if (objective.getUpdateType() == REMOVE) { objectives.remove(objective.getObjectiveName()); // now we can deregister + objective.removed(); } } - if (objective.getUpdateType() == ADD || update) { + + if (globalAdd || globalUpdate) { SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket(); displayObjectivePacket.setObjectiveId(objective.getObjectiveName()); displayObjectivePacket.setDisplayName(objective.getDisplayName()); displayObjectivePacket.setCriteria("dummy"); - displayObjectivePacket.setDisplaySlot(objective.getDisplaySlot()); + displayObjectivePacket.setDisplaySlot(objective.getDisplaySlotName()); displayObjectivePacket.setSortOrder(1); // ?? session.sendUpstreamPacket(displayObjectivePacket); } + objective.setUpdateType(NOTHING); } @@ -218,6 +234,8 @@ public class Scoreboard { setScorePacket.setInfos(addScores); session.sendUpstreamPacket(setScorePacket); } + + lastScoreCount = addScores.size(); } public void despawnObjective(Objective objective) { @@ -234,6 +252,8 @@ public class Scoreboard { 0, "" )); } + + objective.removed(); if (!toRemove.isEmpty()) { SetScorePacket setScorePacket = new SetScorePacket(); diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardUpdater.java b/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardUpdater.java new file mode 100644 index 000000000..3812fb141 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/ScoreboardUpdater.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2019-2020 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.scoreboard; + +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.configuration.GeyserConfiguration; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.cache.WorldCache; +import org.geysermc.connector.utils.LanguageUtils; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class ScoreboardUpdater extends Thread { + public static final int FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD; + public static final int SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD = 250; + + private static final int FIRST_MILLIS_BETWEEN_UPDATES = 250; // 4 updates per second + private static final int SECOND_MILLIS_BETWEEN_UPDATES = 1000 * 3; // 1 update per 3 seconds + + private static final boolean DEBUG_ENABLED; + + private final WorldCache worldCache; + private final GeyserSession session; + + private int millisBetweenUpdates = FIRST_MILLIS_BETWEEN_UPDATES; + private long lastUpdate = System.currentTimeMillis(); + private long lastLog = -1; + + private long lastPacketsPerSecondUpdate = System.currentTimeMillis(); + private final AtomicInteger packetsPerSecond = new AtomicInteger(0); + private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0); + + public ScoreboardUpdater(WorldCache worldCache) { + super("Scoreboard Updater"); + this.worldCache = worldCache; + session = worldCache.getSession(); + } + + @Override + public void run() { + if (!session.isClosed()) { + long currentTime = System.currentTimeMillis(); + + // reset score-packets per second every second + if (currentTime - lastPacketsPerSecondUpdate > 1000) { + lastPacketsPerSecondUpdate = currentTime; + packetsPerSecond.set(pendingPacketsPerSecond.get()); + pendingPacketsPerSecond.set(0); + } + + if (currentTime - lastUpdate > millisBetweenUpdates) { + lastUpdate = currentTime; + + int pps = packetsPerSecond.get(); + if (pps >= FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) { + boolean reachedSecondThreshold = pps >= SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD; + if (reachedSecondThreshold) { + millisBetweenUpdates = SECOND_MILLIS_BETWEEN_UPDATES; + } else { + millisBetweenUpdates = FIRST_MILLIS_BETWEEN_UPDATES; + } + + worldCache.getScoreboard().onUpdate(); + + if (DEBUG_ENABLED && (currentTime - lastLog > 60000)) { // one minute + int threshold = reachedSecondThreshold ? + SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD : + FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD; + + GeyserConnector.getInstance().getLogger().info( + LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached.log", session.getName(), threshold, pps) + + LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached", (millisBetweenUpdates / 1000.0)) + ); + + lastLog = currentTime; + } + } + } + + session.getConnector().getGeneralThreadPool().schedule(this, 50, TimeUnit.MILLISECONDS); + } + } + + public int getPacketsPerSecond() { + return packetsPerSecond.get(); + } + + /** + * Increase the Scoreboard Packets Per Second and return the updated value + */ + public int incrementAndGetPacketsPerSecond() { + return pendingPacketsPerSecond.incrementAndGet(); + } + + static { + GeyserConfiguration config = GeyserConnector.getInstance().getConfig(); + FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD); + DEBUG_ENABLED = config.isDebugMode(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java index c2fcc02c8..a073e2e99 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java @@ -25,6 +25,7 @@ package org.geysermc.connector.scoreboard; +import com.github.steveice10.mc.protocol.data.game.scoreboard.NameTagVisibility; import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; @@ -35,8 +36,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; -@Getter -@Setter +@Getter @Setter @Accessors(chain = true) public class Team { private final Scoreboard scoreboard; @@ -44,6 +44,8 @@ public class Team { private UpdateType updateType = UpdateType.ADD; private String name; + + private NameTagVisibility nameTagVisibility; private String prefix; private TeamColor color; private String suffix; @@ -57,8 +59,7 @@ public class Team { public void addEntities(String... names) { List added = new ArrayList<>(); for (String name : names) { - if (!entities.contains(name)) { - entities.add(name); + if (entities.add(name)) { added.add(name); } } @@ -78,4 +79,35 @@ public class Team { } setUpdateType(UpdateType.UPDATE); } + + public boolean hasEntity(String name) { + return entities.contains(name); + } + + public Team setPrefix(String prefix) { + // replace "null" to an empty string, + // we do this here to improve the performance of Score#getDisplayName + if (prefix.length() == 4 && "null".equals(prefix)) { + this.prefix = ""; + return this; + } + this.prefix = prefix; + return this; + } + + public Team setSuffix(String suffix) { + // replace "null" to an empty string, + // we do this here to improve the performance of Score#getDisplayName + if (suffix.length() == 4 && "null".equals(suffix)) { + this.suffix = ""; + return this; + } + this.suffix = suffix; + return this; + } + + @Override + public int hashCode() { + return id.hashCode(); + } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java index 06b400908..9413bbba5 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -81,7 +81,7 @@ public class ChunkUtils { } } - public static ChunkData translateToBedrock(Column column) { + public static ChunkData translateToBedrock(GeyserSession session, Column column, boolean isNonFullChunk) { ChunkData chunkData = new ChunkData(); Chunk[] chunks = column.getChunks(); chunkData.sections = new ChunkSection[chunks.length]; @@ -97,14 +97,26 @@ public class ChunkUtils { chunkData.sections[chunkY] = new ChunkSection(); Chunk chunk = chunks[chunkY]; - if (chunk == null || chunk.isEmpty()) + // Chunk is null and caching chunks is off or this isn't a non-full chunk + if (chunk == null && (!session.getConnector().getConfig().isCacheChunks() || !isNonFullChunk)) + continue; + + // If chunk is empty then no need to process + if (chunk != null && chunk.isEmpty()) continue; ChunkSection section = chunkData.sections[chunkY]; for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) { - int blockState = chunk.get(x, y, z); + int blockState; + // If a non-full chunk, then grab the block that should be here to create a 'full' chunk + if (chunk == null) { + Position pos = new ChunkPosition(column.getX(), column.getZ()).getBlock(x, (chunkY << 4) + y, z); + blockState = session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()); + } else { + blockState = chunk.get(x, y, z); + } int id = BlockTranslator.getBedrockBlockId(blockState); // Check to see if the name is in BlockTranslator.getBlockEntityString, and therefore must be handled differently @@ -271,7 +283,5 @@ public class ChunkUtils { @Getter private NbtMap[] blockEntities = new NbtMap[0]; - @Getter - private Object2IntMap loadBlockEntitiesLater = new Object2IntOpenHashMap<>(); } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index de09ed8c6..7b283e9cb 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -85,6 +85,10 @@ public class DimensionUtils { stopSoundPacket.setStoppingAllSound(true); stopSoundPacket.setSoundName(""); session.sendUpstreamPacket(stopSoundPacket); + + // TODO - fix this hack of a fix by sending the final dimension switching logic after chunks have been sent. + // The client wants chunks sent to it before it can successfully respawn. + ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); } /** diff --git a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java index 38369d6c8..63255cfa0 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -25,14 +25,22 @@ package org.geysermc.connector.utils; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.geysermc.connector.GeyserConnector; +import org.reflections.Reflections; +import org.reflections.serializers.XmlSerializer; +import org.reflections.util.ConfigurationBuilder; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.security.MessageDigest; import java.util.function.Function; public class FileUtils { @@ -51,6 +59,15 @@ public class FileUtils { return objectMapper.readValue(src, valueType); } + public static T loadYaml(InputStream src, Class valueType) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()).enable(JsonParser.Feature.IGNORE_UNDEFINED).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + return objectMapper.readValue(src, valueType); + } + + public static T loadJson(InputStream src, Class valueType) throws IOException { + return GeyserConnector.JSON_MAPPER.readValue(src, valueType); + } + /** * Open the specified file or copy if from resources * @@ -140,4 +157,38 @@ public class FileUtils { } return stream; } + + /** + * Calculate the SHA256 hash of the resource pack file + * @param file File to calculate the hash for + * @return A byte[] representation of the hash + */ + public static byte[] calculateSHA256(File file) { + byte[] sha256; + + try { + sha256 = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(file.toPath())); + } catch (Exception e) { + throw new RuntimeException("Could not calculate pack hash", e); + } + + return sha256; + } + + /** + * Get the stored reflection data for a given path + * + * @param path The path to get the reflection data for + * @return The created Reflections object + */ + public static Reflections getReflections(String path) { + Reflections reflections = new Reflections(new ConfigurationBuilder()); + XmlSerializer serializer = new XmlSerializer(); + URL resource = FileUtils.class.getClassLoader().getResource("META-INF/reflections/" + path + "-reflections.xml"); + try (InputStream inputStream = resource.openConnection().getInputStream()) { + reflections.merge(serializer.read(inputStream)); + } catch (IOException e) { } + + return reflections; + } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java index cb51e2f3b..c1224e6e2 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java @@ -26,6 +26,9 @@ package org.geysermc.connector.utils; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCreativeInventoryActionPacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientMoveItemToHotbarPacket; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; @@ -33,12 +36,14 @@ import com.nukkitx.nbt.NbtType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; -import org.geysermc.connector.common.ChatColor; +import com.nukkitx.protocol.bedrock.packet.PlayerHotbarPacket; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.ChatColor; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.DoubleChestInventoryTranslator; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; +import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.item.ItemTranslator; @@ -151,4 +156,90 @@ public class InventoryUtils { root.put("display", display.build()); return ItemData.of(ItemRegistry.ITEM_ENTRIES.get(ItemRegistry.BARRIER_INDEX).getBedrockId(), (short) 0, 1, root.build()); } + + /** + * Attempt to find the specified item name in the session's inventory. + * If it is found and in the hotbar, set the user's held item to that slot. + * If it is found in another part of the inventory, move it. + * If it is not found and the user is in creative mode, create the item, + * overriding the current item slot if no other hotbar slots are empty, or otherwise selecting the empty slot. + * + * This attempts to mimic Java Edition behavior as best as it can. + * @param session the Bedrock client's session + * @param itemName the Java identifier of the item to search/select + */ + public static void findOrCreatePickedBlock(GeyserSession session, String itemName) { + // Get the inventory to choose a slot to pick + Inventory inventory = session.getInventoryCache().getOpenInventory(); + if (inventory == null) { + inventory = session.getInventory(); + } + + // Check hotbar for item + for (int i = 36; i < 45; i++) { + if (inventory.getItem(i) == null) { + continue; + } + ItemEntry item = ItemRegistry.getItem(inventory.getItem(i)); + // If this isn't the item we're looking for + if (!item.getJavaIdentifier().equals(itemName)) { + continue; + } + + setHotbarItem(session, i); + // Don't check inventory if item was in hotbar + return; + } + + // Check inventory for item + for (int i = 9; i < 36; i++) { + if (inventory.getItem(i) == null) { + continue; + } + ItemEntry item = ItemRegistry.getItem(inventory.getItem(i)); + // If this isn't the item we're looking for + if (!item.getJavaIdentifier().equals(itemName)) { + continue; + } + + ClientMoveItemToHotbarPacket packetToSend = new ClientMoveItemToHotbarPacket(i); // https://wiki.vg/Protocol#Pick_Item + session.sendDownstreamPacket(packetToSend); + return; + } + + // If we still have not found the item, and we're in creative, ask for the item from the server. + if (session.getGameMode() == GameMode.CREATIVE) { + int slot = session.getInventory().getHeldItemSlot() + 36; + if (session.getInventory().getItemInHand() != null) { // Otherwise we should just use the current slot + for (int i = 36; i < 45; i++) { + if (inventory.getItem(i) == null) { + slot = i; + break; + } + } + } + + ClientCreativeInventoryActionPacket actionPacket = new ClientCreativeInventoryActionPacket(slot, + new ItemStack(ItemRegistry.getItemEntry(itemName).getJavaId())); + if ((slot - 36) != session.getInventory().getHeldItemSlot()) { + setHotbarItem(session, slot); + } + session.sendDownstreamPacket(actionPacket); + } + } + + /** + * Changes the held item slot to the specified slot + * @param session GeyserSession + * @param slot inventory slot to be selected + */ + private static void setHotbarItem(GeyserSession session, int slot) { + PlayerHotbarPacket hotbarPacket = new PlayerHotbarPacket(); + hotbarPacket.setContainerId(0); + // Java inventory slot to hotbar slot ID + hotbarPacket.setSelectedHotbarSlot(slot - 36); + hotbarPacket.setSelectHotbarSlot(true); + session.sendUpstreamPacket(hotbarPacket); + // No need to send a Java packet as Bedrock sends a confirmation packet back that we translate + } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java index 285846a97..d1d59490f 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java @@ -133,7 +133,7 @@ public class LocaleUtils { * @param locale Locale to download */ private static void downloadLocale(String locale) { - File localeFile = Paths.get(GeyserConnector.getInstance().getBootstrap().getConfigFolder().toString(),"locales",locale + ".json").toFile(); + File localeFile = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile(); // Check if we have already downloaded the locale file if (localeFile.exists()) { @@ -150,7 +150,7 @@ public class LocaleUtils { // Get the hash and download the locale String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash(); - WebUtils.downloadFile("http://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString()); + WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString()); } /** @@ -246,6 +246,11 @@ public class LocaleUtils { Map localeStrings = LocaleUtils.LOCALE_MAPPINGS.get(locale.toLowerCase()); if (localeStrings == null) localeStrings = LocaleUtils.LOCALE_MAPPINGS.get(LanguageUtils.getDefaultLocale()); + if (localeStrings == null) { + // Don't cause a NPE if the locale is STILL missing + GeyserConnector.getInstance().getLogger().debug("MISSING DEFAULT LOCALE: " + LanguageUtils.getDefaultLocale()); + return messageText; + } return localeStrings.getOrDefault(messageText, messageText); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java b/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java index 487024922..29dd2cc23 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/MathUtils.java @@ -40,6 +40,26 @@ public class MathUtils { return floatNumber > truncated ? truncated + 1 : truncated; } + /** + * If number is greater than the max, set it to max, and if number is lower than low, set it to low. + * @param num number to calculate + * @param min the lowest value the number can be + * @param max the greatest value the number can be + * @return - min if num is lower than min
+ * - max if num is greater than max
+ * - num otherwise + */ + public static double constrain(double num, double min, double max) { + if (num > max) { + num = max; + } + if (num < min) { + num = min; + } + + return num; + } + /** * Converts the given object from an int or byte to byte. * This is used for NBT data that might be either an int diff --git a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java new file mode 100644 index 000000000..3e9848dbe --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019-2020 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.GeyserConnector; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipFile; + +/** + * This represents a resource pack and all the data relevant to it + */ +public class ResourcePack { + /** + * The list of loaded resource packs + */ + public static final Map PACKS = new HashMap<>(); + + /** + * The size of each chunk to use when sending the resource packs to clients in bytes + */ + public static final int CHUNK_SIZE = 102400; + + private byte[] sha256; + private File file; + private ResourcePackManifest manifest; + private ResourcePackManifest.Version version; + + /** + * Loop through the packs directory and locate valid resource pack files + */ + public static void loadPacks() { + File directory = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("packs").toFile(); + + if (!directory.exists()) { + directory.mkdir(); + + // As we just created the directory it will be empty + return; + } + + for (File file : directory.listFiles()) { + if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) { + ResourcePack pack = new ResourcePack(); + + pack.sha256 = FileUtils.calculateSHA256(file); + + try { + ZipFile zip = new ZipFile(file); + + zip.stream().forEach((x) -> { + if (x.getName().contains("manifest.json")) { + try { + ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class); + + pack.file = file; + pack.manifest = manifest; + pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion()); + + PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } catch (Exception e) { + GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.resource_pack.broken", file.getName())); + e.printStackTrace(); + } + } + } + } + + public byte[] getSha256() { + return sha256; + } + + public File getFile() { + return file; + } + + public ResourcePackManifest getManifest() { + return manifest; + } + + public ResourcePackManifest.Version getVersion() { + return version; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/ResourcePackManifest.java b/connector/src/main/java/org/geysermc/connector/utils/ResourcePackManifest.java new file mode 100644 index 000000000..6a08c4dbc --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePackManifest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2019-2020 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 com.fasterxml.jackson.annotation.JsonProperty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.Value; + +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +/** + * author: NukkitX + * Nukkit Project + */ +@Getter +@EqualsAndHashCode +public class ResourcePackManifest { + @JsonProperty("format_version") + private Integer formatVersion; + private Header header; + private Collection modules; + protected Collection dependencies; + + public Collection getModules() { + return Collections.unmodifiableCollection(modules); + } + + @Getter + @ToString + public static class Header { + private String description; + private String name; + private UUID uuid; + private int[] version; + @JsonProperty("min_engine_version") + private int[] minimumSupportedMinecraftVersion; + + public String getVersionString() { + return version[0] + "." + version[1] + "." + version[2]; + } + } + + @Getter + @ToString + public static class Module { + private String description; + private String name; + private UUID uuid; + private int[] version; + } + + @Getter + @ToString + public static class Dependency { + private UUID uuid; + private int[] version; + } + + @Value + public static class Version { + private final int major; + private final int minor; + private final int patch; + + public static Version fromString(String ver) { + String[] split = ver.replace(']', ' ') + .replace('[', ' ') + .replaceAll(" ", "").split(","); + + return new Version(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); + } + + public static Version fromArray(int[] ver) { + return new Version(ver[0], ver[1], ver[2]); + } + + private Version(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + + @Override + public String toString() { + return major + "." + minor + "." + patch; + } + } +} + diff --git a/connector/src/main/java/org/geysermc/connector/utils/SignUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SignUtils.java new file mode 100644 index 000000000..06406b55e --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/SignUtils.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019-2020 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; + +/** + * Provides utilities for interacting with signs. Mainly, it deals with the widths of each character. + * Since Bedrock auto-wraps signs and Java does not, we have to take this into account when translating signs. + */ +public class SignUtils { + + // TODO: If we send the Java font via resource pack, does width change? + /** + * The maximum character width that a sign can hold in Bedrock + */ + public static final int BEDROCK_CHARACTER_WIDTH_MAX = 88; + + /** + * The maximum character width that a sign can hold in Java + */ + public static final int JAVA_CHARACTER_WIDTH_MAX = 90; + + /** + * Gets the Minecraft width of a character + * @param c character to determine + * @return width of the character + */ + public static int getCharacterWidth(char c) { + switch (c) { + case '!': + case ',': + case '.': + case ':': + case ';': + case 'i': + case '|': + case '¡': + return 2; + + case '\'': + case 'l': + case 'ì': + case 'í': + return 3; + + case ' ': + case 'I': + case '[': + case ']': + case 't': + case '×': + case 'ï': + return 4; + + case '"': + case '(': + case ')': + case '*': + case '<': + case '>': + case 'f': + case 'k': + case '{': + case '}': + return 5; + + case '@': + case '~': + case '®': + return 7; + + default: + return 6; + } + } + +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java index 9b3d737e8..13e6dcbb3 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java @@ -119,7 +119,7 @@ public class SkinProvider { // Schedule Daily Image Expiry if we are caching them if (GeyserConnector.getInstance().getConfig().getCacheImages() > 0) { GeyserConnector.getInstance().getGeneralThreadPool().scheduleAtFixedRate(() -> { - File cacheFolder = Paths.get("cache", "images").toFile(); + File cacheFolder = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").toFile(); if (!cacheFolder.exists()) { return; } @@ -395,7 +395,7 @@ public class SkinProvider { BufferedImage image = null; // First see if we have a cached file. We also update the modification stamp so we know when the file was last used - File imageFile = Paths.get("cache", "images", UUID.nameUUIDFromBytes(imageUrl.getBytes()).toString() + ".png").toFile(); + File imageFile = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("images").resolve(UUID.nameUUIDFromBytes(imageUrl.getBytes()).toString() + ".png").toFile(); if (imageFile.exists()) { try { GeyserConnector.getInstance().getLogger().debug("Reading cached image from file " + imageFile.getPath() + " for " + imageUrl); @@ -600,7 +600,7 @@ public class SkinProvider { @Getter public enum CapeProvider { MINECRAFT, - OPTIFINE("http://s.optifine.net/capes/%s.png", CapeUrlType.USERNAME), + OPTIFINE("https://optifine.net/capes/%s.png", CapeUrlType.USERNAME), LABYMOD("https://www.labymod.net/page/php/getCapeTexture.php?uuid=%s", CapeUrlType.UUID_DASHED), FIVEZIG("https://textures.5zigreborn.eu/profile/%s", CapeUrlType.UUID_DASHED), MINECRAFTCAPES("https://minecraftcapes.net/profile/%s/cape", CapeUrlType.UUID); diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java index f0b88c435..fe2a8aa96 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java @@ -150,14 +150,14 @@ public class SkinUtils { JsonNode textures = skinObject.get("textures"); JsonNode skinTexture = textures.get("SKIN"); - String skinUrl = skinTexture.get("url").asText(); + String skinUrl = skinTexture.get("url").asText().replace("http://", "https://"); isAlex = skinTexture.has("metadata"); String capeUrl = null; if (textures.has("CAPE")) { JsonNode capeTexture = textures.get("CAPE"); - capeUrl = capeTexture.get("url").asText(); + capeUrl = capeTexture.get("url").asText().replace("http://", "https://"); } return new GameProfileData(skinUrl, capeUrl, isAlex); @@ -270,7 +270,7 @@ public class SkinUtils { GeyserConnector.getInstance().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.skin.bedrock.register", playerEntity.getUsername(), playerEntity.getUuid())); try { - byte[] skinBytes = com.github.steveice10.mc.auth.util.Base64.decode(clientData.getSkinData().getBytes("UTF-8")); + byte[] skinBytes = Base64.getDecoder().decode(clientData.getSkinData().getBytes("UTF-8")); byte[] capeBytes = clientData.getCapeData(); byte[] geometryNameBytes = Base64.getDecoder().decode(clientData.getGeometryName().getBytes("UTF-8")); diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index f59939b6a..0602bb546 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -13,13 +13,15 @@ bedrock: address: 0.0.0.0 # The port that will listen for connections port: 19132 - # Some hosting services change your Java port everytime you open the server, and require the same port to be used for Bedrock. + # Some hosting services change your Java port everytime you start the server and require the same port to be used for Bedrock. # This option makes the Bedrock port the same as the Java port every time you start the server. # This option is for the plugin version only. clone-remote-port: false - # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. Irrelevant if "passthrough-motd" is set to true + # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true motd1: "GeyserMC" motd2: "Another GeyserMC forced host." + # The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu. + server-name: "Geyser" remote: # The IP address of the remote (Java Edition) server # If it is "auto", for standalone version the remote address will be set to 127.0.0.1, @@ -53,7 +55,7 @@ floodgate-key-file: public-key.pem # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. command-suggestions: true -# The following three options enable "ping passthrough" -the MOTD, player count and/or protocol name gets retrieved from the Java server. +# The following three options enable "ping passthrough" - the MOTD, player count and/or protocol name gets retrieved from the Java server. # Relay the MOTD from the remote server to Bedrock players. passthrough-motd: false # Relay the protocol name (e.g. BungeeCord [X.X], Paper 1.X) - only really useful when using a custom protocol name! @@ -110,6 +112,11 @@ cache-images: 0 # the end sky in the nether, but ultimately it's the only way for this feature to work. above-bedrock-nether-building: false +# Force clients to load all resource packs if there are any. +# If set to false it allows the user to disconnect from the server if they don't +# want to download the resource packs +force-resource-packs: true + # bStats is a stat tracker that is entirely anonymous and tracks only basic information # about Geyser, such as how many people are online, how many servers are using Geyser, # what OS is being used, etc. You can learn more about bStats here: https://bstats.org/. @@ -122,6 +129,12 @@ metrics: # ADVANCED OPTIONS - DO NOT TOUCH UNLESS YOU KNOW WHAT YOU ARE DOING! +# Geyser updates the Scoreboard after every Scoreboard packet, but when Geyser tries to handle +# a lot of scoreboard packets per second can cause serious lag. +# This option allows you to specify after how many Scoreboard packets per seconds +# the Scoreboard updates will be limited to four updates per second. +scoreboard-packet-threshold: 20 + # Allow connections from ProxyPass and Waterdog. # See https://www.spigotmc.org/wiki/firewall-guide/ for assistance - use UDP instead of TCP. enable-proxy-connections: false diff --git a/connector/src/main/resources/languages b/connector/src/main/resources/languages index 2641db5aa..93b2caed3 160000 --- a/connector/src/main/resources/languages +++ b/connector/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 2641db5aa9100cdbe21b4493489e9be19092a600 +Subproject commit 93b2caed3c4ecd94b3c77a87f1b2304a7bf4f062 diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings index 88678e69b..28a22d2ba 160000 --- a/connector/src/main/resources/mappings +++ b/connector/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 88678e69bf358cd562bd410a2459384aeb7ba482 +Subproject commit 28a22d2baad680f511bffc36d90d06bf626f0527