diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 326a1ebd5..17e88f268 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,34 +7,51 @@ 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** - -**Server Version** - +If applicable, add screenshots to help explain your problem. -**Geyser Version** - +**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/Jenkinsfile b/Jenkinsfile index e7f2ec4e2..501491361 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -26,7 +26,27 @@ pipeline { } steps { - sh 'mvn javadoc:jar source:jar deploy -DskipTests' + rtMavenDeployer( + id: "maven-deployer", + serverId: "opencollab-artifactory", + releaseRepo: "maven-releases", + snapshotRepo: "maven-snapshots" + ) + rtMavenResolver( + id: "maven-resolver", + serverId: "opencollab-artifactory", + releaseRepo: "release", + snapshotRepo: "snapshot" + ) + rtMavenRun( + pom: 'pom.xml', + goals: 'javadoc:jar source:jar install -DskipTests', + deployerId: "maven-deployer", + resolverId: "maven-resolver" + ) + rtPublishBuildInfo( + serverId: "opencollab-artifactory" + ) } } } @@ -69,5 +89,13 @@ pipeline { discordSend description: "**Build:** [${currentBuild.id}](${env.BUILD_URL})\n**Status:** [${currentBuild.currentResult}](${env.BUILD_URL})\n${changes}\n\n[**Artifacts on Jenkins**](https://ci.opencollab.dev/job/GeyserMC/job/Geyser)", footer: 'Open Collaboration Jenkins', link: env.BUILD_URL, successful: currentBuild.resultIsBetterOrEqualTo('SUCCESS'), title: "${env.JOB_NAME} #${currentBuild.id}", webhookURL: DISCORD_WEBHOOK } } + success { + script { + if (env.BRANCH_NAME == 'master') { + build propagate: false, wait: false, job: 'GeyserMC/Geyser-Fabric/java-1.16' + build propagate: false, wait: false, job: 'GeyserMC/GeyserAndroid/master' + } + } + } } } diff --git a/LICENSE b/LICENSE index acd4af141..0e368d546 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2020 GeyserMC. http://geysermc.org +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 diff --git a/README.md b/README.md index 816f765d6..1d1657edc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have now joined us here! -### Currently supporting Minecraft Bedrock v1.16.100/v1.16.101/v1.16.200 and Minecraft Java v1.16.4. +### Currently supporting Minecraft Bedrock v1.16.100 - v1.16.201 and Minecraft Java v1.16.4. ## Setting Up Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set up Geyser. diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 124967b0a..54e0d56ef 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -86,8 +86,8 @@ org.geysermc.platform.bungeecord.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.bungeecord.shaded.adventure + net.kyori + org.geysermc.platform.bungeecord.shaded.kyori diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index adaa7557f..93eebc3d2 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -97,8 +97,8 @@ org.geysermc.platform.spigot.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.spigot.shaded.adventure + net.kyori + org.geysermc.platform.spigot.shaded.kyori 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 74b9e03d4..86247300a 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 @@ -43,6 +43,7 @@ import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor; import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager; import org.geysermc.platform.spigot.command.SpigotCommandSender; +import org.geysermc.platform.spigot.world.GeyserSpigot1_11CraftingListener; import org.geysermc.platform.spigot.world.GeyserSpigotBlockPlaceListener; import org.geysermc.platform.spigot.world.manager.*; import us.myles.ViaVersion.api.Pair; @@ -146,8 +147,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.debug("Legacy version of Minecraft (1.15.2 or older) detected; not using 3D biomes."); } + boolean isPre1_12 = !isCompatible(Bukkit.getServer().getVersion(), "1.12.0"); // Set if we need to use a different method for getting a player's locale - SpigotCommandSender.setUseLegacyLocaleMethod(!isCompatible(Bukkit.getServer().getVersion(), "1.12.0")); + SpigotCommandSender.setUseLegacyLocaleMethod(isPre1_12); if (connector.getConfig().isUseAdapters()) { try { @@ -191,9 +193,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.debug("Using default world manager: " + this.geyserWorldManager.getClass()); } GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(connector, this.geyserWorldManager); - Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this); + if (isPre1_12) { + // Register events needed to send all recipes to the client + Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigot1_11CraftingListener(this, connector), this); + } + this.getCommand("geyser").setExecutor(new GeyserSpigotCommandExecutor(connector)); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java new file mode 100644 index 000000000..d20b7637d --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java @@ -0,0 +1,197 @@ +/* + * 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.platform.spigot.world; + +import com.github.steveice10.mc.protocol.MinecraftConstants; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient; +import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType; +import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; +import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; +import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.ShapelessRecipe; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.item.ItemTranslator; +import org.geysermc.connector.network.translators.item.RecipeRegistry; +import org.geysermc.platform.spigot.GeyserSpigotPlugin; +import us.myles.ViaVersion.api.Pair; +import us.myles.ViaVersion.api.data.MappingData; +import us.myles.ViaVersion.api.protocol.Protocol; +import us.myles.ViaVersion.api.protocol.ProtocolRegistry; +import us.myles.ViaVersion.api.protocol.ProtocolVersion; +import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.Protocol1_13To1_12_2; + +import java.util.*; + +/** + * Used to send all available recipes from the server to the client, as a valid recipe book packet won't be sent by the server. + * Requires ViaVersion. + */ +public class GeyserSpigot1_11CraftingListener implements Listener { + + private final GeyserConnector connector; + /** + * Specific mapping data for 1.12 to 1.13. Used to convert the 1.12 item into 1.13. + */ + private final MappingData mappingData1_12to1_13; + /** + * The list of all protocols from the client's version to 1.13. + */ + private final List> protocolList; + private final ProtocolVersion version; + + public GeyserSpigot1_11CraftingListener(GeyserSpigotPlugin plugin, GeyserConnector connector) { + this.connector = connector; + this.mappingData1_12to1_13 = ProtocolRegistry.getProtocol(Protocol1_13To1_12_2.class).getMappingData(); + this.protocolList = ProtocolRegistry.getProtocolPath(MinecraftConstants.PROTOCOL_VERSION, + ProtocolVersion.v1_13.getVersion()); + this.version = plugin.getServerProtocolVersion(); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + GeyserSession session = null; + for (GeyserSession otherSession : connector.getPlayers()) { + if (otherSession.getName().equals(event.getPlayer().getName())) { + session = otherSession; + break; + } + } + if (session == null) { + return; + } + + System.out.println("Sending recipes!"); + sendServerRecipes(session); + } + + public void sendServerRecipes(GeyserSession session) { + int netId = RecipeRegistry.LAST_RECIPE_NET_ID; + + CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); + craftingDataPacket.setCleanRecipes(true); + Iterator recipeIterator = Bukkit.getServer().recipeIterator(); + while (recipeIterator.hasNext()) { + Recipe recipe = recipeIterator.next(); + Pair outputs = translateToBedrock(session, recipe.getResult()); + ItemStack javaOutput = outputs.getKey(); + ItemData output = outputs.getValue(); + if (output.getId() == 0) continue; // If items make air we don't want that + boolean isNotAllAir = false; // Check for all-air recipes + if (recipe instanceof ShapedRecipe) { + ShapedRecipe shapedRecipe = (ShapedRecipe) recipe; + int size = shapedRecipe.getShape().length * shapedRecipe.getShape()[0].length(); + Ingredient[] ingredients = new Ingredient[size]; + ItemData[] input = new ItemData[size]; + for (int i = 0; i < input.length; i++) { + // Index is converting char to integer, adding i then converting back to char based on ASCII code + Pair result = translateToBedrock(session, shapedRecipe.getIngredientMap().get((char) ('a' + i))); + ingredients[i] = new Ingredient(new ItemStack[]{result.getKey()}); + input[i] = result.getValue(); + isNotAllAir = isNotAllAir || input[i].getId() != 0; + } + if (!isNotAllAir) continue; + UUID uuid = UUID.randomUUID(); + // Add recipe to our internal cache + ShapedRecipeData data = new ShapedRecipeData(shapedRecipe.getShape()[0].length(), shapedRecipe.getShape().length, + "", ingredients, javaOutput); + session.getCraftingRecipes().put(netId, + new com.github.steveice10.mc.protocol.data.game.recipe.Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data)); + // Add recipe for Bedrock + craftingDataPacket.getCraftingData().add(CraftingData.fromShaped(uuid.toString(), + shapedRecipe.getShape()[0].length(), shapedRecipe.getShape().length, Arrays.asList(input), + Collections.singletonList(output), uuid, "crafting_table", 0, netId++)); + } else if (recipe instanceof ShapelessRecipe) { + ShapelessRecipe shapelessRecipe = (ShapelessRecipe) recipe; + Ingredient[] ingredients = new Ingredient[shapelessRecipe.getIngredientList().size()]; + ItemData[] input = new ItemData[shapelessRecipe.getIngredientList().size()]; + for (int i = 0; i < input.length; i++) { + Pair result = translateToBedrock(session, shapelessRecipe.getIngredientList().get(i)); + ingredients[i] = new Ingredient(new ItemStack[]{result.getKey()}); + input[i] = result.getValue(); + isNotAllAir = isNotAllAir || input[i].getId() != 0; + } + if (!isNotAllAir) continue; + UUID uuid = UUID.randomUUID(); + // Add recipe to our internal cache + ShapelessRecipeData data = new ShapelessRecipeData("", ingredients, javaOutput); + session.getCraftingRecipes().put(netId, + new com.github.steveice10.mc.protocol.data.game.recipe.Recipe(RecipeType.CRAFTING_SHAPELESS, uuid.toString(), data)); + // Add recipe for Bedrock + craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(), + Arrays.asList(input), Collections.singletonList(output), uuid, "crafting_table", 0, netId++)); + } + } + + session.sendUpstreamPacket(craftingDataPacket); + } + + @SuppressWarnings("deprecation") + private Pair translateToBedrock(GeyserSession session, org.bukkit.inventory.ItemStack itemStack) { + if (itemStack != null && itemStack.getData() != null) { + if (itemStack.getType().getId() == 0) { + return new Pair<>(null, ItemData.AIR); + } + int legacyId = (itemStack.getType().getId() << 4) | (itemStack.getData().getData() & 0xFFFF); + if (itemStack.getType().getId() == 355 && itemStack.getData().getData() == (byte) 0) { // Handle bed color since the server will always be pre-1.12 + legacyId = (itemStack.getType().getId() << 4) | ((byte) 14 & 0xFFFF); + } + // old version -> 1.13 -> 1.13.1 -> 1.14 -> 1.15 -> 1.16 and so on + int itemId; + if (mappingData1_12to1_13.getItemMappings().containsKey(legacyId)) { + itemId = mappingData1_12to1_13.getNewItemId(legacyId); + } else if (mappingData1_12to1_13.getItemMappings().containsKey((itemStack.getType().getId() << 4) | (0))) { + itemId = mappingData1_12to1_13.getNewItemId((itemStack.getType().getId() << 4) | (0)); + } else { + // No ID found, just send back air + return new Pair<>(null, ItemData.AIR); + } + + for (int i = protocolList.size() - 1; i >= 0; i--) { + MappingData mappingData = protocolList.get(i).getValue().getMappingData(); + if (mappingData != null) { + itemId = mappingData.getNewItemId(itemId); + } + } + + ItemStack mcItemStack = new ItemStack(itemId, itemStack.getAmount()); + ItemData finalData = ItemTranslator.translateToBedrock(session, mcItemStack); + return new Pair<>(mcItemStack, finalData); + } + // Empty slot, most likely + return new Pair<>(null, ItemData.AIR); + } + +} diff --git a/bootstrap/sponge/pom.xml b/bootstrap/sponge/pom.xml index e6ce8f851..97c4ac8a4 100644 --- a/bootstrap/sponge/pom.xml +++ b/bootstrap/sponge/pom.xml @@ -86,8 +86,8 @@ org.geysermc.platform.sponge.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.sponge.shaded.adventure + net.kyori + org.geysermc.platform.sponge.shaded.kyori diff --git a/bootstrap/velocity/pom.xml b/bootstrap/velocity/pom.xml index 2fedca71a..5c0824def 100644 --- a/bootstrap/velocity/pom.xml +++ b/bootstrap/velocity/pom.xml @@ -82,8 +82,8 @@ org.geysermc.platform.velocity.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.velocity.shaded.adventure + net.kyori + org.geysermc.platform.velocity.shaded.kyori diff --git a/connector/pom.xml b/connector/pom.xml index 21f442df5..5be089afd 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -132,6 +132,10 @@ com.github.steveice10 packetlib + + com.github.steveice10 + mcauthlib + @@ -198,6 +202,11 @@ 4.13.1 test + + com.github.GeyserMC + MCAuthLib + 0e48a094f2 + diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 5dd00dabe..d61500e04 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -86,6 +86,11 @@ public class GeyserConnector { public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs public static final String VERSION = "DEV"; // A fallback for running in IDEs + /** + * Oauth client ID for Microsoft authentication + */ + public static final String OAUTH_CLIENT_ID = "204cefd1-4818-4de1-b98d-513fae875d88"; + private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"; private final List players = new ArrayList<>(); @@ -101,8 +106,8 @@ public class GeyserConnector { private final ScheduledExecutorService generalThreadPool; private BedrockServer bedrockServer; - private PlatformType platformType; - private GeyserBootstrap bootstrap; + private final PlatformType platformType; + private final GeyserBootstrap bootstrap; private Metrics metrics; 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 d21893f89..e21aa6bb8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -118,6 +118,8 @@ public interface GeyserConfiguration { String getAuthType(); + boolean isPasswordAuthentication(); + boolean isUseProxyProtocol(); } @@ -125,6 +127,12 @@ public interface GeyserConfiguration { String getEmail(); String getPassword(); + + /** + * Will be removed after Microsoft accounts are fully migrated + */ + @Deprecated + boolean isMicrosoftAccount(); } interface IMetricsInfo { 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 3a8946e00..7c9532ff8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -149,17 +149,24 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("auth-type") private String authType = "online"; + @JsonProperty("allow-password-authentication") + private boolean passwordAuthentication = true; + @JsonProperty("use-proxy-protocol") private boolean useProxyProtocol = false; } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) // DO NOT REMOVE THIS! Otherwise, after we remove microsoft-account configs will not load public static class UserAuthenticationInfo implements IUserAuthenticationInfo { @AsteriskSerializer.Asterisk() private String email; @AsteriskSerializer.Asterisk() private String password; + + @JsonProperty("microsoft-account") + private boolean microsoftAccount = false; } @Getter 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 488c0e90c..2b411109a 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java @@ -38,11 +38,11 @@ public class ItemedFireballEntity extends ThrowableEntity { } @Override - protected void updatePosition(GeyserSession session) { + 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. See if the MoveEntityDeltaPacket in 1.16.100 fixes this, and if not, - // TODO: only use this laggy movement for fireballs that be reflected + // 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); 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 553e558ea..4e0c25ab5 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java @@ -33,50 +33,35 @@ import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - /** * Used as a class for any object-like entity that moves as a projectile */ -public class ThrowableEntity extends Entity { +public class ThrowableEntity extends Entity implements Tickable { private Vector3f lastPosition; - /** - * Updates the position for the Bedrock client. - * - * Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions - */ - protected ScheduledFuture positionUpdater; public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); this.lastPosition = position; } + /** + * Updates the position for the Bedrock client. + * + * Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions + */ @Override - public void spawnEntity(GeyserSession session) { - super.spawnEntity(session); - positionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> { - if (session.isClosed()) { - positionUpdater.cancel(true); - return; - } - updatePosition(session); - }, 0, 50, TimeUnit.MILLISECONDS); - } - - protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); - } - - protected void updatePosition(GeyserSession session) { + public void tick(GeyserSession session) { super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround); float drag = getDrag(session); float gravity = getGravity(); 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); + } + /** * Get the gravity of this entity type. Used for applying gravity while the entity is in motion. * @@ -140,7 +125,6 @@ public class ThrowableEntity extends Entity { @Override public boolean despawnEntity(GeyserSession session) { - positionUpdater.cancel(true); if (entityType == EntityType.THROWN_ENDERPEARL) { LevelEventPacket particlePacket = new LevelEventPacket(); particlePacket.setType(LevelEventType.PARTICLE_TELEPORT); diff --git a/connector/src/main/java/org/geysermc/connector/entity/Tickable.java b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java new file mode 100644 index 000000000..a7d571ccb --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java @@ -0,0 +1,35 @@ +/* + * 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.entity; + +import org.geysermc.connector.network.session.GeyserSession; + +/** + * Implemented onto anything that should have code ran every Minecraft tick - 50 milliseconds. + */ +public interface Tickable { + void tick(GeyserSession session); +} diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java index 7dbd96a44..621679798 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java @@ -28,19 +28,28 @@ package org.geysermc.connector.entity.living.monster; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.AttributeData; +import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; -import com.nukkitx.protocol.bedrock.packet.AddEntityPacket; -import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; +import com.nukkitx.protocol.bedrock.packet.*; import lombok.Data; +import org.geysermc.connector.entity.Tickable; +import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.entity.living.InsentientEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.AttributeUtils; +import org.geysermc.connector.utils.DimensionUtils; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; -public class EnderDragonEntity extends InsentientEntity { +public class EnderDragonEntity extends InsentientEntity implements Tickable { /** * The Ender Dragon has multiple hit boxes, which * are each its own invisible entity @@ -61,9 +70,19 @@ public class EnderDragonEntity extends InsentientEntity { private final Segment[] segmentHistory = new Segment[19]; private int latestSegment = -1; - private boolean hovering; + private int phase; + /** + * The number of ticks since the beginning of the phase + */ + private int phaseTicks; - private ScheduledFuture partPositionUpdater; + private int ticksTillNextGrowl = 100; + + /** + * Used to determine when the wing flap sound should be played + */ + private float wingPosition; + private float lastWingPosition; public EnderDragonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); @@ -73,49 +92,67 @@ public class EnderDragonEntity extends InsentientEntity { @Override public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { - // Phase - if (entityMetadata.getId() == 15) { - int value = (int) entityMetadata.getValue(); - if (value == 5) { - // Performing breath attack + if (entityMetadata.getId() == 15) { // Phase + phase = (int) entityMetadata.getValue(); + phaseTicks = 0; + metadata.getFlags().setFlag(EntityFlag.SITTING, isSitting()); + } + + super.updateBedrockMetadata(entityMetadata, session); + + if (entityMetadata.getId() == 8) { // Health + // Update the health attribute, so that the death animation gets played + // Round health up, so that Bedrock doesn't consider the dragon to be dead when health is between 0 and 1 + float health = (float) Math.ceil(metadata.getFloat(EntityData.HEALTH)); + if (phase == 9 && health <= 0) { // Dying phase EntityEventPacket entityEventPacket = new EntityEventPacket(); - entityEventPacket.setType(EntityEventType.DRAGON_FLAMING); + entityEventPacket.setType(EntityEventType.ENDER_DRAGON_DEATH); entityEventPacket.setRuntimeEntityId(geyserId); entityEventPacket.setData(0); session.sendUpstreamPacket(entityEventPacket); } - metadata.getFlags().setFlag(EntityFlag.SITTING, value == 5 || value == 6 || value == 7); - hovering = value == 10; + attributes.put(AttributeType.HEALTH, AttributeType.HEALTH.getAttribute(health, 200)); + updateBedrockAttributes(session); } - super.updateBedrockMetadata(entityMetadata, session); + } + + /** + * Send an updated list of attributes to the Bedrock client. + * This is overwritten to allow the health attribute to differ from + * the health specified in the metadata. + * + * @param session GeyserSession + */ + @Override + public void updateBedrockAttributes(GeyserSession session) { + if (!valid) return; + + List attributes = new ArrayList<>(); + for (Map.Entry entry : this.attributes.entrySet()) { + if (!entry.getValue().getType().isBedrockAttribute()) + continue; + attributes.add(AttributeUtils.getBedrockAttribute(entry.getValue())); + } + + UpdateAttributesPacket updateAttributesPacket = new UpdateAttributesPacket(); + updateAttributesPacket.setRuntimeEntityId(geyserId); + updateAttributesPacket.setAttributes(attributes); + session.sendUpstreamPacket(updateAttributesPacket); } @Override public void spawnEntity(GeyserSession session) { - AddEntityPacket addEntityPacket = new AddEntityPacket(); - addEntityPacket.setIdentifier("minecraft:" + entityType.name().toLowerCase()); - addEntityPacket.setRuntimeEntityId(geyserId); - addEntityPacket.setUniqueEntityId(geyserId); - addEntityPacket.setPosition(position); - addEntityPacket.setMotion(motion); - addEntityPacket.setRotation(getBedrockRotation()); - addEntityPacket.setEntityType(entityType.getType()); - addEntityPacket.getMetadata().putAll(metadata); + super.spawnEntity(session); - // Otherwise dragon is always 'dying' - addEntityPacket.getAttributes().add(new AttributeData("minecraft:health", 0.0f, 200f, 200f, 200f)); - - valid = true; - session.sendUpstreamPacket(addEntityPacket); - - head = new EnderDragonPartEntity(entityId + 1, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 1, 1); - neck = new EnderDragonPartEntity(entityId + 2, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 3, 3); - body = new EnderDragonPartEntity(entityId + 3, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 5, 3); - leftWing = new EnderDragonPartEntity(entityId + 4, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2); - rightWing = new EnderDragonPartEntity(entityId + 5, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2); + AtomicLong nextEntityId = session.getEntityCache().getNextEntityId(); + head = new EnderDragonPartEntity(entityId + 1, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 1, 1); + neck = new EnderDragonPartEntity(entityId + 2, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 3, 3); + body = new EnderDragonPartEntity(entityId + 3, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 5, 3); + leftWing = new EnderDragonPartEntity(entityId + 4, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2); + rightWing = new EnderDragonPartEntity(entityId + 5, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2); tail = new EnderDragonPartEntity[3]; for (int i = 0; i < 3; i++) { - tail[i] = new EnderDragonPartEntity(entityId + 6 + i, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 2, 2); + tail[i] = new EnderDragonPartEntity(entityId + 6 + i, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 2, 2); } allParts = new EnderDragonPartEntity[]{head, neck, body, leftWing, rightWing, tail[0], tail[1], tail[2]}; @@ -129,25 +166,25 @@ public class EnderDragonEntity extends InsentientEntity { segmentHistory[i].yaw = rotation.getZ(); segmentHistory[i].y = position.getY(); } - - partPositionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> { - pushSegment(); - updateBoundingBoxes(session); - }, 0, 50, TimeUnit.MILLISECONDS); - - session.getConnector().getLogger().debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); } @Override public boolean despawnEntity(GeyserSession session) { - partPositionUpdater.cancel(true); - for (EnderDragonPartEntity part : allParts) { part.despawnEntity(session); } return super.despawnEntity(session); } + @Override + public void tick(GeyserSession session) { + effectTick(session); + if (!metadata.getFlags().getFlag(EntityFlag.NO_AI) && isAlive()) { + pushSegment(); + updateBoundingBoxes(session); + } + } + /** * Updates the positions of the Ender Dragon's multiple bounding boxes * @@ -163,7 +200,7 @@ public class EnderDragonEntity extends InsentientEntity { // Lowers the head when the dragon sits/hovers float headDuck; - if (hovering || metadata.getFlags().getFlag(EntityFlag.SITTING)) { + if (isHovering() || isSitting()) { headDuck = -1f; } else { headDuck = baseSegment.y - getSegment(0).y; @@ -193,6 +230,105 @@ public class EnderDragonEntity extends InsentientEntity { } } + /** + * Handles the particles and sounds of the Ender Dragon + * @param session GeyserSession. + */ + private void effectTick(GeyserSession session) { + Random random = ThreadLocalRandom.current(); + if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) { + if (Math.cos(wingPosition * 2f * Math.PI) <= -0.3f && Math.cos(lastWingPosition * 2f * Math.PI) >= -0.3f) { + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("mob.enderdragon.flap"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(5f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + + if (!isSitting() && !isHovering() && ticksTillNextGrowl-- == 0) { + playGrowlSound(session); + ticksTillNextGrowl = 200 + random.nextInt(200); + } + + lastWingPosition = wingPosition; + } + if (isAlive()) { + if (metadata.getFlags().getFlag(EntityFlag.NO_AI)) { + wingPosition = 0.5f; + } else if (isHovering() || isSitting()) { + wingPosition += 0.1f; + } else { + double speed = motion.length(); + wingPosition += 0.2f / (speed * 10f + 1) * Math.pow(2, motion.getY()); + } + + phaseTicks++; + if (phase == 3) { // Landing Phase + float headHeight = head.getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT); + Vector3f headCenter = head.getPosition().up(headHeight * 0.5f); + + for (int i = 0; i < 8; i++) { + Vector3f particlePos = headCenter.add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f); + // This is missing velocity information + LevelEventPacket particlePacket = new LevelEventPacket(); + particlePacket.setType(LevelEventType.PARTICLE_DRAGONS_BREATH); + particlePacket.setPosition(particlePos); + session.sendUpstreamPacket(particlePacket); + } + } else if (phase == 5) { // Sitting Flaming Phase + if (phaseTicks % 2 == 0 && phaseTicks < 10) { + // Performing breath attack + // Entity event DRAGON_FLAMING seems to create particles from the origin of the dragon, + // so we need to manually spawn particles + for (int i = 0; i < 8; i++) { + SpawnParticleEffectPacket spawnParticleEffectPacket = new SpawnParticleEffectPacket(); + spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); + spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f)); + spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire"); + session.sendUpstreamPacket(spawnParticleEffectPacket); + } + } + } else if (phase == 7) { // Sitting Attacking Phase + playGrowlSound(session); + } else if (phase == 9) { // Dying Phase + // Send explosion particles as the dragon move towards the end portal + if (phaseTicks % 10 == 0) { + float xOffset = 8f * (random.nextFloat() - 0.5f); + float yOffset = 4f * (random.nextFloat() - 0.5f) + 2f; + float zOffset = 8f * (random.nextFloat() - 0.5f); + Vector3f particlePos = position.add(xOffset, yOffset, zOffset); + LevelEventPacket particlePacket = new LevelEventPacket(); + particlePacket.setType(LevelEventType.PARTICLE_EXPLOSION); + particlePacket.setPosition(particlePos); + session.sendUpstreamPacket(particlePacket); + } + } + } + } + + private void playGrowlSound(GeyserSession session) { + Random random = ThreadLocalRandom.current(); + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("mob.enderdragon.growl"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(2.5f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + + private boolean isAlive() { + return metadata.getFloat(EntityData.HEALTH) > 0; + } + + private boolean isHovering() { + return phase == 10; + } + + private boolean isSitting() { + return phase == 5 || phase == 6 || phase == 7; + } + /** * Store the current yaw and y into the circular buffer */ diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java index 095d12b2c..288a3e423 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java @@ -32,11 +32,12 @@ import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.type.EntityType; public class EnderDragonPartEntity extends Entity { - public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, float width, float height) { - super(entityId, geyserId, entityType, position, motion, rotation); + public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, float width, float height) { + super(entityId, geyserId, entityType, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO); metadata.put(EntityData.BOUNDING_BOX_WIDTH, width); metadata.put(EntityData.BOUNDING_BOX_HEIGHT, height); metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); + metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java index e12b60d61..3151ae474 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java @@ -27,8 +27,10 @@ package org.geysermc.connector.entity.living.monster; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.SoundEvent; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; @@ -45,11 +47,21 @@ public class EndermanEntity extends MonsterEntity { if (entityMetadata.getId() == 15) { metadata.put(EntityData.CARRIED_BLOCK, BlockTranslator.getBedrockBlockId((int) entityMetadata.getValue())); } - // 'Angry' - mouth open + // "Is screaming" - controls sound if (entityMetadata.getId() == 16) { + if ((boolean) entityMetadata.getValue()) { + LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet(); + packet.setSound(SoundEvent.STARE); + packet.setPosition(this.position); + packet.setExtraData(-1); + packet.setIdentifier("minecraft:enderman"); + session.sendUpstreamPacket(packet); + } + } + // "Is staring/provoked" - controls visuals + if (entityMetadata.getId() == 17) { metadata.getFlags().setFlag(EntityFlag.ANGRY, (boolean) entityMetadata.getValue()); } - // TODO: ID 17 is stared at but I don't believe it's used - maybe only for the sound effect. Check after particle merge super.updateBedrockMetadata(entityMetadata, session); } } diff --git a/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java index 76f7674a9..7cdaf1801 100644 --- a/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java @@ -95,7 +95,11 @@ public class GeyserItemStack { } public ItemStack getItemStack() { - return isEmpty() ? null : new ItemStack(javaId, amount, nbt); + return getItemStack(amount); + } + + public ItemStack getItemStack(int newAmount) { + return isEmpty() ? null : new ItemStack(javaId, newAmount, nbt); } public ItemData getItemData(GeyserSession session) { 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 79c04f674..87883087d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -30,15 +30,16 @@ import com.nukkitx.protocol.bedrock.BedrockServerEventHandler; import com.nukkitx.protocol.bedrock.BedrockServerSession; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.socket.DatagramPacket; -import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.network.translators.chat.MessageTranslator; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.LanguageUtils; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; public class ConnectorServerEventHandler implements BedrockServerEventHandler { @@ -94,6 +95,20 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { pong.setMaximumPlayerCount(config.getMaxPlayers()); } + // 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) { + // If the top MOTD is still too long, we chop it down + byte[] newMotdArray = new byte[339]; + System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); + pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8)); + } + } + //Bedrock will not even attempt a connection if the client thinks the server is full //so we have to fake it not being full if (pong.getPlayerCount() >= pong.getMaximumPlayerCount()) { diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index a6a369e45..3922a95ff 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -161,6 +161,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { if (info != null) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().getName())); + session.setMicrosoftAccount(info.isMicrosoftAccount()); session.authenticate(info.getEmail(), info.getPassword()); // TODO send a message to bedrock user telling them they are connected (if nothing like a motd 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 e2587ee08..5d8ca3925 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 @@ -26,8 +26,12 @@ package org.geysermc.connector.network.session; import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.mc.auth.exception.request.AuthPendingException; import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException; import com.github.steveice10.mc.auth.exception.request.RequestException; +import com.github.steveice10.mc.auth.service.AuthenticationService; +import com.github.steveice10.mc.auth.service.MojangAuthenticationService; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.github.steveice10.mc.protocol.MinecraftConstants; import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol; @@ -36,9 +40,9 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; import com.github.steveice10.mc.protocol.data.game.statistic.Statistic; import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionRotationPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket; -import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket; import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket; import com.github.steveice10.packetlib.BuiltinFlags; import com.github.steveice10.packetlib.Client; @@ -69,6 +73,7 @@ import lombok.Setter; import org.geysermc.common.window.CustomFormWindow; import org.geysermc.common.window.FormWindow; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.Entity; @@ -113,8 +118,13 @@ public class GeyserSession implements CommandSender { @Setter private BedrockClientData clientData; + @Deprecated + @Setter + private boolean microsoftAccount; + private final SessionPlayerEntity playerEntity; + private BookEditCache bookEditCache; private ChunkCache chunkCache; private EntityCache entityCache; private EntityEffectCache effectCache; @@ -178,7 +188,11 @@ public class GeyserSession implements CommandSender { @Setter private GameMode gameMode = GameMode.SURVIVAL; - private final AtomicInteger pendingDimSwitches = new AtomicInteger(0); + /** + * Keeps track of the world name for respawning. + */ + @Setter + private String worldName = null; private boolean sneaking; @@ -215,9 +229,6 @@ public class GeyserSession implements CommandSender { @Setter private Vector3i lastInteractionPosition = Vector3i.ZERO; - private boolean manyDimPackets = false; - private ServerRespawnPacket lastDimPacket = null; - @Setter private Entity ridingVehicleEntity; @@ -232,6 +243,7 @@ public class GeyserSession implements CommandSender { @Setter private Int2ObjectMap craftingRecipes; private final Set unlockedRecipes; + private AtomicInteger lastRecipeNetId; /** * Saves a list of all stonecutter recipes, for use in a stonecutter inventory. @@ -279,15 +291,14 @@ public class GeyserSession implements CommandSender { private ScheduledFuture bucketScheduledFuture; /** - * Sends a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. + * Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. */ @Setter - private ScheduledFuture movementSendIfIdle; + private long lastMovementTimestamp = System.currentTimeMillis(); /** * Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless. */ - @Setter private boolean daylightCycle = true; private boolean reducedDebugInfo = false; @@ -362,12 +373,18 @@ public class GeyserSession implements CommandSender { private List selectedEmotes = new ArrayList<>(); private final Set emotes = new HashSet<>(); + /** + * The thread that will run every 50 milliseconds - one Minecraft tick. + */ + private ScheduledFuture tickThread = null; + private MinecraftProtocol protocol; public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) { this.connector = connector; this.upstream = new UpstreamSession(bedrockServerSession); + this.bookEditCache = new BookEditCache(this); this.chunkCache = new ChunkCache(this); this.entityCache = new EntityCache(this); this.effectCache = new EntityEffectCache(); @@ -385,6 +402,7 @@ public class GeyserSession implements CommandSender { this.inventoryFuture = CompletableFuture.completedFuture(null); this.craftingRecipes = new Int2ObjectOpenHashMap<>(); this.unlockedRecipes = new ObjectOpenHashSet<>(); + this.lastRecipeNetId = new AtomicInteger(1); this.spawned = false; this.loggedIn = false; @@ -471,146 +489,22 @@ public class GeyserSession implements CommandSender { new Thread(() -> { try { if (password != null && !password.isEmpty()) { - protocol = new MinecraftProtocol(username, password); + AuthenticationService authenticationService; + if (microsoftAccount) { + authenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + } else { + authenticationService = new MojangAuthenticationService(); + } + authenticationService.setUsername(username); + authenticationService.setPassword(password); + authenticationService.login(); + + protocol = new MinecraftProtocol(authenticationService); } else { protocol = new MinecraftProtocol(username); } - boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; - final PublicKey publicKey; - - if (floodgate) { - PublicKey key = null; - try { - key = EncryptionUtil.getKeyFromFile( - connector.getConfig().getFloodgateKeyPath(), - PublicKey.class - ); - } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e); - } - publicKey = key; - } else publicKey = null; - - if (publicKey != null) { - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); - } - - downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); - if (connector.getConfig().getRemote().isUseProxyProtocol()) { - 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); - downstream.getSession().addListener(new SessionAdapter() { - @Override - public void packetSending(PacketSendingEvent event) { - //todo move this somewhere else - if (event.getPacket() instanceof HandshakePacket && floodgate) { - String encrypted = ""; - try { - encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData( - clientData.getGameVersion(), - authData.getName(), - authData.getXboxUUID(), - clientData.getDeviceOS().ordinal(), - clientData.getLanguageCode(), - clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() - )); - } catch (Exception e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); - } - - HandshakePacket handshakePacket = event.getPacket(); - event.setPacket(new HandshakePacket( - handshakePacket.getProtocolVersion(), - handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted, - handshakePacket.getPort(), - handshakePacket.getIntent() - )); - } - } - - @Override - public void connected(ConnectedEvent event) { - loggingIn = false; - loggedIn = true; - if (protocol.getProfile() == null) { - // Java account is offline - disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); - return; - } - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); - playerEntity.setUuid(protocol.getProfile().getId()); - playerEntity.setUsername(protocol.getProfile().getName()); - - String locale = clientData.getLanguageCode(); - - // Let the user know there locale may take some time to download - // as it has to be extracted from a JAR - if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { - // This should probably be left hardcoded as it will only show for en_us clients - sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); - } - - // Download and load the language for the player - LocaleUtils.downloadAndLoadLocale(locale); - } - - @Override - public void disconnected(DisconnectedEvent event) { - loggingIn = false; - loggedIn = false; - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); - if (event.getCause() != null) { - event.getCause().printStackTrace(); - } - - upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); - } - - @Override - public void packetReceived(PacketReceivedEvent event) { - if (!closed) { - //handle consecutive respawn packets - if (event.getPacket().getClass().equals(ServerRespawnPacket.class)) { - manyDimPackets = lastDimPacket != null; - lastDimPacket = event.getPacket(); - return; - } else if (lastDimPacket != null) { - PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(lastDimPacket.getClass(), lastDimPacket, GeyserSession.this); - lastDimPacket = null; - } - - // Required, or else Floodgate players break with Bukkit chunk caching - if (event.getPacket() instanceof LoginSuccessPacket) { - GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); - playerEntity.setUsername(profile.getName()); - playerEntity.setUuid(profile.getId()); - - // Check if they are not using a linked account - if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { - SkinManager.handleBedrockSkin(playerEntity, clientData); - } - } - - PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); - } - } - - @Override - public void packetError(PacketErrorEvent event) { - connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); - if (connector.getConfig().isDebugMode()) - event.getCause().printStackTrace(); - event.setSuppress(true); - } - }); - - downstream.getSession().connect(); - connector.addPlayer(this); + connectDownstream(); } catch (InvalidCredentialsException | IllegalArgumentException e) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username)); disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); @@ -620,6 +514,199 @@ public class GeyserSession implements CommandSender { }).start(); } + /** + * Present a form window to the user asking to log in with another web browser + */ + public void authenticateWithMicrosoftCode() { + if (loggedIn) { + connector.getLogger().severe(LanguageUtils.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().getName())); + return; + } + + loggingIn = true; + // new thread so clients don't timeout + new Thread(() -> { + try { + MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + + MsaAuthenticationService.MsCodeResponse response = msaAuthenticationService.getAuthCode(); + LoginEncryptionUtils.showMicrosoftCodeWindow(this, response); + + // This just looks cool + SetTimePacket packet = new SetTimePacket(); + packet.setTime(16000); + sendUpstreamPacket(packet); + + // Wait for the code to validate + attemptCodeAuthentication(msaAuthenticationService); + } catch (InvalidCredentialsException | IllegalArgumentException e) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", getAuthData().getName())); + disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); + } catch (RequestException ex) { + ex.printStackTrace(); + } + }).start(); + } + + /** + * Poll every second to see if the user has successfully signed in + */ + private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) { + if (loggedIn || closed) { + return; + } + try { + msaAuthenticationService.login(); + protocol = new MinecraftProtocol(msaAuthenticationService); + + connectDownstream(); + } catch (RequestException e) { + if (!(e instanceof AuthPendingException)) { + e.printStackTrace(); + } else { + // Wait one second before trying again + connector.getGeneralThreadPool().schedule(() -> attemptCodeAuthentication(msaAuthenticationService), 1, TimeUnit.SECONDS); + } + } + } + + /** + * After getting whatever credentials needed, we attempt to join the Java server. + */ + private void connectDownstream() { + boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; + final PublicKey publicKey; + + if (floodgate) { + PublicKey key = null; + try { + key = EncryptionUtil.getKeyFromFile( + connector.getConfig().getFloodgateKeyPath(), + PublicKey.class + ); + } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e); + } + publicKey = key; + } else publicKey = null; + + if (publicKey != null) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); + } + + // Start ticking + tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); + + downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); + if (connector.getConfig().getRemote().isUseProxyProtocol()) { + 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); + downstream.getSession().addListener(new SessionAdapter() { + @Override + public void packetSending(PacketSendingEvent event) { + //todo move this somewhere else + if (event.getPacket() instanceof HandshakePacket && floodgate) { + String encrypted = ""; + try { + encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData( + clientData.getGameVersion(), + authData.getName(), + authData.getXboxUUID(), + clientData.getDeviceOS().ordinal(), + clientData.getLanguageCode(), + clientData.getCurrentInputMode().ordinal(), + upstream.getSession().getAddress().getAddress().getHostAddress() + )); + } catch (Exception e) { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); + } + + HandshakePacket handshakePacket = event.getPacket(); + event.setPacket(new HandshakePacket( + handshakePacket.getProtocolVersion(), + handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted, + handshakePacket.getPort(), + handshakePacket.getIntent() + )); + } + } + + @Override + public void connected(ConnectedEvent event) { + loggingIn = false; + loggedIn = true; + if (protocol.getProfile() == null) { + // Java account is offline + disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); + return; + } + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); + playerEntity.setUuid(protocol.getProfile().getId()); + playerEntity.setUsername(protocol.getProfile().getName()); + + String locale = clientData.getLanguageCode(); + + // Let the user know there locale may take some time to download + // as it has to be extracted from a JAR + if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { + // This should probably be left hardcoded as it will only show for en_us clients + sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); + } + + // Download and load the language for the player + LocaleUtils.downloadAndLoadLocale(locale); + } + + @Override + public void disconnected(DisconnectedEvent event) { + loggingIn = false; + loggedIn = false; + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); + if (event.getCause() != null) { + event.getCause().printStackTrace(); + } + + upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); + } + + @Override + public void packetReceived(PacketReceivedEvent event) { + if (!closed) { + // Required, or else Floodgate players break with Bukkit chunk caching + if (event.getPacket() instanceof LoginSuccessPacket) { + GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); + playerEntity.setUsername(profile.getName()); + playerEntity.setUuid(profile.getId()); + + // Check if they are not using a linked account + if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { + SkinManager.handleBedrockSkin(playerEntity, clientData); + } + } + + PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); + } + } + + @Override + public void packetError(PacketErrorEvent event) { + connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); + if (connector.getConfig().isDebugMode()) + event.getCause().printStackTrace(); + event.setSuppress(true); + } + }); + + if (!daylightCycle) { + setDaylightCycle(true); + } + downstream.getSession().connect(); + connector.addPlayer(this); + } + public void disconnect(String reason) { if (!closed) { loggedIn = false; @@ -632,6 +719,11 @@ public class GeyserSession implements CommandSender { } } + if (tickThread != null) { + tickThread.cancel(true); + } + + this.bookEditCache = null; this.chunkCache = null; this.entityCache = null; this.effectCache = null; @@ -645,6 +737,28 @@ public class GeyserSession implements CommandSender { disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.close", getClientData().getLanguageCode())); } + /** + * Called every 50 milliseconds - one Minecraft tick. + */ + public void tick() { + // Check to see if the player's position needs updating - a position update should be sent once every 3 seconds + if (spawned && (System.currentTimeMillis() - lastMovementTimestamp) > 3000) { + // Recalculate in case something else changed position + Vector3d position = collisionManager.adjustBedrockPosition(playerEntity.getPosition(), playerEntity.isOnGround()); + // A null return value cancels the packet + if (position != null) { + ClientPlayerPositionPacket packet = new ClientPlayerPositionPacket(playerEntity.isOnGround(), + position.getX(), position.getY(), position.getZ()); + sendDownstreamPacket(packet); + } + lastMovementTimestamp = System.currentTimeMillis(); + } + + for (Tickable entity : entityCache.getTickableEntities()) { + entity.tick(this); + } + } + public void setAuthenticationData(AuthData authData) { this.authData = authData; } @@ -921,6 +1035,18 @@ public class GeyserSession implements CommandSender { reducedDebugInfo = value; } + /** + * Changes the daylight cycle gamerule on the client + * This is used in the login screen along-side normal usage + * + * @param doCycle If the cycle should continue + */ + public void setDaylightCycle(boolean doCycle) { + sendGameRule("dodaylightcycle", doCycle); + // Save the value so we don't have to constantly send a daylight cycle gamerule update + this.daylightCycle = doCycle; + } + /** * Send a gamerule value to the client * diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java new file mode 100644 index 000000000..c82645dbf --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java @@ -0,0 +1,75 @@ +/* + * 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.session.cache; + +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket; +import lombok.Setter; +import org.geysermc.connector.inventory.GeyserItemStack; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.item.ItemRegistry; + +/** + * Manages updating the current writable book. + * + * Java sends book updates less frequently than Bedrock, and this can cause issues with servers that rate limit + * book packets. Because of this, we need to ensure packets are only send every second or so at maximum. + */ +public class BookEditCache { + private final GeyserSession session; + @Setter + private ClientEditBookPacket packet; + /** + * Stores the last time a book update packet was sent to the server. + */ + private long lastBookUpdate; + + public BookEditCache(GeyserSession session) { + this.session = session; + } + + /** + * Check to see if there is a book edit update to send, and if so, send it. + */ + public void checkForSend() { + if (packet == null) { + // No new packet has to be sent + return; + } + // Prevent kicks due to rate limiting - specifically on Spigot servers + if ((System.currentTimeMillis() - lastBookUpdate) < 1000) { + return; + } + // Don't send the update if the player isn't not holding a book, shouldn't happen if we catch all interactions + GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(); + if (itemStack == null || itemStack.getJavaId() != ItemRegistry.WRITABLE_BOOK.getJavaId()) { + packet = null; + return; + } + session.getDownstream().getSession().send(packet); + packet = null; + lastBookUpdate = System.currentTimeMillis(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java index 62b0dbd6b..40000551c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java @@ -28,6 +28,7 @@ package org.geysermc.connector.network.session.cache; import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; @@ -40,17 +41,21 @@ import java.util.concurrent.atomic.AtomicLong; * for that player (e.g. seeing vanished players from /vanish) */ public class EntityCache { - private GeyserSession session; + private final GeyserSession session; @Getter private Long2ObjectMap entities = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); + /** + * A list of all entities that must be ticked. + */ + private final List tickableEntities = Collections.synchronizedList(new ArrayList<>()); private Long2LongMap entityIdTranslations = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); private Map playerEntities = Collections.synchronizedMap(new HashMap<>()); private Map bossBars = Collections.synchronizedMap(new HashMap<>()); - private Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); + private final Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); @Getter - private AtomicLong nextEntityId = new AtomicLong(2L); + private final AtomicLong nextEntityId = new AtomicLong(2L); public EntityCache(GeyserSession session) { this.session = session; @@ -59,6 +64,11 @@ public class EntityCache { public void spawnEntity(Entity entity) { if (cacheEntity(entity)) { entity.spawnEntity(session); + + if (entity instanceof Tickable) { + // Start ticking it + tickableEntities.add((Tickable) entity); + } } } @@ -76,6 +86,10 @@ public class EntityCache { if (entity != null && entity.isValid() && (force || entity.despawnEntity(session))) { long geyserId = entityIdTranslations.remove(entity.getEntityId()); entities.remove(geyserId); + + if (entity instanceof Tickable) { + tickableEntities.remove(entity); + } return true; } return false; @@ -152,4 +166,8 @@ public class EntityCache { public void addCachedPlayerEntityLink(long playerId, long linkedEntityId) { cachedPlayerEntityLinks.put(playerId, linkedEntityId); } + + public List getTickableEntities() { + return tickableEntities; + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java new file mode 100644 index 000000000..67778e822 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java @@ -0,0 +1,123 @@ +/* + * 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.translators.bedrock; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import com.nukkitx.protocol.bedrock.packet.BookEditPacket; +import org.geysermc.connector.inventory.GeyserItemStack; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +@Translator(packet = BookEditPacket.class) +public class BedrockBookEditTranslator extends PacketTranslator { + + @Override + public void translate(BookEditPacket packet, GeyserSession session) { + GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(); + if (itemStack != null) { + CompoundTag tag = itemStack.getNbt() != null ? itemStack.getNbt() : new CompoundTag(""); + ItemStack bookItem = new ItemStack(itemStack.getJavaId(), itemStack.getAmount(), tag); + List pages = tag.contains("pages") ? new LinkedList<>(((ListTag) tag.get("pages")).getValue()) : new LinkedList<>(); + + int page = packet.getPageNumber(); + // Creative edits the NBT for us + if (session.getGameMode() != GameMode.CREATIVE) { + switch (packet.getAction()) { + case ADD_PAGE: { + // Add empty pages in between + for (int i = pages.size(); i < page; i++) { + pages.add(i, new StringTag("", "")); + } + pages.add(page, new StringTag("", packet.getText())); + break; + } + // Called whenever a page is modified + case REPLACE_PAGE: { + if (page < pages.size()) { + pages.set(page, new StringTag("", packet.getText())); + } else { + // Add empty pages in between + for (int i = pages.size(); i < page; i++) { + pages.add(i, new StringTag("", "")); + } + pages.add(page, new StringTag("", packet.getText())); + } + break; + } + case DELETE_PAGE: { + if (page < pages.size()) { + pages.remove(page); + } + break; + } + case SWAP_PAGES: { + int page2 = packet.getSecondaryPageNumber(); + if (page < pages.size() && page2 < pages.size()) { + Collections.swap(pages, page, page2); + } + break; + } + case SIGN_BOOK: { + tag.put(new StringTag("author", packet.getAuthor())); + tag.put(new StringTag("title", packet.getTitle())); + break; + } + default: + return; + } + } + // Remove empty pages at the end + while (pages.size() > 0) { + StringTag currentPage = (StringTag) pages.get(pages.size() - 1); + if (currentPage.getValue() == null || currentPage.getValue().isEmpty()) { + pages.remove(pages.size() - 1); + } else { + break; + } + } + tag.put(new ListTag("pages", pages)); + session.getPlayerInventory().setItem(36 + session.getPlayerInventory().getHeldItemSlot(), GeyserItemStack.from(bookItem), session); + session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory()); + + session.getBookEditCache().setPacket(new ClientEditBookPacket(bookItem, packet.getAction() == BookEditPacket.Action.SIGN_BOOK, session.getPlayerInventory().getHeldItemSlot())); + // There won't be any more book updates after this, so we can try sending the edit packet immediately + if (packet.getAction() == BookEditPacket.Action.SIGN_BOOK) { + session.getBookEditCache().checkForSend(); + } + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index 2c34236a7..238ab347b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -38,14 +38,13 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlags; import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; -import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; import com.nukkitx.protocol.bedrock.data.inventory.InventorySource; -import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket; -import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.*; import org.geysermc.connector.entity.CommandBlockMinecartEntity; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.ItemFrameEntity; @@ -63,11 +62,23 @@ import org.geysermc.connector.utils.BlockUtils; import java.util.concurrent.TimeUnit; +/** + * BedrockInventoryTransactionTranslator handles most interactions between the client and the world, + * or the client and their inventory. + */ @Translator(packet = InventoryTransactionPacket.class) public class BedrockInventoryTransactionTranslator extends PacketTranslator { + private static final float MAXIMUM_BLOCK_PLACING_DISTANCE = 64f; + private static final int CREATIVE_EYE_HEIGHT_PLACE_DISTANCE = 49; + private static final int SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE = 36; + private static final float MAXIMUM_BLOCK_DESTROYING_DISTANCE = 36f; + @Override public void translate(InventoryTransactionPacket packet, GeyserSession session) { + // Send book updates before opening inventories + session.getBookEditCache().checkForSend(); + switch (packet.getTransactionType()) { case NORMAL: System.out.println(packet); @@ -129,6 +140,46 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator + (session.getGameMode().equals(GameMode.CREATIVE) ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) { + restoreCorrectBlock(session, blockPos, packet); + return; + } + + // Vanilla check + if (!(session.getPlayerEntity().getPosition().sub(0, EntityType.PLAYER.getOffset(), 0) + .distanceSquared(packet.getBlockPosition().toFloat().add(0.5f, 0.5f, 0.5f)) < MAXIMUM_BLOCK_PLACING_DISTANCE)) { + // The client thinks that its blocks have been successfully placed. Restore the server's blocks instead. + restoreCorrectBlock(session, blockPos, packet); + return; + } + /* + Block place checks end - client is good to go + */ + ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket( new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getBlockFace()], @@ -176,7 +227,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator MAXIMUM_BLOCK_DESTROYING_DISTANCE) { + restoreCorrectBlock(session, packet.getBlockPosition(), packet); + return; } + LevelEventPacket blockBreakPacket = new LevelEventPacket(); + blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK); + blockBreakPacket.setPosition(packet.getBlockPosition().toFloat()); + blockBreakPacket.setData(BlockTranslator.getBedrockBlockId(blockState)); + session.sendUpstreamPacket(blockBreakPacket); + session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID); + long frameEntityId = ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition()); if (frameEntityId != -1 && session.getEntityCache().getEntityByJavaId(frameEntityId) != null) { ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) frameEntityId, InteractAction.ATTACK, session.isSneaking()); @@ -286,4 +349,34 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { @@ -50,7 +46,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator 0) return; + if (!session.isSpawned()) return; if (!session.getUpstream().isInitialized()) { MoveEntityAbsolutePacket moveEntityBack = new MoveEntityAbsolutePacket(); @@ -63,9 +59,10 @@ public class BedrockMovePlayerTranslator extends PacketTranslator sendPositionIfIdle(session), - 3, TimeUnit.SECONDS)); } - public boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) { + private boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) { if (mode != MovePlayerPacket.Mode.NORMAL) return true; @@ -171,81 +164,5 @@ public class BedrockMovePlayerTranslator extends PacketTranslator sendPositionIfIdle(session), - 3, TimeUnit.SECONDS)); - } } 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 22e5c95fd..203e4406f 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 @@ -30,8 +30,12 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityFlags; +import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; +import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import lombok.Getter; import lombok.Setter; +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.translators.BlockCollision; @@ -105,6 +109,7 @@ public class CollisionManager { // According to the Minecraft Wiki, when sneaking: // - In Bedrock Edition, the height becomes 1.65 blocks, allowing movement through spaces as small as 1.75 (2 - 1⁄4) blocks high. // - In Java Edition, the height becomes 1.5 blocks. + // TODO: Have this depend on the player's literal bounding box variable if (session.isSneaking()) { playerBoundingBox.setSizeY(1.5); } else { @@ -113,6 +118,65 @@ public class CollisionManager { } } + /** + * Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between + * the two versions. + * + * @param bedrockPosition the current Bedrock position of the client + * @param onGround whether the Bedrock player is on the ground + * @return the position to send to the Java server, or null to cancel sending the packet + */ + public Vector3d adjustBedrockPosition(Vector3f bedrockPosition, boolean onGround) { + // We need to parse the float as a string since casting a float to a double causes us to + // lose precision and thus, causes players to get stuck when walking near walls + double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset(); + + Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY, + Double.parseDouble(Float.toString(bedrockPosition.getZ()))); + + if (session.getConnector().getConfig().isCacheChunks()) { + // With chunk caching, we can do some proper collision checks + updatePlayerBoundingBox(position); + + // Correct player position + if (!correctPlayerPosition()) { + // Cancel the movement if it needs to be cancelled + recalculatePosition(); + return null; + } + + position = Vector3d.from(playerBoundingBox.getMiddleX(), + playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2), + playerBoundingBox.getMiddleZ()); + } else { + // When chunk caching is off, we have to rely on this + // It rounds the Y position up to the nearest 0.5 + // This snaps players to snap to the top of stairs and slabs like on Java Edition + // However, it causes issues such as the player floating on carpets + if (onGround) javaY = Math.ceil(javaY * 2) / 2; + position = position.up(javaY - position.getY()); + } + + return position; + } + + // TODO: This makes the player look upwards for some reason, rotation values must be wrong + public void recalculatePosition() { + PlayerEntity entity = session.getPlayerEntity(); + // Gravity might need to be reset... + SetEntityDataPacket entityDataPacket = new SetEntityDataPacket(); + entityDataPacket.setRuntimeEntityId(entity.getGeyserId()); + entityDataPacket.getMetadata().putAll(entity.getMetadata()); + session.sendUpstreamPacket(entityDataPacket); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(entity.getGeyserId()); + movePlayerPacket.setPosition(entity.getPosition()); + movePlayerPacket.setRotation(entity.getBedrockRotation()); + movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL); + session.sendUpstreamPacket(movePlayerPacket); + } + public List getPlayerCollidableBlocks() { List blocks = new ArrayList<>(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java index f029a6051..654a7b422 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java @@ -968,13 +968,12 @@ public abstract class InventoryTranslator { } public boolean checkNetId(GeyserSession session, Inventory inventory, StackRequestSlotInfoData slotInfoData) { - if (slotInfoData.getStackNetworkId() < 0) + int netId = slotInfoData.getStackNetworkId(); + if (netId < 0 || netId == 1) return true; -// if (slotInfoData.getContainer() == ContainerSlotType.CURSOR) //TODO: temporary -// return true; GeyserItemStack currentItem = isCursor(slotInfoData) ? session.getPlayerInventory().getCursor() : inventory.getItem(bedrockSlotToJava(slotInfoData)); - return currentItem.getNetId() == slotInfoData.getStackNetworkId(); + return currentItem.getNetId() == netId; } /** diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java index b131544b2..9d4fbfeec 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java @@ -26,20 +26,80 @@ package org.geysermc.connector.network.translators.inventory.translators; import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket; +import com.nukkitx.nbt.NbtMap; import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.CraftResultsDeprecatedStackRequestActionData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType; +import com.nukkitx.protocol.bedrock.packet.ItemStackRequestPacket; +import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; import org.geysermc.connector.inventory.AnvilContainer; +import org.geysermc.connector.inventory.GeyserItemStack; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.inventory.PlayerInventory; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; +import org.geysermc.connector.network.translators.item.ItemTranslator; public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator { public AnvilInventoryTranslator() { super(3, "minecraft:anvil[facing=north]", ContainerType.ANVIL, UIInventoryUpdater.INSTANCE); } + /* 1.16.100 support start */ + @Override + @Deprecated + public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { + return action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED; + } + + @Override + @Deprecated + public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequestPacket.Request request) { + if (!(request.getActions()[1] instanceof CraftResultsDeprecatedStackRequestActionData)) { + // Just silently log an error + session.getConnector().getLogger().debug("Something isn't quite right with taking an item out of an anvil."); + return translateRequest(session, inventory, request); + } + CraftResultsDeprecatedStackRequestActionData actionData = (CraftResultsDeprecatedStackRequestActionData) request.getActions()[1]; + ItemData resultItem = actionData.getResultItems()[0]; + if (resultItem.getTag() != null) { + NbtMap displayTag = resultItem.getTag().getCompound("display"); + if (displayTag != null && displayTag.containsKey("Name")) { + ItemData sourceSlot = inventory.getItem(0).getItemData(session); + + if (sourceSlot.getTag() != null) { + NbtMap oldDisplayTag = sourceSlot.getTag().getCompound("display"); + if (oldDisplayTag != null && oldDisplayTag.containsKey("Name")) { + if (!displayTag.getString("Name").equals(oldDisplayTag.getString("Name"))) { + // Name has changed + sendRenamePacket(session, inventory, resultItem, displayTag.getString("Name")); + } + } else { + // No display tag on the old item + sendRenamePacket(session, inventory, resultItem, displayTag.getString("Name")); + } + } else { + // New NBT tag + sendRenamePacket(session, inventory, resultItem, displayTag.getString("Name")); + } + } + } + return translateRequest(session, inventory, request); + } + + private void sendRenamePacket(GeyserSession session, Inventory inventory, ItemData outputItem, String name) { + session.sendDownstreamPacket(new ClientRenameItemPacket(name)); + inventory.setItem(2, GeyserItemStack.from(ItemTranslator.translateToJava(outputItem)), session); + } + + /* 1.16.100 support end */ + @Override public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { if (slotInfoData.getContainer() == ContainerSlotType.ANVIL_INPUT) { 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 4237f7d6a..33181b44e 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 @@ -95,6 +95,10 @@ public class ItemRegistry { * Wheat item entry, used in AbstractHorseEntity.java */ public static ItemEntry WHEAT; + /** + * Writable book item entry, used in BedrockBookEditTranslator.java + */ + public static ItemEntry WRITABLE_BOOK; public static int BARRIER_INDEX = 0; @@ -195,6 +199,9 @@ public class ItemRegistry { case "minecraft:wheat": WHEAT = ITEM_ENTRIES.get(itemIndex); break; + case "minecraft:writable_book": + WRITABLE_BOOK = ITEM_ENTRIES.get(itemIndex); + break; default: break; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java index cf97f643c..90eef3bce 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/BookPagesTranslator.java @@ -78,9 +78,8 @@ public class BookPagesTranslator extends NbtItemStackTranslator { CompoundTag pageTag = (CompoundTag) tag; StringTag textTag = pageTag.get("text"); - pages.add(new StringTag(MessageTranslator.convertToJavaMessage(textTag.getValue()))); + pages.add(new StringTag("", textTag.getValue())); } - itemTag.remove("pages"); itemTag.put(new ListTag("pages", pages)); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java index 3b4f14d71..07b839f9c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java @@ -190,6 +190,7 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator @Override public void translate(ServerRespawnPacket packet, GeyserSession session) { Entity entity = session.getPlayerEntity(); - if (entity == null) - return; float maxHealth = entity.getAttributes().containsKey(AttributeType.MAX_HEALTH) ? entity.getAttributes().get(AttributeType.MAX_HEALTH).getValue() : 20f; // Max health must be divisible by two in bedrock @@ -66,18 +64,24 @@ public class JavaRespawnTranslator extends PacketTranslator session.setRaining(false); } + if (session.isThunder()) { + LevelEventPacket stopThunderPacket = new LevelEventPacket(); + stopThunderPacket.setType(LevelEventType.STOP_THUNDERSTORM); + stopThunderPacket.setData(0); + stopThunderPacket.setPosition(Vector3f.ZERO); + session.sendUpstreamPacket(stopThunderPacket); + session.setThunder(false); + } + String newDimension = DimensionUtils.getNewDimension(packet.getDimension()); - if (!session.getDimension().equals(newDimension)) { - DimensionUtils.switchDimension(session, newDimension); - } else { - if (session.isManyDimPackets()) { //reloading world - String fakeDim = session.getDimension().equals(DimensionUtils.OVERWORLD) ? DimensionUtils.NETHER : DimensionUtils.OVERWORLD; + if (!session.getDimension().equals(newDimension) || !packet.getWorldName().equals(session.getWorldName())) { + if (!packet.getWorldName().equals(session.getWorldName()) && session.getDimension().equals(newDimension)) { + // Switching to a new world (based off the world name change); send a fake dimension change + String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension); DimensionUtils.switchDimension(session, fakeDim); - DimensionUtils.switchDimension(session, newDimension); - } else { - // Handled in JavaPlayerPositionRotationTranslator - session.setSpawned(false); } + session.setWorldName(packet.getWorldName()); + DimensionUtils.switchDimension(session, newDimension); } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java index 107282648..59ea29925 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityStatusTranslator.java @@ -31,8 +31,8 @@ import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; -import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.SetEntityMotionPacket; import org.geysermc.connector.entity.Entity; @@ -183,6 +183,19 @@ public class JavaEntityStatusTranslator extends PacketTranslator { @@ -55,10 +74,207 @@ public class JavaSetSlotTranslator extends PacketTranslator InventoryTranslator translator = session.getInventoryTranslator(); if (translator != null) { + updateCraftingGrid(session, packet, inventory, translator); + GeyserItemStack newItem = GeyserItemStack.from(packet.getItem()); inventory.setItem(packet.getSlot(), newItem, session); translator.updateSlot(session, inventory, packet.getSlot()); } }); } + + private static void updateCraftingGrid(GeyserSession session, ServerSetSlotPacket packet, Inventory inventory, InventoryTranslator translator) { + if (packet.getSlot() == 0) { + int gridSize; + if (translator instanceof PlayerInventoryTranslator) { + gridSize = 4; + } else if (translator instanceof CraftingInventoryTranslator) { + gridSize = 9; + } else { + return; + } + + if (packet.getItem() == null || packet.getItem().getId() == 0) { + return; + } + + int offset = gridSize == 4 ? 28 : 32; + int gridDimensions = gridSize == 4 ? 2 : 3; + int firstRow = -1, height = -1; + int firstCol = -1, width = -1; + for (int row = 0; row < gridDimensions; row++) { + for (int col = 0; col < gridDimensions; col++) { + if (!inventory.getItem(col + (row * gridDimensions) + 1).isEmpty()) { + if (firstRow == -1) { + firstRow = row; + firstCol = col; + } else { + firstCol = Math.min(firstCol, col); + } + height = Math.max(height, row); + width = Math.max(width, col); + } + } + } + + //empty grid + if (firstRow == -1) { + return; + } + + height += -firstRow + 1; + width += -firstCol + 1; + + System.out.println("Start Row: " + firstRow); + System.out.println("Start Column: " + firstCol); + System.out.println("Rows: " + height); + System.out.println("Columns: " + width); + + //TODO + recipes: + for (Recipe recipe : session.getCraftingRecipes().values()) { + if (recipe.getType() == RecipeType.CRAFTING_SHAPED) { + ShapedRecipeData data = (ShapedRecipeData) recipe.getData(); + if (!data.getResult().equals(packet.getItem())) { + continue; + } + if (data.getWidth() != width || data.getHeight() != height || width * height != data.getIngredients().length) { + continue; + } + + Ingredient[] ingredients = data.getIngredients(); + if (!testShapedRecipe(ingredients, inventory, gridDimensions, firstRow, height, firstCol, width)) { + Ingredient[] mirroredIngredients = new Ingredient[data.getIngredients().length]; + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + mirroredIngredients[col + (row * width)] = ingredients[(width - 1 - col) + (row * width)]; + } + } + + if (Arrays.equals(ingredients, mirroredIngredients)) { + continue; + } else if (!testShapedRecipe(mirroredIngredients, inventory, gridDimensions, firstRow, height, firstCol, width)) { + continue; + } + } + System.out.println("FOUND SHAPED RECIPE :)"); + System.out.println(recipe); + // Recipe is had, don't sent packet + return; + } else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) { + ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData(); + if (!data.getResult().equals(packet.getItem())) { + continue; + } + for (int i = 0; i < data.getIngredients().length; i++) { + Ingredient ingredient = data.getIngredients()[i]; + for (ItemStack itemStack : ingredient.getOptions()) { + boolean inventoryHasItem = false; + for (int j = 0; j < inventory.getSize(); j++) { + GeyserItemStack geyserItemStack = inventory.getItem(j); + if (geyserItemStack.isEmpty()) { + inventoryHasItem = itemStack == null || itemStack.getId() == 0; + if (inventoryHasItem) { + break; + } + } else if (itemStack.equals(geyserItemStack.getItemStack(1))) { + inventoryHasItem = true; + break; + } + } + if (!inventoryHasItem) { + continue recipes; + } + } + } + // Recipe is had, don't sent packet + return; + } + } + System.out.println("Sending packet!"); + + UUID uuid = UUID.randomUUID(); + int newRecipeId = session.getLastRecipeNetId().incrementAndGet(); + + ItemData[] ingredients = new ItemData[height * width]; + //construct ingredient list and clear slots on client + Ingredient[] javaIngredients = new Ingredient[height * width]; + int index = 0; + for (int row = firstRow; row < height + firstRow; row++) { + for (int col = firstCol; col < width + firstCol; col++) { + GeyserItemStack geyserItemStack = inventory.getItem(col + (row * gridDimensions) + 1); + ingredients[index] = geyserItemStack.getItemData(session); + ItemStack[] itemStacks = new ItemStack[] {geyserItemStack.isEmpty() ? null : geyserItemStack.getItemStack(1)}; + javaIngredients[index] = new Ingredient(itemStacks); + + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(ContainerId.UI); + slotPacket.setSlot(col + (row * gridDimensions) + offset); + slotPacket.setItem(ItemData.AIR); + session.sendUpstreamPacket(slotPacket); + index++; + } + } + + ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, packet.getItem()); + session.getConnector().getLogger().error(data.toString()); + // Cache this recipe so we know the client has received it + session.getCraftingRecipes().put(newRecipeId, new Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data)); + + CraftingDataPacket craftPacket = new CraftingDataPacket(); + craftPacket.getCraftingData().add(CraftingData.fromShaped( + uuid.toString(), + width, + height, + Arrays.asList(ingredients), + Collections.singletonList(ItemTranslator.translateToBedrock(session, packet.getItem())), + uuid, + "crafting_table", + 0, + newRecipeId + )); + craftPacket.setCleanRecipes(false); + System.out.println(craftPacket); + session.sendUpstreamPacket(craftPacket); + + index = 0; + for (int row = firstRow; row < height + firstRow; row++) { + for (int col = firstCol; col < width + firstCol; col++) { + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(ContainerId.UI); + slotPacket.setSlot(col + (row * gridDimensions) + offset); + slotPacket.setItem(ingredients[index]); + session.sendUpstreamPacket(slotPacket); + index++; + } + } + } + } + + private static boolean testShapedRecipe(Ingredient[] ingredients, Inventory inventory, int gridDimensions, int firstRow, int height, int firstCol, int width) { + int ingredientIndex = 0; + for (int row = firstRow; row < height + firstRow; row++) { + for (int col = firstCol; col < width + firstCol; col++) { + GeyserItemStack geyserItemStack = inventory.getItem(col + (row * gridDimensions) + 1); + Ingredient ingredient = ingredients[ingredientIndex++]; + if (ingredient.getOptions().length == 0) { + if (!geyserItemStack.isEmpty()) { + return false; + } + } else { + boolean inventoryHasItem = false; + for (ItemStack item : ingredient.getOptions()) { + if (Objects.equals(geyserItemStack.getItemStack(1), item)) { + inventoryHasItem = true; + break; + } + } + if (!inventoryHasItem) { + return false; + } + } + } + } + return true; + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java index 7f2b3561e..d74165b14 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaBlockChangeTranslator.java @@ -26,6 +26,7 @@ package org.geysermc.connector.network.translators.java.world; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.SoundEvent; import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket; @@ -37,17 +38,17 @@ import org.geysermc.connector.network.translators.sound.BlockSoundInteractionHan import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.ChunkUtils; -import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket; - @Translator(packet = ServerBlockChangePacket.class) public class JavaBlockChangeTranslator extends PacketTranslator { @Override public void translate(ServerBlockChangePacket packet, GeyserSession session) { Position pos = packet.getRecord().getPosition(); - boolean updatePlacement = !(session.getConnector().getConfig().isCacheChunks() && session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()) == packet.getRecord().getBlock()); - ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), packet.getRecord().getPosition()); - if (updatePlacement && session.getConnector().getPlatformType() != PlatformType.SPIGOT) { + boolean updatePlacement = session.getConnector().getPlatformType() != PlatformType.SPIGOT && // Spigot simply listens for the block place event + !(session.getConnector().getConfig().isCacheChunks() && + session.getConnector().getWorldManager().getBlockAt(session, pos) == packet.getRecord().getBlock()); + ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), pos); + if (updatePlacement) { this.checkPlace(session, packet); } this.checkInteract(session, packet); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java similarity index 93% rename from connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java index bce96f65d..238e9ba32 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java @@ -36,14 +36,14 @@ import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.sound.SoundRegistry; @Translator(packet = ServerPlaySoundPacket.class) -public class JavaPlayerPlaySoundTranslator extends PacketTranslator { +public class JavaPlaySoundTranslator extends PacketTranslator { @Override public void translate(ServerPlaySoundPacket packet, GeyserSession session) { String packetSound; - if(packet.getSound() instanceof BuiltinSound) { + if (packet.getSound() instanceof BuiltinSound) { packetSound = ((BuiltinSound) packet.getSound()).getName(); - } else if(packet.getSound() instanceof CustomSound) { + } else if (packet.getSound() instanceof CustomSound) { packetSound = ((CustomSound) packet.getSound()).getName(); } else { session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString()); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java similarity index 82% rename from connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java index 61856755f..d7d0f0738 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.java.entity.player; +package org.geysermc.connector.network.translators.java.world; import com.github.steveice10.mc.protocol.data.game.world.sound.BuiltinSound; import com.github.steveice10.mc.protocol.data.game.world.sound.CustomSound; @@ -35,26 +35,35 @@ import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.sound.SoundRegistry; @Translator(packet = ServerStopSoundPacket.class) -public class JavaPlayerStopSoundTranslator extends PacketTranslator { +public class JavaStopSoundTranslator extends PacketTranslator { @Override public void translate(ServerStopSoundPacket packet, GeyserSession session) { + // Runs if all sounds are stopped + if (packet.getSound() == null) { + StopSoundPacket stopPacket = new StopSoundPacket(); + stopPacket.setStoppingAllSound(true); + stopPacket.setSoundName(""); + session.sendUpstreamPacket(stopPacket); + return; + } + String packetSound; - if(packet.getSound() instanceof BuiltinSound) { + if (packet.getSound() instanceof BuiltinSound) { packetSound = ((BuiltinSound) packet.getSound()).getName(); - } else if(packet.getSound() instanceof CustomSound) { + } else if (packet.getSound() instanceof CustomSound) { packetSound = ((CustomSound) packet.getSound()).getName(); } else { session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString()); return; } - SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound); + SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound.replace("minecraft:", "")); session.getConnector().getLogger() .debug("[StopSound] Sound mapping " + packetSound + " -> " + soundMapping + (soundMapping == null ? "[not found]" : "") + " - " + packet.toString()); String playsound; - if(soundMapping == null || soundMapping.getPlaysound() == null) { + if (soundMapping == null || soundMapping.getPlaysound() == null) { // no mapping session.getConnector().getLogger() .debug("[StopSound] Defaulting to sound server gave us."); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java index dd1ec68a3..461d8139d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaUpdateTimeTranslator.java @@ -46,17 +46,10 @@ public class JavaUpdateTimeTranslator extends PacketTranslator= 0) { // Client thinks there is no daylight cycle but there is - setDoDaylightCycleGamerule(session, true); + session.setDaylightCycle(true); } else if (session.isDaylightCycle() && time < 0) { // Client thinks there is daylight cycle but there isn't - setDoDaylightCycleGamerule(session, false); + session.setDaylightCycle(false); } } - - private void setDoDaylightCycleGamerule(GeyserSession session, boolean doCycle) { - session.sendGameRule("dodaylightcycle", doCycle); - // Save the value so we don't have to constantly send a daylight cycle gamerule update - session.setDaylightCycle(doCycle); - } - } 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 db6f43fea..b047999e7 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 @@ -87,7 +87,9 @@ public class BlockTranslator { */ public static final int BEDROCK_RUNTIME_COMMAND_BLOCK_ID; - // For block breaking animation math + /** + * A list of all Java runtime wool IDs, for use with block breaking math and shears + */ public static final IntSet JAVA_RUNTIME_WOOL_IDS = new IntOpenHashSet(); public static final int JAVA_RUNTIME_COBWEB_ID; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java index e25bb8263..40f305ad6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java @@ -47,7 +47,7 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator { protected NbtMap getItem(CompoundTag tag) { ItemEntry entry = ItemRegistry.getItemEntry((String) tag.get("id").getValue()); NbtMapBuilder tagBuilder = NbtMap.builder() - .putShort("id", (short) entry.getBedrockId()) + .putString("Name", entry.getBedrockIdentifier()) .putByte("Count", (byte) tag.get("Count").getValue()) .putShort("Damage", (short) entry.getBedrockData()); tagBuilder.put("tag", NbtMap.builder().build()); diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java index 89ea9298e..24b137f77 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java @@ -50,6 +50,7 @@ public class BlockUtils { if (toolType.equals("shears")) return isWoolBlock ? 5.0 : 15.0; if (toolType.equals("")) return 1.0; switch (toolTier) { + // https://minecraft.gamepedia.com/Breaking#Speed case "wooden": return 2.0; case "stone": @@ -58,6 +59,8 @@ public class BlockUtils { return 6.0; case "diamond": return 8.0; + case "netherite": + return 9.0; case "golden": return 12.0; default: diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index 0efa8a5aa..e002162c3 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -57,20 +57,11 @@ public class DimensionUtils { public static void switchDimension(GeyserSession session, String javaDimension) { int bedrockDimension = javaToBedrock(javaDimension); Entity player = session.getPlayerEntity(); - if (javaDimension.equals(session.getDimension())) - return; - - if (session.getMovementSendIfIdle() != null) { - session.getMovementSendIfIdle().cancel(true); - } session.getEntityCache().removeAllEntities(); session.getItemFrameCache().clear(); session.getLecternCache().clear(); session.getSkullCache().clear(); - if (session.getPendingDimSwitches().getAndIncrement() > 0) { - ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); - } Vector3i pos = Vector3i.from(0, Short.MAX_VALUE, 0); @@ -151,4 +142,20 @@ public class DimensionUtils { // Change dimension ID to the End to allow for building above Bedrock BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? 2 : 1; } + + /** + * Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional + * dimension switch. + * + * @param currentDimension the current dimension of the player + * @param newDimension the new dimension that the player will be transferred to + * @return the fake dimension to transfer to + */ + public static String getTemporaryDimension(String currentDimension, String newDimension) { + if (BEDROCK_NETHER_ID == 2) { + // Prevents rare instances of Bedrock locking up + return javaToBedrock(newDimension) == 2 ? OVERWORLD : NETHER; + } + return currentDimension.equals(OVERWORLD) ? NETHER : OVERWORLD; + } } 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 fd3e9a8db..862af548d 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -36,6 +36,7 @@ import org.reflections.util.ConfigurationBuilder; import java.io.*; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.MessageDigest; import java.util.function.Function; @@ -62,7 +63,8 @@ public class FileUtils { } public static T loadJson(InputStream src, Class valueType) throws IOException { - return GeyserConnector.JSON_MAPPER.readValue(src, valueType); + // Read specifically with UTF-8 to allow any non-UTF-encoded JSON to read + return GeyserConnector.JSON_MAPPER.readValue(new InputStreamReader(src, StandardCharsets.UTF_8), valueType); } /** 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 c6ab715d4..1a1f758d6 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -187,7 +187,11 @@ public class LanguageUtils { if (FileUtils.class.getResource("/languages/texts/" + locale + ".properties") == null) { result = false; if (GeyserConnector.getInstance() != null && GeyserConnector.getInstance().getLogger() != null) { // Could be too early for these to be initialized - GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language + if (locale.equals("en_US")) { + GeyserConnector.getInstance().getLogger().error("English locale not found in Geyser. Did you clone the submodules? (git submodule update --init)"); + } else { + GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language + } } } else { if (!LOCALE_MAPPINGS.containsKey(locale)) { diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index 9e16c428d..fd7ef4e64 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -29,19 +29,18 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.nimbusds.jose.JWSObject; import com.nukkitx.network.util.Preconditions; import com.nukkitx.protocol.bedrock.packet.LoginPacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.util.EncryptionUtils; -import org.geysermc.common.window.CustomFormBuilder; -import org.geysermc.common.window.CustomFormWindow; -import org.geysermc.common.window.FormWindow; -import org.geysermc.common.window.SimpleFormWindow; +import org.geysermc.common.window.*; import org.geysermc.common.window.button.FormButton; import org.geysermc.common.window.component.InputComponent; import org.geysermc.common.window.component.LabelComponent; import org.geysermc.common.window.response.CustomFormResponse; +import org.geysermc.common.window.response.ModalFormResponse; import org.geysermc.common.window.response.SimpleFormResponse; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; @@ -156,13 +155,21 @@ public class LoginEncryptionUtils { session.sendUpstreamPacketImmediately(packet); } - private static int AUTH_FORM_ID = 1336; - private static int AUTH_DETAILS_FORM_ID = 1337; + private static final int AUTH_MSA_DETAILS_FORM_ID = 1334; + private static final int AUTH_MSA_CODE_FORM_ID = 1335; + private static final int AUTH_FORM_ID = 1336; + private static final int AUTH_DETAILS_FORM_ID = 1337; public static void showLoginWindow(GeyserSession session) { + // Set DoDaylightCycle to false so the time doesn't accelerate while we're here + session.setDaylightCycle(false); + String userLanguage = session.getLocale(); SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.title", userLanguage), LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.desc", userLanguage)); - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login", userLanguage))); + if (session.getConnector().getConfig().getRemote().isPasswordAuthentication()) { + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.mojang", userLanguage))); + } + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage))); window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); session.sendForm(window, AUTH_FORM_ID); @@ -179,12 +186,33 @@ public class LoginEncryptionUtils { session.sendForm(window, AUTH_DETAILS_FORM_ID); } + /** + * Prompts the user between either OAuth code login or manual password authentication + */ + public static void showMicrosoftAuthenticationWindow(GeyserSession session) { + String userLanguage = session.getLocale(); + SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage), ""); + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.browser", userLanguage))); + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.password", userLanguage))); // This form won't show if password authentication is disabled + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); + session.sendForm(window, AUTH_MSA_DETAILS_FORM_ID); + } + + /** + * Shows the code that a user must input into their browser + */ + public static void showMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse response) { + ModalFormWindow msaCodeWindow = new ModalFormWindow("%xbox.signin", "%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + + response.user_code, "Done", "%menu.disconnect"); + session.sendForm(msaCodeWindow, LoginEncryptionUtils.AUTH_MSA_CODE_FORM_ID); + } + public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, int formId, String formData) { WindowCache windowCache = session.getWindowCache(); if (!windowCache.getWindows().containsKey(formId)) return false; - if(formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID) { + if (formId == AUTH_MSA_DETAILS_FORM_ID || formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID || formId == AUTH_MSA_CODE_FORM_ID) { FormWindow window = windowCache.getWindows().remove(formId); window.setResponse(formData.trim()); @@ -198,23 +226,57 @@ public class LoginEncryptionUtils { String password = response.getInputResponses().get(2); session.authenticate(email, password); + + // Clear windows so authentication data isn't accidentally cached + windowCache.getWindows().clear(); } else { showLoginDetailsWindow(session); } - - // Clear windows so authentication data isn't accidentally cached - windowCache.getWindows().clear(); } else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) { + boolean isPasswordAuthentication = session.getConnector().getConfig().getRemote().isPasswordAuthentication(); + int microsoftButton = isPasswordAuthentication ? 1 : 0; + int disconnectButton = isPasswordAuthentication ? 2 : 1; SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); if (response != null) { - if (response.getClickedButtonId() == 0) { + if (isPasswordAuthentication && response.getClickedButtonId() == 0) { + session.setMicrosoftAccount(false); showLoginDetailsWindow(session); - } else if(response.getClickedButtonId() == 1) { + } else if (response.getClickedButtonId() == microsoftButton) { + session.setMicrosoftAccount(true); + if (isPasswordAuthentication) { + showMicrosoftAuthenticationWindow(session); + } else { + // Just show the OAuth code + session.authenticateWithMicrosoftCode(); + } + } else if (response.getClickedButtonId() == disconnectButton) { session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); } } else { showLoginWindow(session); } + } else if (formId == AUTH_MSA_DETAILS_FORM_ID && window instanceof SimpleFormWindow) { + SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); + if (response != null) { + if (response.getClickedButtonId() == 0) { + session.authenticateWithMicrosoftCode(); + } else if (response.getClickedButtonId() == 1) { + showLoginDetailsWindow(session); + } else if (response.getClickedButtonId() == 2) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + } else { + showLoginWindow(session); + } + } else if (formId == AUTH_MSA_CODE_FORM_ID && window instanceof ModalFormWindow) { + ModalFormResponse response = (ModalFormResponse) window.getResponse(); + if (response != null) { + if (response.getClickedButtonId() == 1) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + } else { + showMicrosoftAuthenticationWindow(session); + } } } } 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 16a1812ee..bcb1ffd50 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java @@ -63,7 +63,7 @@ public class ResourcePack { // As we just created the directory it will be empty return; } - + for (File file : directory.listFiles()) { if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) { ResourcePack pack = new ResourcePack(); @@ -77,12 +77,15 @@ public class ResourcePack { if (x.getName().contains("manifest.json")) { try { ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class); + // Sometimes a pack_manifest file is present and not in a valid format, + // but a manifest file is, so we null check through that one + if (manifest.getHeader().getUuid() != null) { + pack.file = file; + pack.manifest = manifest; + pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion()); - pack.file = file; - pack.manifest = manifest; - pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion()); - - PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack); + PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack); + } } catch (Exception e) { e.printStackTrace(); } diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index ac9ec753d..07b73173e 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -32,10 +32,14 @@ remote: port: 25565 # Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate). auth-type: online + # Allow for password-based authentication methods through Geyser. Only useful in online mode. + # If this is false, users must authenticate to Microsoft using a code provided by Geyser on their desktop. + allow-password-authentication: true # Whether to enable PROXY protocol or not while connecting to the server. # This is useful only when: # 1) Your server supports PROXY protocol (it probably doesn't) - # 2) You run Velocity or BungeeCord with respective option enabled. + # 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config. + # IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT! use-proxy-protocol: false # Floodgate uses encryption to ensure use from authorised sources. @@ -51,10 +55,12 @@ floodgate-key-file: public-key.pem # BedrockAccountUsername: # Your Minecraft: Bedrock Edition username # email: javaccountemail@example.com # Your Minecraft: Java Edition email # password: javaccountpassword123 # Your Minecraft: Java Edition password +# microsoft-account: true # Whether the account is a Mojang or Microsoft account. # # bluerkelp2: # email: not_really_my_email_address_mr_minecrafter53267@gmail.com # password: "this isn't really my password" +# microsoft-account: false # Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands. # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. @@ -132,8 +138,7 @@ above-bedrock-nether-building: false force-resource-packs: true # Allows Xbox achievements to be unlocked. -# This disables certain commands so the Bedrock client can't to "cheat" to get them. -# Commands such as /gamemode and /give will not work from Bedrock with this enabled +# THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating. xbox-achievements-enabled: false # bStats is a stat tracker that is entirely anonymous and tracks only basic information diff --git a/connector/src/main/resources/languages b/connector/src/main/resources/languages index 1a0076684..6f246c24d 160000 --- a/connector/src/main/resources/languages +++ b/connector/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 1a00766840baf1f512d98f5a75c177c8bcfba6f3 +Subproject commit 6f246c24ddbd543a359d651e706da470fe53ceeb diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings index 07f65c380..dd0347bd5 160000 --- a/connector/src/main/resources/mappings +++ b/connector/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 07f65c3803dcd3f83358ee574e54bf129cad0840 +Subproject commit dd0347bd51e00e42ea58faaf68b562526c4d2817 diff --git a/licenseheader.txt b/licenseheader.txt index c22c426c4..8ef205a31 100644 --- a/licenseheader.txt +++ b/licenseheader.txt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * 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 diff --git a/pom.xml b/pom.xml index 1b544f9ee..011b320f4 100644 --- a/pom.xml +++ b/pom.xml @@ -71,19 +71,6 @@ - - - releases - opencollab-releases - https://repo.opencollab.dev/maven-releases - - - snapshots - opencollab-snapshots - https://repo.opencollab.dev/maven-snapshots - - - org.projectlombok