diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 17e88f268..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - - - - - - - -**Describe the bug** - -A clear and concise description of what the bug is. - -**To Reproduce** - -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** - -A clear and concise description of what you expected to happen. - -**Screenshots / Videos** - -If applicable, add screenshots to help explain your problem. - -**Server Version and Plugins** - -If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information. - -If you're running a multi-server instance, or using Geyser Standalone: - -- Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all. -- Please list all plugins on all servers involved. - -If this bug occurs on a server you do not control, please fill this in to the best of your knowledge. - -**Geyser Dump** - -If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you use the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue. - -**Minecraft: Bedrock Edition Version** - -The version of your Minecraft: Bedrock Edition client you tested with, along with your device type (e.g. Windows 10, Switch...). - -**Additional Context** - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..ca83c83f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,64 @@ +name: Bug report +about: Create a report to help us improve +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report for Geyser! Fill out the following form to your best ability to help us fix the problem. + Only use this if you're absolutely sure that you found a bug and can reproduce it. For anything else, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or the [Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues). +- type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true +- type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce this behaviour + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true +- type: textarea + attributes: + label: Expected behaviour + description: A clear and concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Screenshots / Videos + description: If applicable, add screenshots to help explain your problem. +- type: textarea + attributes: + label: Server Version and Plugins + description: | + If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information. + If you're running a multi-server instance or using Geyser Standalone: + * Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all. + * Please list all plugins on all servers involved. + If this bug occurs on a server you do not control, please full this in to the best of your knowledge. +- type: input + attributes: + label: Geyser Dump + description: If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you're using the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue. +- type: input + attributes: + label: Geyser Version + description: What version of Geyser are you running? + placeholder: "For example: 1.2.0-SNAPSHOT (git-master-2d9baf1)" + validations: + required: true +- type: input + attributes: + label: "Minecraft: Bedrock Edition Version" + description: "What version of Minecraft: Bedrock Edition are you using? Leave empty if the bug happens before you can connect with Minecraft: Bedrock Edition." + placeholder: "For example: 1.16.201" +- type: textarea + attributes: + label: Additional Context + description: Add any other context about the problem here diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 92085a35b..a8a5bf95d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,11 @@ blank_issues_enabled: false contact_links: - - name: GeyserMC Discord - url: http://discord.geysermc.org/ + - name: Common Issues + url: https://github.com/GeyserMC/Geyser/wiki/Common-Issues + about: Check the common issues to see if you are not alone with that issue and see how you can fix them. + - name: Frequently Asked Questions + url: https://github.com/GeyserMC/Geyser/wiki/FAQ + about: Look at the FAQ page for answers for frequently asked questions. + - name: Get help on the GeyserMC Discord server + url: https://discord.gg/geysermc about: If your issue seems like it could possibly be an easy fix due to configuration, please hop on our Discord. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 46653e624..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**What feature do you want?** -Add a description - -**Alternatives?** -List any alternatives you might have tried diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..64ad937a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,21 @@ +name: Feature request +about: Suggest an idea for this project +labels: "Feature Request" +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request for Geyser! Fill out the following form to your best ability to help us understand your feature request and greately improve the change of it getting added. + For anything else than a feature request, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or [the Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues). +- type: textarea + attributes: + label: What feature do you want to see added? + description: A clear and concise description of your feature request. + validations: + required: true +- type: textarea + attributes: + label: Are there any alternatives? + description: List any alternatives you might have tried + validations: + required: true \ No newline at end of file diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml index a5ad53cb5..a6a2a86de 100644 --- a/bootstrap/pom.xml +++ b/bootstrap/pom.xml @@ -22,7 +22,7 @@ sponge-repo - https://repo.spongepowered.org/maven + https://repo.spongepowered.org/repository/maven-public/ bungeecord-repo diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index 93eebc3d2..d1cba8aea 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -30,9 +30,9 @@ provided - org.geysermc.adapters + org.geysermc.geyser.adapters spigot-all - 1.0-SNAPSHOT + 1.1-SNAPSHOT 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 e97373def..e9e6a2cad 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 @@ -28,7 +28,6 @@ package org.geysermc.platform.spigot; import com.github.steveice10.mc.protocol.MinecraftConstants; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -import org.geysermc.adapters.spigot.SpigotAdapters; import org.geysermc.common.PlatformType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; @@ -40,6 +39,7 @@ import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; +import org.geysermc.geyser.adapters.spigot.SpigotAdapters; import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor; import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager; import org.geysermc.platform.spigot.command.SpigotCommandSender; @@ -69,6 +69,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { private GeyserConnector connector; + /** + * The Minecraft server version, formatted as 1.#.# + */ + private String minecraftVersion; + @Override public void onEnable() { // This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed @@ -118,6 +123,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserConfig.loadFloodgate(this); + // Turn "(MC: 1.16.4)" into 1.16.4. + this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0]; + this.connector = GeyserConnector.start(PlatformType.SPIGOT, this); if (geyserConfig.isLegacyPingPassthrough()) { @@ -239,6 +247,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return new GeyserSpigotDumpInfo(); } + @Override + public String getMinecraftServerVersion() { + return this.minecraftVersion; + } + public boolean isCompatible(String version, String whichVersion) { int[] currentVersion = parseVersion(version); int[] otherVersion = parseVersion(whichVersion); @@ -277,10 +290,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { * @return the server version before ViaVersion finishes initializing */ public ProtocolVersion getServerProtocolVersion() { - String bukkitVersion = Bukkit.getServer().getVersion(); - // Turn "(MC: 1.16.4)" into 1.16.4. - String version = bukkitVersion.split("\\(MC: ")[1].split("\\)")[0]; - return ProtocolVersion.getClosest(version); + return ProtocolVersion.getClosest(this.minecraftVersion); } /** diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java index ae1992727..2cbec3273 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java @@ -27,10 +27,10 @@ package org.geysermc.platform.spigot.world.manager; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.geysermc.adapters.spigot.SpigotAdapters; -import org.geysermc.adapters.spigot.SpigotWorldAdapter; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.geyser.adapters.spigot.SpigotAdapters; +import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter; import us.myles.ViaVersion.api.Via; import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.storage.BlockStorage; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java index cb450f7f9..423156c97 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java @@ -75,6 +75,10 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager { if (player == null) { return BlockTranslator.JAVA_AIR_ID; } + if (!player.getWorld().isChunkLoaded(x >> 4, z >> 4)) { + // Prevent nasty async errors if a player is loading in + return BlockTranslator.JAVA_AIR_ID; + } // Get block entity storage BlockStorage storage = Via.getManager().getConnection(player.getUniqueId()).get(BlockStorage.class); Block block = player.getWorld().getBlockAt(x, y, z); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java index c7e3a3d4f..469a38f17 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java @@ -27,10 +27,10 @@ package org.geysermc.platform.spigot.world.manager; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.geysermc.adapters.spigot.SpigotAdapters; -import org.geysermc.adapters.spigot.SpigotWorldAdapter; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.geyser.adapters.spigot.SpigotAdapters; +import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter; public class GeyserSpigotNativeWorldManager extends GeyserSpigotWorldManager { protected final SpigotWorldAdapter adapter; 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 5b8bf54b2..1986bbd22 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 @@ -101,7 +101,7 @@ public class GeyserSpongePlugin implements GeyserBootstrap { } } - if (geyserConfig.getBedrock().isCloneRemotePort()){ + if (geyserConfig.getBedrock().isCloneRemotePort()) { geyserConfig.getBedrock().setPort(geyserConfig.getRemote().getPort()); } @@ -163,4 +163,9 @@ public class GeyserSpongePlugin implements GeyserBootstrap { public BootstrapDumpInfo getDumpInfo() { return new GeyserSpongeDumpInfo(); } + + @Override + public String getMinecraftServerVersion() { + return Sponge.getPlatform().getMinecraftVersion().getName(); + } } 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 5aa15635e..551b0e584 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 @@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedField; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import lombok.Getter; import net.minecrell.terminalconsole.TerminalConsoleAppender; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Logger; @@ -167,11 +168,6 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { this.onEnable(); } - public void onEnable(boolean useGui) { - this.useGui = useGui; - this.onEnable(); - } - @Override public void onEnable() { Logger logger = (Logger) LogManager.getRootLogger(); @@ -213,6 +209,9 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { } GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); + // Allow libraries like Protocol to have their debug information passthrough + logger.get().setLevel(geyserConfig.isDebugMode() ? Level.DEBUG : Level.INFO); + connector = GeyserConnector.start(PlatformType.STANDALONE, this); geyserCommandManager = new GeyserCommandManager(connector); diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java index 9c10234f3..7eeba84bd 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java @@ -55,7 +55,7 @@ public class LoopbackUtil { if (!result.contains("minecraftuwp")) { Files.write(Paths.get(System.getenv("temp") + "/loopback_minecraft.bat"), loopbackCommand.getBytes(), new OpenOption[0]); - process = Runtime.getRuntime().exec(startScript); + Runtime.getRuntime().exec(startScript); geyserLogger.info(ChatColor.AQUA + LanguageUtils.getLocaleStringLog("geyser.bootstrap.loopback.added")); } diff --git a/connector/pom.xml b/connector/pom.xml index 04fc918fa..7d08b5c60 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -10,16 +10,21 @@ connector + + 4.1.59.Final + + org.geysermc common 1.2.0-SNAPSHOT + compile com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.9.8 + 2.10.2 compile @@ -27,23 +32,16 @@ Java-Websocket 1.5.1 - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.9.8 - compile - com.github.CloudburstMC.Protocol bedrock-v422 - d41b84e86c + 294e7e5 compile net.sf.trove4j trove - com.nukkitx.network raknet @@ -51,10 +49,16 @@ - com.nukkitx.network + com.github.CloudburstMC.Network raknet - 1.6.20 + a94d2dd compile + + + io.netty + * + + com.nukkitx.fastutil @@ -157,15 +161,51 @@ io.netty netty-resolver-dns - 4.1.43.Final + ${netty.version} compile + + io.netty + netty-resolver-dns-native-macos + ${netty.version} + compile + osx-x86_64 + io.netty netty-codec-haproxy - 4.1.56.Final + ${netty.version} compile + + + io.netty + netty-handler + ${netty.version} + compile + + + io.netty + netty-transport-native-epoll + ${netty.version} + compile + linux-x86_64 + + + io.netty + netty-transport-native-epoll + ${netty.version} + compile + linux-aarch_64 + + + io.netty + netty-transport-native-kqueue + ${netty.version} + compile + osx-x86_64 + + org.reflections reflections @@ -179,25 +219,25 @@ net.kyori adventure-api - 4.3.0 + 4.5.0 compile net.kyori adventure-text-serializer-gson - 4.3.0 + 4.5.0 compile net.kyori adventure-text-serializer-legacy - 4.3.0 + 4.5.0 compile net.kyori adventure-text-serializer-gson-legacy-impl - 4.3.0 + 4.5.0 compile diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 4dc78df1c..049c58221 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.nukkitx.network.raknet.RakNetConstants; +import com.nukkitx.network.util.EventLoops; import com.nukkitx.protocol.bedrock.BedrockServer; import lombok.Getter; import lombok.Setter; @@ -57,10 +58,7 @@ import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.skin.FloodgateSkinUploader; -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.connector.utils.*; import org.geysermc.floodgate.crypto.AesCipher; import org.geysermc.floodgate.crypto.AesKeyProducer; import org.geysermc.floodgate.crypto.Base64Topping; @@ -86,7 +84,8 @@ public class GeyserConnector { .enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.ALLOW_COMMENTS) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) + .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); public static final String NAME = "Geyser"; public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs @@ -211,6 +210,7 @@ public class GeyserConnector { } } + CooldownUtils.setShowCooldown(config.isShowCooldown()); DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls(); @@ -218,7 +218,13 @@ public class GeyserConnector { RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu(); logger.debug("Setting MTU to " + config.getMtu()); - bedrockServer = new BedrockServer(new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort())); + boolean enableProxyProtocol = config.getBedrock().isEnableProxyProtocol(); + bedrockServer = new BedrockServer( + new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort()), + 1, + EventLoops.commonGroup(), + enableProxyProtocol + ); bedrockServer.setHandler(new ConnectorServerEventHandler(this)); bedrockServer.bind().whenComplete((avoid, throwable) -> { if (throwable == null) { @@ -265,6 +271,20 @@ public class GeyserConnector { } return valueMap; })); + + String minecraftVersion = bootstrap.getMinecraftServerVersion(); + if (minecraftVersion != null) { + Map> versionMap = new HashMap<>(); + Map platformMap = new HashMap<>(); + platformMap.put(platformType.getPlatformName(), 1); + versionMap.put(minecraftVersion, platformMap); + + metrics.addCustomChart(new Metrics.DrilldownPie("minecraftServerVersion", () -> { + // By the end, we should return, for example: + // 1.16.5 => (Spigot, 1) + return versionMap; + })); + } } boolean isGui = false; diff --git a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java index 7308fb674..59dc58c2b 100644 --- a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java +++ b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java @@ -33,6 +33,7 @@ import org.geysermc.connector.command.CommandManager; import org.geysermc.connector.network.translators.world.GeyserWorldManager; import org.geysermc.connector.network.translators.world.WorldManager; +import javax.annotation.Nullable; import java.nio.file.Path; public interface GeyserBootstrap { @@ -99,4 +100,18 @@ public interface GeyserBootstrap { * @return The info about the bootstrap */ BootstrapDumpInfo getDumpInfo(); + + /** + * Returns the Minecraft version currently being used on the server. This should be only be implemented on platforms + * that have direct server access - platforms such as proxies always have to be on their latest version to support + * the newest Minecraft version, but older servers can use ViaVersion to enable newer versions to join. + *
+ * If used, this should not be null before {@link org.geysermc.connector.GeyserConnector} initialization. + * + * @return the Minecraft version being used on the server, or null if not applicable + */ + @Nullable + default String getMinecraftServerVersion() { + return null; + } } diff --git a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java index 3f674d7fa..f91da11b5 100644 --- a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java +++ b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java @@ -52,7 +52,7 @@ public class IGeyserMain { * @return The formatted message */ private String createMessage() { - String message = ""; + StringBuilder message = new StringBuilder(); InputStream helpStream = IGeyserMain.class.getClassLoader().getResourceAsStream("languages/run-help/" + Locale.getDefault().toString() + ".txt"); @@ -68,10 +68,10 @@ public class IGeyserMain { line = line.replace("${plugin_type}", this.getPluginType()); line = line.replace("${plugin_folder}", this.getPluginFolder()); - message += line + "\n"; + message.append(line).append("\n"); } - return message; + return message.toString(); } /** 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 e21aa6bb8..6052bd283 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -27,9 +27,11 @@ package org.geysermc.connector.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import org.geysermc.connector.GeyserLogger; +import org.geysermc.connector.network.CIDRMatcher; import org.geysermc.connector.utils.LanguageUtils; import java.nio.file.Path; +import java.util.List; import java.util.Map; public interface GeyserConfiguration { @@ -59,6 +61,8 @@ public interface GeyserConfiguration { int getPingPassthroughInterval(); + boolean isForwardPlayerPing(); + int getMaxPlayers(); boolean isDebugMode(); @@ -104,6 +108,15 @@ public interface GeyserConfiguration { String getMotd2(); String getServerName(); + + boolean isEnableProxyProtocol(); + + List getProxyProtocolWhitelistedIPs(); + + /** + * @return Unmodifiable list of {@link CIDRMatcher}s from {@link #getProxyProtocolWhitelistedIPs()} + */ + List getWhitelistedIPsMatchers(); } interface IRemoteConfiguration { 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 7c9532ff8..70aa3ff5d 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -25,16 +25,21 @@ package org.geysermc.connector.configuration; +import com.fasterxml.jackson.annotation.JsonIgnore; 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 org.geysermc.connector.network.CIDRMatcher; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; @Getter @JsonIgnoreProperties(ignoreUnknown = true) @@ -74,6 +79,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("ping-passthrough-interval") private int pingPassthroughInterval = 3; + @JsonProperty("forward-player-ping") + private boolean forwardPlayerPing = false; + @JsonProperty("max-players") private int maxPlayers = 100; @@ -119,6 +127,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private MetricsInfo metrics = new MetricsInfo(); @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class BedrockConfiguration implements IBedrockConfiguration { @AsteriskSerializer.Asterisk(sensitive = true) private String address = "0.0.0.0"; @@ -134,9 +143,33 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("server-name") private String serverName = GeyserConnector.NAME; + + @JsonProperty("enable-proxy-protocol") + private boolean enableProxyProtocol = false; + + @JsonProperty("proxy-protocol-whitelisted-ips") + private List proxyProtocolWhitelistedIPs = Collections.emptyList(); + + @JsonIgnore + private List whitelistedIPsMatchers = null; + + @Override + public List getWhitelistedIPsMatchers() { + // Effective Java, Third Edition; Item 83: Use lazy initialization judiciously + List matchers = this.whitelistedIPsMatchers; + if (matchers == null) { + synchronized (this) { + this.whitelistedIPsMatchers = matchers = proxyProtocolWhitelistedIPs.stream() + .map(CIDRMatcher::new) + .collect(Collectors.toList()); + } + } + return Collections.unmodifiableList(matchers); + } } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class RemoteConfiguration implements IRemoteConfiguration { @Setter @AsteriskSerializer.Asterisk(sensitive = true) @@ -170,6 +203,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class MetricsInfo implements IMetricsInfo { private boolean enabled = true; diff --git a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java index e9a4a1f98..70dbdf959 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java @@ -35,6 +35,8 @@ public class AbstractArrowEntity extends Entity { public AbstractArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + setMotion(motion); } @Override @@ -47,4 +49,20 @@ public class AbstractArrowEntity extends Entity { super.updateBedrockMetadata(entityMetadata, session); } + + @Override + public void setRotation(Vector3f rotation) { + // Ignore the rotation sent by the Java server since the + // Java client calculates the rotation from the motion + } + + @Override + public void setMotion(Vector3f motion) { + super.setMotion(motion); + + double horizontalSpeed = Math.sqrt(motion.getX() * motion.getX() + motion.getZ() * motion.getZ()); + float yaw = (float) Math.toDegrees(Math.atan2(motion.getX(), motion.getZ())); + float pitch = (float) Math.toDegrees(Math.atan2(motion.getY(), horizontalSpeed)); + rotation = Vector3f.from(yaw, pitch, yaw); + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java b/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java index 1afcf08d3..ed5272b89 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java @@ -43,7 +43,7 @@ public class BoatEntity extends Entity { */ private static final String BUOYANCY_DATA = "{\"apply_gravity\":true,\"base_buoyancy\":1.0,\"big_wave_probability\":0.02999999932944775," + "\"big_wave_speed\":10.0,\"drag_down_on_buoyancy_removed\":0.0,\"liquid_blocks\":[\"minecraft:water\"," + - "\"minecraft:flowing_water\"],\"simulate_waves\":false}}"; + "\"minecraft:flowing_water\"],\"simulate_waves\":false}"; private boolean isPaddlingLeft; private float paddleTimeLeft; diff --git a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java index 9f9b7bc38..06e56ad03 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java @@ -26,43 +26,167 @@ package org.geysermc.connector.entity; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.object.ProjectileData; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityData; -import org.geysermc.connector.GeyserConnector; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket; +import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.collision.BoundingBox; +import org.geysermc.connector.network.translators.collision.CollisionManager; +import org.geysermc.connector.network.translators.collision.CollisionTranslator; +import org.geysermc.connector.network.translators.collision.translators.BlockCollision; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; -public class FishingHookEntity extends Entity { - public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, ProjectileData data) { +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class FishingHookEntity extends ThrowableEntity { + + private boolean hooked = false; + + private final BoundingBox boundingBox; + + private boolean inWater = false; + + public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, PlayerEntity owner) { super(entityId, geyserId, entityType, position, motion, rotation); - for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) { - Entity entity = session.getEntityCache().getEntityByJavaId(data.getOwnerId()); - if (entity == null && session.getPlayerEntity().getEntityId() == data.getOwnerId()) { - entity = session.getPlayerEntity(); - } + this.boundingBox = new BoundingBox(0.125, 0.125, 0.125, 0.25, 0.25, 0.25); - if (entity != null) { - this.metadata.put(EntityData.OWNER_EID, entity.getGeyserId()); - return; - } - } + // In Java, the splash sound depends on the entity's velocity, but in Bedrock the volume doesn't change. + // This splash can be confused with the sound from catching a fish. This silences the splash from Bedrock, + // so that it can be handled by moveAbsoluteImmediate. + this.metadata.putFloat(EntityData.BOUNDING_BOX_HEIGHT, 128); + + this.metadata.put(EntityData.OWNER_EID, owner.getGeyserId()); } @Override public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { - if (entityMetadata.getId() == 7) { - Entity entity = session.getEntityCache().getEntityByJavaId((Integer) entityMetadata.getValue() - 1); - if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue() - 1) { + if (entityMetadata.getId() == 7) { // Hooked entity + int hookedEntityId = (int) entityMetadata.getValue() - 1; + Entity entity = session.getEntityCache().getEntityByJavaId(hookedEntityId); + if (entity == null && session.getPlayerEntity().getEntityId() == hookedEntityId) { entity = session.getPlayerEntity(); } if (entity != null) { metadata.put(EntityData.TARGET_EID, entity.getGeyserId()); + hooked = true; + } else { + hooked = false; } } super.updateBedrockMetadata(entityMetadata, session); } + + @Override + protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { + boundingBox.setMiddleX(position.getX()); + boundingBox.setMiddleY(position.getY() + boundingBox.getSizeY() / 2); + boundingBox.setMiddleZ(position.getZ()); + + CollisionManager collisionManager = session.getCollisionManager(); + List collidableBlocks = collisionManager.getCollidableBlocks(boundingBox); + boolean touchingWater = false; + boolean collided = false; + for (Vector3i blockPos : collidableBlocks) { + if (0 <= blockPos.getY() && blockPos.getY() <= 255) { + int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos); + BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ()); + if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) { + // TODO Push bounding box out of collision to improve movement + collided = true; + } + + int waterLevel = BlockStateValues.getWaterLevel(blockID); + if (BlockTranslator.isWaterlogged(blockID)) { + waterLevel = 0; + } + if (waterLevel >= 0) { + double waterMaxY = blockPos.getY() + 1 - (waterLevel + 1) / 9.0; + // Falling water is a full block + if (waterLevel >= 8) { + waterMaxY = blockPos.getY() + 1; + } + if (position.getY() <= waterMaxY) { + touchingWater = true; + } + } + } + } + + if (!inWater && touchingWater) { + sendSplashSound(session); + } + inWater = touchingWater; + + if (!collided) { + super.moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported); + } else { + super.moveAbsoluteImmediate(session, this.position, rotation, true, true); + } + } + + private void sendSplashSound(GeyserSession session) { + if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) { + float volume = (float) (0.2f * Math.sqrt(0.2 * (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) + motion.getY() * motion.getY())); + if (volume > 1) { + volume = 1; + } + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("random.splash"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(volume); + playSoundPacket.setPitch(1f + ThreadLocalRandom.current().nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + } + + @Override + public void tick(GeyserSession session) { + if (hooked || !isInAir(session) && !isInWater(session) || isOnGround()) { + motion = Vector3f.ZERO; + return; + } + float gravity = getGravity(session); + motion = motion.down(gravity); + + moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false); + + float drag = getDrag(session); + motion = motion.mul(drag); + } + + @Override + protected float getGravity(GeyserSession session) { + if (!isInWater(session) && !onGround) { + return 0.03f; + } + return 0; + } + + /** + * @param session the session of the Bedrock client. + * @return true if this entity is currently in air. + */ + protected boolean isInAir(GeyserSession session) { + if (session.getConnector().getConfig().isCacheChunks()) { + if (0 <= position.getFloorY() && position.getFloorY() <= 255) { + int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); + return block == BlockTranslator.JAVA_AIR_ID; + } + } + return false; + } + + @Override + protected float getDrag(GeyserSession session) { + return 0.92f; + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java index 2b411109a..58a3b6f6c 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java @@ -32,19 +32,44 @@ import org.geysermc.connector.network.session.GeyserSession; public class ItemedFireballEntity extends ThrowableEntity { private final Vector3f acceleration; + /** + * The number of ticks to advance movement before sending to Bedrock + */ + protected int futureTicks = 3; + public ItemedFireballEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, Vector3f.ZERO, rotation); - acceleration = motion; + + float magnitude = motion.length(); + if (magnitude != 0) { + acceleration = motion.div(magnitude).mul(0.1f); + } else { + acceleration = Vector3f.ZERO; + } + } + + private Vector3f tickMovement(GeyserSession session, Vector3f position) { + position = position.add(motion); + float drag = getDrag(session); + motion = motion.add(acceleration).mul(drag); + return position; + } + + @Override + protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { + // Advance the position by a few ticks before sending it to Bedrock + Vector3f lastMotion = motion; + Vector3f newPosition = position; + for (int i = 0; i < futureTicks; i++) { + newPosition = tickMovement(session, newPosition); + } + super.moveAbsoluteImmediate(session, newPosition, rotation, isOnGround, teleported); + this.position = position; + this.motion = lastMotion; } @Override public void tick(GeyserSession session) { - position = position.add(motion); - // TODO: While this reduces latency in position updating (needed for better fireball reflecting), - // TODO: movement is incredibly stiff. - // TODO: Only use this laggy movement for fireballs that be reflected - moveAbsoluteImmediate(session, position, rotation, false, true); - float drag = getDrag(session); - motion = motion.add(acceleration).mul(drag); + moveAbsoluteImmediate(session, tickMovement(session, position), rotation, false, false); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java index 4e0c25ab5..1088b2a0b 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java @@ -29,20 +29,21 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.MoveEntityDeltaPacket; 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.network.translators.world.block.BlockStateValues; /** * Used as a class for any object-like entity that moves as a projectile */ public class ThrowableEntity extends Entity implements Tickable { - private Vector3f lastPosition; + protected Vector3f lastJavaPosition; public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); - this.lastPosition = position; + this.lastJavaPosition = position; } /** @@ -52,22 +53,65 @@ public class ThrowableEntity extends Entity implements Tickable { */ @Override public void tick(GeyserSession session) { - super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround); + moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false); float drag = getDrag(session); - float gravity = getGravity(); + float gravity = getGravity(session); motion = motion.mul(drag).down(gravity); } protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); + MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); + moveEntityDeltaPacket.setRuntimeEntityId(geyserId); + + if (isOnGround) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); + } + setOnGround(isOnGround); + + if (teleported) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING); + } + + if (this.position.getX() != position.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X); + moveEntityDeltaPacket.setX(position.getX()); + } + if (this.position.getY() != position.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y); + moveEntityDeltaPacket.setY(position.getY()); + } + if (this.position.getZ() != position.getZ()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z); + moveEntityDeltaPacket.setZ(position.getZ()); + } + setPosition(position); + + if (this.rotation.getX() != rotation.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); + moveEntityDeltaPacket.setYaw(rotation.getX()); + } + if (this.rotation.getY() != rotation.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); + moveEntityDeltaPacket.setPitch(rotation.getY()); + } + if (this.rotation.getZ() != rotation.getZ()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); + moveEntityDeltaPacket.setHeadYaw(rotation.getZ()); + } + setRotation(rotation); + + if (!moveEntityDeltaPacket.getFlags().isEmpty()) { + session.sendUpstreamPacket(moveEntityDeltaPacket); + } } /** * Get the gravity of this entity type. Used for applying gravity while the entity is in motion. * + * @param session the session of the Bedrock client. * @return the amount of gravity to apply to this entity while in motion. */ - protected float getGravity() { + protected float getGravity(GeyserSession session) { if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY)) { switch (entityType) { case THROWN_POTION: @@ -76,11 +120,14 @@ public class ThrowableEntity extends Entity implements Tickable { case THROWN_EXP_BOTTLE: return 0.07f; case FIREBALL: + case SHULKER_BULLET: return 0; case SNOWBALL: case THROWN_EGG: case THROWN_ENDERPEARL: return 0.03f; + case LLAMA_SPIT: + return 0.06f; } } return 0; @@ -101,11 +148,14 @@ public class ThrowableEntity extends Entity implements Tickable { case SNOWBALL: case THROWN_EGG: case THROWN_ENDERPEARL: + case LLAMA_SPIT: return 0.99f; case FIREBALL: case SMALL_FIREBALL: case DRAGON_FIREBALL: return 0.95f; + case SHULKER_BULLET: + return 1; } } return 1; @@ -117,8 +167,10 @@ public class ThrowableEntity extends Entity implements Tickable { */ protected boolean isInWater(GeyserSession session) { if (session.getConnector().getConfig().isCacheChunks()) { - int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); - return block == BlockTranslator.BEDROCK_WATER_ID; + if (0 <= position.getFloorY() && position.getFloorY() <= 255) { + int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); + return BlockStateValues.getWaterLevel(block) != -1; + } } return false; } @@ -136,14 +188,13 @@ public class ThrowableEntity extends Entity implements Tickable { @Override public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) { - position = lastPosition; - super.moveRelative(session, relX, relY, relZ, rotation, isOnGround); - lastPosition = position; + moveAbsoluteImmediate(session, lastJavaPosition.add(relX, relY, relZ), rotation, isOnGround, false); + lastJavaPosition = position; } @Override public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); - lastPosition = position; + moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported); + lastJavaPosition = position; } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java index ba5b9eb55..3548e0dfe 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java @@ -35,6 +35,8 @@ public class WitherSkullEntity extends ItemedFireballEntity { public WitherSkullEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + this.futureTicks = 1; } @Override diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java index 544014115..752a0d106 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java @@ -44,7 +44,7 @@ public class RabbitEntity extends AnimalEntity { if (entityMetadata.getId() == 15) { metadata.put(EntityData.SCALE, .55f); boolean isBaby = (boolean) entityMetadata.getValue(); - if(isBaby) { + if (isBaby) { metadata.put(EntityData.SCALE, .35f); metadata.getFlags().setFlag(EntityFlag.BABY, true); } diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java index d481cd0c5..56354774d 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java @@ -125,7 +125,7 @@ public class VillagerEntity extends AbstractMerchantEntity { Pattern r = Pattern.compile("facing=([a-z]+)"); Matcher m = r.matcher(bedRotationZ); if (m.find()) { - switch (m.group(0)){ + switch (m.group(0)) { case "facing=south": //bed is facing south z = 180; diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java index 8dcce6a7f..e024b4e55 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java @@ -46,7 +46,7 @@ public class WitherEntity extends MonsterEntity { if (entityMetadata.getId() >= 15 && entityMetadata.getId() <= 17) { Entity entity = session.getEntityCache().getEntityByJavaId((int) entityMetadata.getValue()); - if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue()) { + if (entity == null && session.getPlayerEntity().getEntityId() == (int) entityMetadata.getValue()) { entity = session.getPlayerEntity(); } @@ -62,7 +62,7 @@ public class WitherEntity extends MonsterEntity { } else if (entityMetadata.getId() == 17) { metadata.put(EntityData.WITHER_TARGET_3, targetID); } else if (entityMetadata.getId() == 18) { - metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, (int) entityMetadata.getValue()); + metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, entityMetadata.getValue()); // Show the shield for the first few seconds of spawning (like Java) if ((int) entityMetadata.getValue() >= 165) { 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 e1e531f42..f38e56fd8 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 @@ -30,8 +30,11 @@ import org.geysermc.connector.entity.*; import org.geysermc.connector.entity.living.*; import org.geysermc.connector.entity.living.animal.*; import org.geysermc.connector.entity.living.animal.horse.*; -import org.geysermc.connector.entity.living.animal.tameable.*; -import org.geysermc.connector.entity.living.merchant.*; +import org.geysermc.connector.entity.living.animal.tameable.CatEntity; +import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity; +import org.geysermc.connector.entity.living.animal.tameable.WolfEntity; +import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity; +import org.geysermc.connector.entity.living.merchant.VillagerEntity; import org.geysermc.connector.entity.living.monster.*; import org.geysermc.connector.entity.living.monster.raid.AbstractIllagerEntity; import org.geysermc.connector.entity.living.monster.raid.PillagerEntity; @@ -39,6 +42,9 @@ import org.geysermc.connector.entity.living.monster.raid.RaidParticipantEntity; import org.geysermc.connector.entity.living.monster.raid.SpellcasterIllagerEntity; import org.geysermc.connector.entity.player.PlayerEntity; +import java.util.ArrayList; +import java.util.List; + @Getter public enum EntityType { @@ -112,7 +118,7 @@ public enum EntityType { TRIDENT(TridentEntity.class, 73, 0f, 0f, 0f, 0f, "minecraft:thrown_trident"), TURTLE(TurtleEntity.class, 74, 0.4f, 1.2f), CAT(CatEntity.class, 75, 0.35f, 0.3f), - SHULKER_BULLET(Entity.class, 76, 0.3125f), + SHULKER_BULLET(ThrowableEntity.class, 76, 0.3125f), FISHING_BOBBER(FishingHookEntity.class, 77, 0f, 0f, 0f, 0f, "minecraft:fishing_hook"), CHALKBOARD(Entity.class, 78, 0f), DRAGON_FIREBALL(ItemedFireballEntity.class, 79, 1.0f), @@ -139,7 +145,7 @@ public enum EntityType { MINECART_SPAWNER(SpawnerMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft: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), + LLAMA_SPIT(ThrowableEntity.class, 102, 0.25f), EVOKER_FANGS(Entity.class, 103, 0.8f, 0.5f, 0.5f, 0f, "minecraft:evocation_fang"), EVOKER(SpellcasterIllagerEntity.class, 104, 1.95f, 0.6f, 0.6f, 0f, "minecraft:evocation_illager"), VEX(VexEntity.class, 105, 0.8f, 0.4f), @@ -174,17 +180,33 @@ public enum EntityType { */ ENDER_DRAGON_PART(EnderDragonPartEntity.class, 32, 0, 0, 0, 0, "minecraft:armor_stand"); + /** + * A list of all Java identifiers for use with command suggestions + */ + public static final String[] ALL_JAVA_IDENTIFIERS; private static final EntityType[] VALUES = values(); - private Class entityClass; + static { + List allJavaIdentifiers = new ArrayList<>(); + for (EntityType type : values()) { + if (type == AGENT || type == BALLOON || type == CHALKBOARD || type == NPC || type == TRIPOD_CAMERA || type == ENDER_DRAGON_PART) { + continue; + } + allJavaIdentifiers.add("minecraft:" + type.name().toLowerCase()); + } + ALL_JAVA_IDENTIFIERS = allJavaIdentifiers.toArray(new String[0]); + } + + private final Class entityClass; private final int type; private final float height; private final float width; private final float length; private final float offset; - private String identifier; + private final String identifier; EntityType(Class entityClass, int type, float height) { + //noinspection SuspiciousNameCombination this(entityClass, type, height, height); } @@ -198,8 +220,6 @@ public enum EntityType { EntityType(Class entityClass, int type, float height, float width, float length, float offset) { this(entityClass, type, height, width, length, offset, null); - - this.identifier = "minecraft:" + name().toLowerCase(); } EntityType(Class entityClass, int type, float height, float width, float length, float offset, String identifier) { @@ -209,7 +229,7 @@ public enum EntityType { this.width = width; this.length = length; this.offset = offset + 0.00001f; - this.identifier = identifier; + this.identifier = identifier == null ? "minecraft:" + name().toLowerCase() : identifier; } public static EntityType getFromIdentifier(String identifier) { diff --git a/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java b/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java index 25ae5a36c..1457780b1 100644 --- a/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java +++ b/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java @@ -36,6 +36,7 @@ import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -75,7 +76,7 @@ public class Metrics { private final static ObjectMapper mapper = new ObjectMapper(); - private GeyserConnector connector; + private final GeyserConnector connector; /** * Class constructor. @@ -156,6 +157,7 @@ public class Metrics { String osName = System.getProperty("os.name"); String osArch = System.getProperty("os.arch"); String osVersion = System.getProperty("os.version"); + String javaVersion = System.getProperty("java.version"); int coreCount = Runtime.getRuntime().availableProcessors(); ObjectNode data = mapper.createObjectNode(); @@ -163,6 +165,7 @@ public class Metrics { data.put("serverUUID", serverUUID); data.put("playerAmount", playerAmount); + data.put("javaVersion", javaVersion); data.put("osName", osName); data.put("osArch", osArch); data.put("osVersion", osVersion); @@ -241,7 +244,7 @@ public class Metrics { } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(outputStream); - gzip.write(str.getBytes("UTF-8")); + gzip.write(str.getBytes(StandardCharsets.UTF_8)); gzip.close(); return outputStream.toByteArray(); } diff --git a/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java b/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java new file mode 100644 index 000000000..57e58ecc2 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/* + * Taken & modified from TCPShield, licensed under MIT. See https://github.com/TCPShield/RealIP/blob/master/LICENSE + * + * https://github.com/TCPShield/RealIP/blob/32d422a9523cb6e25b571072851f3306bb8bbc4f/src/main/java/net/tcpshield/tcpshield/validation/cidr/CIDRMatcher.java + */ +public class CIDRMatcher { + private final int maskBits; + private final int maskBytes; + private final boolean simpleCIDR; + private final InetAddress cidrAddress; + + public CIDRMatcher(String ipAddress) { + String[] split = ipAddress.split("/", 2); + + String parsedIPAddress; + if (split.length == 2) { + parsedIPAddress = split[0]; + + this.maskBits = Integer.parseInt(split[1]); + this.simpleCIDR = maskBits == 32; + } else { + parsedIPAddress = ipAddress; + + this.maskBits = -1; + this.simpleCIDR = true; + } + + this.maskBytes = simpleCIDR ? -1 : maskBits / 8; + + try { + cidrAddress = InetAddress.getByName(parsedIPAddress); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + public boolean matches(InetAddress inetAddress) { + // check if IP is IPv4 or IPv6 + if (cidrAddress.getClass() != inetAddress.getClass()) { + return false; + } + + // check for equality if it's a simple CIDR + if (simpleCIDR) { + return inetAddress.equals(cidrAddress); + } + + byte[] inetAddressBytes = inetAddress.getAddress(); + byte[] requiredAddressBytes = cidrAddress.getAddress(); + + byte finalByte = (byte) (0xFF00 >> (maskBits & 0x07)); + + for (int i = 0; i < maskBytes; i++) { + if (inetAddressBytes[i] != requiredAddressBytes[i]) { + return false; + } + } + + if (finalByte != 0) { + return (inetAddressBytes[maskBytes] & finalByte) == (requiredAddressBytes[maskBytes] & finalByte); + } + + return true; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java index 87883087d..bd5030a8b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -40,8 +40,18 @@ import org.geysermc.connector.utils.LanguageUtils; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.List; public class ConnectorServerEventHandler implements BedrockServerEventHandler { + /* + The following constants are all used to ensure the ping does not reach a length where it is unparsable by the Bedrock client + */ + private static final int MINECRAFT_VERSION_BYTES_LENGTH = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion().getBytes(StandardCharsets.UTF_8).length; + private static final int BRAND_BYTES_LENGTH = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8).length; + /** + * The MOTD, sub-MOTD and Minecraft version ({@link #MINECRAFT_VERSION_BYTES_LENGTH}) combined cannot reach this length. + */ + private static final int MAGIC_RAKNET_LENGTH = 338; private final GeyserConnector connector; @@ -51,6 +61,21 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { @Override public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) { + List allowedProxyIPs = connector.getConfig().getBedrock().getProxyProtocolWhitelistedIPs(); + if (connector.getConfig().getBedrock().isEnableProxyProtocol() && !allowedProxyIPs.isEmpty()) { + boolean isWhitelistedIP = false; + for (CIDRMatcher matcher : connector.getConfig().getBedrock().getWhitelistedIPsMatchers()) { + if (matcher.matches(inetSocketAddress.getAddress())) { + isWhitelistedIP = true; + break; + } + } + + if (!isWhitelistedIP) { + return false; + } + } + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.attempt_connect", inetSocketAddress)); return true; } @@ -69,16 +94,16 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { BedrockPong pong = new BedrockPong(); pong.setEdition("MCPE"); - pong.setGameType("Default"); + pong.setGameType("Survival"); // Can only be Survival or Creative as of 1.16.210.59 pong.setNintendoLimited(false); pong.setProtocolVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()); - pong.setVersion(null); // Server tries to connect either way and it looks better + pong.setVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()); // Required to not be empty as of 1.16.210.59. Can only contain . and numbers. pong.setIpv4Port(config.getBedrock().getPort()); if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); String mainMotd = motd[0]; // First line of the motd. - String subMotd = (motd.length != 1) ? motd[1] : ""; // Second line of the motd if present, otherwise blank. + String subMotd = (motd.length != 1) ? motd[1] : GeyserConnector.NAME; // Second line of the motd if present, otherwise default. pong.setMotd(mainMotd.trim()); pong.setSubMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit. @@ -95,15 +120,28 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { pong.setMaximumPlayerCount(config.getMaxPlayers()); } + // Fallbacks to prevent errors and allow Bedrock to see the server + if (pong.getMotd() == null || pong.getMotd().trim().isEmpty()) { + pong.setMotd(GeyserConnector.NAME); + } + if (pong.getSubMotd() == null || pong.getSubMotd().trim().isEmpty()) { + // Sub-MOTD cannot be empty as of 1.16.210.59 + pong.setSubMotd(GeyserConnector.NAME); + } + // The ping will not appear if the MOTD + sub-MOTD is of a certain length. // We don't know why, though byte[] motdArray = pong.getMotd().getBytes(StandardCharsets.UTF_8); - if (motdArray.length + pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length > 338) { - // Remove the sub-MOTD first since that only appears locally - pong.setSubMotd(""); - if (motdArray.length > 338) { + int subMotdLength = pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length; + if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH)) { + // Shorten the sub-MOTD first since that only appears locally + if (subMotdLength > BRAND_BYTES_LENGTH) { + pong.setSubMotd(GeyserConnector.NAME); + subMotdLength = BRAND_BYTES_LENGTH; + } + if (motdArray.length > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength)) { // If the top MOTD is still too long, we chop it down - byte[] newMotdArray = new byte[339]; + byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength]; System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8)); } diff --git a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java index 637f6d99d..87541f704 100644 --- a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java @@ -64,7 +64,7 @@ public class QueryPacketHandler { * @param buffer The Query data */ public QueryPacketHandler(GeyserConnector connector, InetSocketAddress sender, ByteBuf buffer) { - if(!isQueryPacket(buffer)) + if (!isQueryPacket(buffer)) return; this.connector = connector; @@ -225,7 +225,7 @@ public class QueryPacketHandler { query.write(new byte[] { 0x00, 0x00 }); // Fill player names - if(pingInfo != null) { + if (pingInfo != null) { for (String username : pingInfo.getPlayerList()) { query.write(username.getBytes()); query.write((byte) 0x00); 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 722cd2e45..01b9452be 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 @@ -93,6 +93,8 @@ import org.geysermc.cumulus.util.FormBuilder; import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.util.BedrockData; +import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; @@ -168,6 +170,9 @@ public class GeyserSession implements CommandSender { @Setter private boolean sprinting; + /** + * Not updated if cache chunks is enabled. + */ @Setter private boolean jumping; @@ -370,7 +375,8 @@ public class GeyserSession implements CommandSender { tmpPlayers.forEach(player -> this.emotes.addAll(player.getEmotes())); bedrockServerSession.addDisconnectHandler(disconnectReason -> { - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", bedrockServerSession.getAddress().getAddress(), disconnectReason)); + InetAddress address = bedrockServerSession.getRealAddress().getAddress(); + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", address, disconnectReason)); disconnect(disconnectReason.name()); connector.removePlayer(this); @@ -544,8 +550,10 @@ public class GeyserSession implements CommandSender { downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); } - // Let Geyser handle sending the keep alive - downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); + if (connector.getConfig().isForwardPlayerPing()) { + // Let Geyser handle sending the keep alive + downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); + } downstream.getSession().addListener(new SessionAdapter() { @Override public void packetSending(PacketSendingEvent event) { @@ -565,7 +573,7 @@ public class GeyserSession implements CommandSender { clientData.getLanguageCode(), clientData.getUiProfile().ordinal(), clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress(), + upstream.getAddress().getAddress().getHostAddress(), skinUploader.getId(), skinUploader.getVerifyCode() ).toString()); @@ -826,7 +834,14 @@ public class GeyserSession implements CommandSender { startGamePacket.setMultiplayerCorrelationId(""); startGamePacket.setItemEntries(ItemRegistry.ITEMS); startGamePacket.setVanillaVersion("*"); - startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); + startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); // can be removed once 1.16.200 support is dropped + + SyncedPlayerMovementSettings settings = new SyncedPlayerMovementSettings(); + settings.setMovementMode(AuthoritativeMovementMode.CLIENT); + settings.setRewindHistorySize(0); + settings.setServerAuthoritativeBlockBreaking(false); + startGamePacket.setPlayerMovementSettings(settings); + upstream.sendPacket(startGamePacket); } 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 04e208af3..f973574b0 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 @@ -61,6 +61,6 @@ public class UpstreamSession { } public InetSocketAddress getAddress() { - return session.getAddress(); + return session.getRealAddress(); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java index aed98b09c..d87590e5f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java @@ -112,6 +112,8 @@ public final class BedrockClientData { private String skinColor; @JsonProperty(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; + @JsonProperty(value = "PlayFabId") + private String playFabId; public void setJsonData(JsonNode data) { if (this.jsonData == null && data != null) { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java index f4c0f9abc..fb6d5b93d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java @@ -38,7 +38,7 @@ import java.io.InputStream; */ public class EntityIdentifierRegistry { - public static NbtMap ENTITY_IDENTIFIERS; + public static final NbtMap ENTITY_IDENTIFIERS; private EntityIdentifierRegistry() { } 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 b841a79b2..eb26dbff9 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 @@ -30,6 +30,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUpdate import com.github.steveice10.packetlib.packet.Packet; import com.nukkitx.protocol.bedrock.BedrockPacket; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.geysermc.common.PlatformType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.FileUtils; @@ -89,12 +90,15 @@ public class PacketTranslatorRegistry { public

boolean translate(Class clazz, P packet, GeyserSession session) { if (!session.getUpstream().isClosed() && !session.isClosed()) { try { - if (translators.containsKey(clazz)) { - ((PacketTranslator

) translators.get(clazz)).translate(packet, session); + PacketTranslator

translator = (PacketTranslator

) translators.get(clazz); + if (translator != null) { + translator.translate(packet, session); return true; } else { - if (!IGNORED_PACKETS.contains(clazz)) + if ((GeyserConnector.getInstance().getPlatformType() != PlatformType.STANDALONE || !(packet instanceof BedrockPacket)) && !IGNORED_PACKETS.contains(clazz)) { + // Other debug logs already take care of Bedrock packets for us if on standalone GeyserConnector.getInstance().getLogger().debug("Could not find packet for " + (packet.toString().length() > 25 ? packet.getClass().getSimpleName() : packet)); + } } } catch (Throwable ex) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.packet.failed", packet.getClass().getSimpleName()), ex); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java index 64c91f076..577469fdd 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java @@ -40,7 +40,7 @@ import java.util.Collections; import java.util.concurrent.TimeUnit; /** - * Used to send the keep alive packet back to the server + * Used to send the forwarded keep alive packet back to the server */ @Translator(packet = NetworkStackLatencyPacket.class) public class BedrockNetworkStackLatencyTranslator extends PacketTranslator { @@ -60,8 +60,10 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator 0) { - ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(pingId); - session.sendDownstreamPacket(keepAlivePacket); + if (session.getConnector().getConfig().isForwardPlayerPing()) { + ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(pingId); + session.sendDownstreamPacket(keepAlivePacket); + } return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java index c4dbbec40..c248b57a5 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java @@ -37,6 +37,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.PlayerActionType; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; @@ -64,7 +65,7 @@ public class BedrockActionTranslator extends PacketTranslator { - session.setJumping(false); - }, 1, TimeUnit.SECONDS); + if (!session.getConnector().getConfig().isCacheChunks()) { + // Save the jumping status for determining teleport status + session.setJumping(true); + session.getConnector().getGeneralThreadPool().schedule(() -> session.setJumping(false), 1, TimeUnit.SECONDS); + } break; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java index 203e4406f..77392a99a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java @@ -177,24 +177,24 @@ public class CollisionManager { session.sendUpstreamPacket(movePlayerPacket); } - public List getPlayerCollidableBlocks() { + public List getCollidableBlocks(BoundingBox box) { List blocks = new ArrayList<>(); - Vector3d position = Vector3d.from(playerBoundingBox.getMiddleX(), - playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2), - playerBoundingBox.getMiddleZ()); + Vector3d position = Vector3d.from(box.getMiddleX(), + box.getMiddleY() - (box.getSizeY() / 2), + box.getMiddleZ()); - // Loop through all blocks that could collide with the player - int minCollisionX = (int) Math.floor(position.getX() - ((playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE)); - int maxCollisionX = (int) Math.floor(position.getX() + (playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE); + // Loop through all blocks that could collide + int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE)); + int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE); // Y extends 0.5 blocks down because of fence hitboxes int minCollisionY = (int) Math.floor(position.getY() - 0.5); - int maxCollisionY = (int) Math.floor(position.getY() + playerBoundingBox.getSizeY()); + int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY()); - int minCollisionZ = (int) Math.floor(position.getZ() - ((playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE)); - int maxCollisionZ = (int) Math.floor(position.getZ() + (playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE); + int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE)); + int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE); for (int y = minCollisionY; y < maxCollisionY + 1; y++) { for (int x = minCollisionX; x < maxCollisionX + 1; x++) { @@ -207,6 +207,10 @@ public class CollisionManager { return blocks; } + public List getPlayerCollidableBlocks() { + return getCollidableBlocks(playerBoundingBox); + } + /** * Returns false if the movement is invalid, and in this case it shouldn't be sent to the server and should be * cancelled diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java index 769cbd63a..a3b4b6c31 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java @@ -69,6 +69,18 @@ public enum Enchantment { QUICK_CHARGE, SOUL_SPEED; + /** + * A list of all enchantment Java identifiers for use with command suggestions. + */ + public static final String[] ALL_JAVA_IDENTIFIERS; + + static { + ALL_JAVA_IDENTIFIERS = new String[values().length]; + for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) { + ALL_JAVA_IDENTIFIERS[i] = values()[i].javaIdentifier; + } + } + private final String javaIdentifier; Enchantment() { 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 e9b821588..9d1921731 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 @@ -63,6 +63,11 @@ public class ItemRegistry { public static final List ITEMS = new ArrayList<>(); public static final Int2ObjectMap ITEM_ENTRIES = new Int2ObjectOpenHashMap<>(); + /** + * A list of all Java item names. + */ + public static final String[] ITEM_NAMES; + /** * Bamboo item entry, used in PandaEntity.java */ @@ -116,6 +121,8 @@ public class ItemRegistry { // Used to get the Bedrock namespaced ID (in instances where there are small differences) Int2ObjectMap bedrockIdToIdentifier = new Int2ObjectOpenHashMap<>(); + List itemNames = new ArrayList<>(); + List itemEntries; try { itemEntries = GeyserConnector.JSON_MAPPER.readValue(stream, itemEntriesType); @@ -207,6 +214,8 @@ public class ItemRegistry { BUCKETS.add(entry.getValue().get("bedrock_id").intValue()); } + itemNames.add(entry.getKey()); + itemIndex++; } @@ -235,6 +244,8 @@ public class ItemRegistry { creativeItems.add(ItemData.fromNet(netId++, item.getId(), item.getDamage(), item.getCount(), item.getTag())); } CREATIVE_ITEMS = creativeItems.toArray(new ItemData[0]); + + ITEM_NAMES = itemNames.toArray(new String[0]); } /** 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 90acb781a..f63df2f7e 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 @@ -303,9 +303,8 @@ public abstract class ItemTranslator { CompoundTag javaTag = new CompoundTag(name); Map javaValue = javaTag.getValue(); if (tag != null && !tag.isEmpty()) { - for (String str : tag.keySet()) { - Object bedrockTag = tag.get(str); - Tag translatedTag = translateToJavaNBT(str, bedrockTag); + for (Map.Entry entry : tag.entrySet()) { + Tag translatedTag = translateToJavaNBT(entry.getKey(), entry.getValue()); if (translatedTag == null) continue; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java index f6664c1a6..7de101811 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java @@ -33,21 +33,69 @@ import com.nukkitx.protocol.bedrock.data.command.CommandEnumData; import com.nukkitx.protocol.bedrock.data.command.CommandParamData; import com.nukkitx.protocol.bedrock.data.command.CommandParamType; import com.nukkitx.protocol.bedrock.packet.AvailableCommandsPacket; +import it.unimi.dsi.fastutil.Hash; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap; import lombok.Getter; +import lombok.ToString; +import net.kyori.adventure.text.format.NamedTextColor; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.type.EntityType; 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.Enchantment; +import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; @Translator(packet = ServerDeclareCommandsPacket.class) public class JavaDeclareCommandsTranslator extends PacketTranslator { + + private static final String[] ENUM_BOOLEAN = {"true", "false"}; + private static final String[] VALID_COLORS; + private static final String[] VALID_SCOREBOARD_SLOTS; + + private static final Hash.Strategy PARAM_STRATEGY = new Hash.Strategy() { + @Override + public int hashCode(CommandParamData[][] o) { + return Arrays.deepHashCode(o); + } + + @Override + public boolean equals(CommandParamData[][] a, CommandParamData[][] b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + CommandParamData[] a1 = a[i]; + CommandParamData[] b1 = b[i]; + if (a1.length != b1.length) return false; + + for (int j = 0; j < a1.length; j++) { + if (!a1[j].equals(b1[j])) return false; + } + } + return true; + } + }; + + static { + List validColors = new ArrayList<>(NamedTextColor.NAMES.keys()); + validColors.add("reset"); + VALID_COLORS = validColors.toArray(new String[0]); + + List teamOptions = new ArrayList<>(Arrays.asList("list", "sidebar", "belowName")); + for (String color : NamedTextColor.NAMES.keys()) { + teamOptions.add("sidebar.team." + color); + } + VALID_SCOREBOARD_SLOTS = teamOptions.toArray(new String[0]); + } + @Override public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) { // Don't send command suggestions if they are disabled @@ -60,48 +108,50 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator commandData = new ArrayList<>(); - Int2ObjectMap commands = new Int2ObjectOpenHashMap<>(); + IntSet commandNodes = new IntOpenHashSet(); + Set knownAliases = new HashSet<>(); + Map> commands = new Object2ObjectOpenCustomHashMap<>(PARAM_STRATEGY); Int2ObjectMap> commandArgs = new Int2ObjectOpenHashMap<>(); // Get the first node, it should be a root node - CommandNode rootNode = packet.getNodes()[packet.getFirstNodeIndex()]; + CommandNode rootNode = nodes[packet.getFirstNodeIndex()]; // Loop through the root nodes to get all commands for (int nodeIndex : rootNode.getChildIndices()) { - CommandNode node = packet.getNodes()[nodeIndex]; + CommandNode node = nodes[nodeIndex]; // Make sure we don't have duplicated commands (happens if there is more than 1 root node) - if (commands.containsKey(nodeIndex)) { continue; } - if (commands.containsValue(node.getName())) { continue; } + if (!commandNodes.add(nodeIndex) || !knownAliases.add(node.getName().toLowerCase())) continue; // Get and update the commandArgs list with the found arguments if (node.getChildIndices().length >= 1) { for (int childIndex : node.getChildIndices()) { - commandArgs.putIfAbsent(nodeIndex, new ArrayList<>()); - commandArgs.get(nodeIndex).add(packet.getNodes()[childIndex]); + commandArgs.computeIfAbsent(nodeIndex, ArrayList::new).add(nodes[childIndex]); } } - // Insert the command name into the list - commands.put(nodeIndex, node.getName()); + // Get and parse all params + CommandParamData[][] params = getParams(nodes[nodeIndex], nodes); + + // Insert the alias name into the command list + commands.computeIfAbsent(params, index -> new HashSet<>()).add(node.getName().toLowerCase()); } // The command flags, not sure what these do apart from break things List flags = Collections.emptyList(); // Loop through all the found commands - for (int commandID : commands.keySet()) { - String commandName = commands.get(commandID); + + for (Map.Entry> entry : commands.entrySet()) { + String commandName = entry.getValue().iterator().next(); // We know this has a value // Create a basic alias - CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", new String[] { commandName.toLowerCase() }, false); - - // Get and parse all params - CommandParamData[][] params = getParams(packet.getNodes()[commandID], packet.getNodes()); + CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", entry.getValue().toArray(new String[0]), false); // Build the completed command and add it to the final list - CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, params); + CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, entry.getKey()); commandData.add(data); } @@ -109,7 +159,7 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator treeData = rootParam.getTree(); - CommandParamData[][] params = new CommandParamData[treeData.size()][]; - // Fill the nested params array - int i = 0; - for (CommandParamData[] tree : treeData) { - params[i] = tree; - i++; - } - - return params; + return treeData.toArray(new CommandParamData[0][]); } return new CommandParamData[0][0]; @@ -155,14 +196,17 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator=", "==", etc + return CommandParamType.OPERATOR; + case BLOCK_STATE: - case BLOCK_PREDICATE: + return BlockTranslator.getAllBlockIdentifiers(); + case ITEM_STACK: - case ITEM_PREDICATE: - case COLOR: - case COMPONENT: - case OBJECTIVE: - case OBJECTIVE_CRITERIA: - case OPERATION: // Possibly OPERATOR - case PARTICLE: - case ROTATION: - case SCOREBOARD_SLOT: - case SCORE_HOLDER: - case SWIZZLE: - case TEAM: - case ITEM_SLOT: - case MOB_EFFECT: - case FUNCTION: - case ENTITY_ANCHOR: - case RANGE: - case FLOAT_RANGE: + return ItemRegistry.ITEM_NAMES; + case ITEM_ENCHANTMENT: + return Enchantment.ALL_JAVA_IDENTIFIERS; //TODO: inventory branch use Java enums + case ENTITY_SUMMON: - case DIMENSION: - case TIME: + return EntityType.ALL_JAVA_IDENTIFIERS; + + case COLOR: + return VALID_COLORS; + + case SCOREBOARD_SLOT: + return VALID_SCOREBOARD_SLOTS; + default: return CommandParamType.STRING; } } @Getter - private class ParamInfo { - private CommandNode paramNode; - private CommandParamData paramData; - private List children; + @ToString + private static class ParamInfo { + private final CommandNode paramNode; + private final CommandParamData paramData; + private final List children; /** * Create a new parameter info object @@ -252,33 +290,50 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator + // So if paramNode.getName() == "value" and enumData.getName() == "bool": + children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), this.paramNode.isExecutable(), enumData, type, null, Collections.emptyList()))); } } @@ -288,6 +343,64 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator + * Take the gamerule command, and let's present three "subcommands" you can perform: + * + *

    + *
  • gamerule doDaylightCycle true
  • + *
  • gamerule announceAdvancements false
  • + *
  • gamerule randomTickSpeed 3
  • + *
+ * + * While all three of them are indeed part of the same command, the command setting randomTickSpeed parses an int, + * while the others use boolean. In Bedrock, this should be presented as a separate overload to indicate that this + * does something a little different. + *

+ * Therefore, this function will return true if the first two are compared, as they use the same + * parsers. If the third is compared with either of the others, this function will return false. + *

+ * Here's an example of how the above would be presented to Bedrock (as of 1.16.200). Notice how the top two CommandParamData + * classes of each array are identical in type, but the following class is different: + *

+         *     overloads=[
+         *         [
+         *            CommandParamData(name=doDaylightCycle, optional=false, enumData=CommandEnumData(name=announceAdvancements, values=[announceAdvancements, doDaylightCycle], isSoft=false), type=STRING, postfix=null, options=[])
+         *            CommandParamData(name=value, optional=false, enumData=CommandEnumData(name=value, values=[true, false], isSoft=false), type=null, postfix=null, options=[])
+         *         ]
+         *         [
+         *            CommandParamData(name=randomTickSpeed, optional=false, enumData=CommandEnumData(name=randomTickSpeed, values=[randomTickSpeed], isSoft=false), type=STRING, postfix=null, options=[])
+         *            CommandParamData(name=value, optional=false, enumData=null, type=INT, postfix=null, options=[])
+         *         ]
+         *     ]
+         * 
+ * + * @return if these two can be merged into one overload. + */ + private boolean isCompatible(CommandNode[] allNodes, CommandNode a, CommandNode b) { + if (a == b) return true; + if (a.getParser() != b.getParser()) return false; + if (a.getChildIndices().length != b.getChildIndices().length) return false; + + for (int i = 0; i < a.getChildIndices().length; i++) { + boolean hasSimilarity = false; + CommandNode a1 = allNodes[a.getChildIndices()[i]]; + // Search "b" until we find a child that matches this one + for (int j = 0; j < b.getChildIndices().length; j++) { + if (isCompatible(allNodes, a1, allNodes[b.getChildIndices()[j]])) { + hasSimilarity = true; + break; + } + } + + if (!hasSimilarity) { + return false; + } + } + return true; + } + /** * Get the tree of every parameter node (recursive) * @@ -301,13 +414,10 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator childTree = child.getTree(); // Un-pack the tree append the child node to it and push into the list - for (CommandParamData[] subchild : childTree) { - CommandParamData[] tmpTree = new ArrayList() { - { - add(child.getParamData()); - addAll(Arrays.asList(subchild)); - } - }.toArray(new CommandParamData[0]); + for (CommandParamData[] subChild : childTree) { + CommandParamData[] tmpTree = new CommandParamData[subChild.length + 1]; + tmpTree[0] = child.getParamData(); + System.arraycopy(subChild, 0, tmpTree, 1, subChild.length); treeParamData.add(tmpTree); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java index 76e9b0958..8506389f3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java @@ -39,6 +39,9 @@ public class JavaKeepAliveTranslator extends PacketTranslator { titlePacket.setFadeInTime(packet.getFadeIn()); titlePacket.setFadeOutTime(packet.getFadeOut()); titlePacket.setStayTime(packet.getStay()); + titlePacket.setText(""); break; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java index 53c2864c8..735a5ea47 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java @@ -60,6 +60,9 @@ public class JavaEntityAnimationTranslator extends PacketTranslator iterator = session.getSkullCache().keySet().iterator(); while (iterator.hasNext()) { Vector3i position = iterator.next(); - if (Math.floor(position.getX() / 16) == packet.getX() && Math.floor(position.getZ() / 16) == packet.getZ()) { + if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) { session.getSkullCache().get(position).despawnEntity(session); iterator.remove(); } 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 4ffb96d00..f85ac293c 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 @@ -49,70 +49,72 @@ public class BlockStateValues { private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap(); private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap(); private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap(); + private static final Int2IntMap WATER_LEVEL = new Int2IntOpenHashMap(); /** * Determines if the block state contains Bedrock block information * - * @param entry The String to JsonNode map used in BlockTranslator + * @param javaId The Java Identifier of the block * @param javaBlockState the Java Block State of the block + * @param blockData JsonNode of info about the block from blocks.json */ - public static void storeBlockStateValues(Map.Entry entry, int javaBlockState) { - JsonNode bannerColor = entry.getValue().get("banner_color"); + public static void storeBlockStateValues(String javaId, int javaBlockState, JsonNode blockData) { + JsonNode bannerColor = blockData.get("banner_color"); if (bannerColor != null) { BANNER_COLORS.put(javaBlockState, (byte) bannerColor.intValue()); return; // There will never be a banner color and a skull variant } - JsonNode bedColor = entry.getValue().get("bed_color"); + JsonNode bedColor = blockData.get("bed_color"); if (bedColor != null) { BED_COLORS.put(javaBlockState, (byte) bedColor.intValue()); return; } - if (entry.getKey().contains("command_block")) { - COMMAND_BLOCK_VALUES.put(javaBlockState, entry.getKey().contains("conditional=true") ? (byte) 1 : (byte) 0); + if (javaId.contains("command_block")) { + COMMAND_BLOCK_VALUES.put(javaBlockState, javaId.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()) || - (entry.getValue().get("z") != null && entry.getValue().get("z").asBoolean())); - boolean isLeft = (entry.getValue().get("double_chest_position").asText().contains("left")); + if (blockData.get("double_chest_position") != null) { + boolean isX = (blockData.get("x") != null); + boolean isDirectionPositive = ((blockData.get("x") != null && blockData.get("x").asBoolean()) || + (blockData.get("z") != null && blockData.get("z").asBoolean())); + boolean isLeft = (blockData.get("double_chest_position").asText().contains("left")); DOUBLE_CHEST_VALUES.put(javaBlockState, new DoubleChestValue(isX, isDirectionPositive, isLeft)); return; } - if (entry.getKey().contains("potted_") || entry.getKey().contains("flower_pot")) { - FLOWER_POT_VALUES.put(javaBlockState, entry.getKey().replace("potted_", "")); + if (javaId.contains("potted_") || javaId.contains("flower_pot")) { + FLOWER_POT_VALUES.put(javaBlockState, javaId.replace("potted_", "")); return; } - JsonNode notePitch = entry.getValue().get("note_pitch"); + JsonNode notePitch = blockData.get("note_pitch"); if (notePitch != null) { - NOTEBLOCK_PITCHES.put(javaBlockState, entry.getValue().get("note_pitch").intValue()); + NOTEBLOCK_PITCHES.put(javaBlockState, blockData.get("note_pitch").intValue()); return; } - if (entry.getKey().contains("piston")) { + if (javaId.contains("piston")) { // True if extended, false if not - PISTON_VALUES.put(javaBlockState, entry.getKey().contains("extended=true")); - IS_STICKY_PISTON.put(javaBlockState, entry.getKey().contains("sticky")); + PISTON_VALUES.put(javaBlockState, javaId.contains("extended=true")); + IS_STICKY_PISTON.put(javaBlockState, javaId.contains("sticky")); return; } - JsonNode skullVariation = entry.getValue().get("variation"); + JsonNode skullVariation = blockData.get("variation"); if (skullVariation != null) { SKULL_VARIANTS.put(javaBlockState, (byte) skullVariation.intValue()); } - JsonNode skullRotation = entry.getValue().get("skull_rotation"); + JsonNode skullRotation = blockData.get("skull_rotation"); if (skullRotation != null) { SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue()); } - if (entry.getKey().contains("wall_skull") || entry.getKey().contains("wall_head")) { - String direction = entry.getKey().substring(entry.getKey().lastIndexOf("facing=") + 7); + if (javaId.contains("wall_skull") || javaId.contains("wall_head")) { + String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7); int rotation = 0; switch (direction.substring(0, direction.length() - 1)) { case "north": @@ -131,10 +133,16 @@ public class BlockStateValues { SKULL_WALL_DIRECTIONS.put(javaBlockState, rotation); } - JsonNode shulkerDirection = entry.getValue().get("shulker_direction"); + JsonNode shulkerDirection = blockData.get("shulker_direction"); if (shulkerDirection != null) { BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue()); } + + if (javaId.startsWith("minecraft:water")) { + String strLevel = javaId.substring(javaId.lastIndexOf("level=") + 6, javaId.length() - 1); + int level = Integer.parseInt(strLevel); + WATER_LEVEL.put(javaBlockState, level); + } } /** @@ -263,4 +271,15 @@ public class BlockStateValues { public static byte getShulkerBoxDirection(int state) { return SHULKERBOX_DIRECTIONS.getOrDefault(state, (byte) -1); } + + /** + * Get the level of water from the block state. + * This is used in FishingHookEntity to create splash sounds when the hook hits the water. + * + * @param state BlockState of the block + * @return The water level or -1 if the block isn't water + */ + public static int getWaterLevel(int state) { + return WATER_LEVEL.getOrDefault(state, -1); + } } 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 b047999e7..1d7c86a53 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 @@ -185,7 +185,7 @@ public class BlockTranslator { JAVA_ID_BLOCK_MAP.put(javaId, javaRuntimeId); - BlockStateValues.storeBlockStateValues(entry, javaRuntimeId); + BlockStateValues.storeBlockStateValues(entry.getKey(), javaRuntimeId, entry.getValue()); String cleanJavaIdentifier = entry.getKey().split("\\[")[0]; @@ -386,4 +386,11 @@ public class BlockTranslator { } return itemIdentifier; } + + /** + * @return a list of all Java block identifiers. For use with command suggestions. + */ + public static String[] getAllBlockIdentifiers() { + return JAVA_ID_TO_JAVA_IDENTIFIER_MAP.values().toArray(new String[0]); + } } 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 2dbd75852..761d1c3ab 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 @@ -27,6 +27,8 @@ 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.LongTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; import com.nukkitx.nbt.NbtList; import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.nbt.NbtType; @@ -39,7 +41,10 @@ import java.util.LinkedHashMap; public class EndGatewayBlockEntityTranslator extends BlockEntityTranslator { @Override public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) { - builder.put("Age", (int) ((long) tag.get("Age").getValue())); + Tag ageTag = tag.get("Age"); + if (ageTag instanceof LongTag) { + builder.put("Age", (int) ((long) ageTag.getValue())); + } // Java sometimes does not provide this tag, but Bedrock crashes if it doesn't exist // Linked coordinates IntList tagsList = new IntArrayList(); diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java index ae3abc943..5a0e41ed5 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java @@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; import lombok.AllArgsConstructor; import lombok.Getter; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.auth.BedrockClientData; @@ -80,7 +81,7 @@ public class SkinManager { String capeId, byte[] capeData, SkinProvider.SkinGeometry geometry) { SerializedSkin serializedSkin = SerializedSkin.of( - skinId, geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), + skinId, "", geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), ImageData.of(capeData), geometry.getGeometryData(), "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId ); @@ -163,7 +164,7 @@ public class SkinManager { geometry = SkinProvider.SkinGeometry.getEars(data.isAlex()); // Store the skin and geometry for the ears - SkinProvider.storeEarSkin(entity.getUuid(), skin); + SkinProvider.storeEarSkin(skin); SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex()); } } @@ -267,7 +268,10 @@ public class SkinManager { return new GameProfileData(skinUrl, capeUrl, isAlex); } catch (Exception exception) { - GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName() + ": " + exception.getMessage()); + GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName()); + if (GeyserConnector.getInstance().getConfig().isDebugMode()) { + exception.printStackTrace(); + } return loadBedrockOrOfflineSkin(profile); } } @@ -282,7 +286,7 @@ public class SkinManager { String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl(); String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl(); - if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) { + if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) { GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId()); if (session != null) { diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java index 0fa3fc698..96e390e75 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java @@ -79,13 +79,12 @@ public class SkinProvider { .build(); private static final Map> requestedCapes = new ConcurrentHashMap<>(); - public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false); private static final Map cachedGeometry = new ConcurrentHashMap<>(); public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars(); - public static String EARS_GEOMETRY; - public static String EARS_GEOMETRY_SLIM; - public static SkinGeometry SKULL_GEOMETRY; + public static final String EARS_GEOMETRY; + public static final String EARS_GEOMETRY_SLIM; + public static final SkinGeometry SKULL_GEOMETRY; public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -229,15 +228,15 @@ public class SkinProvider { return CompletableFuture.completedFuture(officialCape); } - public static CompletableFuture requestEars(String earsUrl, EarsProvider provider, boolean newThread, Skin skin) { + public static CompletableFuture requestEars(String earsUrl, boolean newThread, Skin skin) { if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin); CompletableFuture future; if (newThread) { - future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl, provider), EXECUTOR_SERVICE) + future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE) .whenCompleteAsync((outSkin, throwable) -> { }); } else { - Skin ears = supplyEars(skin, earsUrl, provider); // blocking + Skin ears = supplyEars(skin, earsUrl); // blocking future = CompletableFuture.completedFuture(ears); } return future; @@ -255,7 +254,7 @@ public class SkinProvider { public static CompletableFuture requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) { for (EarsProvider provider : EarsProvider.VALUES) { Skin skin1 = getOrDefault( - requestEars(provider.getUrlFor(playerId, username), provider, newThread, officialSkin), + requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin), officialSkin, 4 ); if (skin1.isEars()) { @@ -295,12 +294,11 @@ public class SkinProvider { } /** - * Stores the ajusted skin with the ear texture to the cache + * Stores the adjusted skin with the ear texture to the cache * - * @param playerID The UUID to cache it against * @param skin The skin to cache */ - public static void storeEarSkin(UUID playerID, Skin skin) { + public static void storeEarSkin(Skin skin) { cachedSkins.put(skin.getTextureUrl(), skin); } @@ -324,7 +322,7 @@ public class SkinProvider { } private static Cape supplyCape(String capeUrl, CapeProvider provider) { - byte[] cape = new byte[0]; + byte[] cape = EMPTY_CAPE.getCapeData(); try { cape = requestImage(capeUrl, provider); } catch (Exception ignored) {} // just ignore I guess @@ -334,7 +332,7 @@ public class SkinProvider { return new Cape( capeUrl, urlSection[urlSection.length - 1], // get the texture id and use it as cape id - cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(), + cape, System.currentTimeMillis(), cape.length == 0 ); @@ -345,10 +343,9 @@ public class SkinProvider { * * @param existingSkin The players current skin * @param earsUrl The URL to get the ears texture from - * @param provider The ears texture provider * @return The updated skin with ears */ - private static Skin supplyEars(Skin existingSkin, String earsUrl, EarsProvider provider) { + private static Skin supplyEars(Skin existingSkin, String earsUrl) { try { // Get the ears texture BufferedImage ears = ImageIO.read(new URL(earsUrl)); @@ -415,14 +412,15 @@ public class SkinProvider { // if the requested image is a cape if (provider != null) { - while(image.getWidth() > 64) { - image = scale(image); + if (image.getWidth() > 64) { + image = scale(image, 64, 32); + } + } else { + // Very rarely, skins can be larger than Minecraft's default. + // Bedrock will not render anything above a width of 128. + if (image.getWidth() > 128) { + image = scale(image, 128, image.getHeight() / (image.getWidth() / 128)); } - BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB); - Graphics g = newImage.createGraphics(); - g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null); - g.dispose(); - image = newImage; } byte[] data = bufferedImageToImageData(image); @@ -506,12 +504,13 @@ public class SkinProvider { return null; } - public static BufferedImage scale(BufferedImage bufferedImage) { - BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB); + public static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) { + BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = resized.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(bufferedImage, 0, 0, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, null); + g2.drawImage(bufferedImage, 0, 0, newWidth, newHeight, null); g2.dispose(); + bufferedImage.flush(); return resized; } @@ -578,17 +577,17 @@ public class SkinProvider { @AllArgsConstructor @Getter public static class SkinAndCape { - private Skin skin; - private Cape cape; + private final Skin skin; + private final Cape cape; } @AllArgsConstructor @Getter public static class Skin { private UUID skinOwner; - private String textureUrl; - private byte[] skinData; - private long requestedOn; + private final String textureUrl; + private final byte[] skinData; + private final long requestedOn; private boolean updated; private boolean ears; @@ -602,19 +601,19 @@ public class SkinProvider { @AllArgsConstructor @Getter public static class Cape { - private String textureUrl; - private String capeId; - private byte[] capeData; - private long requestedOn; - private boolean failed; + private final String textureUrl; + private final String capeId; + private final byte[] capeData; + private final long requestedOn; + private final boolean failed; } @AllArgsConstructor @Getter public static class SkinGeometry { - private String geometryName; - private String geometryData; - private boolean failed; + private final String geometryName; + private final String geometryData; + private final boolean failed; /** * Generate generic geometry diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java index 644323a42..7481b70bc 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java @@ -27,65 +27,42 @@ package org.geysermc.connector.skin; import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; -import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; +import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.Collections; -import java.util.UUID; import java.util.function.Consumer; public class SkullSkinManager extends SkinManager { - public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId, - String skinId, byte[] skinData) { + public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) { // Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png skinId = skinId + "_skull"; - SerializedSkin serializedSkin = SerializedSkin.of( - skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), + return SerializedSkin.of( + skinId, "", SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(), "", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId ); - - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid); - entry.setName(username); - entry.setEntityId(geyserId); - entry.setSkin(serializedSkin); - entry.setXuid(""); - entry.setPlatformChatId(""); - entry.setTeacher(false); - entry.setTrustedSkin(true); - return entry; } public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session, Consumer skinConsumer) { GameProfileData data = GameProfileData.from(entity.getProfile()); - SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false) + SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), true) .whenCompleteAsync((skin, throwable) -> { try { if (session.getUpstream().isInitialized()) { - PlayerListPacket.Entry updatedEntry = buildSkullEntryManually( - entity.getUuid(), - entity.getUsername(), - entity.getGeyserId(), - skin.getTextureUrl(), - skin.getSkinData() - ); - - PlayerListPacket playerAddPacket = new PlayerListPacket(); - playerAddPacket.setAction(PlayerListPacket.Action.ADD); - playerAddPacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerAddPacket); - - // It's a skull. We don't want them in the player list. - PlayerListPacket playerRemovePacket = new PlayerListPacket(); - playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE); - playerRemovePacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerRemovePacket); + PlayerSkinPacket packet = new PlayerSkinPacket(); + packet.setUuid(entity.getUuid()); + packet.setOldSkinName(""); + packet.setNewSkinName(skin.getTextureUrl()); + packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData())); + packet.setTrustedSkin(true); + session.sendUpstreamPacket(packet); } } catch (Exception e) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); diff --git a/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java b/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java index 5a49fd9be..816b718aa 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java @@ -26,7 +26,6 @@ package org.geysermc.connector.utils; import com.nukkitx.protocol.bedrock.packet.SetTitlePacket; -import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import java.util.concurrent.TimeUnit; @@ -36,11 +35,10 @@ import java.util.concurrent.TimeUnit; * Much of the work here is from the wonderful folks from ViaRewind: https://github.com/ViaVersion/ViaRewind */ public class CooldownUtils { + private static boolean SHOW_COOLDOWN; - private final static boolean SHOW_COOLDOWN; - - static { - SHOW_COOLDOWN = GeyserConnector.getInstance().getConfig().isShowCooldown(); + public static void setShowCooldown(boolean showCooldown) { + SHOW_COOLDOWN = showCooldown; } /** @@ -116,5 +114,4 @@ public class CooldownUtils { } return builder.toString(); } - } 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 d1dd6fd78..a96d29da2 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -90,21 +90,22 @@ public class FileUtils { */ public static File fileOrCopiedFromResource(File file, String name, Function format) throws IOException { if (!file.exists()) { + //noinspection ResultOfMethodCallIgnored file.createNewFile(); - FileOutputStream fos = new FileOutputStream(file); - InputStream input = GeyserConnector.class.getResourceAsStream("/" + name); // resources need leading "/" prefix + try (FileOutputStream fos = new FileOutputStream(file)) { + try (InputStream input = GeyserConnector.class.getResourceAsStream("/" + name)) { // resources need leading "/" prefix + byte[] bytes = new byte[input.available()]; - byte[] bytes = new byte[input.available()]; + //noinspection ResultOfMethodCallIgnored + input.read(bytes); - input.read(bytes); + for(char c : format.apply(new String(bytes)).toCharArray()) { + fos.write(c); + } - for(char c : format.apply(new String(bytes)).toCharArray()) { - fos.write(c); + fos.flush(); + } } - - fos.flush(); - input.close(); - fos.close(); } return file; @@ -122,14 +123,13 @@ public class FileUtils { file.createNewFile(); } - FileOutputStream fos = new FileOutputStream(file); + try (FileOutputStream fos = new FileOutputStream(file)) { + for (char c : data) { + fos.write(c); + } - for (char c : data) { - fos.write(c); + fos.flush(); } - - fos.flush(); - fos.close(); } /** @@ -232,9 +232,10 @@ public class FileUtils { try { int size = stream.available(); byte[] bytes = new byte[size]; - BufferedInputStream buf = new BufferedInputStream(stream); - buf.read(bytes, 0, bytes.length); - buf.close(); + try (BufferedInputStream buf = new BufferedInputStream(stream)) { + //noinspection ResultOfMethodCallIgnored + buf.read(bytes, 0, bytes.length); + } return bytes; } catch (IOException e) { throw new RuntimeException("Error while trying to read input stream!"); diff --git a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java index a70c291f0..5f5c1f17f 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -69,8 +69,8 @@ public class LanguageUtils { // Load the locale if (localeStream != null) { Properties localeProp = new Properties(); - try { - localeProp.load(new InputStreamReader(localeStream, StandardCharsets.UTF_8)); + try (InputStreamReader reader = new InputStreamReader(localeStream, StandardCharsets.UTF_8)) { + localeProp.load(reader); } catch (Exception e) { throw new AssertionError(getLocaleStringLog("geyser.language.load_failed", locale), e); } 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 f2ec43bf6..db5457244 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java @@ -101,7 +101,7 @@ public class LocaleUtils { ASSET_MAP.put(entry.getKey(), asset); } } catch (Exception e) { - GeyserConnector.getInstance().getLogger().info(LanguageUtils.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace()))); + GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace()))); } } @@ -147,6 +147,12 @@ public class LocaleUtils { } } } catch (IOException ignored) { } + + if (clientJarInfo == null) { + // Likely failed to download + GeyserConnector.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null."); + return; + } targetHash = clientJarInfo.getSha1(); } else { curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile)); @@ -168,9 +174,13 @@ public class LocaleUtils { return; } - // Get the hash and download the locale - String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash(); - WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString()); + try { + // Get the hash and download the locale + String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash(); + WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString()); + } catch (Exception e) { + GeyserConnector.getInstance().getLogger().error("Unable to download locale file hash", e); + } } /** @@ -236,31 +246,30 @@ public class LocaleUtils { WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString()); // Load in the JAR as a zip and extract the file - ZipFile localeJar = new ZipFile(tmpFilePath.toString()); - InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json")); - FileOutputStream outStream = new FileOutputStream(localeFile); + try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) { + try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"))) { + try (FileOutputStream outStream = new FileOutputStream(localeFile)) { - // Write the file to the locale dir - byte[] buf = new byte[fileStream.available()]; - int length; - while ((length = fileStream.read(buf)) != -1) { - outStream.write(buf, 0, length); + // Write the file to the locale dir + byte[] buf = new byte[fileStream.available()]; + int length; + while ((length = fileStream.read(buf)) != -1) { + outStream.write(buf, 0, length); + } + + // Flush all changes to disk and cleanup + outStream.flush(); + } + } } - // Flush all changes to disk and cleanup - outStream.flush(); - outStream.close(); - - fileStream.close(); - localeJar.close(); - // Store the latest jar hash FileUtils.writeFile(GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toString(), clientJarInfo.getSha1().toCharArray()); // Delete the nolonger needed client/server jar Files.delete(tmpFilePath); } catch (Exception e) { - throw new AssertionError(LanguageUtils.getLocaleStringLog("geyser.locale.fail.en_us"), e); + GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.locale.fail.en_us"), e); } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java index bcb1ffd50..91d1b782e 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java @@ -30,6 +30,8 @@ import org.geysermc.connector.GeyserConnector; import java.io.File; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** @@ -70,10 +72,12 @@ public class ResourcePack { pack.sha256 = FileUtils.calculateSHA256(file); + Stream stream = null; try { ZipFile zip = new ZipFile(file); - zip.stream().forEach((x) -> { + stream = zip.stream(); + stream.forEach((x) -> { if (x.getName().contains("manifest.json")) { try { ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class); @@ -94,6 +98,10 @@ public class ResourcePack { } catch (Exception e) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.resource_pack.broken", file.getName())); e.printStackTrace(); + } finally { + if (stream != null) { + stream.close(); + } } } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java index 874fb0620..ba008da41 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java @@ -88,8 +88,7 @@ public class WebUtils { } public static String post(String reqURL, String postContent) throws IOException { - URL url = null; - url = new URL(reqURL); + URL url = new URL(reqURL); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty("Content-Type", "text/plain"); @@ -112,7 +111,7 @@ public class WebUtils { */ private static String connectionToString(HttpURLConnection con) throws IOException { // Send the request (we dont use this but its required for getErrorStream() to work) - int code = con.getResponseCode(); + con.getResponseCode(); // Read the error message if there is one if not just read normally InputStream inputStream = con.getErrorStream(); @@ -120,18 +119,18 @@ public class WebUtils { inputStream = con.getInputStream(); } - BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); - String inputLine; - StringBuffer content = new StringBuffer(); + StringBuilder content = new StringBuilder(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) { + String inputLine; - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); - content.append("\n"); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + content.append("\n"); + } + + con.disconnect(); } - in.close(); - con.disconnect(); - return content.toString(); } } diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 234c4a697..7eccb249f 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -18,10 +18,19 @@ bedrock: # This option is for the plugin version only. clone-remote-port: false # 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." + # If either of these are empty, the respective string will default to "Geyser" + motd1: "Geyser" + motd2: "Another Geyser server." # 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" + # Whether to enable PROXY protocol or not for clients. You DO NOT WANT this feature unless you run UDP reverse proxy + # in front of your Geyser instance. + enable-proxy-protocol: false + # A list of allowed PROXY protocol speaking proxy IP addresses/subnets. Only effective when "enable-proxy-protocol" is enabled, and + # should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.). + # Keeping this list empty means there is no IP address whitelist. + # Both IP addresses and subnets are supported. + #proxy-protocol-whitelisted-ips: [ "127.0.0.1", "172.18.0.0/16" ] 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, @@ -81,7 +90,11 @@ legacy-ping-passthrough: false # Increase if you are getting BrokenPipe errors. ping-passthrough-interval: 3 -# Maximum amount of players that can connect +# Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate +# ping, it may also cause players to time out more easily. +forward-player-ping: false + +# Maximum amount of players that can connect. This is only visual at this time and does not actually limit player count. max-players: 100 # If debug messages should be sent through console diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings index 2e52b01cc..bf0610450 160000 --- a/connector/src/main/resources/mappings +++ b/connector/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 2e52b01cc541c8925346f93be8940087d9af1661 +Subproject commit bf0610450ce94507a18286e94af2965550ff9eaa