diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 17e88f268..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - - - - - - - -**Describe the bug** - -A clear and concise description of what the bug is. - -**To Reproduce** - -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** - -A clear and concise description of what you expected to happen. - -**Screenshots / Videos** - -If applicable, add screenshots to help explain your problem. - -**Server Version and Plugins** - -If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information. - -If you're running a multi-server instance, or using Geyser Standalone: - -- Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all. -- Please list all plugins on all servers involved. - -If this bug occurs on a server you do not control, please fill this in to the best of your knowledge. - -**Geyser Dump** - -If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you use the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue. - -**Minecraft: Bedrock Edition Version** - -The version of your Minecraft: Bedrock Edition client you tested with, along with your device type (e.g. Windows 10, Switch...). - -**Additional Context** - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..ca83c83f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,64 @@ +name: Bug report +about: Create a report to help us improve +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report for Geyser! Fill out the following form to your best ability to help us fix the problem. + Only use this if you're absolutely sure that you found a bug and can reproduce it. For anything else, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or the [Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues). +- type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true +- type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce this behaviour + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true +- type: textarea + attributes: + label: Expected behaviour + description: A clear and concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Screenshots / Videos + description: If applicable, add screenshots to help explain your problem. +- type: textarea + attributes: + label: Server Version and Plugins + description: | + If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information. + If you're running a multi-server instance or using Geyser Standalone: + * Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all. + * Please list all plugins on all servers involved. + If this bug occurs on a server you do not control, please full this in to the best of your knowledge. +- type: input + attributes: + label: Geyser Dump + description: If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you're using the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue. +- type: input + attributes: + label: Geyser Version + description: What version of Geyser are you running? + placeholder: "For example: 1.2.0-SNAPSHOT (git-master-2d9baf1)" + validations: + required: true +- type: input + attributes: + label: "Minecraft: Bedrock Edition Version" + description: "What version of Minecraft: Bedrock Edition are you using? Leave empty if the bug happens before you can connect with Minecraft: Bedrock Edition." + placeholder: "For example: 1.16.201" +- type: textarea + attributes: + label: Additional Context + description: Add any other context about the problem here diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 92085a35b..a8a5bf95d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,11 @@ blank_issues_enabled: false contact_links: - - name: GeyserMC Discord - url: http://discord.geysermc.org/ + - name: Common Issues + url: https://github.com/GeyserMC/Geyser/wiki/Common-Issues + about: Check the common issues to see if you are not alone with that issue and see how you can fix them. + - name: Frequently Asked Questions + url: https://github.com/GeyserMC/Geyser/wiki/FAQ + about: Look at the FAQ page for answers for frequently asked questions. + - name: Get help on the GeyserMC Discord server + url: https://discord.gg/geysermc about: If your issue seems like it could possibly be an easy fix due to configuration, please hop on our Discord. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 46653e624..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**What feature do you want?** -Add a description - -**Alternatives?** -List any alternatives you might have tried diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..64ad937a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,21 @@ +name: Feature request +about: Suggest an idea for this project +labels: "Feature Request" +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request for Geyser! Fill out the following form to your best ability to help us understand your feature request and greately improve the change of it getting added. + For anything else than a feature request, use: [our Discord server](https://discord.gg/geysermc), [the FAQ](https://github.com/GeyserMC/Geyser/wiki/FAQ) or [the Common Issues](https://github.com/GeyserMC/Geyser/wiki/Common-Issues). +- type: textarea + attributes: + label: What feature do you want to see added? + description: A clear and concise description of your feature request. + validations: + required: true +- type: textarea + attributes: + label: Are there any alternatives? + description: List any alternatives you might have tried + validations: + required: true \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 6564bd1f9..d76a6f737 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -22,7 +22,10 @@ pipeline { stage ('Deploy') { when { - branch "master" + anyOf { + branch "master" + branch "floodgate-2.0" + } } steps { @@ -35,8 +38,8 @@ pipeline { rtMavenResolver( id: "maven-resolver", serverId: "opencollab-artifactory", - releaseRepo: "release", - snapshotRepo: "snapshot" + releaseRepo: "maven-deploy-release", + snapshotRepo: "maven-deploy-snapshot" ) rtMavenRun( pom: 'pom.xml', diff --git a/README.md b/README.md index 343fca94a..65a1261bd 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.201 and Minecraft Java v1.16.4 - v1.16.5. +### Currently supporting Minecraft Bedrock v1.16.100 - v1.16.210 and Minecraft Java v1.16.4 - v1.16.5. ## Setting Up Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set up Geyser. @@ -34,20 +34,10 @@ Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set - Test Server: `test.geysermc.org` port `25565` for Java and `19132` for Bedrock ## What's Left to be Added/Fixed -- Lecterns - Near-perfect movement (to the point where anticheat on large servers is unlikely to ban you) - Resource pack conversion/CustomModelData - Some Entity Flags -- The Following Inventories - - Enchantment Table (as a proper GUI) - - Beacon - - Cartography Table - - Stonecutter - - Structure Block - - Horse Inventory - - Loom - - Smithing Table - - Grindstone +- Structure block UI ## What can't be fixed The following things can't be fixed because of Bedrock limitations. They might be fixable in the future, but not as of now. diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 10855aed4..5bbff0647 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-bungeecord @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml index a5ad53cb5..d58cb2ca4 100644 --- a/bootstrap/pom.xml +++ b/bootstrap/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-parent pom @@ -16,13 +16,9 @@ spigot-public https://hub.spigotmc.org/nexus/content/repositories/public/ - - bukkit-public - https://repo.md-5.net/content/repositories/public/ - sponge-repo - https://repo.spongepowered.org/maven + https://repo.spongepowered.org/repository/maven-public/ bungeecord-repo diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index 93eebc3d2..dc9a95d5f 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-spigot @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT compile @@ -30,9 +30,9 @@ provided - org.geysermc.adapters + org.geysermc.geyser.adapters spigot-all - 1.0-SNAPSHOT + 1.1-SNAPSHOT diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java index e97373def..0d99f89cc 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java @@ -28,7 +28,6 @@ package org.geysermc.platform.spigot; import com.github.steveice10.mc.protocol.MinecraftConstants; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -import org.geysermc.adapters.spigot.SpigotAdapters; import org.geysermc.common.PlatformType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.bootstrap.GeyserBootstrap; @@ -40,9 +39,11 @@ import org.geysermc.connector.ping.GeyserLegacyPingPassthrough; import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; +import org.geysermc.geyser.adapters.spigot.SpigotAdapters; import org.geysermc.platform.spigot.command.GeyserSpigotCommandExecutor; import org.geysermc.platform.spigot.command.GeyserSpigotCommandManager; import org.geysermc.platform.spigot.command.SpigotCommandSender; +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; @@ -69,6 +70,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { private GeyserConnector connector; + /** + * The Minecraft server version, formatted as 1.#.# + */ + private String minecraftVersion; + @Override public void onEnable() { // This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed @@ -118,6 +124,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserConfig.loadFloodgate(this); + // Turn "(MC: 1.16.4)" into 1.16.4. + this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0]; + this.connector = GeyserConnector.start(PlatformType.SPIGOT, this); if (geyserConfig.isLegacyPingPassthrough()) { @@ -146,8 +155,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 { @@ -157,14 +167,14 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { if (isViaVersion && isViaVersionNeeded()) { if (isLegacy) { // Pre-1.13 - this.geyserWorldManager = new GeyserSpigot1_12NativeWorldManager(); + this.geyserWorldManager = new GeyserSpigot1_12NativeWorldManager(this); } else { // Post-1.13 this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this, use3dBiomes); } } else { // No ViaVersion - this.geyserWorldManager = new GeyserSpigotNativeWorldManager(use3dBiomes); + this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this, use3dBiomes); } geyserLogger.debug("Using NMS adapter: " + this.geyserWorldManager.getClass() + ", " + nmsVersion); } catch (Exception e) { @@ -180,20 +190,24 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { // No NMS adapter if (isLegacy && isViaVersion) { // Use ViaVersion for converting pre-1.13 block states - this.geyserWorldManager = new GeyserSpigot1_12WorldManager(); + this.geyserWorldManager = new GeyserSpigot1_12WorldManager(this); } else if (isLegacy) { // Not sure how this happens - without ViaVersion, we don't know any block states, so just assume everything is air - this.geyserWorldManager = new GeyserSpigotFallbackWorldManager(); + this.geyserWorldManager = new GeyserSpigotFallbackWorldManager(this); } else { // Post-1.13 - this.geyserWorldManager = new GeyserSpigotWorldManager(use3dBiomes); + this.geyserWorldManager = new GeyserSpigotWorldManager(this, use3dBiomes); } 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(connector), this); + } + this.getCommand("geyser").setExecutor(new GeyserSpigotCommandExecutor(connector)); } @@ -239,6 +253,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return new GeyserSpigotDumpInfo(); } + @Override + public String getMinecraftServerVersion() { + return this.minecraftVersion; + } + public boolean isCompatible(String version, String whichVersion) { int[] currentVersion = parseVersion(version); int[] otherVersion = parseVersion(whichVersion); @@ -277,10 +296,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { * @return the server version before ViaVersion finishes initializing */ public ProtocolVersion getServerProtocolVersion() { - String bukkitVersion = Bukkit.getServer().getVersion(); - // Turn "(MC: 1.16.4)" into 1.16.4. - String version = bukkitVersion.split("\\(MC: ")[1].split("\\)")[0]; - return ProtocolVersion.getClosest(version); + return ProtocolVersion.getClosest(this.minecraftVersion); } /** diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java new file mode 100644 index 000000000..2ee6457ac --- /dev/null +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigot1_11CraftingListener.java @@ -0,0 +1,205 @@ +/* + * 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 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; + + public GeyserSpigot1_11CraftingListener(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()); + } + + @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; + } + + 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 |= 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 |= 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/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotBlockPlaceListener.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotBlockPlaceListener.java index 56fa7581b..51e68a263 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotBlockPlaceListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/GeyserSpigotBlockPlaceListener.java @@ -53,11 +53,11 @@ public class GeyserSpigotBlockPlaceListener implements Listener { placeBlockSoundPacket.setPosition(Vector3f.from(event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ())); placeBlockSoundPacket.setBabySound(false); if (worldManager.isLegacy()) { - placeBlockSoundPacket.setExtraData(BlockTranslator.getBedrockBlockId(worldManager.getBlockAt(session, + placeBlockSoundPacket.setExtraData(session.getBlockTranslator().getBedrockBlockId(worldManager.getBlockAt(session, event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ()))); } else { String javaBlockId = event.getBlockPlaced().getBlockData().getAsString(); - placeBlockSoundPacket.setExtraData(BlockTranslator.getBedrockBlockId(BlockTranslator.getJavaIdBlockMap().getOrDefault(javaBlockId, BlockTranslator.JAVA_AIR_ID))); + placeBlockSoundPacket.setExtraData(session.getBlockTranslator().getBedrockBlockId(BlockTranslator.getJavaIdBlockMap().getOrDefault(javaBlockId, BlockTranslator.JAVA_AIR_ID))); } placeBlockSoundPacket.setIdentifier(":"); session.sendUpstreamPacket(placeBlockSoundPacket); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java index ae1992727..02347f5de 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12NativeWorldManager.java @@ -27,10 +27,11 @@ package org.geysermc.platform.spigot.world.manager; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.geysermc.adapters.spigot.SpigotAdapters; -import org.geysermc.adapters.spigot.SpigotWorldAdapter; +import org.bukkit.plugin.Plugin; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.geyser.adapters.spigot.SpigotAdapters; +import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter; import us.myles.ViaVersion.api.Via; import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.storage.BlockStorage; @@ -40,7 +41,8 @@ import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.storage.BlockStorage; public class GeyserSpigot1_12NativeWorldManager extends GeyserSpigot1_12WorldManager { private final SpigotWorldAdapter adapter; - public GeyserSpigot1_12NativeWorldManager() { + public GeyserSpigot1_12NativeWorldManager(Plugin plugin) { + super(plugin); this.adapter = SpigotAdapters.getWorldAdapter(); // Unlike post-1.13, we can't build up a cache of block states, because block entities need some special conversion } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java index cb450f7f9..a28eef5b4 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigot1_12WorldManager.java @@ -30,6 +30,7 @@ import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import us.myles.ViaVersion.api.Pair; @@ -61,8 +62,8 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager { */ private final List> protocolList; - public GeyserSpigot1_12WorldManager() { - super(false); + public GeyserSpigot1_12WorldManager(Plugin plugin) { + super(plugin, false); this.mappingData1_12to1_13 = ProtocolRegistry.getProtocol(Protocol1_13To1_12_2.class).getMappingData(); this.protocolList = ProtocolRegistry.getProtocolPath(CLIENT_PROTOCOL_VERSION, ProtocolVersion.v1_13.getVersion()); @@ -75,6 +76,10 @@ public class GeyserSpigot1_12WorldManager extends GeyserSpigotWorldManager { if (player == null) { return BlockTranslator.JAVA_AIR_ID; } + if (!player.getWorld().isChunkLoaded(x >> 4, z >> 4)) { + // Prevent nasty async errors if a player is loading in + return BlockTranslator.JAVA_AIR_ID; + } // Get block entity storage BlockStorage storage = Via.getManager().getConnection(player.getUniqueId()).get(BlockStorage.class); Block block = player.getWorld().getBlockAt(x, y, z); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java index f2ae8a641..a9de94db5 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java @@ -26,6 +26,7 @@ package org.geysermc.platform.spigot.world.manager; import com.github.steveice10.mc.protocol.data.game.chunk.Chunk; +import org.bukkit.plugin.Plugin; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; @@ -35,9 +36,9 @@ import org.geysermc.connector.network.translators.world.block.BlockTranslator; * If this occurs to you somehow, please let us know!! */ public class GeyserSpigotFallbackWorldManager extends GeyserSpigotWorldManager { - public GeyserSpigotFallbackWorldManager() { + public GeyserSpigotFallbackWorldManager(Plugin plugin) { // Since this is pre-1.13 (and thus pre-1.15), there will never be 3D biomes. - super(false); + super(plugin, false); } @Override @@ -51,7 +52,7 @@ public class GeyserSpigotFallbackWorldManager extends GeyserSpigotWorldManager { } @Override - public boolean hasMoreBlockDataThanChunkCache() { + public boolean hasOwnChunkCache() { return false; } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java index 8ed1b3883..8f407de0a 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java @@ -47,7 +47,7 @@ public class GeyserSpigotLegacyNativeWorldManager extends GeyserSpigotNativeWorl private final Int2IntMap oldToNewBlockId; public GeyserSpigotLegacyNativeWorldManager(GeyserSpigotPlugin plugin, boolean use3dBiomes) { - super(use3dBiomes); + super(plugin, use3dBiomes); IntList allBlockStates = adapter.getAllBlockStates(); oldToNewBlockId = new Int2IntOpenHashMap(allBlockStates.size()); ProtocolVersion serverVersion = plugin.getServerProtocolVersion(); diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java index c7e3a3d4f..cc9d5bddc 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java @@ -27,16 +27,17 @@ package org.geysermc.platform.spigot.world.manager; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.geysermc.adapters.spigot.SpigotAdapters; -import org.geysermc.adapters.spigot.SpigotWorldAdapter; +import org.bukkit.plugin.Plugin; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.geyser.adapters.spigot.SpigotAdapters; +import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter; public class GeyserSpigotNativeWorldManager extends GeyserSpigotWorldManager { protected final SpigotWorldAdapter adapter; - public GeyserSpigotNativeWorldManager(boolean use3dBiomes) { - super(use3dBiomes); + public GeyserSpigotNativeWorldManager(Plugin plugin, boolean use3dBiomes) { + super(plugin, use3dBiomes); adapter = SpigotAdapters.getWorldAdapter(); } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java index 748d0f1ef..ba61eeb72 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java @@ -28,23 +28,35 @@ package org.geysermc.platform.spigot.world.manager; import com.fasterxml.jackson.databind.JsonNode; import com.github.steveice10.mc.protocol.MinecraftConstants; import com.github.steveice10.mc.protocol.data.game.chunk.Chunk; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.nbt.NbtType; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.Block; +import org.bukkit.block.Lectern; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.plugin.Plugin; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.translators.LecternInventoryTranslator; import org.geysermc.connector.network.translators.world.GeyserWorldManager; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.utils.BlockEntityUtils; import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.GameRule; import org.geysermc.connector.utils.LanguageUtils; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; /** * The base world manager to use when there is no supported NMS revision @@ -72,8 +84,11 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager { */ private final Int2IntMap biomeToIdMap = new Int2IntOpenHashMap(Biome.values().length); - public GeyserSpigotWorldManager(boolean use3dBiomes) { + private final Plugin plugin; + + public GeyserSpigotWorldManager(Plugin plugin, boolean use3dBiomes) { this.use3dBiomes = use3dBiomes; + this.plugin = plugin; // Load the values into the biome-to-ID map InputStream biomeStream = FileUtils.getResource("biomes.json"); @@ -125,16 +140,13 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager { } @Override - public boolean hasMoreBlockDataThanChunkCache() { + public boolean hasOwnChunkCache() { return true; } @Override @SuppressWarnings("deprecation") public int[] getBiomeDataAt(GeyserSession session, int x, int z) { - if (session.getPlayerEntity() == null) { - return new int[1024]; - } int[] biomeData = new int[1024]; World world = Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld(); int chunkX = x << 4; @@ -167,6 +179,77 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager { return biomeData; } + @Override + public NbtMap getLecternDataAt(GeyserSession session, int x, int y, int z, boolean isChunkLoad) { + // Run as a task to prevent async issues + Runnable lecternInfoGet = () -> { + Player bukkitPlayer; + if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) { + return; + } + + Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z); + if (!(block.getState() instanceof Lectern)) { + session.getConnector().getLogger().error("Lectern expected at: " + Vector3i.from(x, y, z).toString() + " but was not! " + block.toString()); + return; + } + + Lectern lectern = (Lectern) block.getState(); + ItemStack itemStack = lectern.getInventory().getItem(0); + if (itemStack == null || !(itemStack.getItemMeta() instanceof BookMeta)) { + if (!isChunkLoad) { + // We need to update the lectern since it's not going to be updated otherwise + BlockEntityUtils.updateBlockEntity(session, LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build(), Vector3i.from(x, y, z)); + } + // We don't care; return + return; + } + + BookMeta bookMeta = (BookMeta) itemStack.getItemMeta(); + // On the count: allow the book to show/open even there are no pages. We know there is a book here, after all, and this matches Java behavior + boolean hasBookPages = bookMeta.getPageCount() > 0; + NbtMapBuilder lecternTag = LecternInventoryTranslator.getBaseLecternTag(x, y, z, hasBookPages ? bookMeta.getPageCount() : 1); + lecternTag.putInt("page", lectern.getPage() / 2); + NbtMapBuilder bookTag = NbtMap.builder() + .putByte("Count", (byte) itemStack.getAmount()) + .putShort("Damage", (short) 0) + .putString("Name", "minecraft:writable_book"); + List pages = new ArrayList<>(bookMeta.getPageCount()); + if (hasBookPages) { + for (String page : bookMeta.getPages()) { + NbtMapBuilder pageBuilder = NbtMap.builder() + .putString("photoname", "") + .putString("text", page); + pages.add(pageBuilder.build()); + } + } else { + // Empty page + NbtMapBuilder pageBuilder = NbtMap.builder() + .putString("photoname", "") + .putString("text", ""); + pages.add(pageBuilder.build()); + } + + bookTag.putCompound("tag", NbtMap.builder().putList("pages", NbtType.COMPOUND, pages).build()); + lecternTag.putCompound("book", bookTag.build()); + NbtMap blockEntityTag = lecternTag.build(); + BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z)); + }; + + if (isChunkLoad) { + // Delay to ensure the chunk is sent first, and then the lectern data + Bukkit.getScheduler().runTaskLater(this.plugin, lecternInfoGet, 5); + } else { + Bukkit.getScheduler().runTask(this.plugin, lecternInfoGet); + } + return LecternInventoryTranslator.getBaseLecternTag(x, y, z, 0).build(); // Will be updated later + } + + @Override + public boolean shouldExpectLecternHandled() { + return true; + } + public Boolean getGameRuleBool(GeyserSession session, GameRule gameRule) { return Boolean.parseBoolean(Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getGameRuleValue(gameRule.getJavaID())); } diff --git a/bootstrap/spigot/src/main/resources/plugin.yml b/bootstrap/spigot/src/main/resources/plugin.yml index fee71ab1f..ea655f902 100644 --- a/bootstrap/spigot/src/main/resources/plugin.yml +++ b/bootstrap/spigot/src/main/resources/plugin.yml @@ -3,7 +3,7 @@ name: ${outputName}-Spigot author: ${project.organization.name} website: ${project.organization.url} version: ${project.version} -softdepend: ["ViaVersion"] +softdepend: ["ViaVersion", "floodgate"] api-version: 1.13 commands: geyser: diff --git a/bootstrap/sponge/pom.xml b/bootstrap/sponge/pom.xml index 97c4ac8a4..adeaa91de 100644 --- a/bootstrap/sponge/pom.xml +++ b/bootstrap/sponge/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-sponge @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java index 5b8bf54b2..1986bbd22 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java @@ -101,7 +101,7 @@ public class GeyserSpongePlugin implements GeyserBootstrap { } } - if (geyserConfig.getBedrock().isCloneRemotePort()){ + if (geyserConfig.getBedrock().isCloneRemotePort()) { geyserConfig.getBedrock().setPort(geyserConfig.getRemote().getPort()); } @@ -163,4 +163,9 @@ public class GeyserSpongePlugin implements GeyserBootstrap { public BootstrapDumpInfo getDumpInfo() { return new GeyserSpongeDumpInfo(); } + + @Override + public String getMinecraftServerVersion() { + return Sponge.getPlatform().getMinecraftVersion().getName(); + } } diff --git a/bootstrap/standalone/pom.xml b/bootstrap/standalone/pom.xml index 831239f66..6610af3b5 100644 --- a/bootstrap/standalone/pom.xml +++ b/bootstrap/standalone/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-standalone @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java index 5aa15635e..551b0e584 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java @@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedField; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import lombok.Getter; import net.minecrell.terminalconsole.TerminalConsoleAppender; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Logger; @@ -167,11 +168,6 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { this.onEnable(); } - public void onEnable(boolean useGui) { - this.useGui = useGui; - this.onEnable(); - } - @Override public void onEnable() { Logger logger = (Logger) LogManager.getRootLogger(); @@ -213,6 +209,9 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { } GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); + // Allow libraries like Protocol to have their debug information passthrough + logger.get().setLevel(geyserConfig.isDebugMode() ? Level.DEBUG : Level.INFO); + connector = GeyserConnector.start(PlatformType.STANDALONE, this); geyserCommandManager = new GeyserCommandManager(connector); diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java index 9c10234f3..7eeba84bd 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java @@ -55,7 +55,7 @@ public class LoopbackUtil { if (!result.contains("minecraftuwp")) { Files.write(Paths.get(System.getenv("temp") + "/loopback_minecraft.bat"), loopbackCommand.getBytes(), new OpenOption[0]); - process = Runtime.getRuntime().exec(startScript); + Runtime.getRuntime().exec(startScript); geyserLogger.info(ChatColor.AQUA + LanguageUtils.getLocaleStringLog("geyser.bootstrap.loopback.added")); } diff --git a/bootstrap/velocity/pom.xml b/bootstrap/velocity/pom.xml index 58eee1f77..5dadc83c4 100644 --- a/bootstrap/velocity/pom.xml +++ b/bootstrap/velocity/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-velocity @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/common/pom.xml b/common/pom.xml index 0f4d5421b..be90f0146 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT common diff --git a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java index 5f449fe2d..cbf49e126 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java +++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java @@ -26,8 +26,8 @@ package org.geysermc.floodgate.util; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; /** * This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. This @@ -35,9 +35,9 @@ import lombok.Getter; * present in the API module of the Floodgate repo) */ @Getter -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public final class BedrockData implements Cloneable { - public static final int EXPECTED_LENGTH = 10; + public static final int EXPECTED_LENGTH = 13; private final String version; private final String username; @@ -50,19 +50,26 @@ public final class BedrockData implements Cloneable { private final LinkedPlayer linkedPlayer; private final boolean fromProxy; + private final int subscribeId; + private final String verifyCode; + + private final long timestamp; private final int dataLength; public static BedrockData of(String version, String username, String xuid, int deviceOs, String languageCode, int uiProfile, int inputMode, String ip, - LinkedPlayer linkedPlayer, boolean fromProxy) { + LinkedPlayer linkedPlayer, boolean fromProxy, int subscribeId, + String verifyCode) { return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode, - uiProfile, ip, linkedPlayer, fromProxy, EXPECTED_LENGTH); + uiProfile, ip, linkedPlayer, fromProxy, subscribeId, verifyCode, + System.currentTimeMillis(), EXPECTED_LENGTH); } public static BedrockData of(String version, String username, String xuid, int deviceOs, - String languageCode, int uiProfile, int inputMode, String ip) { - return of(version, username, xuid, deviceOs, languageCode, - uiProfile, inputMode, ip, null, false); + String languageCode, int uiProfile, int inputMode, String ip, + int subscribeId, String verifyCode) { + return of(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null, + false, subscribeId, verifyCode); } public static BedrockData fromString(String data) { @@ -75,13 +82,14 @@ public final class BedrockData implements Cloneable { // The format is the same as the order of the fields in this class return new BedrockData( split[0], split[1], split[2], Integer.parseInt(split[3]), split[4], - Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], - linkedPlayer, "1".equals(split[8]), split.length + Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], linkedPlayer, + "1".equals(split[9]), Integer.parseInt(split[10]), split[11], Long.parseLong(split[12]), split.length ); } private static BedrockData emptyData(int dataLength) { - return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, dataLength); + return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, -1, null, -1, + dataLength); } public boolean hasPlayerLink() { @@ -94,7 +102,8 @@ public final class BedrockData implements Cloneable { return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' + languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' + (fromProxy ? 1 : 0) + '\0' + - (linkedPlayer != null ? linkedPlayer.toString() : "null"); + (linkedPlayer != null ? linkedPlayer.toString() : "null") + '\0' + + subscribeId + '\0' + verifyCode + '\0' + timestamp; } @Override diff --git a/common/src/main/java/org/geysermc/floodgate/util/InputMode.java b/common/src/main/java/org/geysermc/floodgate/util/InputMode.java index 9664e18ae..d49d2ea84 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/InputMode.java +++ b/common/src/main/java/org/geysermc/floodgate/util/InputMode.java @@ -29,7 +29,7 @@ package org.geysermc.floodgate.util; public enum InputMode { UNKNOWN, KEYBOARD_MOUSE, - TOUCH, // I guess Touch? + TOUCH, CONTROLLER, VR; diff --git a/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java index 950f0eb55..7d67a44a0 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java +++ b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java @@ -57,7 +57,7 @@ public final class LinkedPlayer implements Cloneable { return new LinkedPlayer(javaUsername, javaUniqueId, bedrockId); } - static LinkedPlayer fromString(String data) { + public static LinkedPlayer fromString(String data) { String[] split = data.split(";"); if (split.length != 3) { return null; diff --git a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java deleted file mode 100644 index 470b7e24a..000000000 --- a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.floodgate.util; - -import lombok.AllArgsConstructor; - -import java.nio.ByteBuffer; -import java.util.Base64; - -import static java.lang.String.format; - -@AllArgsConstructor -public final class RawSkin { - public int width; - public int height; - public byte[] data; - public boolean alex; - - private RawSkin() { - } - - public static RawSkin decode(byte[] data, int offset) throws InvalidFormatException { - if (data == null || offset < 0 || data.length <= offset) { - return null; - } - if (offset == 0) { - return decode(data); - } - - byte[] rawSkin = new byte[data.length - offset]; - System.arraycopy(data, offset, rawSkin, 0, rawSkin.length); - return decode(rawSkin); - } - - public static RawSkin decode(byte[] data) throws InvalidFormatException { - // offset is an amount of bytes before the Base64 starts - if (data == null) { - return null; - } - - int maxEncodedLength = Base64Utils.getEncodedLength(64 * 64 * 4 + 9); - // if the RawSkin is longer then the max Java Edition skin length - if (data.length > maxEncodedLength) { - throw new InvalidFormatException(format( - "Encoded data cannot be longer then %s bytes! Got %s", - maxEncodedLength, data.length - )); - } - - // if the encoded data doesn't even contain the width, height (8 bytes, 2 ints) and isAlex - if (data.length < Base64Utils.getEncodedLength(9)) { - throw new InvalidFormatException("Encoded data must be at least 16 bytes long!"); - } - - data = Base64.getDecoder().decode(data); - - ByteBuffer buffer = ByteBuffer.wrap(data); - - RawSkin skin = new RawSkin(); - skin.width = buffer.getInt(); - skin.height = buffer.getInt(); - if (buffer.remaining() - 1 != (skin.width * skin.height * 4)) { - throw new InvalidFormatException(format( - "Expected skin length to be %s, got %s", - (skin.width * skin.height * 4), buffer.remaining() - )); - } - skin.data = new byte[buffer.remaining() - 1]; - buffer.get(skin.data); - skin.alex = buffer.get() == 1; - return skin; - } - - public byte[] encode() { - // 2 x int + 1 = 9 bytes - ByteBuffer buffer = ByteBuffer.allocate(9 + data.length); - buffer.putInt(width); - buffer.putInt(height); - buffer.put(data); - buffer.put((byte) (alex ? 1 : 0)); - return Base64.getEncoder().encode(buffer.array()); - } -} diff --git a/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java b/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java new file mode 100644 index 000000000..1527f2360 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java @@ -0,0 +1,40 @@ +/* + * 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.floodgate.util; + +public enum WebsocketEventType { + SUBSCRIBER_CREATED, + SUBSCRIBERS_COUNT, + ADDED_TO_QUEUE, + SKIN_UPLOADED, + CREATOR_DISCONNECTED; + + public static final WebsocketEventType[] VALUES = values(); + + public static WebsocketEventType getById(int id) { + return VALUES.length > id ? VALUES[id] : null; + } +} diff --git a/connector/pom.xml b/connector/pom.xml index 5e78fcfc3..4e244dd00 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -6,39 +6,46 @@ org.geysermc geyser-parent - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT connector + + 4.1.59.Final + 8.5.2 + 4.7.0 + + org.geysermc common - 1.2.0-SNAPSHOT - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - 2.9.8 + 1.3.0-SNAPSHOT compile com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.9.8 + 2.10.2 compile + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.10.2 + compile + + + org.java-websocket + Java-WebSocket + 1.5.1 + com.github.CloudburstMC.Protocol - bedrock-v422 - d41b84e86c + bedrock-v428 + 42da92f compile - - net.sf.trove4j - trove - - com.nukkitx.network raknet @@ -48,67 +55,73 @@ com.nukkitx.network raknet - 1.6.20 + 1.6.26-20210217.205834-2 compile + + + io.netty + * + + com.nukkitx.fastutil fastutil-int-int-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-int-float-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-long-long-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-object-long-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-int-byte-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-int-double-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-int-boolean-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-object-int-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-object-byte-maps - 8.3.1 + ${fastutil.version} compile com.nukkitx.fastutil fastutil-object-object-maps - 8.3.1 + ${fastutil.version} compile @@ -138,9 +151,9 @@ - com.github.steveice10 + com.github.GeyserMC PacketLib - 54f761c + b77a427 compile @@ -152,15 +165,51 @@ io.netty netty-resolver-dns - 4.1.43.Final + ${netty.version} compile + + io.netty + netty-resolver-dns-native-macos + ${netty.version} + compile + osx-x86_64 + io.netty netty-codec-haproxy - 4.1.56.Final + ${netty.version} compile + + + io.netty + netty-handler + ${netty.version} + compile + + + io.netty + netty-transport-native-epoll + ${netty.version} + compile + linux-x86_64 + + + io.netty + netty-transport-native-epoll + ${netty.version} + compile + linux-aarch_64 + + + io.netty + netty-transport-native-kqueue + ${netty.version} + compile + osx-x86_64 + + org.reflections reflections @@ -174,25 +223,25 @@ net.kyori adventure-api - 4.3.0 + ${adventure.version} compile net.kyori adventure-text-serializer-gson - 4.3.0 + ${adventure.version} compile net.kyori adventure-text-serializer-legacy - 4.3.0 + ${adventure.version} compile net.kyori adventure-text-serializer-gson-legacy-impl - 4.3.0 + ${adventure.version} compile diff --git a/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java b/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java index 9b9ee5681..7f5bca8c2 100644 --- a/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java +++ b/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java @@ -33,9 +33,18 @@ import java.nio.file.Path; public class FloodgateKeyLoader { public static Path getKeyPath(GeyserJacksonConfiguration config, Object floodgate, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) { + if (!config.getRemote().getAuthType().equals("floodgate")) { + return geyserDataFolder.resolve(config.getFloodgateKeyFile()); + } + Path floodgateKey = geyserDataFolder.resolve(config.getFloodgateKeyFile()); - if (!Files.exists(floodgateKey) && config.getRemote().getAuthType().equals("floodgate")) { + if (config.getFloodgateKeyFile().equals("public-key.pem")) { + logger.info("Floodgate 2.0 doesn't use a public/private key system anymore. We'll search for key.pem instead"); + floodgateKey = geyserDataFolder.resolve("key.pem"); + } + + if (!Files.exists(floodgateKey)) { if (floodgate != null) { Path autoKey = floodgateDataFolder.resolve("key.pem"); if (Files.exists(autoKey)) { diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 38c2aa29a..5e279c121 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.nukkitx.network.raknet.RakNetConstants; +import com.nukkitx.network.util.EventLoops; import com.nukkitx.protocol.bedrock.BedrockServer; import lombok.Getter; import lombok.Setter; @@ -39,7 +40,6 @@ import org.geysermc.connector.common.AuthType; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.metrics.Metrics; import org.geysermc.connector.network.ConnectorServerEventHandler; -import org.geysermc.connector.network.remote.RemoteServer; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.BiomeTranslator; import org.geysermc.connector.network.translators.EntityIdentifierRegistry; @@ -56,10 +56,8 @@ import org.geysermc.connector.network.translators.world.WorldManager; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; -import org.geysermc.connector.utils.DimensionUtils; -import org.geysermc.connector.utils.LanguageUtils; -import org.geysermc.connector.utils.LocaleUtils; -import org.geysermc.connector.utils.ResourcePack; +import org.geysermc.connector.skin.FloodgateSkinUploader; +import org.geysermc.connector.utils.*; import org.geysermc.floodgate.crypto.AesCipher; import org.geysermc.floodgate.crypto.AesKeyProducer; import org.geysermc.floodgate.crypto.Base64Topping; @@ -85,7 +83,8 @@ public class GeyserConnector { .enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.ALLOW_COMMENTS) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) + .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); public static final String NAME = "Geyser"; public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs @@ -103,11 +102,11 @@ public class GeyserConnector { private static GeyserConnector instance; - private RemoteServer remoteServer; @Setter - private AuthType authType; + private AuthType defaultAuthType; private FloodgateCipher cipher; + private FloodgateSkinUploader skinUploader; private boolean shuttingDown = false; @@ -174,7 +173,7 @@ public class GeyserConnector { String remoteAddress = config.getRemote().getAddress(); int remotePort = config.getRemote().getPort(); // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. - if ((config.isLegacyPingPassthrough() || platformType == PlatformType.STANDALONE) && !remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { + if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { try { // Searches for a server address and a port from a SRV record of the specified host name InitialDirContext ctx = new InitialDirContext(); @@ -194,20 +193,21 @@ public class GeyserConnector { } } - remoteServer = new RemoteServer(config.getRemote().getAddress(), remotePort); - authType = AuthType.getByName(config.getRemote().getAuthType()); + defaultAuthType = AuthType.getByName(config.getRemote().getAuthType()); - if (authType == AuthType.FLOODGATE) { + if (defaultAuthType == AuthType.FLOODGATE) { try { Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath()); cipher = new AesCipher(new Base64Topping()); cipher.init(key); logger.info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); + skinUploader = new FloodgateSkinUploader(this).start(); } catch (Exception exception) { logger.severe(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), exception); } } + CooldownUtils.setShowCooldown(config.isShowCooldown()); DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls(); @@ -215,7 +215,13 @@ public class GeyserConnector { RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu(); logger.debug("Setting MTU to " + config.getMtu()); - bedrockServer = new BedrockServer(new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort())); + boolean enableProxyProtocol = config.getBedrock().isEnableProxyProtocol(); + bedrockServer = new BedrockServer( + new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort()), + 1, + EventLoops.commonGroup(), + enableProxyProtocol + ); bedrockServer.setHandler(new ConnectorServerEventHandler(this)); bedrockServer.bind().whenComplete((avoid, throwable) -> { if (throwable == null) { @@ -262,6 +268,20 @@ public class GeyserConnector { } return valueMap; })); + + String minecraftVersion = bootstrap.getMinecraftServerVersion(); + if (minecraftVersion != null) { + Map> versionMap = new HashMap<>(); + Map platformMap = new HashMap<>(); + platformMap.put(platformType.getPlatformName(), 1); + versionMap.put(minecraftVersion, platformMap); + + metrics.addCustomChart(new Metrics.DrilldownPie("minecraftServerVersion", () -> { + // By the end, we should return, for example: + // 1.16.5 => (Spigot, 1) + return versionMap; + })); + } } boolean isGui = false; @@ -332,8 +352,7 @@ public class GeyserConnector { generalThreadPool.shutdown(); bedrockServer.close(); players.clear(); - remoteServer = null; - authType = null; + defaultAuthType = null; this.getCommandManager().getCommands().clear(); bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.done")); @@ -369,6 +388,7 @@ public class GeyserConnector { * @param xuid the Xbox user identifier * @return the player or null if there is no player online with this xuid */ + @SuppressWarnings("unused") // API usage public GeyserSession getPlayerByXuid(String xuid) { for (GeyserSession session : players) { if (session.getAuthData() != null && session.getAuthData().getXboxUUID().equals(xuid)) { diff --git a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java index 7308fb674..59dc58c2b 100644 --- a/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java +++ b/connector/src/main/java/org/geysermc/connector/bootstrap/GeyserBootstrap.java @@ -33,6 +33,7 @@ import org.geysermc.connector.command.CommandManager; import org.geysermc.connector.network.translators.world.GeyserWorldManager; import org.geysermc.connector.network.translators.world.WorldManager; +import javax.annotation.Nullable; import java.nio.file.Path; public interface GeyserBootstrap { @@ -99,4 +100,18 @@ public interface GeyserBootstrap { * @return The info about the bootstrap */ BootstrapDumpInfo getDumpInfo(); + + /** + * Returns the Minecraft version currently being used on the server. This should be only be implemented on platforms + * that have direct server access - platforms such as proxies always have to be on their latest version to support + * the newest Minecraft version, but older servers can use ViaVersion to enable newer versions to join. + *
+ * If used, this should not be null before {@link org.geysermc.connector.GeyserConnector} initialization. + * + * @return the Minecraft version being used on the server, or null if not applicable + */ + @Nullable + default String getMinecraftServerVersion() { + return null; + } } diff --git a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java index 3f674d7fa..f91da11b5 100644 --- a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java +++ b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java @@ -52,7 +52,7 @@ public class IGeyserMain { * @return The formatted message */ private String createMessage() { - String message = ""; + StringBuilder message = new StringBuilder(); InputStream helpStream = IGeyserMain.class.getClassLoader().getResourceAsStream("languages/run-help/" + Locale.getDefault().toString() + ".txt"); @@ -68,10 +68,10 @@ public class IGeyserMain { line = line.replace("${plugin_type}", this.getPluginType()); line = line.replace("${plugin_folder}", this.getPluginFolder()); - message += line + "\n"; + message.append(line).append("\n"); } - return message; + return message.toString(); } /** diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index e21aa6bb8..6052bd283 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -27,9 +27,11 @@ package org.geysermc.connector.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import org.geysermc.connector.GeyserLogger; +import org.geysermc.connector.network.CIDRMatcher; import org.geysermc.connector.utils.LanguageUtils; import java.nio.file.Path; +import java.util.List; import java.util.Map; public interface GeyserConfiguration { @@ -59,6 +61,8 @@ public interface GeyserConfiguration { int getPingPassthroughInterval(); + boolean isForwardPlayerPing(); + int getMaxPlayers(); boolean isDebugMode(); @@ -104,6 +108,15 @@ public interface GeyserConfiguration { String getMotd2(); String getServerName(); + + boolean isEnableProxyProtocol(); + + List getProxyProtocolWhitelistedIPs(); + + /** + * @return Unmodifiable list of {@link CIDRMatcher}s from {@link #getProxyProtocolWhitelistedIPs()} + */ + List getWhitelistedIPsMatchers(); } interface IRemoteConfiguration { diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index 7c9532ff8..70aa3ff5d 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -25,16 +25,21 @@ package org.geysermc.connector.configuration; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.Setter; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.serializer.AsteriskSerializer; +import org.geysermc.connector.network.CIDRMatcher; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; @Getter @JsonIgnoreProperties(ignoreUnknown = true) @@ -74,6 +79,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("ping-passthrough-interval") private int pingPassthroughInterval = 3; + @JsonProperty("forward-player-ping") + private boolean forwardPlayerPing = false; + @JsonProperty("max-players") private int maxPlayers = 100; @@ -119,6 +127,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private MetricsInfo metrics = new MetricsInfo(); @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class BedrockConfiguration implements IBedrockConfiguration { @AsteriskSerializer.Asterisk(sensitive = true) private String address = "0.0.0.0"; @@ -134,9 +143,33 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("server-name") private String serverName = GeyserConnector.NAME; + + @JsonProperty("enable-proxy-protocol") + private boolean enableProxyProtocol = false; + + @JsonProperty("proxy-protocol-whitelisted-ips") + private List proxyProtocolWhitelistedIPs = Collections.emptyList(); + + @JsonIgnore + private List whitelistedIPsMatchers = null; + + @Override + public List getWhitelistedIPsMatchers() { + // Effective Java, Third Edition; Item 83: Use lazy initialization judiciously + List matchers = this.whitelistedIPsMatchers; + if (matchers == null) { + synchronized (this) { + this.whitelistedIPsMatchers = matchers = proxyProtocolWhitelistedIPs.stream() + .map(CIDRMatcher::new) + .collect(Collectors.toList()); + } + } + return Collections.unmodifiableList(matchers); + } } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class RemoteConfiguration implements IRemoteConfiguration { @Setter @AsteriskSerializer.Asterisk(sensitive = true) @@ -170,6 +203,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class MetricsInfo implements IMetricsInfo { private boolean enabled = true; diff --git a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java index e9a4a1f98..70dbdf959 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java @@ -35,6 +35,8 @@ public class AbstractArrowEntity extends Entity { public AbstractArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + setMotion(motion); } @Override @@ -47,4 +49,20 @@ public class AbstractArrowEntity extends Entity { super.updateBedrockMetadata(entityMetadata, session); } + + @Override + public void setRotation(Vector3f rotation) { + // Ignore the rotation sent by the Java server since the + // Java client calculates the rotation from the motion + } + + @Override + public void setMotion(Vector3f motion) { + super.setMotion(motion); + + double horizontalSpeed = Math.sqrt(motion.getX() * motion.getX() + motion.getZ() * motion.getZ()); + float yaw = (float) Math.toDegrees(Math.atan2(motion.getX(), motion.getZ())); + float pitch = (float) Math.toDegrees(Math.atan2(motion.getY(), horizontalSpeed)); + rotation = Vector3f.from(yaw, pitch, yaw); + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java b/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java index 1afcf08d3..d07ecc965 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/BoatEntity.java @@ -28,6 +28,7 @@ package org.geysermc.connector.entity; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.packet.AnimatePacket; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; @@ -43,7 +44,7 @@ public class BoatEntity extends Entity { */ private static final String BUOYANCY_DATA = "{\"apply_gravity\":true,\"base_buoyancy\":1.0,\"big_wave_probability\":0.02999999932944775," + "\"big_wave_speed\":10.0,\"drag_down_on_buoyancy_removed\":0.0,\"liquid_blocks\":[\"minecraft:water\"," + - "\"minecraft:flowing_water\"],\"simulate_waves\":false}}"; + "\"minecraft:flowing_water\"],\"simulate_waves\":false}"; private boolean isPaddlingLeft; private float paddleTimeLeft; @@ -105,27 +106,38 @@ public class BoatEntity extends Entity { metadata.put(EntityData.VARIANT, entityMetadata.getValue()); } else if (entityMetadata.getId() == 11) { isPaddlingLeft = (boolean) entityMetadata.getValue(); - if (!isPaddlingLeft) { - metadata.put(EntityData.ROW_TIME_LEFT, 0f); - } - else { + if (isPaddlingLeft) { // Java sends simply "true" and "false" (is_paddling_left), Bedrock keeps sending packets as you're rowing // This is an asynchronous method that emulates Bedrock rowing until "false" is sent. paddleTimeLeft = 0f; - session.getConnector().getGeneralThreadPool().execute(() -> - updateLeftPaddle(session, entityMetadata) - ); + if (!this.passengers.isEmpty()) { + // Get the entity by the first stored passenger and convey motion in this manner + Entity entity = session.getEntityCache().getEntityByJavaId(this.passengers.iterator().nextLong()); + if (entity != null) { + session.getConnector().getGeneralThreadPool().execute(() -> + updateLeftPaddle(session, entity) + ); + } + } + } else { + // Indicate that the row position should be reset + metadata.put(EntityData.ROW_TIME_LEFT, 0.0f); } } else if (entityMetadata.getId() == 12) { isPaddlingRight = (boolean) entityMetadata.getValue(); - if (!isPaddlingRight) { - metadata.put(EntityData.ROW_TIME_RIGHT, 0f); - } else { + if (isPaddlingRight) { paddleTimeRight = 0f; - session.getConnector().getGeneralThreadPool().execute(() -> - updateRightPaddle(session, entityMetadata) - ); + if (!this.passengers.isEmpty()) { + Entity entity = session.getEntityCache().getEntityByJavaId(this.passengers.iterator().nextLong()); + if (entity != null) { + session.getConnector().getGeneralThreadPool().execute(() -> + updateRightPaddle(session, entity) + ); + } + } + } else { + metadata.put(EntityData.ROW_TIME_RIGHT, 0.0f); } } else if (entityMetadata.getId() == 13) { // Possibly - I don't think this does anything? @@ -135,27 +147,46 @@ public class BoatEntity extends Entity { super.updateBedrockMetadata(entityMetadata, session); } - public void updateLeftPaddle(GeyserSession session, EntityMetadata entityMetadata) { + @Override + public void updateBedrockMetadata(GeyserSession session) { + super.updateBedrockMetadata(session); + + // As these indicate to reset rowing, remove them until it is time to send them out again. + metadata.remove(EntityData.ROW_TIME_LEFT); + metadata.remove(EntityData.ROW_TIME_RIGHT); + } + + private void updateLeftPaddle(GeyserSession session, Entity rower) { if (isPaddlingLeft) { paddleTimeLeft += ROWING_SPEED; - metadata.put(EntityData.ROW_TIME_LEFT, paddleTimeLeft); - super.updateBedrockMetadata(entityMetadata, session); + sendAnimationPacket(session, rower, AnimatePacket.Action.ROW_LEFT, paddleTimeLeft); + session.getConnector().getGeneralThreadPool().schedule(() -> - updateLeftPaddle(session, entityMetadata), + updateLeftPaddle(session, rower), 100, TimeUnit.MILLISECONDS ); - }} + } + } - public void updateRightPaddle(GeyserSession session, EntityMetadata entityMetadata) { + private void updateRightPaddle(GeyserSession session, Entity rower) { if (isPaddlingRight) { paddleTimeRight += ROWING_SPEED; - metadata.put(EntityData.ROW_TIME_RIGHT, paddleTimeRight); - super.updateBedrockMetadata(entityMetadata, session); + sendAnimationPacket(session, rower, AnimatePacket.Action.ROW_RIGHT, paddleTimeRight); + session.getConnector().getGeneralThreadPool().schedule(() -> - updateRightPaddle(session, entityMetadata), + updateRightPaddle(session, rower), 100, TimeUnit.MILLISECONDS ); - }} + } + } + + private void sendAnimationPacket(GeyserSession session, Entity rower, AnimatePacket.Action action, float rowTime) { + AnimatePacket packet = new AnimatePacket(); + packet.setRuntimeEntityId(rower.getGeyserId()); + packet.setAction(action); + packet.setRowingTime(rowTime); + session.sendUpstreamPacket(packet); + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java index 6ae65643c..52183c431 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/CommandBlockMinecartEntity.java @@ -31,7 +31,6 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData; import net.kyori.adventure.text.Component; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.chat.MessageTranslator; public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity { @@ -60,8 +59,8 @@ public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity { * By default, the command block shown is purple on Bedrock, which does not match Java Edition's orange. */ @Override - public void updateDefaultBlockMetadata() { - metadata.put(EntityData.DISPLAY_ITEM, BlockTranslator.BEDROCK_RUNTIME_COMMAND_BLOCK_ID); + public void updateDefaultBlockMetadata(GeyserSession session) { + metadata.put(EntityData.DISPLAY_ITEM, session.getBlockTranslator().getBedrockRuntimeCommandBlockId()); metadata.put(EntityData.DISPLAY_OFFSET, 6); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java index 8ab368e70..805105c64 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/DefaultBlockMinecartEntity.java @@ -30,7 +30,6 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; /** * This class is used as a base for minecarts with a default block to display like furnaces and spawners @@ -44,10 +43,15 @@ public class DefaultBlockMinecartEntity extends MinecartEntity { public DefaultBlockMinecartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); - updateDefaultBlockMetadata(); metadata.put(EntityData.CUSTOM_DISPLAY, (byte) 1); } + @Override + public void spawnEntity(GeyserSession session) { + updateDefaultBlockMetadata(session); + super.spawnEntity(session); + } + @Override public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { @@ -56,7 +60,7 @@ public class DefaultBlockMinecartEntity extends MinecartEntity { customBlock = (int) entityMetadata.getValue(); if (showCustomBlock) { - metadata.put(EntityData.DISPLAY_ITEM, BlockTranslator.getBedrockBlockId(customBlock)); + metadata.put(EntityData.DISPLAY_ITEM, session.getBlockTranslator().getBedrockBlockId(customBlock)); } } @@ -73,16 +77,16 @@ public class DefaultBlockMinecartEntity extends MinecartEntity { if (entityMetadata.getId() == 12) { if ((boolean) entityMetadata.getValue()) { showCustomBlock = true; - metadata.put(EntityData.DISPLAY_ITEM, BlockTranslator.getBedrockBlockId(customBlock)); + metadata.put(EntityData.DISPLAY_ITEM, session.getBlockTranslator().getBedrockBlockId(customBlock)); metadata.put(EntityData.DISPLAY_OFFSET, customBlockOffset); } else { showCustomBlock = false; - updateDefaultBlockMetadata(); + updateDefaultBlockMetadata(session); } } super.updateBedrockMetadata(entityMetadata, session); } - public void updateDefaultBlockMetadata() { } + public void updateDefaultBlockMetadata(GeyserSession session) { } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/Entity.java b/connector/src/main/java/org/geysermc/connector/entity/Entity.java index d41578db4..2dcd49fb0 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/Entity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/Entity.java @@ -28,12 +28,6 @@ package org.geysermc.connector.entity; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; -import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; -import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; -import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace; -import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket; -import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.AttributeData; import com.nukkitx.protocol.bedrock.data.entity.EntityData; @@ -51,9 +45,8 @@ import org.geysermc.connector.entity.living.ArmorStandEntity; 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.item.ItemRegistry; -import org.geysermc.connector.utils.AttributeUtils; import org.geysermc.connector.network.translators.chat.MessageTranslator; +import org.geysermc.connector.utils.AttributeUtils; import java.util.ArrayList; import java.util.HashMap; @@ -273,35 +266,9 @@ public class Entity { metadata.getFlags().setFlag(EntityFlag.SWIMMING, ((xd & 0x10) == 0x10) && metadata.getFlags().getFlag(EntityFlag.SPRINTING)); // Otherwise swimming is enabled on older servers metadata.getFlags().setFlag(EntityFlag.GLIDING, (xd & 0x80) == 0x80); - if ((xd & 0x20) == 0x20) { - // Armour stands are handled in their own class - if (!this.is(ArmorStandEntity.class)) { - metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); - } - } else { - metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false); - } - - // Shield code - if (session.getPlayerEntity().getEntityId() == entityId && metadata.getFlags().getFlag(EntityFlag.SNEAKING)) { - if ((session.getInventory().getItemInHand() != null && session.getInventory().getItemInHand().getId() == ItemRegistry.SHIELD.getJavaId()) || - (session.getInventoryCache().getPlayerInventory().getItem(45) != null && session.getInventoryCache().getPlayerInventory().getItem(45).getId() == ItemRegistry.SHIELD.getJavaId())) { - ClientPlayerUseItemPacket useItemPacket; - metadata.getFlags().setFlag(EntityFlag.BLOCKING, true); - if (session.getInventory().getItemInHand() != null && session.getInventory().getItemInHand().getId() == ItemRegistry.SHIELD.getJavaId()) { - useItemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND); - } - // Else we just assume it's the offhand, to simplify logic and to assure the packet gets sent - else { - useItemPacket = new ClientPlayerUseItemPacket(Hand.OFF_HAND); - } - session.sendDownstreamPacket(useItemPacket); - } - } else if (session.getPlayerEntity().getEntityId() == entityId && !metadata.getFlags().getFlag(EntityFlag.SNEAKING) && metadata.getFlags().getFlag(EntityFlag.BLOCKING)) { - metadata.getFlags().setFlag(EntityFlag.BLOCKING, false); - metadata.getFlags().setFlag(EntityFlag.IS_AVOIDING_BLOCK, true); - ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, new Position(0, 0, 0), BlockFace.DOWN); - session.sendDownstreamPacket(releaseItemPacket); + // Armour stands are handled in their own class + if (!this.is(ArmorStandEntity.class)) { + metadata.getFlags().setFlag(EntityFlag.INVISIBLE, (xd & 0x20) == 0x20); } } break; @@ -333,15 +300,12 @@ public class Entity { case 6: // Pose change if (entityMetadata.getValue().equals(Pose.SLEEPING)) { metadata.getFlags().setFlag(EntityFlag.SLEEPING, true); - // Has to be a byte or it does not work - metadata.put(EntityData.PLAYER_FLAGS, (byte) 2); metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.2f); metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.2f); } else if (metadata.getFlags().getFlag(EntityFlag.SLEEPING)) { metadata.getFlags().setFlag(EntityFlag.SLEEPING, false); metadata.put(EntityData.BOUNDING_BOX_WIDTH, getEntityType().getWidth()); metadata.put(EntityData.BOUNDING_BOX_HEIGHT, getEntityType().getHeight()); - metadata.put(EntityData.PLAYER_FLAGS, (byte) 0); } break; } diff --git a/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java index 76ca0567e..bd0fe9b80 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FallingBlockEntity.java @@ -31,14 +31,19 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; public class FallingBlockEntity extends Entity { + private final int javaId; public FallingBlockEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, int javaId) { super(entityId, geyserId, entityType, position, motion, rotation); + this.javaId = javaId; + } - this.metadata.put(EntityData.VARIANT, BlockTranslator.getBedrockBlockId(javaId)); + @Override + public void spawnEntity(GeyserSession session) { + this.metadata.put(EntityData.VARIANT, session.getBlockTranslator().getBedrockBlockId(javaId)); + super.spawnEntity(session); } @Override diff --git a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java index 9f9b7bc38..0738c3819 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java @@ -26,43 +26,163 @@ package org.geysermc.connector.entity; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.object.ProjectileData; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityData; -import org.geysermc.connector.GeyserConnector; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket; +import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.collision.BoundingBox; +import org.geysermc.connector.network.translators.collision.CollisionManager; +import org.geysermc.connector.network.translators.collision.CollisionTranslator; +import org.geysermc.connector.network.translators.collision.translators.BlockCollision; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; -public class FishingHookEntity extends Entity { - public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, ProjectileData data) { +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class FishingHookEntity extends ThrowableEntity { + + private boolean hooked = false; + + private final BoundingBox boundingBox; + + private boolean inWater = false; + + public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, PlayerEntity owner) { super(entityId, geyserId, entityType, position, motion, rotation); - for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) { - Entity entity = session.getEntityCache().getEntityByJavaId(data.getOwnerId()); - if (entity == null && session.getPlayerEntity().getEntityId() == data.getOwnerId()) { - entity = session.getPlayerEntity(); - } + this.boundingBox = new BoundingBox(0.125, 0.125, 0.125, 0.25, 0.25, 0.25); - if (entity != null) { - this.metadata.put(EntityData.OWNER_EID, entity.getGeyserId()); - return; - } - } + // In Java, the splash sound depends on the entity's velocity, but in Bedrock the volume doesn't change. + // This splash can be confused with the sound from catching a fish. This silences the splash from Bedrock, + // so that it can be handled by moveAbsoluteImmediate. + this.metadata.putFloat(EntityData.BOUNDING_BOX_HEIGHT, 128); + + this.metadata.put(EntityData.OWNER_EID, owner.getGeyserId()); } @Override public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { - if (entityMetadata.getId() == 7) { - Entity entity = session.getEntityCache().getEntityByJavaId((Integer) entityMetadata.getValue() - 1); - if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue() - 1) { + if (entityMetadata.getId() == 7) { // Hooked entity + int hookedEntityId = (int) entityMetadata.getValue() - 1; + Entity entity = session.getEntityCache().getEntityByJavaId(hookedEntityId); + if (entity == null && session.getPlayerEntity().getEntityId() == hookedEntityId) { entity = session.getPlayerEntity(); } if (entity != null) { metadata.put(EntityData.TARGET_EID, entity.getGeyserId()); + hooked = true; + } else { + hooked = false; } } super.updateBedrockMetadata(entityMetadata, session); } + + @Override + protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { + boundingBox.setMiddleX(position.getX()); + boundingBox.setMiddleY(position.getY() + boundingBox.getSizeY() / 2); + boundingBox.setMiddleZ(position.getZ()); + + CollisionManager collisionManager = session.getCollisionManager(); + List collidableBlocks = collisionManager.getCollidableBlocks(boundingBox); + boolean touchingWater = false; + boolean collided = false; + for (Vector3i blockPos : collidableBlocks) { + int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos); + BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ()); + if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) { + // TODO Push bounding box out of collision to improve movement + collided = true; + } + + int waterLevel = BlockStateValues.getWaterLevel(blockID); + if (BlockTranslator.isWaterlogged(blockID)) { + waterLevel = 0; + } + if (waterLevel >= 0) { + double waterMaxY = blockPos.getY() + 1 - (waterLevel + 1) / 9.0; + // Falling water is a full block + if (waterLevel >= 8) { + waterMaxY = blockPos.getY() + 1; + } + if (position.getY() <= waterMaxY) { + touchingWater = true; + } + } + } + + if (!inWater && touchingWater) { + sendSplashSound(session); + } + inWater = touchingWater; + + if (!collided) { + super.moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported); + } else { + super.moveAbsoluteImmediate(session, this.position, rotation, true, true); + } + } + + private void sendSplashSound(GeyserSession session) { + if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) { + float volume = (float) (0.2f * Math.sqrt(0.2 * (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) + motion.getY() * motion.getY())); + if (volume > 1) { + volume = 1; + } + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("random.splash"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(volume); + playSoundPacket.setPitch(1f + ThreadLocalRandom.current().nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + } + + @Override + public void tick(GeyserSession session) { + if (hooked || !isInAir(session) && !isInWater(session) || isOnGround()) { + motion = Vector3f.ZERO; + return; + } + float gravity = getGravity(session); + motion = motion.down(gravity); + + moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false); + + float drag = getDrag(session); + motion = motion.mul(drag); + } + + @Override + protected float getGravity(GeyserSession session) { + if (!isInWater(session) && !onGround) { + return 0.03f; + } + return 0; + } + + /** + * @param session the session of the Bedrock client. + * @return true if this entity is currently in air. + */ + protected boolean isInAir(GeyserSession session) { + if (session.getConnector().getConfig().isCacheChunks()) { + int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); + return block == BlockTranslator.JAVA_AIR_ID; + } + return false; + } + + @Override + protected float getDrag(GeyserSession session) { + return 0.92f; + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java index e3af51be6..fdf24f176 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FurnaceMinecartEntity.java @@ -44,15 +44,15 @@ public class FurnaceMinecartEntity extends DefaultBlockMinecartEntity { public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { if (entityMetadata.getId() == 13 && !showCustomBlock) { hasFuel = (boolean) entityMetadata.getValue(); - updateDefaultBlockMetadata(); + updateDefaultBlockMetadata(session); } super.updateBedrockMetadata(entityMetadata, session); } @Override - public void updateDefaultBlockMetadata() { - metadata.put(EntityData.DISPLAY_ITEM, BlockTranslator.getBedrockBlockId(hasFuel ? BlockTranslator.JAVA_RUNTIME_FURNACE_LIT_ID : BlockTranslator.JAVA_RUNTIME_FURNACE_ID)); + public void updateDefaultBlockMetadata(GeyserSession session) { + metadata.put(EntityData.DISPLAY_ITEM, session.getBlockTranslator().getBedrockBlockId(hasFuel ? BlockTranslator.JAVA_RUNTIME_FURNACE_LIT_ID : BlockTranslator.JAVA_RUNTIME_FURNACE_ID)); metadata.put(EntityData.DISPLAY_OFFSET, 6); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java index 4f0a224e2..a898ea389 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemFrameEntity.java @@ -40,7 +40,6 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.item.ItemTranslator; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; import java.util.concurrent.TimeUnit; @@ -49,15 +48,19 @@ import java.util.concurrent.TimeUnit; */ public class ItemFrameEntity extends Entity { + /** + * Used to construct the block entity tag on spawning. + */ + private final HangingDirection direction; /** * Used for getting the Bedrock block position. * Blocks deal with integers whereas entities deal with floats. */ - private final Vector3i bedrockPosition; + private Vector3i bedrockPosition; /** * Specific block 'state' we are emulating in Bedrock. */ - private final int bedrockRuntimeId; + private int bedrockRuntimeId; /** * Rotation of item in frame. */ @@ -69,19 +72,21 @@ public class ItemFrameEntity extends Entity { public ItemFrameEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, HangingDirection direction) { super(entityId, geyserId, entityType, position, motion, rotation); - NbtMapBuilder blockBuilder = NbtMap.builder() - .putString("name", "minecraft:frame") - .putInt("version", BlockTranslator.getBlockStateVersion()); - blockBuilder.put("states", NbtMap.builder() - .putInt("facing_direction", direction.ordinal()) - .putByte("item_frame_map_bit", (byte) 0) - .build()); - bedrockRuntimeId = BlockTranslator.getItemFrame(blockBuilder.build()); - bedrockPosition = Vector3i.from(position.getFloorX(), position.getFloorY(), position.getFloorZ()); + this.direction = direction; } @Override public void spawnEntity(GeyserSession session) { + NbtMapBuilder blockBuilder = NbtMap.builder() + .putString("name", "minecraft:frame") + .putInt("version", session.getBlockTranslator().getBlockStateVersion()); + blockBuilder.put("states", NbtMap.builder() + .putInt("facing_direction", direction.ordinal()) + .putByte("item_frame_map_bit", (byte) 0) + .build()); + bedrockRuntimeId = session.getBlockTranslator().getItemFrame(blockBuilder.build()); + bedrockPosition = Vector3i.from(position.getFloorX(), position.getFloorY(), position.getFloorZ()); + session.getItemFrameCache().put(bedrockPosition, entityId); // Delay is required, or else loading in frames on chunk load is sketchy at best session.getConnector().getGeneralThreadPool().schedule(() -> { @@ -136,7 +141,7 @@ public class ItemFrameEntity extends Entity { UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); updateBlockPacket.setDataLayer(0); updateBlockPacket.setBlockPosition(bedrockPosition); - updateBlockPacket.setRuntimeId(BlockTranslator.BEDROCK_AIR_ID); + updateBlockPacket.setRuntimeId(session.getBlockTranslator().getBedrockAirId()); updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.PRIORITY); updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK); updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS); diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java index 2b411109a..58a3b6f6c 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java @@ -32,19 +32,44 @@ import org.geysermc.connector.network.session.GeyserSession; public class ItemedFireballEntity extends ThrowableEntity { private final Vector3f acceleration; + /** + * The number of ticks to advance movement before sending to Bedrock + */ + protected int futureTicks = 3; + public ItemedFireballEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, Vector3f.ZERO, rotation); - acceleration = motion; + + float magnitude = motion.length(); + if (magnitude != 0) { + acceleration = motion.div(magnitude).mul(0.1f); + } else { + acceleration = Vector3f.ZERO; + } + } + + private Vector3f tickMovement(GeyserSession session, Vector3f position) { + position = position.add(motion); + float drag = getDrag(session); + motion = motion.add(acceleration).mul(drag); + return position; + } + + @Override + protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { + // Advance the position by a few ticks before sending it to Bedrock + Vector3f lastMotion = motion; + Vector3f newPosition = position; + for (int i = 0; i < futureTicks; i++) { + newPosition = tickMovement(session, newPosition); + } + super.moveAbsoluteImmediate(session, newPosition, rotation, isOnGround, teleported); + this.position = position; + this.motion = lastMotion; } @Override public void tick(GeyserSession session) { - position = position.add(motion); - // TODO: While this reduces latency in position updating (needed for better fireball reflecting), - // TODO: movement is incredibly stiff. - // TODO: Only use this laggy movement for fireballs that be reflected - moveAbsoluteImmediate(session, position, rotation, false, true); - float drag = getDrag(session); - motion = motion.add(acceleration).mul(drag); + moveAbsoluteImmediate(session, tickMovement(session, position), rotation, false, false); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java b/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java index f38f1e6b2..4dc0998aa 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java @@ -99,6 +99,13 @@ public class LivingEntity extends Entity { // Bed has to be updated, or else player is floating in the air ChunkUtils.updateBlock(session, bed, bedPosition); } + // Indicate that the player should enter the sleep cycle + // Has to be a byte or it does not work + // (Bed position is what actually triggers sleep - "pose" is only optional) + metadata.put(EntityData.PLAYER_FLAGS, (byte) 2); + } else { + // Player is no longer sleeping + metadata.put(EntityData.PLAYER_FLAGS, (byte) 0); } break; } diff --git a/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java index ed5f28d17..49b12a3e1 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/MinecartEntity.java @@ -30,7 +30,6 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; public class MinecartEntity extends Entity { @@ -58,7 +57,7 @@ public class MinecartEntity extends Entity { if (!(this instanceof DefaultBlockMinecartEntity)) { // Handled in the DefaultBlockMinecartEntity class // Custom block if (entityMetadata.getId() == 10) { - metadata.put(EntityData.DISPLAY_ITEM, BlockTranslator.getBedrockBlockId((int) entityMetadata.getValue())); + metadata.put(EntityData.DISPLAY_ITEM, session.getBlockTranslator().getBedrockBlockId((int) entityMetadata.getValue())); } // Custom block offset diff --git a/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java index 143e36373..2f7af73eb 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/SpawnerMinecartEntity.java @@ -28,6 +28,7 @@ package org.geysermc.connector.entity; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import org.geysermc.connector.entity.type.EntityType; +import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; public class SpawnerMinecartEntity extends DefaultBlockMinecartEntity { @@ -37,8 +38,8 @@ public class SpawnerMinecartEntity extends DefaultBlockMinecartEntity { } @Override - public void updateDefaultBlockMetadata() { - metadata.put(EntityData.DISPLAY_ITEM, BlockTranslator.getBedrockBlockId(BlockTranslator.JAVA_RUNTIME_SPAWNER_ID)); + public void updateDefaultBlockMetadata(GeyserSession session) { + metadata.put(EntityData.DISPLAY_ITEM, session.getBlockTranslator().getBedrockBlockId(BlockTranslator.JAVA_RUNTIME_SPAWNER_ID)); metadata.put(EntityData.DISPLAY_OFFSET, 6); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java index 4e0c25ab5..1088b2a0b 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java @@ -29,20 +29,21 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.MoveEntityDeltaPacket; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; /** * Used as a class for any object-like entity that moves as a projectile */ public class ThrowableEntity extends Entity implements Tickable { - private Vector3f lastPosition; + protected Vector3f lastJavaPosition; public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); - this.lastPosition = position; + this.lastJavaPosition = position; } /** @@ -52,22 +53,65 @@ public class ThrowableEntity extends Entity implements Tickable { */ @Override public void tick(GeyserSession session) { - super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround); + moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false); float drag = getDrag(session); - float gravity = getGravity(); + float gravity = getGravity(session); motion = motion.mul(drag).down(gravity); } protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); + MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); + moveEntityDeltaPacket.setRuntimeEntityId(geyserId); + + if (isOnGround) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); + } + setOnGround(isOnGround); + + if (teleported) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING); + } + + if (this.position.getX() != position.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X); + moveEntityDeltaPacket.setX(position.getX()); + } + if (this.position.getY() != position.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y); + moveEntityDeltaPacket.setY(position.getY()); + } + if (this.position.getZ() != position.getZ()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z); + moveEntityDeltaPacket.setZ(position.getZ()); + } + setPosition(position); + + if (this.rotation.getX() != rotation.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); + moveEntityDeltaPacket.setYaw(rotation.getX()); + } + if (this.rotation.getY() != rotation.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); + moveEntityDeltaPacket.setPitch(rotation.getY()); + } + if (this.rotation.getZ() != rotation.getZ()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); + moveEntityDeltaPacket.setHeadYaw(rotation.getZ()); + } + setRotation(rotation); + + if (!moveEntityDeltaPacket.getFlags().isEmpty()) { + session.sendUpstreamPacket(moveEntityDeltaPacket); + } } /** * Get the gravity of this entity type. Used for applying gravity while the entity is in motion. * + * @param session the session of the Bedrock client. * @return the amount of gravity to apply to this entity while in motion. */ - protected float getGravity() { + protected float getGravity(GeyserSession session) { if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY)) { switch (entityType) { case THROWN_POTION: @@ -76,11 +120,14 @@ public class ThrowableEntity extends Entity implements Tickable { case THROWN_EXP_BOTTLE: return 0.07f; case FIREBALL: + case SHULKER_BULLET: return 0; case SNOWBALL: case THROWN_EGG: case THROWN_ENDERPEARL: return 0.03f; + case LLAMA_SPIT: + return 0.06f; } } return 0; @@ -101,11 +148,14 @@ public class ThrowableEntity extends Entity implements Tickable { case SNOWBALL: case THROWN_EGG: case THROWN_ENDERPEARL: + case LLAMA_SPIT: return 0.99f; case FIREBALL: case SMALL_FIREBALL: case DRAGON_FIREBALL: return 0.95f; + case SHULKER_BULLET: + return 1; } } return 1; @@ -117,8 +167,10 @@ public class ThrowableEntity extends Entity implements Tickable { */ protected boolean isInWater(GeyserSession session) { if (session.getConnector().getConfig().isCacheChunks()) { - int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); - return block == BlockTranslator.BEDROCK_WATER_ID; + if (0 <= position.getFloorY() && position.getFloorY() <= 255) { + int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); + return BlockStateValues.getWaterLevel(block) != -1; + } } return false; } @@ -136,14 +188,13 @@ public class ThrowableEntity extends Entity implements Tickable { @Override public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) { - position = lastPosition; - super.moveRelative(session, relX, relY, relZ, rotation, isOnGround); - lastPosition = position; + moveAbsoluteImmediate(session, lastJavaPosition.add(relX, relY, relZ), rotation, isOnGround, false); + lastJavaPosition = position; } @Override public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); - lastPosition = position; + moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported); + lastJavaPosition = position; } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java index ba5b9eb55..3548e0dfe 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java @@ -35,6 +35,8 @@ public class WitherSkullEntity extends ItemedFireballEntity { public WitherSkullEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + this.futureTicks = 1; } @Override diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java index e0f4d9a11..3d1005510 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/ArmorStandEntity.java @@ -29,6 +29,9 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadat import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket; import lombok.Getter; import org.geysermc.connector.entity.LivingEntity; import org.geysermc.connector.entity.type.EntityType; @@ -42,15 +45,68 @@ public class ArmorStandEntity extends LivingEntity { private boolean isInvisible = false; private boolean isSmall = false; + /** + * On Java Edition, armor stands always show their name. Invisibility hides the name on Bedrock. + * By having a second entity, we can allow an invisible entity with the name tag. + * (This lets armor on armor stands still show) + */ + private ArmorStandEntity secondEntity = null; + /** + * Whether this is the primary armor stand that holds the armor and not the name tag. + */ + private boolean primaryEntity = true; + /** + * Whether the entity's position must be updated to included the offset. + * + * This should be true when the Java server marks the armor stand as invisible, but we shrink the entity + * to allow the nametag to appear. Basically: + * - Is visible: this is irrelevant (false) + * - Has armor, no name: false + * - Has armor, has name: false, with a second entity + * - No armor, no name: false + * - No armor, yes name: true + */ + private boolean positionRequiresOffset = false; + /** + * Whether we should update the position of this armor stand after metadata updates. + */ + private boolean positionUpdateRequired = false; + private GeyserSession session; + public ArmorStandEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); } + @Override + public void spawnEntity(GeyserSession session) { + this.session = session; + this.rotation = Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX()); + super.spawnEntity(session); + } + + @Override + public boolean despawnEntity(GeyserSession session) { + if (secondEntity != null) { + secondEntity.despawnEntity(session); + } + return super.despawnEntity(session); + } + + @Override + public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) { + if (secondEntity != null) { + secondEntity.moveRelative(session, relX, relY, relZ, rotation, isOnGround); + } + super.moveRelative(session, relX, relY, relZ, rotation, isOnGround); + } + @Override public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - // Fake the height to be above where it is so the nametag appears in the right location for invisible non-marker armour stands - if (!isMarker && isInvisible && passengers.isEmpty()) { - position = position.add(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d); + if (secondEntity != null) { + secondEntity.moveAbsolute(session, applyOffsetToPosition(position), rotation, isOnGround, teleported); + } else if (positionRequiresOffset) { + // Fake the height to be above where it is so the nametag appears in the right location for invisible non-marker armour stands + position = applyOffsetToPosition(position); } super.moveAbsolute(session, position, Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX()), isOnGround, teleported); @@ -58,47 +114,250 @@ public class ArmorStandEntity extends LivingEntity { @Override public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { + super.updateBedrockMetadata(entityMetadata, session); if (entityMetadata.getId() == 0 && entityMetadata.getType() == MetadataType.BYTE) { byte xd = (byte) entityMetadata.getValue(); // Check if the armour stand is invisible and store accordingly - if ((xd & 0x20) == 0x20) { - metadata.put(EntityData.SCALE, 0.0f); - isInvisible = true; + if (primaryEntity) { + isInvisible = (xd & 0x20) == 0x20; + updateSecondEntityStatus(false); } + } else if (entityMetadata.getId() == 2) { + updateSecondEntityStatus(false); } else if (entityMetadata.getId() == 14 && entityMetadata.getType() == MetadataType.BYTE) { byte xd = (byte) entityMetadata.getValue(); // isSmall - if ((xd & 0x01) == 0x01) { - isSmall = true; - - if (metadata.getFloat(EntityData.SCALE) != 0.55f && metadata.getFloat(EntityData.SCALE) != 0.0f) { - metadata.put(EntityData.SCALE, 0.55f); + boolean newIsSmall = (xd & 0x01) == 0x01; + if (newIsSmall != isSmall) { + if (positionRequiresOffset) { + // Fix new inconsistency with offset + this.position = fixOffsetForSize(position, newIsSmall); + positionUpdateRequired = true; } - if (metadata.get(EntityData.BOUNDING_BOX_WIDTH) != null && metadata.get(EntityData.BOUNDING_BOX_WIDTH).equals(0.5f)) { - metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.25f); - metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.9875f); + isSmall = newIsSmall; + if (!isMarker) { + toggleSmallStatus(); } - } else if (metadata.get(EntityData.BOUNDING_BOX_WIDTH) != null && metadata.get(EntityData.BOUNDING_BOX_WIDTH).equals(0.25f)) { - metadata.put(EntityData.BOUNDING_BOX_WIDTH, entityType.getWidth()); - metadata.put(EntityData.BOUNDING_BOX_HEIGHT, entityType.getHeight()); } // setMarker - if ((xd & 0x10) == 0x10 && (metadata.get(EntityData.BOUNDING_BOX_WIDTH) == null || !metadata.get(EntityData.BOUNDING_BOX_WIDTH).equals(0.0f))) { - metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.0f); - metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f); - isMarker = true; + boolean oldIsMarker = isMarker; + isMarker = (xd & 0x10) == 0x10; + if (oldIsMarker != isMarker) { + if (isMarker) { + metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.0f); + metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f); + metadata.put(EntityData.SCALE, 0f); + } else { + toggleSmallStatus(); + } + + updateSecondEntityStatus(false); } } - super.updateBedrockMetadata(entityMetadata, session); + if (secondEntity != null) { + secondEntity.updateBedrockMetadata(entityMetadata, session); + } } @Override - public void spawnEntity(GeyserSession session) { - this.rotation = Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX()); - super.spawnEntity(session); + public void updateBedrockMetadata(GeyserSession session) { + if (secondEntity != null) { + secondEntity.updateBedrockMetadata(session); + } + super.updateBedrockMetadata(session); + if (positionUpdateRequired) { + positionUpdateRequired = false; + updatePosition(); + } + } + + @Override + public void setHelmet(ItemData helmet) { + super.setHelmet(helmet); + updateSecondEntityStatus(true); + } + + @Override + public void setChestplate(ItemData chestplate) { + super.setChestplate(chestplate); + updateSecondEntityStatus(true); + } + + @Override + public void setLeggings(ItemData leggings) { + super.setLeggings(leggings); + updateSecondEntityStatus(true); + } + + @Override + public void setBoots(ItemData boots) { + super.setBoots(boots); + updateSecondEntityStatus(true); + } + + @Override + public void setHand(ItemData hand) { + super.setHand(hand); + updateSecondEntityStatus(true); + } + + @Override + public void setOffHand(ItemData offHand) { + super.setOffHand(offHand); + updateSecondEntityStatus(true); + } + + /** + * Determine if we need to load or unload the second entity. + * + * @param sendMetadata whether to send a metadata update after a change. + */ + private void updateSecondEntityStatus(boolean sendMetadata) { + // A secondary entity always has to have the offset applied, so it remains invisible and the nametag shows. + if (!primaryEntity) return; + if (!isInvisible || isMarker) { + // It is either impossible to show armor, or the armor stand isn't invisible. We good. + metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false); + updateOffsetRequirement(false); + if (positionUpdateRequired) { + positionUpdateRequired = false; + updatePosition(); + } + + if (secondEntity != null) { + secondEntity.despawnEntity(session); + secondEntity = null; + } + return; + } + //boolean isNametagEmpty = metadata.getString(EntityData.NAMETAG).isEmpty() || metadata.getByte(EntityData.NAMETAG_ALWAYS_SHOW, (byte) -1) == (byte) 0; - may not be necessary? + boolean isNametagEmpty = metadata.getString(EntityData.NAMETAG).isEmpty(); + if (!isNametagEmpty && (!helmet.equals(ItemData.AIR) || !chestplate.equals(ItemData.AIR) || !leggings.equals(ItemData.AIR) + || !boots.equals(ItemData.AIR) || !hand.equals(ItemData.AIR) || !offHand.equals(ItemData.AIR))) { + // If the second entity exists, no need to recreate it. + // We can't stuff this check above or else it'll fall into another else case and delete the second entity + if (secondEntity != null) return; + + // Create the second entity. It doesn't need to worry about the items, but it does need to worry about + // the metadata as it will hold the name tag. + secondEntity = new ArmorStandEntity(0, session.getEntityCache().getNextEntityId().incrementAndGet(), + EntityType.ARMOR_STAND, position, motion, rotation); + secondEntity.primaryEntity = false; + if (!this.positionRequiresOffset) { + // Ensure the offset is applied for the 0 scale + secondEntity.position = secondEntity.applyOffsetToPosition(secondEntity.position); + } + // Copy metadata + secondEntity.isSmall = isSmall; + secondEntity.getMetadata().putAll(metadata); + // Copy the flags so they aren't the same object in memory + secondEntity.getMetadata().putFlags(metadata.getFlags().copy()); + // Guarantee this copy is NOT invisible + secondEntity.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false); + // Scale to 0 to show nametag + secondEntity.getMetadata().put(EntityData.SCALE, 0.0f); + // No bounding box as we don't want to interact with this entity + secondEntity.getMetadata().put(EntityData.BOUNDING_BOX_WIDTH, 0.0f); + secondEntity.getMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f); + secondEntity.spawnEntity(session); + + // Reset scale of the proper armor stand + this.metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f); + // Set the proper armor stand to invisible to show armor + this.metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); + // Update the position of the armor stand + updateOffsetRequirement(false); + } else if (isNametagEmpty) { + // We can just make an invisible entity + // Reset scale of the proper armor stand + metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f); + // Set the proper armor stand to invisible to show armor + metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); + // Update offset + updateOffsetRequirement(false); + + if (secondEntity != null) { + secondEntity.despawnEntity(session); + secondEntity = null; + } + } else { + // Nametag is not empty and there is no armor + // We don't need to make a new entity + metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false); + metadata.put(EntityData.SCALE, 0.0f); + // As the above is applied, we need an offset + updateOffsetRequirement(true); + + if (secondEntity != null) { + secondEntity.despawnEntity(session); + secondEntity = null; + } + } + if (sendMetadata) { + this.updateBedrockMetadata(session); + } + } + + /** + * If this armor stand is not a marker, set its bounding box size and scale. + */ + private void toggleSmallStatus() { + metadata.put(EntityData.BOUNDING_BOX_WIDTH, isSmall ? 0.25f : entityType.getWidth()); + metadata.put(EntityData.BOUNDING_BOX_HEIGHT, isSmall ? 0.9875f : entityType.getHeight()); + metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f); + } + + /** + * @return the selected position with the position offset applied. + */ + private Vector3f applyOffsetToPosition(Vector3f position) { + return position.add(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d); + } + + /** + * @return an adjusted offset for the new small status. + */ + private Vector3f fixOffsetForSize(Vector3f position, boolean isNowSmall) { + position = removeOffsetFromPosition(position); + return position.add(0d, entityType.getHeight() * (isNowSmall ? 0.55d : 1d), 0d); + } + + /** + * @return the selected position with the position offset removed. + */ + private Vector3f removeOffsetFromPosition(Vector3f position) { + return position.sub(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d); + } + + /** + * Set the offset to a new value; if it changed, update the position, too. + */ + private void updateOffsetRequirement(boolean newValue) { + if (newValue != positionRequiresOffset) { + this.positionRequiresOffset = newValue; + if (positionRequiresOffset) { + this.position = applyOffsetToPosition(position); + } else { + this.position = removeOffsetFromPosition(position); + } + positionUpdateRequired = true; + } + } + + /** + * Updates position without calling movement code. + */ + private void updatePosition() { + MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket(); + moveEntityPacket.setRuntimeEntityId(geyserId); + moveEntityPacket.setPosition(position); + moveEntityPacket.setRotation(Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX())); + moveEntityPacket.setOnGround(onGround); + moveEntityPacket.setTeleported(false); + session.sendUpstreamPacket(moveEntityPacket); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java index 544014115..752a0d106 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java @@ -44,7 +44,7 @@ public class RabbitEntity extends AnimalEntity { if (entityMetadata.getId() == 15) { metadata.put(EntityData.SCALE, .55f); boolean isBaby = (boolean) entityMetadata.getValue(); - if(isBaby) { + if (isBaby) { metadata.put(EntityData.SCALE, .35f); metadata.getFlags().setFlag(EntityFlag.BABY, true); } diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java index 628beff1b..41073246e 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/AbstractHorseEntity.java @@ -30,6 +30,7 @@ import com.nukkitx.math.vector.Vector3f; 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.data.inventory.ContainerType; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.connector.entity.living.animal.AnimalEntity; import org.geysermc.connector.entity.type.EntityType; @@ -40,6 +41,9 @@ public class AbstractHorseEntity extends AnimalEntity { public AbstractHorseEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + // Specifies the size of the entity's inventory. Required to place slots in the entity. + metadata.put(EntityData.CONTAINER_BASE_SIZE, 2); } @Override @@ -75,6 +79,9 @@ public class AbstractHorseEntity extends AnimalEntity { entityEventPacket.setData(ItemRegistry.WHEAT.getBedrockId() << 16); session.sendUpstreamPacket(entityEventPacket); } + + // Set container type if tamed + metadata.put(EntityData.CONTAINER_TYPE, ((xd & 0x02) == 0x02) ? (byte) ContainerType.HORSE.getId() : (byte) 0); } // Needed to control horses diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java index f67567c90..461d636bd 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/ChestedHorseEntity.java @@ -27,6 +27,7 @@ package org.geysermc.connector.entity.living.animal.horse; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; @@ -35,6 +36,8 @@ public class ChestedHorseEntity extends AbstractHorseEntity { public ChestedHorseEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + metadata.put(EntityData.CONTAINER_BASE_SIZE, 16); } @Override diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java index a04539dca..5fdde5272 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/horse/LlamaEntity.java @@ -38,6 +38,8 @@ public class LlamaEntity extends ChestedHorseEntity { public LlamaEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + metadata.put(EntityData.CONTAINER_STRENGTH_MODIFIER, 3); // Presumably 3 slots for every 1 strength } @Override @@ -56,7 +58,7 @@ public class LlamaEntity extends ChestedHorseEntity { // The damage value is the dye color that Java sends us // Always going to be a carpet so we can hardcode 171 in BlockTranslator // The int then short conversion is required or we get a ClassCastException - equipmentPacket.setChestplate(ItemData.of(BlockTranslator.CARPET, (short)((int) entityMetadata.getValue()), 1)); + equipmentPacket.setChestplate(ItemData.of(BlockTranslator.CARPET, (short) ((int) entityMetadata.getValue()), 1)); } else { equipmentPacket.setChestplate(ItemData.AIR); } diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java index d481cd0c5..fa5785fe5 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java @@ -26,7 +26,6 @@ package org.geysermc.connector.entity.living.merchant; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; @@ -101,11 +100,17 @@ public class VillagerEntity extends AbstractMerchantEntity { @Override public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) { + if (!metadata.getFlags().getFlag(EntityFlag.SLEEPING)) { + // No need to worry about extra processing to compensate for sleeping + super.moveRelative(session, relX, relY, relZ, rotation, isOnGround); + return; + } + int z = 0; int bedId = 0; float bedPositionSubtractorW = 0; float bedPositionSubtractorN = 0; - Vector3i bedPosition = metadata.getPos(EntityData.BED_POSITION); + Vector3i bedPosition = metadata.getPos(EntityData.BED_POSITION, null); if (session.getConnector().getConfig().isCacheChunks() && bedPosition != null) { bedId = session.getConnector().getWorldManager().getBlockAt(session, bedPosition); } @@ -117,39 +122,33 @@ public class VillagerEntity extends AbstractMerchantEntity { MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket(); moveEntityPacket.setRuntimeEntityId(geyserId); //Sets Villager position and rotation when sleeping - if (!metadata.getFlags().getFlag(EntityFlag.SLEEPING)) { - moveEntityPacket.setPosition(position); - moveEntityPacket.setRotation(getBedrockRotation()); - } else { - //String Setup - Pattern r = Pattern.compile("facing=([a-z]+)"); - Matcher m = r.matcher(bedRotationZ); - if (m.find()) { - switch (m.group(0)){ - case "facing=south": - //bed is facing south - z = 180; - bedPositionSubtractorW = -.5f; - break; - case "facing=east": - //bed is facing east - z = 90; - bedPositionSubtractorW = -.5f; - break; - case "facing=west": - //bed is facing west - z = 270; - bedPositionSubtractorW = .5f; - break; - case "facing=north": - //rotation does not change because north is 0 - bedPositionSubtractorN = .5f; - break; - } + Pattern r = Pattern.compile("facing=([a-z]+)"); + Matcher m = r.matcher(bedRotationZ); + if (m.find()) { + switch (m.group(0)) { + case "facing=south": + //bed is facing south + z = 180; + bedPositionSubtractorW = -.5f; + break; + case "facing=east": + //bed is facing east + z = 90; + bedPositionSubtractorW = -.5f; + break; + case "facing=west": + //bed is facing west + z = 270; + bedPositionSubtractorW = .5f; + break; + case "facing=north": + //rotation does not change because north is 0 + bedPositionSubtractorN = .5f; + break; } - moveEntityPacket.setRotation(Vector3f.from(0, 0, z)); - moveEntityPacket.setPosition(Vector3f.from(position.getX() + bedPositionSubtractorW, position.getY(), position.getZ() + bedPositionSubtractorN)); } + moveEntityPacket.setRotation(Vector3f.from(0, 0, z)); + moveEntityPacket.setPosition(Vector3f.from(position.getX() + bedPositionSubtractorW, position.getY(), position.getZ() + bedPositionSubtractorN)); moveEntityPacket.setOnGround(isOnGround); moveEntityPacket.setTeleported(false); session.sendUpstreamPacket(moveEntityPacket); 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 3151ae474..0d265b56e 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 @@ -33,7 +33,6 @@ 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; public class EndermanEntity extends MonsterEntity { @@ -45,7 +44,7 @@ public class EndermanEntity extends MonsterEntity { public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { // Held block if (entityMetadata.getId() == 15) { - metadata.put(EntityData.CARRIED_BLOCK, BlockTranslator.getBedrockBlockId((int) entityMetadata.getValue())); + metadata.put(EntityData.CARRIED_BLOCK, session.getBlockTranslator().getBedrockBlockId((int) entityMetadata.getValue())); } // "Is screaming" - controls sound if (entityMetadata.getId() == 16) { diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java index 8dcce6a7f..e024b4e55 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java @@ -46,7 +46,7 @@ public class WitherEntity extends MonsterEntity { if (entityMetadata.getId() >= 15 && entityMetadata.getId() <= 17) { Entity entity = session.getEntityCache().getEntityByJavaId((int) entityMetadata.getValue()); - if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue()) { + if (entity == null && session.getPlayerEntity().getEntityId() == (int) entityMetadata.getValue()) { entity = session.getPlayerEntity(); } @@ -62,7 +62,7 @@ public class WitherEntity extends MonsterEntity { } else if (entityMetadata.getId() == 17) { metadata.put(EntityData.WITHER_TARGET_3, targetID); } else if (entityMetadata.getId() == 18) { - metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, (int) entityMetadata.getValue()); + metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, entityMetadata.getValue()); // Show the shield for the first few seconds of spawning (like Java) if ((int) entityMetadata.getValue() >= 165) { diff --git a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java index e1e531f42..f38e56fd8 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java +++ b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java @@ -30,8 +30,11 @@ import org.geysermc.connector.entity.*; import org.geysermc.connector.entity.living.*; import org.geysermc.connector.entity.living.animal.*; import org.geysermc.connector.entity.living.animal.horse.*; -import org.geysermc.connector.entity.living.animal.tameable.*; -import org.geysermc.connector.entity.living.merchant.*; +import org.geysermc.connector.entity.living.animal.tameable.CatEntity; +import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity; +import org.geysermc.connector.entity.living.animal.tameable.WolfEntity; +import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity; +import org.geysermc.connector.entity.living.merchant.VillagerEntity; import org.geysermc.connector.entity.living.monster.*; import org.geysermc.connector.entity.living.monster.raid.AbstractIllagerEntity; import org.geysermc.connector.entity.living.monster.raid.PillagerEntity; @@ -39,6 +42,9 @@ import org.geysermc.connector.entity.living.monster.raid.RaidParticipantEntity; import org.geysermc.connector.entity.living.monster.raid.SpellcasterIllagerEntity; import org.geysermc.connector.entity.player.PlayerEntity; +import java.util.ArrayList; +import java.util.List; + @Getter public enum EntityType { @@ -112,7 +118,7 @@ public enum EntityType { TRIDENT(TridentEntity.class, 73, 0f, 0f, 0f, 0f, "minecraft:thrown_trident"), TURTLE(TurtleEntity.class, 74, 0.4f, 1.2f), CAT(CatEntity.class, 75, 0.35f, 0.3f), - SHULKER_BULLET(Entity.class, 76, 0.3125f), + SHULKER_BULLET(ThrowableEntity.class, 76, 0.3125f), FISHING_BOBBER(FishingHookEntity.class, 77, 0f, 0f, 0f, 0f, "minecraft:fishing_hook"), CHALKBOARD(Entity.class, 78, 0f), DRAGON_FIREBALL(ItemedFireballEntity.class, 79, 1.0f), @@ -139,7 +145,7 @@ public enum EntityType { MINECART_SPAWNER(SpawnerMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"), MINECART_COMMAND_BLOCK(CommandBlockMinecartEntity.class, 100, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:command_block_minecart"), LINGERING_POTION(ThrowableEntity.class, 101, 0f), - LLAMA_SPIT(Entity.class, 102, 0.25f), + LLAMA_SPIT(ThrowableEntity.class, 102, 0.25f), EVOKER_FANGS(Entity.class, 103, 0.8f, 0.5f, 0.5f, 0f, "minecraft:evocation_fang"), EVOKER(SpellcasterIllagerEntity.class, 104, 1.95f, 0.6f, 0.6f, 0f, "minecraft:evocation_illager"), VEX(VexEntity.class, 105, 0.8f, 0.4f), @@ -174,17 +180,33 @@ public enum EntityType { */ ENDER_DRAGON_PART(EnderDragonPartEntity.class, 32, 0, 0, 0, 0, "minecraft:armor_stand"); + /** + * A list of all Java identifiers for use with command suggestions + */ + public static final String[] ALL_JAVA_IDENTIFIERS; private static final EntityType[] VALUES = values(); - private Class entityClass; + static { + List allJavaIdentifiers = new ArrayList<>(); + for (EntityType type : values()) { + if (type == AGENT || type == BALLOON || type == CHALKBOARD || type == NPC || type == TRIPOD_CAMERA || type == ENDER_DRAGON_PART) { + continue; + } + allJavaIdentifiers.add("minecraft:" + type.name().toLowerCase()); + } + ALL_JAVA_IDENTIFIERS = allJavaIdentifiers.toArray(new String[0]); + } + + private final Class entityClass; private final int type; private final float height; private final float width; private final float length; private final float offset; - private String identifier; + private final String identifier; EntityType(Class entityClass, int type, float height) { + //noinspection SuspiciousNameCombination this(entityClass, type, height, height); } @@ -198,8 +220,6 @@ public enum EntityType { EntityType(Class entityClass, int type, float height, float width, float length, float offset) { this(entityClass, type, height, width, length, offset, null); - - this.identifier = "minecraft:" + name().toLowerCase(); } EntityType(Class entityClass, int type, float height, float width, float length, float offset, String identifier) { @@ -209,7 +229,7 @@ public enum EntityType { this.width = width; this.length = length; this.offset = offset + 0.00001f; - this.identifier = identifier; + this.identifier = identifier == null ? "minecraft:" + name().toLowerCase() : identifier; } public static EntityType getFromIdentifier(String identifier) { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/Click.java b/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java similarity index 78% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/Click.java rename to connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java index fdfc2d57b..71b5cbda9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/Click.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java @@ -23,16 +23,15 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory.action; +package org.geysermc.connector.inventory; -import com.github.steveice10.mc.protocol.data.game.window.ClickItemParam; -import com.github.steveice10.mc.protocol.data.game.window.WindowActionParam; -import lombok.AllArgsConstructor; +import com.github.steveice10.mc.protocol.data.game.window.WindowType; -@AllArgsConstructor -enum Click { - LEFT(ClickItemParam.LEFT_CLICK), - RIGHT(ClickItemParam.RIGHT_CLICK); - - public final WindowActionParam actionParam; +/** + * Used to determine if rename packets should be sent. + */ +public class AnvilContainer extends Container { + public AnvilContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); + } } diff --git a/connector/src/main/java/org/geysermc/connector/inventory/BeaconContainer.java b/connector/src/main/java/org/geysermc/connector/inventory/BeaconContainer.java new file mode 100644 index 000000000..3798d9009 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/BeaconContainer.java @@ -0,0 +1,41 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BeaconContainer extends Container { + private int primaryId; + private int secondaryId; + + public BeaconContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/CartographyContainer.java b/connector/src/main/java/org/geysermc/connector/inventory/CartographyContainer.java new file mode 100644 index 000000000..0ac93b431 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/CartographyContainer.java @@ -0,0 +1,34 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; + +public class CartographyContainer extends Container { + public CartographyContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/Container.java b/connector/src/main/java/org/geysermc/connector/inventory/Container.java new file mode 100644 index 000000000..d61b2b71d --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/Container.java @@ -0,0 +1,85 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import lombok.Getter; +import lombok.NonNull; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; + +/** + * Combination of {@link Inventory} and {@link PlayerInventory} + */ +@Getter +public class Container extends Inventory { + private final PlayerInventory playerInventory; + private final int containerSize; + + /** + * Whether we are using a real block when opening this inventory. + */ + private boolean isUsingRealBlock = false; + + public Container(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType); + this.playerInventory = playerInventory; + this.containerSize = this.size + InventoryTranslator.PLAYER_INVENTORY_SIZE; + } + + @Override + public GeyserItemStack getItem(int slot) { + if (slot < this.size) { + return super.getItem(slot); + } else { + return playerInventory.getItem(slot - this.size + InventoryTranslator.PLAYER_INVENTORY_OFFSET); + } + } + + @Override + public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { + if (slot < this.size) { + super.setItem(slot, newItem, session); + } else { + playerInventory.setItem(slot - this.size + InventoryTranslator.PLAYER_INVENTORY_OFFSET, newItem, session); + } + } + + @Override + public int getSize() { + return this.containerSize; + } + + /** + * Will be overwritten for droppers. + * + * @param usingRealBlock whether this container is using a real container or not + * @param javaBlockId the Java block string of the block, if real + */ + public void setUsingRealBlock(boolean usingRealBlock, String javaBlockId) { + isUsingRealBlock = usingRealBlock; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/EnchantingContainer.java b/connector/src/main/java/org/geysermc/connector/inventory/EnchantingContainer.java new file mode 100644 index 000000000..e8c935649 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/EnchantingContainer.java @@ -0,0 +1,56 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.nukkitx.protocol.bedrock.data.inventory.EnchantOptionData; +import lombok.Getter; + +public class EnchantingContainer extends Container { + /** + * A cache of what Bedrock sees + */ + @Getter + private final EnchantOptionData[] enchantOptions; + /** + * A mutable cache of what the server sends us + */ + @Getter + private final GeyserEnchantOption[] geyserEnchantOptions; + + public EnchantingContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); + + enchantOptions = new EnchantOptionData[3]; + geyserEnchantOptions = new GeyserEnchantOption[3]; + for (int i = 0; i < geyserEnchantOptions.length; i++) { + geyserEnchantOptions[i] = new GeyserEnchantOption(i); + // Options cannot be null, so we build initial options + // GeyserSession can be safely null here because it's only needed for net IDs + enchantOptions[i] = geyserEnchantOptions[i].build(null); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/Generic3X3Container.java b/connector/src/main/java/org/geysermc/connector/inventory/Generic3X3Container.java new file mode 100644 index 000000000..080e11982 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/Generic3X3Container.java @@ -0,0 +1,52 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import lombok.Getter; +import org.geysermc.connector.network.session.GeyserSession; + +public class Generic3X3Container extends Container { + /** + * Whether we need to set the container type as {@link com.nukkitx.protocol.bedrock.data.inventory.ContainerType#DROPPER}. + * + * Used at {@link org.geysermc.connector.network.translators.inventory.translators.Generic3X3InventoryTranslator#openInventory(GeyserSession, Inventory)} + */ + @Getter + private boolean isDropper = false; + + public Generic3X3Container(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); + } + + @Override + public void setUsingRealBlock(boolean usingRealBlock, String javaBlockId) { + super.setUsingRealBlock(usingRealBlock, javaBlockId); + if (usingRealBlock) { + isDropper = javaBlockId.startsWith("minecraft:dropper"); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/GeyserEnchantOption.java b/connector/src/main/java/org/geysermc/connector/inventory/GeyserEnchantOption.java new file mode 100644 index 000000000..e9ad81a6a --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/GeyserEnchantOption.java @@ -0,0 +1,74 @@ +/* + * 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.inventory; + +import com.nukkitx.protocol.bedrock.data.inventory.EnchantData; +import com.nukkitx.protocol.bedrock.data.inventory.EnchantOptionData; +import lombok.Getter; +import lombok.Setter; +import org.geysermc.connector.network.session.GeyserSession; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * A mutable "wrapper" around {@link EnchantOptionData} + */ +@Setter +public class GeyserEnchantOption { + private static final List EMPTY = Collections.emptyList(); + /** + * This: https://cdn.discordapp.com/attachments/613168850925649981/791030657169227816/unknown.png + * is controlled by the server. + * So, of course, we have to throw in some easter eggs. ;) + */ + private static final List ENCHANT_NAMES = Arrays.asList("tougher armor", "lukeeey", "fall better", + "explode less", "camo toy", "breathe better", "rtm five one six", "armor stab", "water walk", "you are elsa", + "tim two zero three", "fast walk nether", "oof ouch owie", "enemy on fire", "spider sad", "aj ferguson", "redned", + "more items thx", "long sword reach", "fast tool", "give me block", "less breaky break", "cube craft", + "strong arrow", "fist arrow", "spicy arrow", "many many arrows", "geyser", "come here fish", "i like this", + "stabby stab", "supreme mortal", "avatar i guess", "more arrows", "fly finder seventeen", "in and out", + "xp heals tools", "dragon proxy waz here"); + + @Getter + private final int javaIndex; + + private int xpCost = 0; + private int javaEnchantIndex = -1; + private int bedrockEnchantIndex = -1; + private int enchantLevel = -1; + + public GeyserEnchantOption(int javaIndex) { + this.javaIndex = javaIndex; + } + + public EnchantOptionData build(GeyserSession session) { + return new EnchantOptionData(xpCost, javaIndex + 16, EMPTY, + enchantLevel == -1 ? EMPTY : Collections.singletonList(new EnchantData(bedrockEnchantIndex, enchantLevel)), EMPTY, + javaEnchantIndex == -1 ? "unknown" : ENCHANT_NAMES.get(javaEnchantIndex), enchantLevel == -1 ? 0 : session.getNextItemNetId()); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java new file mode 100644 index 000000000..b4e91c1d6 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java @@ -0,0 +1,122 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import lombok.Data; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.item.ItemEntry; +import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.network.translators.item.ItemTranslator; + +@Data +public class GeyserItemStack { + public static final GeyserItemStack EMPTY = new GeyserItemStack(0, 0, null); + + private final int javaId; + private int amount; + private CompoundTag nbt; + private int netId; + + public GeyserItemStack(int javaId) { + this(javaId, 1); + } + + public GeyserItemStack(int javaId, int amount) { + this(javaId, amount, null); + } + + public GeyserItemStack(int javaId, int amount, CompoundTag nbt) { + this(javaId, amount, nbt, 1); + } + + public GeyserItemStack(int javaId, int amount, CompoundTag nbt, int netId) { + this.javaId = javaId; + this.amount = amount; + this.nbt = nbt; + this.netId = netId; + } + + public static GeyserItemStack from(ItemStack itemStack) { + return itemStack == null ? EMPTY : new GeyserItemStack(itemStack.getId(), itemStack.getAmount(), itemStack.getNbt()); + } + + public int getJavaId() { + return isEmpty() ? 0 : javaId; + } + + public int getAmount() { + return isEmpty() ? 0 : amount; + } + + public CompoundTag getNbt() { + return isEmpty() ? null : nbt; + } + + public int getNetId() { + return isEmpty() ? 0 : netId; + } + + public void add(int add) { + amount += add; + } + + public void sub(int sub) { + amount -= sub; + } + + public ItemStack getItemStack() { + return getItemStack(amount); + } + + public ItemStack getItemStack(int newAmount) { + return isEmpty() ? null : new ItemStack(javaId, newAmount, nbt); + } + + public ItemData getItemData(GeyserSession session) { + ItemData itemData = ItemTranslator.translateToBedrock(session, getItemStack()); + itemData.setNetId(getNetId()); + return itemData; + } + + public ItemEntry getItemEntry() { + return ItemRegistry.ITEM_ENTRIES.get(getJavaId()); + } + + public boolean isEmpty() { + return amount <= 0 || javaId == 0; + } + + public GeyserItemStack copy() { + return copy(amount); + } + + public GeyserItemStack copy(int newAmount) { + return isEmpty() ? EMPTY : new GeyserItemStack(javaId, newAmount, nbt == null ? null : nbt.clone(), netId); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java b/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java index 41ae994f5..11a0034ad 100644 --- a/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java @@ -25,36 +25,39 @@ package org.geysermc.connector.inventory; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.window.WindowType; import com.nukkitx.math.vector.Vector3i; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.Arrays; public class Inventory { @Getter - protected int id; - - @Getter - @Setter - protected boolean open; - - @Getter - protected WindowType windowType; + protected final int id; @Getter protected final int size; + /** + * Used for smooth transitions between two windows of the same type. + */ + @Getter + protected final WindowType windowType; + @Getter @Setter protected String title; - @Setter - protected ItemStack[] items; + protected GeyserItemStack[] items; + /** + * The location of the inventory block. Will either be a fake block above the player's head, or the actual block location + */ @Getter @Setter protected Vector3i holderPosition = Vector3i.ZERO; @@ -64,27 +67,67 @@ public class Inventory { protected long holderId = -1; @Getter - protected AtomicInteger transactionId = new AtomicInteger(1); + protected short transactionId = 0; - public Inventory(int id, WindowType windowType, int size) { - this("Inventory", id, windowType, size); + @Getter + @Setter + private boolean pending = false; + + protected Inventory(int id, int size, WindowType windowType) { + this("Inventory", id, size, windowType); } - public Inventory(String title, int id, WindowType windowType, int size) { + protected Inventory(String title, int id, int size, WindowType windowType) { this.title = title; this.id = id; - this.windowType = windowType; this.size = size; - this.items = new ItemStack[size]; + this.windowType = windowType; + this.items = new GeyserItemStack[size]; + Arrays.fill(items, GeyserItemStack.EMPTY); } - public ItemStack getItem(int slot) { + public GeyserItemStack getItem(int slot) { + if (slot > this.size) { + GeyserConnector.getInstance().getLogger().debug("Tried to get an item out of bounds! " + this.toString()); + return GeyserItemStack.EMPTY; + } return items[slot]; } - public void setItem(int slot, ItemStack item) { - if (item != null && (item.getId() == 0 || item.getAmount() < 1)) - item = null; - items[slot] = item; + public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { + if (slot > this.size) { + session.getConnector().getLogger().debug("Tried to set an item out of bounds! " + this.toString()); + return; + } + GeyserItemStack oldItem = items[slot]; + updateItemNetId(oldItem, newItem, session); + items[slot] = newItem; + } + + protected static void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) { + if (!newItem.isEmpty()) { + if (newItem.getItemData(session).equals(oldItem.getItemData(session), false, false, false)) { + newItem.setNetId(oldItem.getNetId()); + } else { + newItem.setNetId(session.getNextItemNetId()); + } + } + } + + public short getNextTransactionId() { + return ++transactionId; + } + + @Override + public String toString() { + return "Inventory{" + + "id=" + id + + ", size=" + size + + ", title='" + title + '\'' + + ", items=" + Arrays.toString(items) + + ", holderPosition=" + holderPosition + + ", holderId=" + holderId + + ", transactionId=" + transactionId + + '}'; } } diff --git a/connector/src/main/java/org/geysermc/connector/inventory/LecternContainer.java b/connector/src/main/java/org/geysermc/connector/inventory/LecternContainer.java new file mode 100644 index 000000000..be1b8b34b --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/LecternContainer.java @@ -0,0 +1,45 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NbtMap; +import lombok.Getter; +import lombok.Setter; + +public class LecternContainer extends Container { + @Getter @Setter + private int currentBedrockPage = 0; + @Getter @Setter + private NbtMap blockEntityTag; + @Getter @Setter + private Vector3i position; + + public LecternContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/MerchantContainer.java b/connector/src/main/java/org/geysermc/connector/inventory/MerchantContainer.java new file mode 100644 index 000000000..5941b2a7d --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/inventory/MerchantContainer.java @@ -0,0 +1,43 @@ +/* + * 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.inventory; + +import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import lombok.Getter; +import lombok.Setter; +import org.geysermc.connector.entity.Entity; + +@Getter +@Setter +public class MerchantContainer extends Container { + private Entity villager; + private VillagerTrade[] villagerTrades; + + public MerchantContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java b/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java index 4816e3c3a..76b2e5fbe 100644 --- a/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java @@ -25,9 +25,11 @@ package org.geysermc.connector.inventory; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; public class PlayerInventory extends Inventory { @@ -40,20 +42,36 @@ public class PlayerInventory extends Inventory { private int heldItemSlot; @Getter - private ItemStack cursor; + @NonNull + private GeyserItemStack cursor = GeyserItemStack.EMPTY; public PlayerInventory() { - super(0, null, 46); + super(0, 46, null); heldItemSlot = 0; } - public void setCursor(ItemStack stack) { - if (stack != null && (stack.getId() == 0 || stack.getAmount() < 1)) - stack = null; - cursor = stack; + public void setCursor(@NonNull GeyserItemStack newCursor, GeyserSession session) { + updateItemNetId(cursor, newCursor, session); + cursor = newCursor; } - public ItemStack getItemInHand() { + public GeyserItemStack getItemInHand() { + if (36 + heldItemSlot > this.size) { + GeyserConnector.getInstance().getLogger().debug("Held item slot was larger than expected!"); + return GeyserItemStack.EMPTY; + } return items[36 + heldItemSlot]; } + + public void setItemInHand(@NonNull GeyserItemStack item) { + if (36 + heldItemSlot > this.size) { + GeyserConnector.getInstance().getLogger().debug("Held item slot was larger than expected!"); + return; + } + items[36 + heldItemSlot] = item; + } + + public GeyserItemStack getOffhand() { + return items[45]; + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/InventoryCache.java b/connector/src/main/java/org/geysermc/connector/inventory/StonecutterContainer.java similarity index 61% rename from connector/src/main/java/org/geysermc/connector/network/session/cache/InventoryCache.java rename to connector/src/main/java/org/geysermc/connector/inventory/StonecutterContainer.java index 3ead687fc..d558fab34 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/InventoryCache.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/StonecutterContainer.java @@ -23,39 +23,32 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.session.cache; +package org.geysermc.connector.inventory; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import com.github.steveice10.mc.protocol.data.game.window.WindowType; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; -import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; -public class InventoryCache { - - private GeyserSession session; - +public class StonecutterContainer extends Container { + /** + * The button that has currently been pressed Java-side + */ @Getter @Setter - private Inventory openInventory; + private int stonecutterButton = -1; - @Getter - private Int2ObjectMap inventories = new Int2ObjectOpenHashMap<>(); - - public InventoryCache(GeyserSession session) { - this.session = session; + public StonecutterContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) { + super(title, id, size, windowType, playerInventory); } - public Inventory getPlayerInventory() { - return inventories.get(0); - } - - public void cacheInventory(Inventory inventory) { - inventories.put(inventory.getId(), inventory); - } - - public void uncacheInventory(int id) { - inventories.remove(id); + @Override + public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { + if (slot == 0 && newItem.getJavaId() != items[slot].getJavaId()) { + // The pressed stonecutter button output resets whenever the input item changes + this.stonecutterButton = -1; + } + super.setItem(slot, newItem, session); } } diff --git a/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java b/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java index 25ae5a36c..1457780b1 100644 --- a/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java +++ b/connector/src/main/java/org/geysermc/connector/metrics/Metrics.java @@ -36,6 +36,7 @@ import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -75,7 +76,7 @@ public class Metrics { private final static ObjectMapper mapper = new ObjectMapper(); - private GeyserConnector connector; + private final GeyserConnector connector; /** * Class constructor. @@ -156,6 +157,7 @@ public class Metrics { String osName = System.getProperty("os.name"); String osArch = System.getProperty("os.arch"); String osVersion = System.getProperty("os.version"); + String javaVersion = System.getProperty("java.version"); int coreCount = Runtime.getRuntime().availableProcessors(); ObjectNode data = mapper.createObjectNode(); @@ -163,6 +165,7 @@ public class Metrics { data.put("serverUUID", serverUUID); data.put("playerAmount", playerAmount); + data.put("javaVersion", javaVersion); data.put("osName", osName); data.put("osArch", osArch); data.put("osVersion", osVersion); @@ -241,7 +244,7 @@ public class Metrics { } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(outputStream); - gzip.write(str.getBytes("UTF-8")); + gzip.write(str.getBytes(StandardCharsets.UTF_8)); gzip.close(); return outputStream.toByteArray(); } diff --git a/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java b/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java index d24cea328..3b5af7f99 100644 --- a/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java +++ b/connector/src/main/java/org/geysermc/connector/network/BedrockProtocol.java @@ -28,6 +28,7 @@ package org.geysermc.connector.network; import com.nukkitx.protocol.bedrock.BedrockPacketCodec; import com.nukkitx.protocol.bedrock.v419.Bedrock_v419; import com.nukkitx.protocol.bedrock.v422.Bedrock_v422; +import com.nukkitx.protocol.bedrock.v428.Bedrock_v428; import java.util.ArrayList; import java.util.List; @@ -40,9 +41,7 @@ public class BedrockProtocol { * Default Bedrock codec that should act as a fallback. Should represent the latest available * release of the game that Geyser supports. */ - public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v422.V422_CODEC.toBuilder() - .minecraftVersion("1.16.201") - .build(); + public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v428.V428_CODEC; /** * A list of all supported Bedrock versions that can join Geyser */ @@ -52,9 +51,10 @@ public class BedrockProtocol { SUPPORTED_BEDROCK_CODECS.add(Bedrock_v419.V419_CODEC.toBuilder() .minecraftVersion("1.16.100/1.16.101") // We change this as 1.16.100.60 is a beta .build()); - SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder() + SUPPORTED_BEDROCK_CODECS.add(Bedrock_v422.V422_CODEC.toBuilder() .minecraftVersion("1.16.200/1.16.201") .build()); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } /** diff --git a/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java b/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java new file mode 100644 index 000000000..57e58ecc2 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/* + * Taken & modified from TCPShield, licensed under MIT. See https://github.com/TCPShield/RealIP/blob/master/LICENSE + * + * https://github.com/TCPShield/RealIP/blob/32d422a9523cb6e25b571072851f3306bb8bbc4f/src/main/java/net/tcpshield/tcpshield/validation/cidr/CIDRMatcher.java + */ +public class CIDRMatcher { + private final int maskBits; + private final int maskBytes; + private final boolean simpleCIDR; + private final InetAddress cidrAddress; + + public CIDRMatcher(String ipAddress) { + String[] split = ipAddress.split("/", 2); + + String parsedIPAddress; + if (split.length == 2) { + parsedIPAddress = split[0]; + + this.maskBits = Integer.parseInt(split[1]); + this.simpleCIDR = maskBits == 32; + } else { + parsedIPAddress = ipAddress; + + this.maskBits = -1; + this.simpleCIDR = true; + } + + this.maskBytes = simpleCIDR ? -1 : maskBits / 8; + + try { + cidrAddress = InetAddress.getByName(parsedIPAddress); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + public boolean matches(InetAddress inetAddress) { + // check if IP is IPv4 or IPv6 + if (cidrAddress.getClass() != inetAddress.getClass()) { + return false; + } + + // check for equality if it's a simple CIDR + if (simpleCIDR) { + return inetAddress.equals(cidrAddress); + } + + byte[] inetAddressBytes = inetAddress.getAddress(); + byte[] requiredAddressBytes = cidrAddress.getAddress(); + + byte finalByte = (byte) (0xFF00 >> (maskBits & 0x07)); + + for (int i = 0; i < maskBytes; i++) { + if (inetAddressBytes[i] != requiredAddressBytes[i]) { + return false; + } + } + + if (finalByte != 0) { + return (inetAddressBytes[maskBytes] & finalByte) == (requiredAddressBytes[maskBytes] & finalByte); + } + + return true; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java index 87883087d..bd5030a8b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -40,8 +40,18 @@ import org.geysermc.connector.utils.LanguageUtils; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.List; public class ConnectorServerEventHandler implements BedrockServerEventHandler { + /* + The following constants are all used to ensure the ping does not reach a length where it is unparsable by the Bedrock client + */ + private static final int MINECRAFT_VERSION_BYTES_LENGTH = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion().getBytes(StandardCharsets.UTF_8).length; + private static final int BRAND_BYTES_LENGTH = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8).length; + /** + * The MOTD, sub-MOTD and Minecraft version ({@link #MINECRAFT_VERSION_BYTES_LENGTH}) combined cannot reach this length. + */ + private static final int MAGIC_RAKNET_LENGTH = 338; private final GeyserConnector connector; @@ -51,6 +61,21 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { @Override public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) { + List allowedProxyIPs = connector.getConfig().getBedrock().getProxyProtocolWhitelistedIPs(); + if (connector.getConfig().getBedrock().isEnableProxyProtocol() && !allowedProxyIPs.isEmpty()) { + boolean isWhitelistedIP = false; + for (CIDRMatcher matcher : connector.getConfig().getBedrock().getWhitelistedIPsMatchers()) { + if (matcher.matches(inetSocketAddress.getAddress())) { + isWhitelistedIP = true; + break; + } + } + + if (!isWhitelistedIP) { + return false; + } + } + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.attempt_connect", inetSocketAddress)); return true; } @@ -69,16 +94,16 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { BedrockPong pong = new BedrockPong(); pong.setEdition("MCPE"); - pong.setGameType("Default"); + pong.setGameType("Survival"); // Can only be Survival or Creative as of 1.16.210.59 pong.setNintendoLimited(false); pong.setProtocolVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()); - pong.setVersion(null); // Server tries to connect either way and it looks better + pong.setVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()); // Required to not be empty as of 1.16.210.59. Can only contain . and numbers. pong.setIpv4Port(config.getBedrock().getPort()); if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); String mainMotd = motd[0]; // First line of the motd. - String subMotd = (motd.length != 1) ? motd[1] : ""; // Second line of the motd if present, otherwise blank. + String subMotd = (motd.length != 1) ? motd[1] : GeyserConnector.NAME; // Second line of the motd if present, otherwise default. pong.setMotd(mainMotd.trim()); pong.setSubMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit. @@ -95,15 +120,28 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { pong.setMaximumPlayerCount(config.getMaxPlayers()); } + // Fallbacks to prevent errors and allow Bedrock to see the server + if (pong.getMotd() == null || pong.getMotd().trim().isEmpty()) { + pong.setMotd(GeyserConnector.NAME); + } + if (pong.getSubMotd() == null || pong.getSubMotd().trim().isEmpty()) { + // Sub-MOTD cannot be empty as of 1.16.210.59 + pong.setSubMotd(GeyserConnector.NAME); + } + // The ping will not appear if the MOTD + sub-MOTD is of a certain length. // We don't know why, though byte[] motdArray = pong.getMotd().getBytes(StandardCharsets.UTF_8); - if (motdArray.length + pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length > 338) { - // Remove the sub-MOTD first since that only appears locally - pong.setSubMotd(""); - if (motdArray.length > 338) { + int subMotdLength = pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length; + if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH)) { + // Shorten the sub-MOTD first since that only appears locally + if (subMotdLength > BRAND_BYTES_LENGTH) { + pong.setSubMotd(GeyserConnector.NAME); + subMotdLength = BRAND_BYTES_LENGTH; + } + if (motdArray.length > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength)) { // If the top MOTD is still too long, we chop it down - byte[] newMotdArray = new byte[339]; + byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength]; System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8)); } diff --git a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java index 637f6d99d..87541f704 100644 --- a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java @@ -64,7 +64,7 @@ public class QueryPacketHandler { * @param buffer The Query data */ public QueryPacketHandler(GeyserConnector connector, InetSocketAddress sender, ByteBuf buffer) { - if(!isQueryPacket(buffer)) + if (!isQueryPacket(buffer)) return; this.connector = connector; @@ -225,7 +225,7 @@ public class QueryPacketHandler { query.write(new byte[] { 0x00, 0x00 }); // Fill player names - if(pingInfo != null) { + if (pingInfo != null) { for (String username : pingInfo.getPlayerList()) { query.write(username.getBytes()); query.write((byte) 0x00); diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index d22645467..b073e3baf 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -26,15 +26,18 @@ package org.geysermc.connector.network; import com.nukkitx.protocol.bedrock.BedrockPacket; -import com.nukkitx.protocol.bedrock.data.ResourcePackType; import com.nukkitx.protocol.bedrock.BedrockPacketCodec; +import com.nukkitx.protocol.bedrock.data.ResourcePackType; import com.nukkitx.protocol.bedrock.packet.*; +import com.nukkitx.protocol.bedrock.v428.Bedrock_v428; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.cache.AdvancementsCache; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; +import org.geysermc.connector.network.translators.world.block.BlockTranslator1_16_100; +import org.geysermc.connector.network.translators.world.block.BlockTranslator1_16_210; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LoginEncryptionUtils; import org.geysermc.connector.utils.MathUtils; @@ -72,6 +75,10 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { session.getUpstream().getSession().setPacketCodec(packetCodec); + // Set the block translation based off of version + session.setBlockTranslator(packetCodec.getProtocolVersion() >= Bedrock_v428.V428_CODEC.getProtocolVersion() + ? BlockTranslator1_16_210.INSTANCE : BlockTranslator1_16_100.INSTANCE); + LoginEncryptionUtils.encryptPlayerConnection(connector, session, loginPacket); PlayStatusPacket playStatus = new PlayStatusPacket(); @@ -94,7 +101,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { public boolean handle(ResourcePackClientResponsePacket packet) { switch (packet.getStatus()) { case COMPLETED: - session.connect(connector.getRemoteServer()); + session.connect(); connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.connect", session.getAuthData().getName())); break; @@ -169,7 +176,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { public boolean handle(SetLocalPlayerAsInitializedPacket packet) { LanguageUtils.loadGeyserLocale(session.getLocale()); - if (!session.isLoggedIn() && !session.isLoggingIn() && session.getConnector().getAuthType() == AuthType.ONLINE) { + if (!session.isLoggedIn() && !session.isLoggingIn() && session.getRemoteAuthType() == AuthType.ONLINE) { // TODO it is safer to key authentication on something that won't change (UUID, not username) if (!couldLoginUserByName(session.getAuthData().getName())) { LoginEncryptionUtils.buildAndShowLoginWindow(session); 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 8bdb7f2b1..253088bba 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 @@ -36,12 +36,13 @@ import com.github.steveice10.mc.protocol.MinecraftConstants; import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +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.login.client.LoginPluginResponsePacket; import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket; import com.github.steveice10.packetlib.BuiltinFlags; import com.github.steveice10.packetlib.Client; @@ -57,24 +58,27 @@ import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.packet.*; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2LongMap; import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; import lombok.Setter; 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; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.entity.player.SessionPlayerEntity; import org.geysermc.connector.entity.player.SkullPlayerEntity; +import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.inventory.PlayerInventory; -import org.geysermc.connector.network.remote.RemoteServer; import org.geysermc.connector.network.session.auth.AuthData; import org.geysermc.connector.network.session.auth.BedrockClientData; import org.geysermc.connector.network.session.cache.*; @@ -83,8 +87,10 @@ import org.geysermc.connector.network.translators.EntityIdentifierRegistry; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.geysermc.connector.network.translators.collision.CollisionManager; -import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.skin.FloodgateSkinUploader; import org.geysermc.connector.skin.SkinManager; import org.geysermc.connector.utils.*; import org.geysermc.cumulus.Form; @@ -92,47 +98,83 @@ import org.geysermc.cumulus.util.FormBuilder; import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.util.BedrockData; +import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; @Getter public class GeyserSession implements CommandSender { private final GeyserConnector connector; private final UpstreamSession upstream; - private RemoteServer remoteServer; private Client downstream; @Setter private AuthData authData; @Setter private BedrockClientData clientData; + /* Setter for GeyserConnect */ + @Setter + private String remoteAddress; + @Setter + private int remotePort; + @Setter + private AuthType remoteAuthType; + /* Setter for GeyserConnect */ + @Deprecated @Setter private boolean microsoftAccount; private final SessionPlayerEntity playerEntity; - private PlayerInventory inventory; private AdvancementsCache advancementsCache; private BookEditCache bookEditCache; private ChunkCache chunkCache; private EntityCache entityCache; private EntityEffectCache effectCache; - private InventoryCache inventoryCache; private WorldCache worldCache; private FormCache formCache; private final Int2ObjectMap teleportMap = new Int2ObjectOpenHashMap<>(); + private final PlayerInventory playerInventory; + @Setter + private Inventory openInventory; + @Setter + private boolean closingInventory; + + @Setter + private InventoryTranslator inventoryTranslator = InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR; + + /** + * Use {@link #getNextItemNetId()} instead for consistency + */ + @Getter(AccessLevel.NONE) + private final AtomicInteger itemNetId = new AtomicInteger(2); + + @Getter(AccessLevel.NONE) + private final Object inventoryLock = new Object(); + @Getter(AccessLevel.NONE) + private CompletableFuture inventoryFuture; + + @Setter + private ScheduledFuture craftingGridFuture; + /** * Stores session collision */ private final CollisionManager collisionManager; + /** + * Stores the block translations for this specific version. + */ + @Setter + private BlockTranslator blockTranslator; + private final Map skullCache = new ConcurrentHashMap<>(); private final Long2ObjectMap storedMaps = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); @@ -142,6 +184,16 @@ public class GeyserSession implements CommandSender { */ private final Object2LongMap itemFrameCache = new Object2LongOpenHashMap<>(); + /** + * Stores a list of all lectern locations and their block entity tags. + * See {@link org.geysermc.connector.network.translators.world.WorldManager#getLecternDataAt(GeyserSession, int, int, int, boolean)} + * for more information. + */ + private final Set lecternCache = new ObjectOpenHashSet<>(); + + @Setter + private boolean droppingLecternBook; + @Setter private Vector2i lastChunkPosition = null; private int renderDistance; @@ -167,6 +219,9 @@ public class GeyserSession implements CommandSender { @Setter private boolean sprinting; + /** + * Not updated if cache chunks is enabled. + */ @Setter private boolean jumping; @@ -195,26 +250,36 @@ public class GeyserSession implements CommandSender { * Initialized as (0, 0, 0) so it is always not-null. */ @Setter - private Vector3i lastInteractionPosition = Vector3i.ZERO; + private Vector3i lastInteractionBlockPosition = Vector3i.ZERO; + + /** + * Stores the position of the player the last time they interacted. + * Used to verify that the player did not move since their last interaction.
+ * Initialized as (0, 0, 0) so it is always not-null. + */ + @Setter + private Vector3f lastInteractionPlayerPosition = Vector3f.ZERO; @Setter private Entity ridingVehicleEntity; + /** + * The entity that the client is currently looking at. + */ @Setter - private int craftSlot = 0; + private Entity mouseoverEntity; @Setter - private long lastWindowCloseTime = 0; - - @Setter - private VillagerTrade[] villagerTrades; - @Setter - private long lastInteractedVillagerEid; + private Int2ObjectMap craftingRecipes; + private final Set unlockedRecipes; + private final AtomicInteger lastRecipeNetId; /** - * Stores the enchantment information the client has received if they are in an enchantment table GUI + * Saves a list of all stonecutter recipes, for use in a stonecutter inventory. + * The key is the Java ID of the item; the values are all the possible outputs' Java IDs sorted by their string identifier */ - private final EnchantmentInventoryTranslator.EnchantmentSlotData[] enchantmentSlotData = new EnchantmentInventoryTranslator.EnchantmentSlotData[3]; + @Setter + private Int2ObjectMap stonecutterRecipes; /** * The current attack speed of the player. Used for sending proper cooldown timings. @@ -350,35 +415,43 @@ public class GeyserSession implements CommandSender { this.chunkCache = new ChunkCache(this); this.entityCache = new EntityCache(this); this.effectCache = new EntityEffectCache(); - this.inventoryCache = new InventoryCache(this); this.worldCache = new WorldCache(this); this.formCache = new FormCache(this); this.collisionManager = new CollisionManager(this); - this.playerEntity = new SessionPlayerEntity(this); - this.inventory = new PlayerInventory(); + + this.playerInventory = new PlayerInventory(); + this.openInventory = null; + this.inventoryFuture = CompletableFuture.completedFuture(null); + this.craftingRecipes = new Int2ObjectOpenHashMap<>(); + this.unlockedRecipes = new ObjectOpenHashSet<>(); + this.lastRecipeNetId = new AtomicInteger(1); this.spawned = false; this.loggedIn = false; - this.inventoryCache.getInventories().put(0, inventory); - // Make a copy to prevent ConcurrentModificationException final List tmpPlayers = new ArrayList<>(connector.getPlayers()); tmpPlayers.forEach(player -> this.emotes.addAll(player.getEmotes())); bedrockServerSession.addDisconnectHandler(disconnectReason -> { - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", bedrockServerSession.getAddress().getAddress(), disconnectReason)); + InetAddress address = bedrockServerSession.getRealAddress().getAddress(); + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", address, disconnectReason)); disconnect(disconnectReason.name()); connector.removePlayer(this); }); } - public void connect(RemoteServer remoteServer) { + /** + * Send all necessary packets to load Bedrock into the server + */ + public void connect() { startGame(); - this.remoteServer = remoteServer; + this.remoteAddress = connector.getConfig().getRemote().getAddress(); + this.remotePort = connector.getConfig().getRemote().getPort(); + this.remoteAuthType = connector.getDefaultAuthType(); // Set the hardcoded shield ID to the ID we just defined in StartGamePacket upstream.getSession().getHardcodedBlockingId().set(ItemRegistry.SHIELD.getBedrockId()); @@ -423,8 +496,8 @@ public class GeyserSession implements CommandSender { } public void login() { - if (connector.getAuthType() != AuthType.ONLINE) { - if (connector.getAuthType() == AuthType.OFFLINE) { + if (this.remoteAuthType != AuthType.ONLINE) { + if (this.remoteAuthType == AuthType.OFFLINE) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.offline")); } else { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.floodgate")); @@ -533,18 +606,21 @@ public class GeyserSession implements CommandSender { * After getting whatever credentials needed, we attempt to join the Java server. */ private void connectDownstream() { - boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; + boolean floodgate = this.remoteAuthType == AuthType.FLOODGATE; // Start ticking tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); - downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); + downstream = new Client(this.remoteAddress, this.remotePort, protocol, new TcpSessionFactory()); + disableSrvResolving(); 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); + if (connector.getConfig().isForwardPlayerPing()) { + // Let Geyser handle sending the keep alive + downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); + } downstream.getSession().addListener(new SessionAdapter() { @Override public void packetSending(PacketSendingEvent event) { @@ -553,7 +629,9 @@ public class GeyserSession implements CommandSender { byte[] encryptedData; try { + FloodgateSkinUploader skinUploader = connector.getSkinUploader(); FloodgateCipher cipher = connector.getCipher(); + encryptedData = cipher.encryptFromString(BedrockData.of( clientData.getGameVersion(), authData.getName(), @@ -562,7 +640,9 @@ public class GeyserSession implements CommandSender { clientData.getLanguageCode(), clientData.getUiProfile().ordinal(), clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() + upstream.getAddress().getAddress().getHostAddress(), + skinUploader.getId(), + skinUploader.getVerifyCode() ).toString()); } catch (Exception e) { connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); @@ -570,13 +650,7 @@ public class GeyserSession implements CommandSender { return; } - byte[] rawSkin = clientData.getAndTransformImage("Skin").encode(); - byte[] finalData = new byte[encryptedData.length + rawSkin.length + 1]; - System.arraycopy(encryptedData, 0, finalData, 0, encryptedData.length); - finalData[encryptedData.length] = 0x21; // splitter - System.arraycopy(rawSkin, 0, finalData, encryptedData.length + 1, rawSkin.length); - - String finalDataString = new String(finalData, StandardCharsets.UTF_8); + String finalDataString = new String(encryptedData, StandardCharsets.UTF_8); HandshakePacket handshakePacket = event.getPacket(); event.setPacket(new HandshakePacket( @@ -597,7 +671,7 @@ public class GeyserSession implements CommandSender { 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())); + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteAddress)); playerEntity.setUuid(protocol.getProfile().getId()); playerEntity.setUsername(protocol.getProfile().getName()); @@ -618,7 +692,7 @@ public class GeyserSession implements CommandSender { public void disconnected(DisconnectedEvent event) { loggingIn = false; loggedIn = false; - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteAddress, event.getReason())); if (event.getCause() != null) { event.getCause().printStackTrace(); } @@ -636,9 +710,13 @@ public class GeyserSession implements CommandSender { playerEntity.setUuid(profile.getId()); // Check if they are not using a linked account - if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { + if (remoteAuthType == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { SkinManager.handleBedrockSkin(playerEntity, clientData); } + + // We'll send the skin upload a bit after the handshake packet (aka this packet), + // because otherwise the global server returns the data too fast. + getAuthData().upload(connector); } PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); @@ -683,7 +761,6 @@ public class GeyserSession implements CommandSender { this.entityCache = null; this.effectCache = null; this.worldCache = null; - this.inventoryCache = null; this.formCache = null; closed = true; @@ -723,6 +800,18 @@ public class GeyserSession implements CommandSender { this.sneaking = sneaking; collisionManager.updatePlayerBoundingBox(); collisionManager.updateScaffoldingFlags(); + + if (mouseoverEntity != null) { + // Horses, etc can change their property depending on if you're sneaking + InteractiveTagManager.updateTag(this, mouseoverEntity); + } + } + + /** + * Will be overwritten for GeyserConnect. + */ + protected void disableSrvResolving() { + this.downstream.getSession().setFlag(BuiltinFlags.ATTEMPT_SRV_RESOLVE, false); } @Override @@ -823,10 +912,58 @@ public class GeyserSession implements CommandSender { startGamePacket.setMultiplayerCorrelationId(""); startGamePacket.setItemEntries(ItemRegistry.ITEMS); startGamePacket.setVanillaVersion("*"); - startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); + startGamePacket.setInventoriesServerAuthoritative(true); + startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); // can be removed once 1.16.200 support is dropped + + SyncedPlayerMovementSettings settings = new SyncedPlayerMovementSettings(); + settings.setMovementMode(AuthoritativeMovementMode.CLIENT); + settings.setRewindHistorySize(0); + settings.setServerAuthoritativeBlockBreaking(false); + startGamePacket.setPlayerMovementSettings(settings); + upstream.sendPacket(startGamePacket); } + /** + * Adds a new inventory task. + * Inventory tasks are executed one at a time, in order. + * + * @param task the task to run + */ + public void addInventoryTask(Runnable task) { + synchronized (inventoryLock) { + inventoryFuture = inventoryFuture.thenRun(task).exceptionally(throwable -> { + GeyserConnector.getInstance().getLogger().error("Error processing inventory task", throwable.getCause()); + return null; + }); + } + } + + /** + * Adds a new inventory task with a delay. + * The delay is achieved by scheduling with the Geyser general thread pool. + * Inventory tasks are executed one at a time, in order. + * + * @param task the delayed task to run + * @param delayMillis delay in milliseconds + */ + public void addInventoryTask(Runnable task, long delayMillis) { + synchronized (inventoryLock) { + Executor delayedExecutor = command -> GeyserConnector.getInstance().getGeneralThreadPool().schedule(command, delayMillis, TimeUnit.MILLISECONDS); + inventoryFuture = inventoryFuture.thenRunAsync(task, delayedExecutor).exceptionally(throwable -> { + GeyserConnector.getInstance().getLogger().error("Error processing inventory task", throwable.getCause()); + return null; + }); + } + } + + /** + * @return the next Bedrock item network ID to use for a new item + */ + public int getNextItemNetId() { + return itemNetId.getAndIncrement(); + } + public void addTeleport(TeleportCache teleportCache) { teleportMap.put(teleportCache.getTeleportConfirmId(), teleportCache); @@ -931,7 +1068,7 @@ public class GeyserSession implements CommandSender { * @param packet the java edition packet from MCProtocolLib */ public void sendDownstreamPacket(Packet packet) { - if (downstream != null && downstream.getSession() != null && protocol.getSubProtocol().equals(SubProtocol.GAME)) { + if (downstream != null && downstream.getSession() != null && (protocol.getSubProtocol().equals(SubProtocol.GAME) || packet.getClass() == LoginPluginResponsePacket.class)) { downstream.getSession().send(packet); } else { connector.getLogger().debug("Tried to send downstream packet " + packet.getClass().getSimpleName() + " before connected to the server"); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java index 04e208af3..f973574b0 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java @@ -61,6 +61,6 @@ public class UpstreamSession { } public InetSocketAddress getAddress() { - return session.getAddress(); + return session.getRealAddress(); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java index ba9e13548..463276891 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java @@ -25,16 +25,26 @@ package org.geysermc.connector.network.session.auth; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.geysermc.connector.GeyserConnector; import java.util.UUID; -@Getter -@AllArgsConstructor +@RequiredArgsConstructor public class AuthData { + @Getter private final String name; + @Getter private final UUID UUID; + @Getter private final String xboxUUID; - private String name; - private UUID UUID; - private String xboxUUID; + private final JsonNode certChainData; + private final String clientData; + + public void upload(GeyserConnector connector) { + // we can't upload the skin in LoginEncryptionUtil since the global server would return + // the skin too fast, that's why we upload it after we know for sure that the target server + // is ready to handle the result of the global server + connector.getSkinUploader().uploadSkin(certChainData, clientData); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java index 7a7069d07..d87590e5f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java @@ -34,10 +34,8 @@ import lombok.Getter; import org.geysermc.connector.skin.SkinProvider; import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.InputMode; -import org.geysermc.floodgate.util.RawSkin; import org.geysermc.floodgate.util.UiProfile; -import java.awt.image.BufferedImage; import java.util.Base64; import java.util.UUID; @@ -114,26 +112,8 @@ public final class BedrockClientData { private String skinColor; @JsonProperty(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; - - private static RawSkin getLegacyImage(byte[] imageData, boolean alex) { - if (imageData == null) { - return null; - } - - // width * height * 4 (rgba) - switch (imageData.length) { - case 8192: - return new RawSkin(64, 32, imageData, alex); - case 16384: - return new RawSkin(64, 64, imageData, alex); - case 32768: - return new RawSkin(64, 128, imageData, alex); - case 65536: - return new RawSkin(128, 128, imageData, alex); - default: - throw new IllegalArgumentException("Unknown legacy skin size"); - } - } + @JsonProperty(value = "PlayFabId") + private String playFabId; public void setJsonData(JsonNode data) { if (this.jsonData == null && data != null) { @@ -141,51 +121,6 @@ public final class BedrockClientData { } } - /** - * Taken from https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/network/protocol/LoginPacket.java
- * Internally only used for Skins, but can be used for Capes too - */ - public RawSkin getImage(String name) { - if (jsonData == null || !jsonData.has(name + "Data")) { - return null; - } - - boolean alex = false; - if (name.equals("Skin")) { - alex = isAlex(); - } - - byte[] image = Base64.getDecoder().decode(jsonData.get(name + "Data").asText()); - if (jsonData.has(name + "ImageWidth") && jsonData.has(name + "ImageHeight")) { - return new RawSkin( - jsonData.get(name + "ImageWidth").asInt(), - jsonData.get(name + "ImageHeight").asInt(), - image, alex - ); - } - return getLegacyImage(image, alex); - } - - public RawSkin getAndTransformImage(String name) { - RawSkin skin = getImage(name); - if (skin != null && (skin.width > 64 || skin.height > 64)) { - BufferedImage scaledImage = - SkinProvider.imageDataToBufferedImage(skin.data, skin.width, skin.height); - - int max = Math.max(skin.width, skin.height); - while (max > 64) { - max /= 2; - scaledImage = SkinProvider.scale(scaledImage); - } - - byte[] skinData = SkinProvider.bufferedImageToImageData(scaledImage); - skin.width = scaledImage.getWidth(); - skin.height = scaledImage.getHeight(); - skin.data = skinData; - } - return skin; - } - public boolean isAlex() { try { byte[] bytes = Base64.getDecoder().decode(geometryName.getBytes(Charsets.UTF_8)); 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 index f81a9fdf9..c82645dbf 100644 --- 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 @@ -25,9 +25,9 @@ package org.geysermc.connector.network.session.cache; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; 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; @@ -63,8 +63,8 @@ public class BookEditCache { return; } // Don't send the update if the player isn't not holding a book, shouldn't happen if we catch all interactions - ItemStack itemStack = session.getInventory().getItemInHand(); - if (itemStack == null || itemStack.getId() != ItemRegistry.WRITABLE_BOOK.getJavaId()) { + GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(); + if (itemStack == null || itemStack.getJavaId() != ItemRegistry.WRITABLE_BOOK.getJavaId()) { packet = null; return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java index a48b20cee..d182a6f12 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java @@ -29,23 +29,24 @@ import com.github.steveice10.mc.protocol.data.game.chunk.Chunk; import com.github.steveice10.mc.protocol.data.game.chunk.Column; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import org.geysermc.connector.bootstrap.GeyserBootstrap; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.MathUtils; public class ChunkCache { + private static final int MINIMUM_WORLD_HEIGHT = 0; private final boolean cache; - private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + private final Long2ObjectMap chunks; public ChunkCache(GeyserSession session) { - if (session.getConnector().getWorldManager().getClass() == GeyserBootstrap.DEFAULT_CHUNK_MANAGER.getClass()) { - this.cache = session.getConnector().getConfig().isCacheChunks(); - } else { + if (session.getConnector().getWorldManager().hasOwnChunkCache()) { this.cache = false; // To prevent Spigot from initializing + } else { + this.cache = session.getConnector().getConfig().isCacheChunks(); } + chunks = cache ? new Long2ObjectOpenHashMap<>() : null; } public Column addToCache(Column chunk) { @@ -86,6 +87,11 @@ public class ChunkCache { return; } + if (y < MINIMUM_WORLD_HEIGHT || (y >> 4) > column.getChunks().length - 1) { + // Y likely goes above or below the height limit of this world + return; + } + Chunk chunk = column.getChunks()[y >> 4]; if (chunk != null) { chunk.set(x & 0xF, y & 0xF, z & 0xF, block); @@ -102,6 +108,11 @@ public class ChunkCache { return BlockTranslator.JAVA_AIR_ID; } + if (y < MINIMUM_WORLD_HEIGHT || (y >> 4) > column.getChunks().length - 1) { + // Y likely goes above or below the height limit of this world + return BlockTranslator.JAVA_AIR_ID; + } + Chunk chunk = column.getChunks()[y >> 4]; if (chunk != null) { return chunk.get(x & 0xF, y & 0xF, z & 0xF); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java index f4c0f9abc..fb6d5b93d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java @@ -38,7 +38,7 @@ import java.io.InputStream; */ public class EntityIdentifierRegistry { - public static NbtMap ENTITY_IDENTIFIERS; + public static final NbtMap ENTITY_IDENTIFIERS; private EntityIdentifierRegistry() { } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java index b841a79b2..eb26dbff9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java @@ -30,6 +30,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUpdate import com.github.steveice10.packetlib.packet.Packet; import com.nukkitx.protocol.bedrock.BedrockPacket; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.geysermc.common.PlatformType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.FileUtils; @@ -89,12 +90,15 @@ public class PacketTranslatorRegistry { public

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

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

translator = (PacketTranslator

) translators.get(clazz); + if (translator != null) { + translator.translate(packet, session); return true; } else { - if (!IGNORED_PACKETS.contains(clazz)) + if ((GeyserConnector.getInstance().getPlatformType() != PlatformType.STANDALONE || !(packet instanceof BedrockPacket)) && !IGNORED_PACKETS.contains(clazz)) { + // Other debug logs already take care of Bedrock packets for us if on standalone GeyserConnector.getInstance().getLogger().debug("Could not find packet for " + (packet.toString().length() > 25 ? packet.getClass().getSimpleName() : packet)); + } } } catch (Throwable ex) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.packet.failed", packet.getClass().getSimpleName()), ex); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java index dd5d08a2c..8e2d77df7 100644 --- 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 @@ -26,17 +26,16 @@ 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 org.geysermc.connector.network.translators.inventory.InventoryTranslator; import java.util.Collections; import java.util.LinkedList; @@ -47,58 +46,55 @@ public class BedrockBookEditTranslator extends PacketTranslator @Override public void translate(BookEditPacket packet, GeyserSession session) { - ItemStack itemStack = session.getInventory().getItemInHand(); + GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(); if (itemStack != null) { CompoundTag tag = itemStack.getNbt() != null ? itemStack.getNbt() : new CompoundTag(""); - ItemStack bookItem = new ItemStack(itemStack.getId(), itemStack.getAmount(), tag); + 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: { + 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; } - // 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; + 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) { @@ -110,10 +106,10 @@ public class BedrockBookEditTranslator extends PacketTranslator } } tag.put(new ListTag("pages", pages)); - session.getInventory().setItem(36 + session.getInventory().getHeldItemSlot(), bookItem); - InventoryTranslator.INVENTORY_TRANSLATORS.get(null).updateInventory(session, session.getInventory()); + 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.getInventory().getHeldItemSlot())); + 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/BedrockContainerCloseTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockContainerCloseTranslator.java index 21eb73be0..21bc1e437 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockContainerCloseTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockContainerCloseTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.connector.network.translators.bedrock; import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCloseWindowPacket; import com.nukkitx.protocol.bedrock.packet.ContainerClosePacket; import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.inventory.MerchantContainer; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; @@ -38,24 +39,29 @@ public class BedrockContainerCloseTranslator extends PacketTranslator { + byte windowId = packet.getId(); + + //Client wants close confirmation + session.sendUpstreamPacket(packet); + session.setClosingInventory(false); + + if (windowId == -1 && session.getOpenInventory() instanceof MerchantContainer) { + // 1.16.200 - window ID is always -1 sent from Bedrock + windowId = (byte) session.getOpenInventory().getId(); } - } - if (windowId == 0 || (openInventory != null && openInventory.getId() == windowId)) { - ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(windowId); - session.getDownstream().getSession().send(closeWindowPacket); - InventoryUtils.closeInventory(session, windowId); - } - - //Client wants close confirmation - session.sendUpstreamPacket(packet); + Inventory openInventory = session.getOpenInventory(); + if (openInventory != null) { + if (windowId == openInventory.getId()) { + ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(windowId); + session.sendDownstreamPacket(closeWindowPacket); + InventoryUtils.closeInventory(session, windowId, false); + } else if (openInventory.isPending()) { + InventoryUtils.displayInventory(session, openInventory); + openInventory.setPending(false); + } + } + }); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java index db01df150..3b017dfbd 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java @@ -25,7 +25,10 @@ package org.geysermc.connector.network.translators.bedrock; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket; import com.nukkitx.protocol.bedrock.packet.FilterTextPacket; +import org.geysermc.connector.inventory.AnvilContainer; +import org.geysermc.connector.inventory.CartographyContainer; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; @@ -39,7 +42,17 @@ public class BedrockFilterTextTranslator extends PacketTranslator { + if (session.getPlayerInventory().getHeldItemSlot() != containerAction.getSlot() || + session.getPlayerInventory().getItemInHand().isEmpty()) { + return; + } + + boolean dropAll = worldAction.getToItem().getCount() > 1; + ClientPlayerActionPacket dropAllPacket = new ClientPlayerActionPacket( + dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM, + BlockUtils.POSITION_ZERO, + BlockFace.DOWN + ); + session.sendDownstreamPacket(dropAllPacket); + + if (dropAll) { + session.getPlayerInventory().setItemInHand(GeyserItemStack.EMPTY); + } else { + session.getPlayerInventory().getItemInHand().sub(1); + } + }); + } + } break; case INVENTORY_MISMATCH: - Inventory inv = session.getInventoryCache().getOpenInventory(); - if (inv == null) inv = session.getInventory(); - InventoryTranslator.INVENTORY_TRANSLATORS.get(inv.getWindowType()).updateInventory(session, inv); - InventoryUtils.updateCursor(session); break; case ITEM_USE: switch (packet.getActionType()) { @@ -98,8 +116,9 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator= 2 && session.getGameMode() == GameMode.CREATIVE) { // Otherwise insufficient permissions - int blockState = BlockTranslator.getJavaBlockState(packet.getBlockRuntimeId()); + int blockState = session.getBlockTranslator().getJavaBlockState(packet.getBlockRuntimeId()); String blockName = BlockTranslator.getJavaIdBlockMap().inverse().getOrDefault(blockState, ""); // In the future this can be used for structure blocks too, however not all elements // are available in each GUI @@ -215,9 +234,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { @Override public void translate(ItemFrameDropItemPacket packet, GeyserSession session) { - Vector3i position = Vector3i.from(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()); - ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) ItemFrameEntity.getItemFrameEntityId(session, position), + ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition()), InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking()); session.sendDownstreamPacket(interactPacket); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemStackRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemStackRequestTranslator.java new file mode 100644 index 000000000..bdbb88eee --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemStackRequestTranslator.java @@ -0,0 +1,50 @@ +/* + * 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.nukkitx.protocol.bedrock.packet.ItemStackRequestPacket; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; + +/** + * The packet sent for server-authoritative-style inventory transactions. + */ +@Translator(packet = ItemStackRequestPacket.class) +public class BedrockItemStackRequestTranslator extends PacketTranslator { + + @Override + public void translate(ItemStackRequestPacket packet, GeyserSession session) { + Inventory inventory = session.getOpenInventory(); + if (inventory == null) + return; + + InventoryTranslator translator = session.getInventoryTranslator(); + session.addInventoryTask(() -> translator.translateRequests(session, inventory, packet.getRequests())); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java new file mode 100644 index 000000000..ae99fec07 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java @@ -0,0 +1,98 @@ +/* + * 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.Position; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; +import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace; +import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPlaceBlockPacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCloseWindowPacket; +import com.nukkitx.protocol.bedrock.packet.LecternUpdatePacket; +import org.geysermc.connector.inventory.LecternContainer; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.utils.InventoryUtils; + +/** + * Used to translate moving pages, or closing the inventory + */ +@Translator(packet = LecternUpdatePacket.class) +public class BedrockLecternUpdateTranslator extends PacketTranslator { + + @Override + public void translate(LecternUpdatePacket packet, GeyserSession session) { + if (packet.isDroppingBook()) { + // Bedrock drops the book outside of the GUI. Java drops it in the GUI + // So, we enter the GUI and then drop it! :) + session.setDroppingLecternBook(true); + + // Emulate an interact packet + ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket( + new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), + BlockFace.values()[0], + Hand.MAIN_HAND, + 0, 0, 0, // Java doesn't care about these when dealing with a lectern + false); + session.sendDownstreamPacket(blockPacket); + } else { + // Bedrock wants to either move a page or exit + if (!(session.getOpenInventory() instanceof LecternContainer)) { + session.getConnector().getLogger().debug("Expected lectern but it wasn't open!"); + return; + } + + LecternContainer lecternContainer = (LecternContainer) session.getOpenInventory(); + if (lecternContainer.getCurrentBedrockPage() == packet.getPage()) { + // The same page means Bedrock is closing the window + ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(lecternContainer.getId()); + session.sendDownstreamPacket(closeWindowPacket); + InventoryUtils.closeInventory(session, lecternContainer.getId(), false); + } else { + // Each "page" Bedrock gives to us actually represents two pages (think opening a book and seeing two pages) + // Each "page" on Java is just one page (think a spiral notebook folded back to only show one page) + int newJavaPage = (packet.getPage() * 2); + int currentJavaPage = (lecternContainer.getCurrentBedrockPage() * 2); + + // Send as many click button packets as we need to + // Java has the option to specify exact page numbers by adding 100 to the number, but buttonId variable + // is a byte when transmitted over the network and therefore this stops us at 128 + if (newJavaPage > currentJavaPage) { + for (int i = currentJavaPage; i < newJavaPage; i++) { + ClientClickWindowButtonPacket clickButtonPacket = new ClientClickWindowButtonPacket(session.getOpenInventory().getId(), 2); + session.sendDownstreamPacket(clickButtonPacket); + } + } else { + for (int i = currentJavaPage; i > newJavaPage; i--) { + ClientClickWindowButtonPacket clickButtonPacket = new ClientClickWindowButtonPacket(session.getOpenInventory().getId(), 1); + session.sendDownstreamPacket(clickButtonPacket); + } + } + } + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java index 47fb97027..e07f0ae1e 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java @@ -25,14 +25,19 @@ package org.geysermc.connector.network.translators.bedrock; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; +import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerChangeHeldItemPacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; +import com.nukkitx.protocol.bedrock.packet.MobEquipmentPacket; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; - -import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerChangeHeldItemPacket; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; -import com.nukkitx.protocol.bedrock.packet.MobEquipmentPacket; +import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.utils.CooldownUtils; +import org.geysermc.connector.utils.InteractiveTagManager; + +import java.util.concurrent.TimeUnit; @Translator(packet = MobEquipmentPacket.class) public class BedrockMobEquipmentTranslator extends PacketTranslator { @@ -40,7 +45,7 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator 8 || - packet.getContainerId() != ContainerId.INVENTORY || session.getInventory().getHeldItemSlot() == packet.getHotbarSlot()) { + packet.getContainerId() != ContainerId.INVENTORY || session.getPlayerInventory().getHeldItemSlot() == packet.getHotbarSlot()) { // For the last condition - Don't update the slot if the slot is the same - not Java Edition behavior and messes with plugins such as Grief Prevention return; } @@ -48,12 +53,25 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator session.sendDownstreamPacket(new ClientPlayerUseItemPacket(Hand.MAIN_HAND)), + 50, TimeUnit.MILLISECONDS); + } + // Java sends a cooldown indicator whenever you switch an item CooldownUtils.sendCooldown(session); + + // Update the interactive tag, if an entity is present + if (session.getMouseoverEntity() != null) { + InteractiveTagManager.updateTag(session, session.getMouseoverEntity()); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java index 64c91f076..577469fdd 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java @@ -40,7 +40,7 @@ import java.util.Collections; import java.util.concurrent.TimeUnit; /** - * Used to send the keep alive packet back to the server + * Used to send the forwarded keep alive packet back to the server */ @Translator(packet = NetworkStackLatencyPacket.class) public class BedrockNetworkStackLatencyTranslator extends PacketTranslator { @@ -60,8 +60,10 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator 0) { - ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(pingId); - session.sendDownstreamPacket(keepAlivePacket); + if (session.getConnector().getConfig().isForwardPlayerPing()) { + ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(pingId); + session.sendDownstreamPacket(keepAlivePacket); + } return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java index f6c6ad1e5..501ed4468 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java @@ -33,6 +33,8 @@ import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.utils.SettingsUtils; import org.geysermc.cumulus.CustomForm; +import java.util.concurrent.TimeUnit; + @Translator(packet = ServerSettingsRequestPacket.class) public class BedrockServerSettingsRequestTranslator extends PacketTranslator { @Override @@ -40,9 +42,12 @@ public class BedrockServerSettingsRequestTranslator extends PacketTranslator { + ServerSettingsResponsePacket serverSettingsResponsePacket = new ServerSettingsResponsePacket(); + serverSettingsResponsePacket.setFormData(window.getJsonData()); + serverSettingsResponsePacket.setFormId(windowId); + session.sendUpstreamPacket(serverSettingsResponsePacket); + }, 1, TimeUnit.SECONDS); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java index a89bfdfb4..6267597fd 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/BedrockEntityEventTranslator.java @@ -26,12 +26,13 @@ package org.geysermc.connector.network.translators.bedrock.entity; import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; -import com.github.steveice10.mc.protocol.data.game.window.WindowType; import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientSelectTradePacket; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.inventory.GeyserItemStack; import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.inventory.MerchantContainer; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; @@ -47,20 +48,25 @@ public class BedrockEntityEventTranslator extends PacketTranslator { + ClientSelectTradePacket selectTradePacket = new ClientSelectTradePacket(packet.getData()); + session.sendDownstreamPacket(selectTradePacket); + }); - Entity villager = session.getPlayerEntity(); - Inventory openInventory = session.getInventoryCache().getOpenInventory(); - if (openInventory != null && openInventory.getWindowType() == WindowType.MERCHANT) { - VillagerTrade[] trades = session.getVillagerTrades(); - if (trades != null && packet.getData() >= 0 && packet.getData() < trades.length) { - VillagerTrade trade = session.getVillagerTrades()[packet.getData()]; - openInventory.setItem(2, trade.getOutput()); - villager.getMetadata().put(EntityData.TRADE_XP, trade.getXp() + villager.getMetadata().getInt(EntityData.TRADE_XP)); - villager.updateBedrockMetadata(session); + session.addInventoryTask(() -> { + Entity villager = session.getPlayerEntity(); + Inventory openInventory = session.getOpenInventory(); + if (openInventory instanceof MerchantContainer) { + MerchantContainer merchantInventory = (MerchantContainer) openInventory; + VillagerTrade[] trades = merchantInventory.getVillagerTrades(); + if (trades != null && packet.getData() >= 0 && packet.getData() < trades.length) { + VillagerTrade trade = merchantInventory.getVillagerTrades()[packet.getData()]; + openInventory.setItem(2, GeyserItemStack.from(trade.getOutput()), session); + villager.getMetadata().put(EntityData.TRADE_XP, trade.getXp() + villager.getMetadata().getInt(EntityData.TRADE_XP)); + villager.updateBedrockMetadata(session); + } } - } + }, 100); return; } session.getConnector().getLogger().debug("Did not translate incoming EntityEventPacket: " + packet.toString()); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java index c4dbbec40..7751fb024 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java @@ -25,24 +25,23 @@ package org.geysermc.connector.network.translators.bedrock.entity.player; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; -import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState; +import com.github.steveice10.mc.protocol.data.game.entity.player.*; import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace; -import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerAbilitiesPacket; -import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket; -import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.player.*; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.PlayerActionType; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; import com.nukkitx.protocol.bedrock.packet.PlayStatusPacket; import com.nukkitx.protocol.bedrock.packet.PlayerActionPacket; import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.ItemFrameEntity; +import org.geysermc.connector.inventory.GeyserItemStack; import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; @@ -64,7 +63,7 @@ public class BedrockActionTranslator extends PacketTranslator { - session.setJumping(false); - }, 1, TimeUnit.SECONDS); + if (!session.getConnector().getConfig().isCacheChunks()) { + // Save the jumping status for determining teleport status + session.setJumping(true); + session.getConnector().getGeneralThreadPool().schedule(() -> session.setJumping(false), 1, TimeUnit.SECONDS); + } break; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java index c08a2f311..740ab8a37 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java @@ -31,59 +31,21 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket; import com.nukkitx.protocol.bedrock.data.entity.EntityData; -import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import com.nukkitx.protocol.bedrock.packet.InteractPacket; -import lombok.Getter; import org.geysermc.connector.entity.Entity; -import org.geysermc.connector.entity.type.EntityType; +import org.geysermc.connector.entity.living.animal.horse.AbstractHorseEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.network.translators.item.ItemEntry; import org.geysermc.connector.network.translators.item.ItemRegistry; - -import java.util.Arrays; -import java.util.List; +import org.geysermc.connector.utils.InteractiveTagManager; @Translator(packet = InteractPacket.class) public class BedrockInteractTranslator extends PacketTranslator { - /** - * A list of all foods a horse/donkey can eat on Java Edition. - * Used to display interactive tag if needed. - */ - private static final List DONKEY_AND_HORSE_FOODS = Arrays.asList("golden_apple", "enchanted_golden_apple", - "golden_carrot", "sugar", "apple", "wheat", "hay_block"); - - /** - * A list of all flowers. Used for feeding bees. - */ - private static final List FLOWERS = Arrays.asList("dandelion", "poppy", "blue_orchid", "allium", "azure_bluet", - "red_tulip", "pink_tulip", "white_tulip", "orange_tulip", "cornflower", "lily_of_the_valley", "wither_rose", - "sunflower", "lilac", "rose_bush", "peony"); - - /** - * All entity types that can be leashed on Java Edition - */ - private static final List LEASHABLE_MOB_TYPES = Arrays.asList(EntityType.BEE, EntityType.CAT, EntityType.CHICKEN, - EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.HOGLIN, EntityType.HORSE, EntityType.SKELETON_HORSE, - EntityType.ZOMBIE_HORSE, EntityType.IRON_GOLEM, EntityType.LLAMA, EntityType.TRADER_LLAMA, EntityType.MOOSHROOM, - EntityType.MULE, EntityType.OCELOT, EntityType.PARROT, EntityType.PIG, EntityType.POLAR_BEAR, EntityType.RABBIT, - EntityType.SHEEP, EntityType.SNOW_GOLEM, EntityType.STRIDER, EntityType.WOLF, EntityType.ZOGLIN); - - private static final List SADDLEABLE_WHEN_TAMED_MOB_TYPES = Arrays.asList(EntityType.DONKEY, EntityType.HORSE, - EntityType.ZOMBIE_HORSE, EntityType.MULE); - /** - * A list of all foods a wolf can eat on Java Edition. - * Used to display interactive tag if needed. - */ - private static final List WOLF_FOODS = Arrays.asList("pufferfish", "tropical_fish", "chicken", "cooked_chicken", - "porkchop", "beef", "rabbit", "cooked_porkchop", "cooked_beef", "rotten_flesh", "mutton", "cooked_mutton", - "cooked_rabbit"); - @Override public void translate(InteractPacket packet, GeyserSession session) { Entity entity; @@ -98,7 +60,7 @@ public class BedrockInteractTranslator extends PacketTranslator switch (packet.getAction()) { case INTERACT: - if (session.getInventory().getItem(session.getInventory().getHeldItemSlot() + 36).getId() == ItemRegistry.SHIELD.getJavaId()) { + if (session.getPlayerInventory().getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) { break; } ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(), @@ -119,313 +81,42 @@ public class BedrockInteractTranslator extends PacketTranslator // Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc if (packet.getRuntimeEntityId() != 0) { Entity interactEntity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId()); - if (interactEntity == null) + session.setMouseoverEntity(interactEntity); + if (interactEntity == null) { return; - EntityDataMap entityMetadata = interactEntity.getMetadata(); - ItemEntry itemEntry = session.getInventory().getItemInHand() == null ? ItemEntry.AIR : ItemRegistry.getItem(session.getInventory().getItemInHand()); - String javaIdentifierStripped = itemEntry.getJavaIdentifier().replace("minecraft:", ""); - - // TODO - in the future, update these in the metadata? So the client doesn't have to wiggle their cursor around for it to happen - // TODO - also, might be good to abstract out the eating thing. I know there will need to be food tracked for https://github.com/GeyserMC/Geyser/issues/1005 but not all food is breeding food - InteractiveTag interactiveTag = InteractiveTag.NONE; - if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) { - // Unleash the entity - interactiveTag = InteractiveTag.REMOVE_LEASH; - } else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) && - ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) || - interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) { - // Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed) - interactiveTag = InteractiveTag.SADDLE; - } else if (javaIdentifierStripped.equals("name_tag") && session.getInventory().getItemInHand().getNbt() != null && - session.getInventory().getItemInHand().getNbt().contains("display")) { - // Holding a named name tag - interactiveTag = InteractiveTag.NAME; - } else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) && - entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == -1L) { - // Holding a leash and the mob is leashable for sure - // (Plugins can change this behavior so that's something to look into in the far far future) - interactiveTag = InteractiveTag.LEASH; - } else { - switch (interactEntity.getEntityType()) { - case BEE: - if (FLOWERS.contains(javaIdentifierStripped)) { - interactiveTag = InteractiveTag.FEED; - } - break; - case BOAT: - interactiveTag = InteractiveTag.BOARD_BOAT; - break; - case CAT: - if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) { - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && - entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) { - // Tamed and owned by player - can sit/stand - interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; - break; - } - break; - case CHICKEN: - if (javaIdentifierStripped.contains("seeds")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case MOOSHROOM: - // Shear the mooshroom - if (javaIdentifierStripped.equals("shears")) { - interactiveTag = InteractiveTag.MOOSHROOM_SHEAR; - break; - } - // Bowls are acceptable here - else if (javaIdentifierStripped.equals("bowl")) { - interactiveTag = InteractiveTag.MOOSHROOM_MILK_STEW; - break; - } - // Fall down to COW as this works on mooshrooms - case COW: - if (javaIdentifierStripped.equals("wheat")) { - interactiveTag = InteractiveTag.FEED; - } else if (javaIdentifierStripped.equals("bucket")) { - // Milk the cow - interactiveTag = InteractiveTag.MILK; - } - break; - case CREEPER: - if (javaIdentifierStripped.equals("flint_and_steel")) { - // Today I learned that you can ignite a creeper with flint and steel! Huh. - interactiveTag = InteractiveTag.IGNITE_CREEPER; - } - break; - case DONKEY: - case LLAMA: - case MULE: - if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED) - && javaIdentifierStripped.equals("chest")) { - // Can attach a chest - interactiveTag = InteractiveTag.ATTACH_CHEST; - break; - } - // Intentional fall-through - case HORSE: - case SKELETON_HORSE: - case TRADER_LLAMA: - case ZOMBIE_HORSE: - // have another switch statement as, while these share mount attributes they don't share food - switch (interactEntity.getEntityType()) { - case LLAMA: - case TRADER_LLAMA: - if (javaIdentifierStripped.equals("wheat") || javaIdentifierStripped.equals("hay_block")) { - interactiveTag = InteractiveTag.FEED; - break; - } - case DONKEY: - case HORSE: - // Undead can't eat - if (DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped)) { - interactiveTag = InteractiveTag.FEED; - break; - } - } - if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) { - // Can't ride a baby - if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) { - interactiveTag = InteractiveTag.RIDE_HORSE; - } else if (!entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && itemEntry.equals(ItemEntry.AIR)) { - // Can't hide an untamed entity without having your hand empty - interactiveTag = InteractiveTag.MOUNT; - } - } - break; - case FOX: - if (javaIdentifierStripped.equals("sweet_berries")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case HOGLIN: - if (javaIdentifierStripped.equals("crimson_fungus")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case MINECART: - interactiveTag = InteractiveTag.RIDE_MINECART; - break; - case MINECART_CHEST: - case MINECART_COMMAND_BLOCK: - case MINECART_HOPPER: - interactiveTag = InteractiveTag.OPEN_CONTAINER; - break; - case OCELOT: - if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case PANDA: - if (javaIdentifierStripped.equals("bamboo")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case PARROT: - if (javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case PIG: - if (javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot")) { - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) { - interactiveTag = InteractiveTag.MOUNT; - } - break; - case PIGLIN: - if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) { - interactiveTag = InteractiveTag.BARTER; - } - break; - case RABBIT: - if (javaIdentifierStripped.equals("dandelion") || javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("golden_carrot")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case SHEEP: - if (javaIdentifierStripped.equals("wheat")) { - interactiveTag = InteractiveTag.FEED; - } else if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) { - if (javaIdentifierStripped.equals("shears")) { - // Shear the sheep - interactiveTag = InteractiveTag.SHEAR; - } else if (javaIdentifierStripped.contains("_dye")) { - // Dye the sheep - interactiveTag = InteractiveTag.DYE; - } - } - break; - case STRIDER: - if (javaIdentifierStripped.equals("warped_fungus")) { - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) { - interactiveTag = InteractiveTag.RIDE_STRIDER; - } - break; - case TURTLE: - if (javaIdentifierStripped.equals("seagrass")) { - interactiveTag = InteractiveTag.FEED; - } - break; - case VILLAGER: - if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0 - && entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby - interactiveTag = InteractiveTag.TRADE; - } - break; - case WANDERING_TRADER: - interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably. - break; - case WOLF: - if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) { - // Bone and untamed - can tame - interactiveTag = InteractiveTag.TAME; - } else if (WOLF_FOODS.contains(javaIdentifierStripped)) { - // Compatible food in hand - feed - // Sometimes just sits/stands when the wolf isn't hungry - there doesn't appear to be a way to fix this - interactiveTag = InteractiveTag.FEED; - } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && - entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) { - // Tamed and owned by player - can sit/stand - interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; - } - break; - case ZOMBIE_VILLAGER: - // We can't guarantee the existence of the weakness effect so we just always show it. - if (javaIdentifierStripped.equals("golden_apple")) { - interactiveTag = InteractiveTag.CURE; - } - break; - default: - break; - } } - session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue()); - session.getPlayerEntity().updateBedrockMetadata(session); + + InteractiveTagManager.updateTag(session, interactEntity); } else { - if (!session.getPlayerEntity().getMetadata().getString(EntityData.INTERACTIVE_TAG).isEmpty()) { + if (session.getMouseoverEntity() != null) { // No interactive tag should be sent - session.getPlayerEntity().getMetadata().remove(EntityData.INTERACTIVE_TAG); + session.setMouseoverEntity(null); + session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, ""); session.getPlayerEntity().updateBedrockMetadata(session); } } break; case OPEN_INVENTORY: - if (!session.getInventory().isOpen()) { - ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket(); - containerOpenPacket.setId((byte) 0); - containerOpenPacket.setType(ContainerType.INVENTORY); - containerOpenPacket.setUniqueEntityId(-1); - containerOpenPacket.setBlockPosition(entity.getPosition().toInt()); - session.sendUpstreamPacket(containerOpenPacket); - session.getInventory().setOpen(true); + if (session.getOpenInventory() == null) { + Entity ridingEntity = session.getRidingVehicleEntity(); + if (ridingEntity instanceof AbstractHorseEntity) { + if (ridingEntity.getMetadata().getFlags().getFlag(EntityFlag.TAMED)) { + // We should request to open the horse inventory instead + ClientPlayerStatePacket openHorseWindowPacket = new ClientPlayerStatePacket((int) session.getPlayerEntity().getEntityId(), PlayerState.OPEN_HORSE_INVENTORY); + session.sendDownstreamPacket(openHorseWindowPacket); + } + } else { + session.setOpenInventory(session.getPlayerInventory()); + + ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket(); + containerOpenPacket.setId((byte) 0); + containerOpenPacket.setType(ContainerType.INVENTORY); + containerOpenPacket.setUniqueEntityId(-1); + containerOpenPacket.setBlockPosition(entity.getPosition().toInt()); + session.sendUpstreamPacket(containerOpenPacket); + } } break; } } - - /** - * All interactive tags in enum form. For potential API usage. - */ - public enum InteractiveTag { - NONE(true), - IGNITE_CREEPER("creeper"), - EDIT, - LEAVE_BOAT("exit.boat"), - FEED, - FISH("fishing"), - MILK, - MOOSHROOM_SHEAR("mooshear"), - MOOSHROOM_MILK_STEW("moostew"), - BOARD_BOAT("ride.boat"), - RIDE_MINECART("ride.minecart"), - RIDE_HORSE("ride.horse"), - RIDE_STRIDER("ride.strider"), - SHEAR, - SIT, - STAND, - TALK, - TAME, - DYE, - CURE, - OPEN_CONTAINER("opencontainer"), - CREATE_MAP("createMap"), - TAKE_PICTURE("takepicture"), - SADDLE, - MOUNT, - BOOST, - WRITE, - LEASH, - REMOVE_LEASH("unleash"), - NAME, - ATTACH_CHEST("attachchest"), - TRADE, - POSE_ARMOR_STAND("armorstand.pose"), - EQUIP_ARMOR_STAND("armorstand.equip"), - READ, - WAKE_VILLAGER("wakevillager"), - BARTER; - - /** - * The full string that should be passed on to the client. - */ - @Getter - private final String value; - - InteractiveTag(boolean isNone) { - this.value = ""; - } - - InteractiveTag(String value) { - this.value = "action.interact." + value; - } - - InteractiveTag() { - this.value = "action.interact." + name().toLowerCase(); - } - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java index 203e4406f..77392a99a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java @@ -177,24 +177,24 @@ public class CollisionManager { session.sendUpstreamPacket(movePlayerPacket); } - public List getPlayerCollidableBlocks() { + public List getCollidableBlocks(BoundingBox box) { List blocks = new ArrayList<>(); - Vector3d position = Vector3d.from(playerBoundingBox.getMiddleX(), - playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2), - playerBoundingBox.getMiddleZ()); + Vector3d position = Vector3d.from(box.getMiddleX(), + box.getMiddleY() - (box.getSizeY() / 2), + box.getMiddleZ()); - // Loop through all blocks that could collide with the player - int minCollisionX = (int) Math.floor(position.getX() - ((playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE)); - int maxCollisionX = (int) Math.floor(position.getX() + (playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE); + // Loop through all blocks that could collide + int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE)); + int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE); // Y extends 0.5 blocks down because of fence hitboxes int minCollisionY = (int) Math.floor(position.getY() - 0.5); - int maxCollisionY = (int) Math.floor(position.getY() + playerBoundingBox.getSizeY()); + int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY()); - int minCollisionZ = (int) Math.floor(position.getZ() - ((playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE)); - int maxCollisionZ = (int) Math.floor(position.getZ() + (playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE); + int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE)); + int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE); for (int y = minCollisionY; y < maxCollisionY + 1; y++) { for (int x = minCollisionX; x < maxCollisionX + 1; x++) { @@ -207,6 +207,10 @@ public class CollisionManager { return blocks; } + public List getPlayerCollidableBlocks() { + return getCollidableBlocks(playerBoundingBox); + } + /** * Returns false if the movement is invalid, and in this case it shouldn't be sent to the server and should be * cancelled diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java deleted file mode 100644 index fb487bea2..000000000 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.inventory; - -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.google.gson.JsonSyntaxException; -import com.nukkitx.nbt.NbtMap; -import com.nukkitx.protocol.bedrock.data.inventory.*; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.geysermc.connector.inventory.Inventory; -import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater; - -import java.util.List; -import java.util.stream.Collectors; - -public class AnvilInventoryTranslator extends BlockInventoryTranslator { - public AnvilInventoryTranslator() { - super(3, "minecraft:anvil[facing=north]", ContainerType.ANVIL, new CursorInventoryUpdater()); - } - - @Override - public int bedrockSlotToJava(InventoryActionData action) { - if (action.getSource().getContainerId() == ContainerId.UI) { - switch (action.getSlot()) { - case 1: - return 0; - case 2: - return 1; - case 50: - return 2; - } - } - if (action.getSource().getContainerId() == ContainerId.ANVIL_RESULT) { - return 2; - } - return super.bedrockSlotToJava(action); - } - - @Override - public int javaSlotToBedrock(int slot) { - switch (slot) { - case 0: - return 1; - case 1: - return 2; - case 2: - return 50; - } - return super.javaSlotToBedrock(slot); - } - - @Override - public SlotType getSlotType(int javaSlot) { - if (javaSlot == 2) - return SlotType.OUTPUT; - return SlotType.NORMAL; - } - - @Override - public void translateActions(GeyserSession session, Inventory inventory, List actions) { - InventoryActionData anvilResult = null; - InventoryActionData anvilInput = null; - for (InventoryActionData action : actions) { - if (action.getSource().getContainerId() == ContainerId.ANVIL_MATERIAL) { - //useless packet - return; - } else if (action.getSource().getContainerId() == ContainerId.ANVIL_RESULT) { - anvilResult = action; - } else if (bedrockSlotToJava(action) == 0) { - anvilInput = action; - } - } - ItemData itemName = null; - if (anvilResult != null) { - itemName = anvilResult.getFromItem(); - } else if (anvilInput != null) { - itemName = anvilInput.getToItem(); - } - if (itemName != null) { - String rename; - NbtMap tag = itemName.getTag(); - if (tag != null && tag.containsKey("display")) { - String name = tag.getCompound("display").getString("Name"); - try { - Component component = GsonComponentSerializer.gson().deserialize(name); - rename = LegacyComponentSerializer.legacySection().serialize(component); - } catch (JsonSyntaxException e) { - rename = name; - } - } else { - rename = ""; - } - ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(rename); - session.sendDownstreamPacket(renameItemPacket); - } - if (anvilResult != null) { - //Strip unnecessary actions - List strippedActions = actions.stream() - .filter(action -> action.getSource().getContainerId() == ContainerId.ANVIL_RESULT - || (action.getSource().getType() == InventorySource.Type.CONTAINER - && !(action.getSource().getContainerId() == ContainerId.UI && action.getSlot() != 0))) - .collect(Collectors.toList()); - super.translateActions(session, inventory, strippedActions); - return; - } - - super.translateActions(session, inventory, actions); - } - - @Override - public void updateSlot(GeyserSession session, Inventory inventory, int slot) { - if (slot == 0) { - ItemStack item = inventory.getItem(slot); - if (item != null) { - String rename; - CompoundTag tag = item.getNbt(); - if (tag != null) { - CompoundTag displayTag = tag.get("display"); - if (displayTag != null && displayTag.contains("Name")) { - String itemName = displayTag.get("Name").getValue().toString(); - try { - Component component = GsonComponentSerializer.gson().deserialize(itemName); - rename = LegacyComponentSerializer.legacySection().serialize(component); - } catch (JsonSyntaxException e) { - rename = itemName; - } - } else { - rename = ""; - } - } else { - rename = ""; - } - ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(rename); - session.sendDownstreamPacket(renameItemPacket); - } - } - super.updateSlot(session, inventory, slot); - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BedrockContainerSlot.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BedrockContainerSlot.java new file mode 100644 index 000000000..47d1f0709 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BedrockContainerSlot.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.network.translators.inventory; + +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import lombok.Value; + +@Value +public class BedrockContainerSlot { + ContainerSlotType container; + int slot; +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java deleted file mode 100644 index b7b98bf73..000000000 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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.inventory; - -import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket; -import com.nukkitx.nbt.NbtMap; -import com.nukkitx.nbt.NbtMapBuilder; -import com.nukkitx.nbt.NbtType; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; -import com.nukkitx.protocol.bedrock.data.inventory.ItemData; -import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; -import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import org.geysermc.connector.common.ChatColor; -import org.geysermc.connector.inventory.Inventory; -import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; -import org.geysermc.connector.network.translators.item.ItemRegistry; -import org.geysermc.connector.network.translators.item.ItemTranslator; -import org.geysermc.connector.utils.InventoryUtils; -import org.geysermc.connector.utils.LocaleUtils; - -import java.util.*; - -/** - * A temporary reconstruction of the enchantment table UI until our inventory rewrite is complete. - * The enchantment table on Bedrock without server authoritative inventories doesn't tell us which button is pressed - * when selecting an enchantment. - */ -public class EnchantmentInventoryTranslator extends BlockInventoryTranslator { - - private static final int DYE_ID = ItemRegistry.getItemEntry("minecraft:lapis_lazuli").getBedrockId(); - private static final int ENCHANTED_BOOK_ID = ItemRegistry.getItemEntry("minecraft:enchanted_book").getBedrockId(); - - public EnchantmentInventoryTranslator(InventoryUpdater updater) { - super(2, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER, updater); - } - - @Override - public void translateActions(GeyserSession session, Inventory inventory, List actions) { - for (InventoryActionData action : actions) { - if (action.getSource().getContainerId() == inventory.getId()) { - // This is the hopper UI - switch (action.getSlot()) { - case 1: - // Don't allow the slot to be put through if the item isn't lapis - if ((action.getToItem().getId() != DYE_ID) && action.getToItem() != ItemData.AIR) { - updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - return; - } - break; - case 2: - case 3: - case 4: - // The books here act as buttons - ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), action.getSlot() - 2); - session.sendDownstreamPacket(packet); - updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - return; - default: - break; - } - } - } - - super.translateActions(session, inventory, actions); - } - - @Override - public void updateInventory(GeyserSession session, Inventory inventory) { - super.updateInventory(session, inventory); - List items = new ArrayList<>(5); - items.add(ItemTranslator.translateToBedrock(session, inventory.getItem(0))); - items.add(ItemTranslator.translateToBedrock(session, inventory.getItem(1))); - for (int i = 0; i < 3; i++) { - items.add(session.getEnchantmentSlotData()[i].getItem() != null ? session.getEnchantmentSlotData()[i].getItem() : createEnchantmentBook()); - } - - InventoryContentPacket contentPacket = new InventoryContentPacket(); - contentPacket.setContainerId(inventory.getId()); - contentPacket.setContents(items); - session.sendUpstreamPacket(contentPacket); - } - - @Override - public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { - int bookSlotToUpdate; - switch (key) { - case 0: - case 1: - case 2: - // Experience required - bookSlotToUpdate = key; - session.getEnchantmentSlotData()[bookSlotToUpdate].setExperienceRequired(value); - break; - case 4: - case 5: - case 6: - // Enchantment name - bookSlotToUpdate = key - 4; - if (value != -1) { - session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentType(EnchantmentTableEnchantments.values()[value - 1]); - } else { - // -1 means no enchantment specified - session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentType(null); - } - break; - case 7: - case 8: - case 9: - // Enchantment level - bookSlotToUpdate = key - 7; - session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentLevel(value); - break; - default: - return; - } - updateEnchantmentBook(session, inventory, bookSlotToUpdate); - } - - @Override - public void openInventory(GeyserSession session, Inventory inventory) { - super.openInventory(session, inventory); - for (int i = 0; i < session.getEnchantmentSlotData().length; i++) { - session.getEnchantmentSlotData()[i] = new EnchantmentSlotData(); - } - } - - @Override - public void closeInventory(GeyserSession session, Inventory inventory) { - super.closeInventory(session, inventory); - Arrays.fill(session.getEnchantmentSlotData(), null); - } - - private ItemData createEnchantmentBook() { - NbtMapBuilder root = NbtMap.builder(); - NbtMapBuilder display = NbtMap.builder(); - - display.putString("Name", ChatColor.RESET + "No Enchantment"); - - root.put("display", display.build()); - return ItemData.of(ENCHANTED_BOOK_ID, (short) 0, 1, root.build()); - } - - private void updateEnchantmentBook(GeyserSession session, Inventory inventory, int slot) { - NbtMapBuilder root = NbtMap.builder(); - NbtMapBuilder display = NbtMap.builder(); - EnchantmentSlotData data = session.getEnchantmentSlotData()[slot]; - if (data.getEnchantmentType() != null) { - display.putString("Name", ChatColor.ITALIC + data.getEnchantmentType().toString(session) + - (data.getEnchantmentLevel() != -1 ? " " + toRomanNumeral(session, data.getEnchantmentLevel()) : "") + "?"); - } else { - display.putString("Name", ChatColor.RESET + "No Enchantment"); - } - - display.putList("Lore", NbtType.STRING, Collections.singletonList(ChatColor.DARK_GRAY + data.getExperienceRequired() + "xp")); - root.put("display", display.build()); - ItemData book = ItemData.of(ENCHANTED_BOOK_ID, (short) 0, 1, root.build()); - - InventorySlotPacket slotPacket = new InventorySlotPacket(); - slotPacket.setContainerId(inventory.getId()); - slotPacket.setSlot(slot + 2); - slotPacket.setItem(book); - session.sendUpstreamPacket(slotPacket); - data.setItem(book); - } - - private String toRomanNumeral(GeyserSession session, int level) { - return LocaleUtils.getLocaleString("enchantment.level." + level, - session.getLocale()); - } - - /** - * Stores the data of each slot in an enchantment table - */ - @NoArgsConstructor - @Getter - @Setter - @ToString - public static class EnchantmentSlotData { - private EnchantmentTableEnchantments enchantmentType = null; - private int enchantmentLevel = 0; - private int experienceRequired = 0; - private ItemData item; - } - - /** - * Classifies enchantments by Java order - */ - public enum EnchantmentTableEnchantments { - PROTECTION, - FIRE_PROTECTION, - FEATHER_FALLING, - BLAST_PROTECTION, - PROJECTILE_PROTECTION, - RESPIRATION, - AQUA_AFFINITY, - THORNS, - DEPTH_STRIDER, - FROST_WALKER, - BINDING_CURSE, - SHARPNESS, - SMITE, - BANE_OF_ARTHROPODS, - KNOCKBACK, - FIRE_ASPECT, - LOOTING, - SWEEPING, - EFFICIENCY, - SILK_TOUCH, - UNBREAKING, - FORTUNE, - POWER, - PUNCH, - FLAME, - INFINITY, - LUCK_OF_THE_SEA, - LURE, - LOYALTY, - IMPALING, - RIPTIDE, - CHANNELING, - MENDING, - VANISHING_CURSE, // After this is not documented - MULTISHOT, - PIERCING, - QUICK_CHARGE, - SOUL_SPEED; - - public String toString(GeyserSession session) { - return LocaleUtils.getLocaleString("enchantment.minecraft." + this.toString().toLowerCase(), - session.getLocale()); - } - } -} 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 f8ef0f7ce..4a45b5c9f 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 @@ -25,52 +25,86 @@ package org.geysermc.connector.network.translators.inventory; +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.data.game.recipe.Ingredient; +import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; +import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; +import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; import com.github.steveice10.mc.protocol.data.game.window.WindowType; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.*; +import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; +import it.unimi.dsi.fastutil.ints.*; import lombok.AllArgsConstructor; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.inventory.CartographyContainer; +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.updater.ContainerInventoryUpdater; -import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; +import org.geysermc.connector.network.translators.inventory.click.Click; +import org.geysermc.connector.network.translators.inventory.click.ClickPlan; +import org.geysermc.connector.network.translators.inventory.translators.*; +import org.geysermc.connector.network.translators.inventory.translators.chest.DoubleChestInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.chest.SingleChestInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.furnace.BlastFurnaceInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.furnace.FurnaceInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.furnace.SmokerInventoryTranslator; +import org.geysermc.connector.utils.InventoryUtils; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; @AllArgsConstructor public abstract class InventoryTranslator { + public static final InventoryTranslator PLAYER_INVENTORY_TRANSLATOR = new PlayerInventoryTranslator(); public static final Map INVENTORY_TRANSLATORS = new HashMap() { { - put(null, new PlayerInventoryTranslator()); //player inventory + /* Player Inventory */ + put(null, PLAYER_INVENTORY_TRANSLATOR); + + /* Chest UIs */ put(WindowType.GENERIC_9X1, new SingleChestInventoryTranslator(9)); put(WindowType.GENERIC_9X2, new SingleChestInventoryTranslator(18)); put(WindowType.GENERIC_9X3, new SingleChestInventoryTranslator(27)); put(WindowType.GENERIC_9X4, new DoubleChestInventoryTranslator(36)); put(WindowType.GENERIC_9X5, new DoubleChestInventoryTranslator(45)); put(WindowType.GENERIC_9X6, new DoubleChestInventoryTranslator(54)); - put(WindowType.BREWING_STAND, new BrewingInventoryTranslator()); + + /* Furnaces */ + put(WindowType.FURNACE, new FurnaceInventoryTranslator()); + put(WindowType.BLAST_FURNACE, new BlastFurnaceInventoryTranslator()); + put(WindowType.SMOKER, new SmokerInventoryTranslator()); + + /* Specific Inventories */ put(WindowType.ANVIL, new AnvilInventoryTranslator()); + put(WindowType.BEACON, new BeaconInventoryTranslator()); + put(WindowType.BREWING_STAND, new BrewingInventoryTranslator()); + put(WindowType.CARTOGRAPHY, new CartographyInventoryTranslator()); put(WindowType.CRAFTING, new CraftingInventoryTranslator()); - //put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator()); //FIXME + put(WindowType.ENCHANTMENT, new EnchantingInventoryTranslator()); + put(WindowType.HOPPER, new HopperInventoryTranslator()); + put(WindowType.GENERIC_3X3, new Generic3X3InventoryTranslator()); + put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator()); + put(WindowType.LOOM, new LoomInventoryTranslator()); put(WindowType.MERCHANT, new MerchantInventoryTranslator()); - //put(WindowType.SMITHING, new SmithingInventoryTranslator()); //TODO for server authoritative inventories + put(WindowType.SHULKER_BOX, new ShulkerInventoryTranslator()); + put(WindowType.SMITHING, new SmithingInventoryTranslator()); + put(WindowType.STONECUTTER, new StonecutterInventoryTranslator()); - InventoryTranslator furnace = new FurnaceInventoryTranslator(); - put(WindowType.FURNACE, furnace); - put(WindowType.BLAST_FURNACE, furnace); - put(WindowType.SMOKER, furnace); - - InventoryUpdater containerUpdater = new ContainerInventoryUpdater(); - put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator(containerUpdater)); //TODO - put(WindowType.GENERIC_3X3, new BlockInventoryTranslator(9, "minecraft:dispenser[facing=north,triggered=false]", ContainerType.DISPENSER, containerUpdater)); - put(WindowType.HOPPER, new BlockInventoryTranslator(5, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER, containerUpdater)); - put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(27, "minecraft:shulker_box[facing=north]", ContainerType.CONTAINER, containerUpdater)); - //put(WindowType.BEACON, new BlockInventoryTranslator(1, "minecraft:beacon", ContainerType.BEACON)); //TODO + /* Lectern */ + put(WindowType.LECTERN, new LecternInventoryTranslator()); } }; + public static final int PLAYER_INVENTORY_SIZE = 36; + public static final int PLAYER_INVENTORY_OFFSET = 9; + public final int size; public abstract void prepareInventory(GeyserSession session, Inventory inventory); @@ -79,8 +113,779 @@ public abstract class InventoryTranslator { public abstract void updateProperty(GeyserSession session, Inventory inventory, int key, int value); public abstract void updateInventory(GeyserSession session, Inventory inventory); public abstract void updateSlot(GeyserSession session, Inventory inventory, int slot); - public abstract int bedrockSlotToJava(InventoryActionData action); - public abstract int javaSlotToBedrock(int slot); + public abstract int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData); + public abstract int javaSlotToBedrock(int javaSlot); + public abstract BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot); public abstract SlotType getSlotType(int javaSlot); - public abstract void translateActions(GeyserSession session, Inventory inventory, List actions); + public abstract Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory); + + /** + * Should be overwritten in cases where specific inventories should reject an item being in a specific spot. + * For examples, looms use this to reject items that are dyes in Bedrock but not in Java. + * + * The source/destination slot will be -1 if the cursor is the slot + * + * @return true if this transfer should be rejected + */ + public boolean shouldRejectItemPlace(GeyserSession session, Inventory inventory, ContainerSlotType bedrockSourceContainer, + int javaSourceSlot, ContainerSlotType bedrockDestinationContainer, int javaDestinationSlot) { + return false; + } + + /** + * Should be overrided if this request matches a certain criteria and shouldn't be treated normally. + * E.G. anvil renaming or enchanting + */ + public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { + return false; + } + + /** + * If {@link #shouldHandleRequestFirst(StackRequestActionData, Inventory)} returns true, this will be called + */ + public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + return rejectRequest(request); + } + + public void translateRequests(GeyserSession session, Inventory inventory, List requests) { + boolean refresh = false; + ItemStackResponsePacket responsePacket = new ItemStackResponsePacket(); + for (ItemStackRequest request : requests) { + ItemStackResponsePacket.Response response; + if (request.getActions().length > 0) { + StackRequestActionData firstAction = request.getActions()[0]; + if (shouldHandleRequestFirst(firstAction, inventory)) { + // Some special request that shouldn't be processed normally + response = translateSpecialRequest(session, inventory, request); + } else { + switch (firstAction.getType()) { + case CRAFT_RECIPE: + response = translateCraftingRequest(session, inventory, request); + break; + case CRAFT_RECIPE_AUTO: + response = translateAutoCraftingRequest(session, inventory, request); + break; + case CRAFT_CREATIVE: + // This is also used for pulling items out of creative + response = translateCreativeRequest(session, inventory, request); + break; + default: + response = translateRequest(session, inventory, request); + break; + } + } + } else { + response = rejectRequest(request); + } + + if (response.getResult() != ItemStackResponsePacket.ResponseStatus.OK) { + // Sync our copy of the inventory with Bedrock's to prevent desyncs + refresh = true; + } + + responsePacket.getEntries().add(response); + } + session.sendUpstreamPacket(responsePacket); + + if (refresh) { + InventoryUtils.updateCursor(session); + updateInventory(session, inventory); + } + } + + public ItemStackResponsePacket.Response translateRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + ClickPlan plan = new ClickPlan(session, this, inventory); + IntSet affectedSlots = new IntOpenHashSet(); + for (StackRequestActionData action : request.getActions()) { + GeyserItemStack cursor = session.getPlayerInventory().getCursor(); + switch (action.getType()) { + case TAKE: + case PLACE: { + TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action; + if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) { + if (session.getGameMode().equals(GameMode.CREATIVE) && transferAction.getSource().getContainer() == ContainerSlotType.CRAFTING_INPUT && + transferAction.getSource().getSlot() >= 28 && transferAction.getSource().getSlot() <= 31) { + return rejectRequest(request, false); + } + if (session.getConnector().getConfig().isDebugMode()) { + session.getConnector().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.getName()); + dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination()); + } + return rejectRequest(request); + } + + int sourceSlot = bedrockSlotToJava(transferAction.getSource()); + int destSlot = bedrockSlotToJava(transferAction.getDestination()); + + if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(), + isCursor(transferAction.getSource()) ? -1 : sourceSlot, + transferAction.getDestination().getContainer(), isCursor(transferAction.getDestination()) ? -1 : destSlot)) { + // This item would not be here in Java + return rejectRequest(request, false); + } + + if (isCursor(transferAction.getSource()) && isCursor(transferAction.getDestination())) { //??? + return rejectRequest(request); + } else if (isCursor(transferAction.getSource())) { //releasing cursor + int sourceAmount = cursor.getAmount(); + if (transferAction.getCount() == sourceAmount) { //release all + plan.add(Click.LEFT, destSlot); + } else { //release some + for (int i = 0; i < transferAction.getCount(); i++) { + plan.add(Click.RIGHT, destSlot); + } + } + } else if (isCursor(transferAction.getDestination())) { //picking up into cursor + GeyserItemStack sourceItem = plan.getItem(sourceSlot); + int sourceAmount = sourceItem.getAmount(); + if (cursor.isEmpty()) { //picking up into empty cursor + if (transferAction.getCount() == sourceAmount) { //pickup all + plan.add(Click.LEFT, sourceSlot); + } else if (transferAction.getCount() == sourceAmount - (sourceAmount / 2)) { //larger half; simple right click + plan.add(Click.RIGHT, sourceSlot); + } else { //pickup some; not a simple right click + plan.add(Click.LEFT, sourceSlot); //first pickup all + for (int i = 0; i < sourceAmount - transferAction.getCount(); i++) { + plan.add(Click.RIGHT, sourceSlot); //release extra items back into source slot + } + } + } else { //pickup into non-empty cursor + if (!InventoryUtils.canStack(cursor, plan.getItem(sourceSlot))) { //doesn't make sense, reject + return rejectRequest(request); + } + if (transferAction.getCount() != sourceAmount) { + int tempSlot = findTempSlot(inventory, cursor, false, sourceSlot); + if (tempSlot == -1) { + return rejectRequest(request); + } + plan.add(Click.LEFT, tempSlot); //place cursor into temp slot + plan.add(Click.LEFT, sourceSlot); //pickup source items into cursor + for (int i = 0; i < transferAction.getCount(); i++) { + plan.add(Click.RIGHT, tempSlot); //partially transfer source items into temp slot (original cursor) + } + plan.add(Click.LEFT, sourceSlot); //return remaining source items + plan.add(Click.LEFT, tempSlot); //retrieve original cursor items from temp slot + } else { + if (getSlotType(sourceSlot).equals(SlotType.NORMAL)) { + plan.add(Click.LEFT, sourceSlot); //release cursor onto source slot + } + plan.add(Click.LEFT, sourceSlot); //pickup combined cursor and source + } + } + } else { //transfer from one slot to another + int tempSlot = -1; + if (!plan.getCursor().isEmpty()) { + tempSlot = findTempSlot(inventory, cursor, false, sourceSlot, destSlot); + if (tempSlot == -1) { + return rejectRequest(request); + } + plan.add(Click.LEFT, tempSlot); //place cursor into temp slot + } + + transferSlot(plan, sourceSlot, destSlot, transferAction.getCount()); + + if (tempSlot != -1) { + plan.add(Click.LEFT, tempSlot); //retrieve original cursor + } + } + break; + } + case SWAP: { + SwapStackRequestActionData swapAction = (SwapStackRequestActionData) action; + if (!(checkNetId(session, inventory, swapAction.getSource()) && checkNetId(session, inventory, swapAction.getDestination()))) { + if (session.getConnector().getConfig().isDebugMode()) { + session.getConnector().getLogger().error("DEBUG: About to reject SWAP request made by " + session.getName()); + dumpStackRequestDetails(session, inventory, swapAction.getSource(), swapAction.getDestination()); + } + return rejectRequest(request); + } + + int sourceSlot = bedrockSlotToJava(swapAction.getSource()); + int destSlot = bedrockSlotToJava(swapAction.getDestination()); + boolean isSourceCursor = isCursor(swapAction.getSource()); + boolean isDestCursor = isCursor(swapAction.getDestination()); + + if (shouldRejectItemPlace(session, inventory, swapAction.getSource().getContainer(), + isSourceCursor ? -1 : sourceSlot, + swapAction.getDestination().getContainer(), isDestCursor ? -1 : destSlot)) { + // This item would not be here in Java + return rejectRequest(request, false); + } + + if (isSourceCursor && isDestCursor) { //??? + return rejectRequest(request); + } else if (isSourceCursor) { //swap cursor + if (InventoryUtils.canStack(cursor, plan.getItem(destSlot))) { //TODO: cannot simply swap if cursor stacks with slot (temp slot) + return rejectRequest(request); + } + plan.add(Click.LEFT, destSlot); + } else if (isDestCursor) { //swap cursor + if (InventoryUtils.canStack(cursor, plan.getItem(sourceSlot))) { //TODO + return rejectRequest(request); + } + plan.add(Click.LEFT, sourceSlot); + } else { + if (!cursor.isEmpty()) { //TODO: (temp slot) + return rejectRequest(request); + } + if (sourceSlot == destSlot) { //doesn't make sense + return rejectRequest(request); + } + if (InventoryUtils.canStack(plan.getItem(sourceSlot), plan.getItem(destSlot))) { //TODO: (temp slot) + return rejectRequest(request); + } + plan.add(Click.LEFT, sourceSlot); //pickup source into cursor + plan.add(Click.LEFT, destSlot); //swap cursor with dest slot + plan.add(Click.LEFT, sourceSlot); //release cursor onto source + } + break; + } + case DROP: { + DropStackRequestActionData dropAction = (DropStackRequestActionData) action; + if (!checkNetId(session, inventory, dropAction.getSource())) + return rejectRequest(request); + + if (isCursor(dropAction.getSource())) { //clicking outside of window + int sourceAmount = plan.getCursor().getAmount(); + if (dropAction.getCount() == sourceAmount) { //drop all + plan.add(Click.LEFT_OUTSIDE, Click.OUTSIDE_SLOT); + } else { //drop some + for (int i = 0; i < dropAction.getCount(); i++) { + plan.add(Click.RIGHT_OUTSIDE, Click.OUTSIDE_SLOT); //drop one until goal is met + } + } + } else { //dropping from inventory + int sourceSlot = bedrockSlotToJava(dropAction.getSource()); + int sourceAmount = plan.getItem(sourceSlot).getAmount(); + if (dropAction.getCount() == sourceAmount && sourceAmount > 1) { //dropping all? (prefer DROP_ONE if only one) + plan.add(Click.DROP_ALL, sourceSlot); + } else { //drop some + for (int i = 0; i < dropAction.getCount(); i++) { + plan.add(Click.DROP_ONE, sourceSlot); //drop one until goal is met + } + } + } + break; + } + case CONSUME: { // Tends to be called for UI inventories + if (inventory instanceof CartographyContainer) { + // TODO add this for more inventories? Only seems to glitch out the cartography table, though. + ConsumeStackRequestActionData consumeData = (ConsumeStackRequestActionData) action; + + int sourceSlot = bedrockSlotToJava(consumeData.getSource()); + if ((sourceSlot == 0 && inventory.getItem(1).isEmpty()) || (sourceSlot == 1 && inventory.getItem(0).isEmpty())) { + // Java doesn't allow an item to be renamed; this is why one of the slots could remain empty for Bedrock + // We check this now since setting the inventory slots here messes up shouldRejectItemPlace + return rejectRequest(request, false); + } + + if (sourceSlot == 1) { + // Decrease the item count, but only after both slots are checked. + // Otherwise, the slot 1 check will fail + GeyserItemStack item = inventory.getItem(sourceSlot); + item.setAmount(item.getAmount() - consumeData.getCount()); + if (item.isEmpty()) { + inventory.setItem(sourceSlot, GeyserItemStack.EMPTY, session); + } + + GeyserItemStack itemZero = inventory.getItem(0); + itemZero.setAmount(itemZero.getAmount() - consumeData.getCount()); + if (itemZero.isEmpty()) { + inventory.setItem(0, GeyserItemStack.EMPTY, session); + } + } + affectedSlots.add(sourceSlot); + } + break; + } + case CRAFT_RECIPE_AUTO: // Called by villagers + case CRAFT_NON_IMPLEMENTED_DEPRECATED: // Tends to be called for UI inventories + case CRAFT_RESULTS_DEPRECATED: // Tends to be called for UI inventories + case CRAFT_RECIPE_OPTIONAL: { // Anvils and cartography tables will handle this + break; + } + default: + return rejectRequest(request); + } + } + plan.execute(false); + affectedSlots.addAll(plan.getAffectedSlots()); + return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); + } + + public ItemStackResponsePacket.Response translateCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + int resultSize = 0; + int timesCrafted; + CraftState craftState = CraftState.START; + + int leftover = 0; + ClickPlan plan = new ClickPlan(session, this, inventory); + for (StackRequestActionData action : request.getActions()) { + switch (action.getType()) { + case CRAFT_RECIPE: { + if (craftState != CraftState.START) { + return rejectRequest(request); + } + craftState = CraftState.RECIPE_ID; + break; + } + case CRAFT_RESULTS_DEPRECATED: { + CraftResultsDeprecatedStackRequestActionData deprecatedCraftAction = (CraftResultsDeprecatedStackRequestActionData) action; + if (craftState != CraftState.RECIPE_ID) { + return rejectRequest(request); + } + craftState = CraftState.DEPRECATED; + + if (deprecatedCraftAction.getResultItems().length != 1) { + return rejectRequest(request); + } + resultSize = deprecatedCraftAction.getResultItems()[0].getCount(); + timesCrafted = deprecatedCraftAction.getTimesCrafted(); + if (resultSize <= 0 || timesCrafted <= 0) { + return rejectRequest(request); + } + break; + } + case CONSUME: { + if (craftState != CraftState.DEPRECATED && craftState != CraftState.INGREDIENTS) { + return rejectRequest(request); + } + craftState = CraftState.INGREDIENTS; + break; + } + case TAKE: + case PLACE: { + TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action; + if (craftState != CraftState.INGREDIENTS && craftState != CraftState.TRANSFER) { + return rejectRequest(request); + } + craftState = CraftState.TRANSFER; + + if (transferAction.getSource().getContainer() != ContainerSlotType.CREATIVE_OUTPUT) { + return rejectRequest(request); + } + if (transferAction.getCount() <= 0) { + return rejectRequest(request); + } + + int sourceSlot = bedrockSlotToJava(transferAction.getSource()); + int destSlot = bedrockSlotToJava(transferAction.getDestination()); + + if (isCursor(transferAction.getDestination())) { + plan.add(Click.LEFT, sourceSlot); + craftState = CraftState.DONE; + } else { + if (leftover != 0) { + if (transferAction.getCount() > leftover) { + return rejectRequest(request); + } + if (transferAction.getCount() == leftover) { + plan.add(Click.LEFT, destSlot); + } else { + for (int i = 0; i < transferAction.getCount(); i++) { + plan.add(Click.RIGHT, destSlot); + } + } + leftover -= transferAction.getCount(); + break; + } + + int remainder = transferAction.getCount() % resultSize; + int timesToCraft = transferAction.getCount() / resultSize; + for (int i = 0; i < timesToCraft; i++) { + plan.add(Click.LEFT, sourceSlot); + plan.add(Click.LEFT, destSlot); + } + if (remainder > 0) { + plan.add(Click.LEFT, 0); + for (int i = 0; i < remainder; i++) { + plan.add(Click.RIGHT, destSlot); + } + leftover = resultSize - remainder; + } + } + break; + } + default: + return rejectRequest(request); + } + } + plan.execute(false); + return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots())); + } + + public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + int gridSize; + int gridDimensions; + if (this instanceof PlayerInventoryTranslator) { + gridSize = 4; + gridDimensions = 2; + } else if (this instanceof CraftingInventoryTranslator) { + gridSize = 9; + gridDimensions = 3; + } else { + return rejectRequest(request); + } + + Recipe recipe; + Ingredient[] ingredients = new Ingredient[0]; + ItemStack output = null; + int recipeWidth = 0; + int ingRemaining = 0; + int ingredientIndex = -1; + + Int2IntMap consumedSlots = new Int2IntOpenHashMap(); + int prioritySlot = -1; + int tempSlot; + + int resultSize; + int timesCrafted = 0; + Int2ObjectMap ingredientMap = new Int2ObjectOpenHashMap<>(); + CraftState craftState = CraftState.START; + + ClickPlan plan = new ClickPlan(session, this, inventory); + requestLoop: + for (StackRequestActionData action : request.getActions()) { + switch (action.getType()) { + case CRAFT_RECIPE_AUTO: { + AutoCraftRecipeStackRequestActionData autoCraftAction = (AutoCraftRecipeStackRequestActionData) action; + if (craftState != CraftState.START) { + return rejectRequest(request); + } + craftState = CraftState.RECIPE_ID; + + int recipeId = autoCraftAction.getRecipeNetworkId(); + recipe = session.getCraftingRecipes().get(recipeId); + if (recipe == null) { + return rejectRequest(request); + } + if (!plan.getCursor().isEmpty()) { + return rejectRequest(request); + } + //reject if crafting grid is not clear + for (int i = 1; i <= gridSize; i++) { + if (!inventory.getItem(i).isEmpty()) { + return rejectRequest(request); + } + } + + switch (recipe.getType()) { + case CRAFTING_SHAPED: + ShapedRecipeData shapedData = (ShapedRecipeData) recipe.getData(); + ingredients = shapedData.getIngredients(); + recipeWidth = shapedData.getWidth(); + output = shapedData.getResult(); + if (shapedData.getWidth() > gridDimensions || shapedData.getHeight() > gridDimensions) { + return rejectRequest(request); + } + break; + case CRAFTING_SHAPELESS: + ShapelessRecipeData shapelessData = (ShapelessRecipeData) recipe.getData(); + ingredients = shapelessData.getIngredients(); + recipeWidth = gridDimensions; + output = shapelessData.getResult(); + if (ingredients.length > gridSize) { + return rejectRequest(request); + } + break; + } + break; + } + case CRAFT_RESULTS_DEPRECATED: { + CraftResultsDeprecatedStackRequestActionData deprecatedCraftAction = (CraftResultsDeprecatedStackRequestActionData) action; + if (craftState != CraftState.RECIPE_ID) { + return rejectRequest(request); + } + craftState = CraftState.DEPRECATED; + + if (deprecatedCraftAction.getResultItems().length != 1) { + return rejectRequest(request); + } + resultSize = deprecatedCraftAction.getResultItems()[0].getCount(); + timesCrafted = deprecatedCraftAction.getTimesCrafted(); + if (resultSize <= 0 || timesCrafted <= 0) { + return rejectRequest(request); + } + break; + } + case CONSUME: { + ConsumeStackRequestActionData consumeAction = (ConsumeStackRequestActionData) action; + if (craftState != CraftState.DEPRECATED && craftState != CraftState.INGREDIENTS) { + return rejectRequest(request); + } + craftState = CraftState.INGREDIENTS; + + if (ingRemaining == 0) { + while (++ingredientIndex < ingredients.length) { + if (ingredients[ingredientIndex].getOptions().length != 0) { + ingRemaining = timesCrafted; + break; + } + } + } + + ingRemaining -= consumeAction.getCount(); + if (ingRemaining < 0) + return rejectRequest(request); + + int javaSlot = bedrockSlotToJava(consumeAction.getSource()); + consumedSlots.merge(javaSlot, consumeAction.getCount(), Integer::sum); + + int gridSlot = 1 + ingredientIndex + ((ingredientIndex / recipeWidth) * (gridDimensions - recipeWidth)); + Int2IntMap sources = ingredientMap.computeIfAbsent(gridSlot, k -> new Int2IntOpenHashMap()); + sources.put(javaSlot, consumeAction.getCount()); + break; + } + case TAKE: + case PLACE: { + TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action; + if (craftState != CraftState.INGREDIENTS && craftState != CraftState.TRANSFER) { + return rejectRequest(request); + } + craftState = CraftState.TRANSFER; + + if (transferAction.getSource().getContainer() != ContainerSlotType.CREATIVE_OUTPUT) { + return rejectRequest(request); + } + if (transferAction.getCount() <= 0) { + return rejectRequest(request); + } + + int javaSlot = bedrockSlotToJava(transferAction.getDestination()); + if (isCursor(transferAction.getDestination())) { //TODO + if (timesCrafted > 1) { + tempSlot = findTempSlot(inventory, GeyserItemStack.from(output), true); + if (tempSlot == -1) { + return rejectRequest(request); + } + } + break requestLoop; + } else if (inventory.getItem(javaSlot).getAmount() == consumedSlots.get(javaSlot)) { + prioritySlot = bedrockSlotToJava(transferAction.getDestination()); + break requestLoop; + } + break; + } + default: + return rejectRequest(request); + } + } + + final int maxLoops = Math.min(64, timesCrafted); + for (int loops = 0; loops < maxLoops; loops++) { + boolean done = true; + for (Int2ObjectMap.Entry entry : ingredientMap.int2ObjectEntrySet()) { + Int2IntMap sources = entry.getValue(); + if (sources.isEmpty()) + continue; + + done = false; + int gridSlot = entry.getIntKey(); + if (!plan.getItem(gridSlot).isEmpty()) + continue; + + int sourceSlot; + if (loops == 0 && sources.containsKey(prioritySlot)) { + sourceSlot = prioritySlot; + } else { + sourceSlot = sources.keySet().iterator().nextInt(); + } + int transferAmount = sources.remove(sourceSlot); + transferSlot(plan, sourceSlot, gridSlot, transferAmount); + } + + if (!done) { + //TODO: sometimes the server does not agree on this slot? + plan.add(Click.LEFT_SHIFT, 0, true); + } else { + break; + } + } + + inventory.setItem(0, GeyserItemStack.from(output), session); + plan.execute(true); + return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots())); + } + + /** + * Handled in {@link PlayerInventoryTranslator} + */ + public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + return rejectRequest(request); + } + + private void transferSlot(ClickPlan plan, int sourceSlot, int destSlot, int transferAmount) { + boolean tempSwap = !plan.getCursor().isEmpty(); + int sourceAmount = plan.getItem(sourceSlot).getAmount(); + if (transferAmount == sourceAmount) { //transfer all + plan.add(Click.LEFT, sourceSlot); //pickup source + plan.add(Click.LEFT, destSlot); //let go of all items and done + } else { //transfer some + //try to transfer items with least clicks possible + int halfSource = sourceAmount - (sourceAmount / 2); //larger half + int holding; + if (!tempSwap && transferAmount <= halfSource) { //faster to take only half. CURSOR MUST BE EMPTY + plan.add(Click.RIGHT, sourceSlot); + holding = halfSource; + } else { //need all + plan.add(Click.LEFT, sourceSlot); + holding = sourceAmount; + } + if (!tempSwap && transferAmount > holding / 2) { //faster to release extra items onto source or dest slot? + for (int i = 0; i < holding - transferAmount; i++) { + plan.add(Click.RIGHT, sourceSlot); //prepare cursor + } + plan.add(Click.LEFT, destSlot); //release cursor onto dest slot + } else { + for (int i = 0; i < transferAmount; i++) { + plan.add(Click.RIGHT, destSlot); //right click until transfer goal is met + } + plan.add(Click.LEFT, sourceSlot); //return extra items to source slot + } + } + } + + public static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List containerEntries) { + return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.OK, request.getRequestId(), containerEntries); + } + + /** + * Reject an incorrect ItemStackRequest. + */ + public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) { + return rejectRequest(request, true); + } + + /** + * Reject an incorrect ItemStackRequest. + * + * @param throwError whether this request was truly erroneous (true), or known as an outcome and should not be treated + * as bad (false). + */ + public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) { + if (throwError && GeyserConnector.getInstance().getConfig().isDebugMode()) { + new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace(); + } + return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.ERROR, request.getRequestId(), Collections.emptyList()); + } + + /** + * Print out the contents of an ItemStackRequest, should the net ID check fail. + */ + protected void dumpStackRequestDetails(GeyserSession session, Inventory inventory, StackRequestSlotInfoData source, StackRequestSlotInfoData destination) { + session.getConnector().getLogger().error("Source: " + source.toString() + " Result: " + checkNetId(session, inventory, source)); + session.getConnector().getLogger().error("Destination: " + destination.toString() + " Result: " + checkNetId(session, inventory, destination)); + session.getConnector().getLogger().error("Geyser's record of source slot: " + inventory.getItem(bedrockSlotToJava(source))); + session.getConnector().getLogger().error("Geyser's record of destination slot: " + inventory.getItem(bedrockSlotToJava(destination))); + } + + public boolean checkNetId(GeyserSession session, Inventory inventory, StackRequestSlotInfoData slotInfoData) { + int netId = slotInfoData.getStackNetworkId(); + // "In my testing, sometimes the client thinks the netId of an item in the crafting grid is 1, even though we never said it was. + // I think it only happens when we manually set the grid but that was my quick fix" + if (netId < 0 || netId == 1) + return true; + + GeyserItemStack currentItem = isCursor(slotInfoData) ? session.getPlayerInventory().getCursor() : inventory.getItem(bedrockSlotToJava(slotInfoData)); + return currentItem.getNetId() == netId; + } + + /** + * Try to find a slot that can temporarily store the given item. + * Only looks in the main inventory and hotbar (excluding offhand). + * Only slots that are empty or contain a different type of item are valid. + * + * @return java id for the temporary slot, or -1 if no viable slot was found + */ + //TODO: compatibility for simulated inventory (ClickPlan) + private static int findTempSlot(Inventory inventory, GeyserItemStack item, boolean emptyOnly, int... slotBlacklist) { + int offset = inventory.getId() == 0 ? 1 : 0; //offhand is not a viable temp slot + HashSet itemBlacklist = new HashSet<>(slotBlacklist.length + 1); + itemBlacklist.add(item); + + IntSet potentialSlots = new IntOpenHashSet(36); + for (int i = inventory.getSize() - (36 + offset); i < inventory.getSize() - offset; i++) { + potentialSlots.add(i); + } + for (int i : slotBlacklist) { + potentialSlots.remove(i); + GeyserItemStack blacklistedItem = inventory.getItem(i); + if (!blacklistedItem.isEmpty()) { + itemBlacklist.add(blacklistedItem); + } + } + + for (int i : potentialSlots) { + GeyserItemStack testItem = inventory.getItem(i); + if ((emptyOnly && !testItem.isEmpty())) { + continue; + } + + boolean viable = true; + for (GeyserItemStack blacklistedItem : itemBlacklist) { + if (InventoryUtils.canStack(testItem, blacklistedItem)) { + viable = false; + break; + } + } + if (!viable) { + continue; + } + return i; + } + //could not find a viable temp slot + return -1; + } + + public List makeContainerEntries(GeyserSession session, Inventory inventory, Set affectedSlots) { + Map> containerMap = new HashMap<>(); + for (int slot : affectedSlots) { + BedrockContainerSlot bedrockSlot = javaSlotToBedrockContainer(slot); + List list = containerMap.computeIfAbsent(bedrockSlot.getContainer(), k -> new ArrayList<>()); + list.add(makeItemEntry(bedrockSlot.getSlot(), inventory.getItem(slot))); + } + + List containerEntries = new ArrayList<>(); + for (Map.Entry> entry : containerMap.entrySet()) { + containerEntries.add(new ItemStackResponsePacket.ContainerEntry(entry.getKey(), entry.getValue())); + } + + ItemStackResponsePacket.ItemEntry cursorEntry = makeItemEntry(0, session.getPlayerInventory().getCursor()); + containerEntries.add(new ItemStackResponsePacket.ContainerEntry(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry))); + + return containerEntries; + } + + public static ItemStackResponsePacket.ItemEntry makeItemEntry(int bedrockSlot, GeyserItemStack itemStack) { + ItemStackResponsePacket.ItemEntry itemEntry; + if (!itemStack.isEmpty()) { + // As of 1.16.210: Bedrock needs confirmation on what the current item durability is. + // If 0 is sent, then Bedrock thinks the item is not damaged + int durability = 0; + if (itemStack.getNbt() != null) { + Tag damage = itemStack.getNbt().get("Damage"); + if (damage instanceof IntTag) { + durability = ((IntTag) damage).getValue(); + } + } + + itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) itemStack.getAmount(), itemStack.getNetId(), "", durability); + } else { + itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) 0, 0, "", 0); + } + return itemEntry; + } + + protected static boolean isCursor(StackRequestSlotInfoData slotInfoData) { + return slotInfoData.getContainer() == ContainerSlotType.CURSOR; + } + + protected enum CraftState { + START, + RECIPE_ID, + DEPRECATED, + INGREDIENTS, + TRANSFER, + DONE + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java deleted file mode 100644 index 0ff20772b..000000000 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * 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.inventory; - -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCreativeInventoryActionPacket; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; -import com.nukkitx.protocol.bedrock.data.inventory.InventorySource; -import com.nukkitx.protocol.bedrock.data.inventory.ItemData; -import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; -import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; -import org.geysermc.connector.inventory.Inventory; -import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.action.InventoryActionDataTranslator; -import org.geysermc.connector.network.translators.item.ItemTranslator; -import org.geysermc.connector.utils.InventoryUtils; -import org.geysermc.connector.utils.LanguageUtils; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class PlayerInventoryTranslator extends InventoryTranslator { - private static final ItemData UNUSUABLE_CRAFTING_SPACE_BLOCK = InventoryUtils.createUnusableSpaceBlock(LanguageUtils.getLocaleStringLog("geyser.inventory.unusable_item.creative")); - - public PlayerInventoryTranslator() { - super(46); - } - - @Override - public void updateInventory(GeyserSession session, Inventory inventory) { - updateCraftingGrid(session, inventory); - - InventoryContentPacket inventoryContentPacket = new InventoryContentPacket(); - inventoryContentPacket.setContainerId(ContainerId.INVENTORY); - ItemData[] contents = new ItemData[36]; - // Inventory - for (int i = 9; i < 36; i++) { - contents[i] = ItemTranslator.translateToBedrock(session, inventory.getItem(i)); - } - // Hotbar - for (int i = 36; i < 45; i++) { - contents[i - 36] = ItemTranslator.translateToBedrock(session, inventory.getItem(i)); - } - inventoryContentPacket.setContents(Arrays.asList(contents)); - session.sendUpstreamPacket(inventoryContentPacket); - - // Armor - InventoryContentPacket armorContentPacket = new InventoryContentPacket(); - armorContentPacket.setContainerId(ContainerId.ARMOR); - contents = new ItemData[4]; - for (int i = 5; i < 9; i++) { - contents[i - 5] = ItemTranslator.translateToBedrock(session, inventory.getItem(i)); - } - armorContentPacket.setContents(Arrays.asList(contents)); - session.sendUpstreamPacket(armorContentPacket); - - // Offhand - InventoryContentPacket offhandPacket = new InventoryContentPacket(); - offhandPacket.setContainerId(ContainerId.OFFHAND); - offhandPacket.setContents(Collections.singletonList(ItemTranslator.translateToBedrock(session, inventory.getItem(45)))); - session.sendUpstreamPacket(offhandPacket); - } - - /** - * Update the crafting grid for the player to hide/show the barriers in the creative inventory - * @param session Session of the player - * @param inventory Inventory of the player - */ - public static void updateCraftingGrid(GeyserSession session, Inventory inventory) { - // Crafting grid - for (int i = 1; i < 5; i++) { - InventorySlotPacket slotPacket = new InventorySlotPacket(); - slotPacket.setContainerId(ContainerId.UI); - slotPacket.setSlot(i + 27); - - if (session.getGameMode() == GameMode.CREATIVE) { - slotPacket.setItem(UNUSUABLE_CRAFTING_SPACE_BLOCK); - }else{ - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(i))); - } - - session.sendUpstreamPacket(slotPacket); - } - } - - @Override - public void updateSlot(GeyserSession session, Inventory inventory, int slot) { - if (slot >= 1 && slot <= 44) { - InventorySlotPacket slotPacket = new InventorySlotPacket(); - if (slot >= 9) { - slotPacket.setContainerId(ContainerId.INVENTORY); - if (slot >= 36) { - slotPacket.setSlot(slot - 36); - } else { - slotPacket.setSlot(slot); - } - } else if (slot >= 5) { - slotPacket.setContainerId(ContainerId.ARMOR); - slotPacket.setSlot(slot - 5); - } else { - slotPacket.setContainerId(ContainerId.UI); - slotPacket.setSlot(slot + 27); - } - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(slot))); - session.sendUpstreamPacket(slotPacket); - } else if (slot == 45) { - InventoryContentPacket offhandPacket = new InventoryContentPacket(); - offhandPacket.setContainerId(ContainerId.OFFHAND); - offhandPacket.setContents(Collections.singletonList(ItemTranslator.translateToBedrock(session, inventory.getItem(slot)))); - session.sendUpstreamPacket(offhandPacket); - } - } - - @Override - public int bedrockSlotToJava(InventoryActionData action) { - int slotnum = action.getSlot(); - switch (action.getSource().getContainerId()) { - case ContainerId.INVENTORY: - // Inventory - if (slotnum >= 9 && slotnum <= 35) { - return slotnum; - } - // Hotbar - if (slotnum >= 0 && slotnum <= 8) { - return slotnum + 36; - } - break; - case ContainerId.ARMOR: - if (slotnum >= 0 && slotnum <= 3) { - return slotnum + 5; - } - break; - case ContainerId.OFFHAND: - return 45; - case ContainerId.UI: - if (slotnum >= 28 && 31 >= slotnum) { - return slotnum - 27; - } - break; - case ContainerId.CRAFTING_RESULT: - return 0; - } - return slotnum; - } - - @Override - public int javaSlotToBedrock(int slot) { - return slot; - } - - @Override - public SlotType getSlotType(int javaSlot) { - if (javaSlot == 0) - return SlotType.OUTPUT; - return SlotType.NORMAL; - } - - @Override - public void translateActions(GeyserSession session, Inventory inventory, List actions) { - if (session.getGameMode() == GameMode.CREATIVE) { - //crafting grid is not visible in creative mode in java edition - for (InventoryActionData action : actions) { - if (action.getSource().getContainerId() == ContainerId.UI && (action.getSlot() >= 28 && 31 >= action.getSlot())) { - updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - return; - } - } - - ItemStack javaItem; - for (InventoryActionData action : actions) { - switch (action.getSource().getContainerId()) { - case ContainerId.INVENTORY: - case ContainerId.ARMOR: - case ContainerId.OFFHAND: - int javaSlot = bedrockSlotToJava(action); - if (action.getToItem().getId() == 0) { - javaItem = new ItemStack(-1, 0, null); - } else { - javaItem = ItemTranslator.translateToJava(action.getToItem()); - } - ClientCreativeInventoryActionPacket creativePacket = new ClientCreativeInventoryActionPacket(javaSlot, javaItem); - session.sendDownstreamPacket(creativePacket); - inventory.setItem(javaSlot, javaItem); - break; - case ContainerId.UI: - if (action.getSlot() == 0) { - session.getInventory().setCursor(ItemTranslator.translateToJava(action.getToItem())); - } - break; - case ContainerId.NONE: - if (action.getSource().getType() == InventorySource.Type.WORLD_INTERACTION - && action.getSource().getFlag() == InventorySource.Flag.DROP_ITEM) { - javaItem = ItemTranslator.translateToJava(action.getToItem()); - ClientCreativeInventoryActionPacket creativeDropPacket = new ClientCreativeInventoryActionPacket(-1, javaItem); - session.sendDownstreamPacket(creativeDropPacket); - } - break; - } - } - return; - } - - InventoryActionDataTranslator.translate(this, session, inventory, actions); - } - - @Override - public void prepareInventory(GeyserSession session, Inventory inventory) { - } - - @Override - public void openInventory(GeyserSession session, Inventory inventory) { - } - - @Override - public void closeInventory(GeyserSession session, Inventory inventory) { - } - - @Override - public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/ClickPlan.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/ClickPlan.java deleted file mode 100644 index c72954bf3..000000000 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/ClickPlan.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.inventory.action; - -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.game.window.WindowAction; -import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientConfirmTransactionPacket; -import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientWindowActionPacket; -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.InventoryTranslator; -import org.geysermc.connector.network.translators.inventory.SlotType; -import org.geysermc.connector.utils.InventoryUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; - -class ClickPlan { - private final List plan = new ArrayList<>(); - - public void add(Click click, int slot) { - plan.add(new ClickAction(click, slot)); - } - - public void execute(GeyserSession session, InventoryTranslator translator, Inventory inventory, boolean refresh) { - PlayerInventory playerInventory = session.getInventory(); - ListIterator planIter = plan.listIterator(); - while (planIter.hasNext()) { - final ClickAction action = planIter.next(); - final ItemStack cursorItem = playerInventory.getCursor(); - final ItemStack clickedItem = inventory.getItem(action.slot); - final short actionId = (short) inventory.getTransactionId().getAndIncrement(); - - //TODO: stop relying on refreshing the inventory for crafting to work properly - if (translator.getSlotType(action.slot) != SlotType.NORMAL) - refresh = true; - - ClientWindowActionPacket clickPacket = new ClientWindowActionPacket(inventory.getId(), - actionId, action.slot, !planIter.hasNext() && refresh ? InventoryUtils.REFRESH_ITEM : clickedItem, - WindowAction.CLICK_ITEM, action.click.actionParam); - - if (translator.getSlotType(action.slot) == SlotType.OUTPUT) { - if (cursorItem == null && clickedItem != null) { - playerInventory.setCursor(clickedItem); - } else if (InventoryUtils.canStack(cursorItem, clickedItem)) { - playerInventory.setCursor(new ItemStack(cursorItem.getId(), - cursorItem.getAmount() + clickedItem.getAmount(), cursorItem.getNbt())); - } - } else { - switch (action.click) { - case LEFT: - if (!InventoryUtils.canStack(cursorItem, clickedItem)) { - playerInventory.setCursor(clickedItem); - inventory.setItem(action.slot, cursorItem); - } else { - playerInventory.setCursor(null); - inventory.setItem(action.slot, new ItemStack(clickedItem.getId(), - clickedItem.getAmount() + cursorItem.getAmount(), clickedItem.getNbt())); - } - break; - case RIGHT: - if (cursorItem == null && clickedItem != null) { - ItemStack halfItem = new ItemStack(clickedItem.getId(), - clickedItem.getAmount() / 2, clickedItem.getNbt()); - inventory.setItem(action.slot, halfItem); - playerInventory.setCursor(new ItemStack(clickedItem.getId(), - clickedItem.getAmount() - halfItem.getAmount(), clickedItem.getNbt())); - } else if (cursorItem != null && clickedItem == null) { - playerInventory.setCursor(new ItemStack(cursorItem.getId(), - cursorItem.getAmount() - 1, cursorItem.getNbt())); - inventory.setItem(action.slot, new ItemStack(cursorItem.getId(), - 1, cursorItem.getNbt())); - } else if (InventoryUtils.canStack(cursorItem, clickedItem)) { - playerInventory.setCursor(new ItemStack(cursorItem.getId(), - cursorItem.getAmount() - 1, cursorItem.getNbt())); - inventory.setItem(action.slot, new ItemStack(clickedItem.getId(), - clickedItem.getAmount() + 1, clickedItem.getNbt())); - } - break; - } - } - session.sendDownstreamPacket(clickPacket); - session.sendDownstreamPacket(new ClientConfirmTransactionPacket(inventory.getId(), actionId, true)); - } - - /*if (refresh) { - translator.updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - }*/ - } - - private static class ClickAction { - final Click click; - final int slot; - ClickAction(Click click, int slot) { - this.click = click; - this.slot = slot; - } - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/InventoryActionDataTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/InventoryActionDataTranslator.java deleted file mode 100644 index c313e3669..000000000 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/InventoryActionDataTranslator.java +++ /dev/null @@ -1,338 +0,0 @@ -/* - * 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.inventory.action; - -import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; -import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; -import com.github.steveice10.mc.protocol.data.game.window.*; -import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace; -import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket; -import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientWindowActionPacket; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; -import com.nukkitx.protocol.bedrock.data.inventory.InventorySource; -import com.nukkitx.protocol.bedrock.data.inventory.ItemData; -import org.geysermc.connector.inventory.Inventory; -import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.InventoryTranslator; -import org.geysermc.connector.network.translators.inventory.SlotType; -import org.geysermc.connector.network.translators.item.ItemTranslator; -import org.geysermc.connector.utils.InventoryUtils; - -import java.util.*; - -public class InventoryActionDataTranslator { - public static void translate(InventoryTranslator translator, GeyserSession session, Inventory inventory, List actions) { - if (actions.size() != 2) - return; - - InventoryActionData worldAction = null; - InventoryActionData cursorAction = null; - InventoryActionData containerAction = null; - boolean refresh = false; - for (InventoryActionData action : actions) { - if (action.getSource().getContainerId() == ContainerId.CRAFTING_USE_INGREDIENT) { - return; - } else if (action.getSource().getType() == InventorySource.Type.WORLD_INTERACTION) { - worldAction = action; - } else if (action.getSource().getContainerId() == ContainerId.UI && action.getSlot() == 0) { - cursorAction = action; - ItemData translatedCursor = ItemTranslator.translateToBedrock(session, session.getInventory().getCursor()); - if (!translatedCursor.equals(action.getFromItem())) { - refresh = true; - } - } else { - containerAction = action; - ItemData translatedItem = ItemTranslator.translateToBedrock(session, inventory.getItem(translator.bedrockSlotToJava(action))); - if (!translatedItem.equals(action.getFromItem())) { - refresh = true; - } - } - } - - final int craftSlot = session.getCraftSlot(); - session.setCraftSlot(0); - - if (worldAction != null) { - InventoryActionData sourceAction; - if (cursorAction != null) { - sourceAction = cursorAction; - } else { - sourceAction = containerAction; - } - - if (sourceAction != null) { - if (worldAction.getSource().getFlag() == InventorySource.Flag.DROP_ITEM) { - //quick dropping from hotbar? - if (session.getInventoryCache().getOpenInventory() == null && sourceAction.getSource().getContainerId() == ContainerId.INVENTORY) { - int heldSlot = session.getInventory().getHeldItemSlot(); - if (sourceAction.getSlot() == heldSlot) { - ClientPlayerActionPacket actionPacket = new ClientPlayerActionPacket( - sourceAction.getToItem().getCount() == 0 ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM, - new Position(0, 0, 0), BlockFace.DOWN); - session.sendDownstreamPacket(actionPacket); - ItemStack item = session.getInventory().getItem(heldSlot); - if (item != null) { - session.getInventory().setItem(heldSlot, new ItemStack(item.getId(), item.getAmount() - 1, item.getNbt())); - } - return; - } - } - int dropAmount = sourceAction.getFromItem().getCount() - sourceAction.getToItem().getCount(); - if (sourceAction != cursorAction) { //dropping directly from inventory - int javaSlot = translator.bedrockSlotToJava(sourceAction); - if (dropAmount == sourceAction.getFromItem().getCount()) { - ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(), - inventory.getTransactionId().getAndIncrement(), - javaSlot, null, WindowAction.DROP_ITEM, - DropItemParam.DROP_SELECTED_STACK); - session.sendDownstreamPacket(dropPacket); - } else { - for (int i = 0; i < dropAmount; i++) { - ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(), - inventory.getTransactionId().getAndIncrement(), - javaSlot, null, WindowAction.DROP_ITEM, - DropItemParam.DROP_FROM_SELECTED); - session.sendDownstreamPacket(dropPacket); - } - } - ItemStack item = inventory.getItem(javaSlot); - if (item != null) { - inventory.setItem(javaSlot, new ItemStack(item.getId(), item.getAmount() - dropAmount, item.getNbt())); - } - return; - } else { //clicking outside of inventory - ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(), - -999, null, WindowAction.CLICK_ITEM, - dropAmount > 1 ? ClickItemParam.LEFT_CLICK : ClickItemParam.RIGHT_CLICK); - session.sendDownstreamPacket(dropPacket); - ItemStack cursor = session.getInventory().getCursor(); - if (cursor != null) { - session.getInventory().setCursor(new ItemStack(cursor.getId(), dropAmount > 1 ? 0 : cursor.getAmount() - 1, cursor.getNbt())); - } - return; - } - } - } - } else if (cursorAction != null && containerAction != null) { - //left/right click - ClickPlan plan = new ClickPlan(); - int javaSlot = translator.bedrockSlotToJava(containerAction); - if (cursorAction.getFromItem().equals(containerAction.getToItem()) - && containerAction.getFromItem().equals(cursorAction.getToItem()) - && !InventoryUtils.canStack(cursorAction.getFromItem(), containerAction.getFromItem())) { //simple swap - plan.add(Click.LEFT, javaSlot); - } else if (cursorAction.getFromItem().getCount() > cursorAction.getToItem().getCount()) { //release - if (cursorAction.getToItem().getCount() == 0) { - plan.add(Click.LEFT, javaSlot); - } else { - int difference = cursorAction.getFromItem().getCount() - cursorAction.getToItem().getCount(); - for (int i = 0; i < difference; i++) { - plan.add(Click.RIGHT, javaSlot); - } - } - } else { //pickup - if (cursorAction.getFromItem().getCount() == 0) { - if (containerAction.getToItem().getCount() == 0) { //pickup all - plan.add(Click.LEFT, javaSlot); - } else { //pickup some - if (translator.getSlotType(javaSlot) == SlotType.FURNACE_OUTPUT - || containerAction.getToItem().getCount() == containerAction.getFromItem().getCount() / 2) { //right click - plan.add(Click.RIGHT, javaSlot); - } else { - plan.add(Click.LEFT, javaSlot); - int difference = containerAction.getFromItem().getCount() - cursorAction.getToItem().getCount(); - for (int i = 0; i < difference; i++) { - plan.add(Click.RIGHT, javaSlot); - } - } - } - } else { //pickup into non-empty cursor - if (translator.getSlotType(javaSlot) == SlotType.FURNACE_OUTPUT) { - if (containerAction.getToItem().getCount() == 0) { - plan.add(Click.LEFT, javaSlot); - } else { - ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(), - inventory.getTransactionId().getAndIncrement(), - javaSlot, InventoryUtils.REFRESH_ITEM, WindowAction.SHIFT_CLICK_ITEM, - ShiftClickItemParam.LEFT_CLICK); - session.sendDownstreamPacket(shiftClickPacket); - translator.updateInventory(session, inventory); - return; - } - } else if (translator.getSlotType(javaSlot) == SlotType.OUTPUT) { - plan.add(Click.LEFT, javaSlot); - } else { - int cursorSlot = findTempSlot(inventory, session.getInventory().getCursor(), Collections.singletonList(javaSlot), false); - if (cursorSlot != -1) { - plan.add(Click.LEFT, cursorSlot); - } else { - translator.updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - return; - } - plan.add(Click.LEFT, javaSlot); - int difference = cursorAction.getToItem().getCount() - cursorAction.getFromItem().getCount(); - for (int i = 0; i < difference; i++) { - plan.add(Click.RIGHT, cursorSlot); - } - plan.add(Click.LEFT, javaSlot); - plan.add(Click.LEFT, cursorSlot); - } - } - } - plan.execute(session, translator, inventory, refresh); - return; - } else { - ClickPlan plan = new ClickPlan(); - InventoryActionData fromAction; - InventoryActionData toAction; - if (actions.get(0).getFromItem().getCount() >= actions.get(0).getToItem().getCount()) { - fromAction = actions.get(0); - toAction = actions.get(1); - } else { - fromAction = actions.get(1); - toAction = actions.get(0); - } - int fromSlot = translator.bedrockSlotToJava(fromAction); - int toSlot = translator.bedrockSlotToJava(toAction); - - if (translator.getSlotType(fromSlot) == SlotType.OUTPUT) { - if ((craftSlot != 0 && craftSlot != -2) && (inventory.getItem(toSlot) == null - || InventoryUtils.canStack(session.getInventory().getCursor(), inventory.getItem(toSlot)))) { - if (fromAction.getToItem().getCount() == 0) { - refresh = true; - plan.add(Click.LEFT, toSlot); - if (craftSlot != -1) { - plan.add(Click.LEFT, craftSlot); - } - } else { - int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount(); - for (int i = 0; i < difference; i++) { - plan.add(Click.RIGHT, toSlot); - } - session.setCraftSlot(craftSlot); - } - plan.execute(session, translator, inventory, refresh); - return; - } else { - session.setCraftSlot(-2); - } - } - - int cursorSlot = -1; - if (session.getInventory().getCursor() != null) { //move cursor contents to a temporary slot - cursorSlot = findTempSlot(inventory, - session.getInventory().getCursor(), - Arrays.asList(fromSlot, toSlot), - translator.getSlotType(fromSlot) == SlotType.OUTPUT); - if (cursorSlot != -1) { - plan.add(Click.LEFT, cursorSlot); - } else { - translator.updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - return; - } - } - if ((fromAction.getFromItem().equals(toAction.getToItem()) && !InventoryUtils.canStack(fromAction.getFromItem(), toAction.getFromItem())) - || fromAction.getToItem().getId() == 0) { //slot swap - plan.add(Click.LEFT, fromSlot); - plan.add(Click.LEFT, toSlot); - if (fromAction.getToItem().getId() != 0) { - plan.add(Click.LEFT, fromSlot); - } - } else if (InventoryUtils.canStack(fromAction.getFromItem(), toAction.getToItem())) { //partial item move - if (translator.getSlotType(fromSlot) == SlotType.FURNACE_OUTPUT) { - ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(), - inventory.getTransactionId().getAndIncrement(), - fromSlot, InventoryUtils.REFRESH_ITEM, WindowAction.SHIFT_CLICK_ITEM, - ShiftClickItemParam.LEFT_CLICK); - session.sendDownstreamPacket(shiftClickPacket); - translator.updateInventory(session, inventory); - return; - } else if (translator.getSlotType(fromSlot) == SlotType.OUTPUT) { - session.setCraftSlot(cursorSlot); - plan.add(Click.LEFT, fromSlot); - int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount(); - for (int i = 0; i < difference; i++) { - plan.add(Click.RIGHT, toSlot); - } - //client will send additional packets later to finish transferring crafting output - //translator will know how to handle this using the craftSlot variable - } else { - plan.add(Click.LEFT, fromSlot); - int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount(); - for (int i = 0; i < difference; i++) { - plan.add(Click.RIGHT, toSlot); - } - plan.add(Click.LEFT, fromSlot); - } - } - if (cursorSlot != -1) { - plan.add(Click.LEFT, cursorSlot); - } - plan.execute(session, translator, inventory, refresh); - return; - } - - translator.updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - } - - private static int findTempSlot(Inventory inventory, ItemStack item, List slotBlacklist, boolean emptyOnly) { - /*try and find a slot that can temporarily store the given item - only look in the main inventory and hotbar - only slots that are empty or contain a different type of item are valid*/ - int offset = inventory.getId() == 0 ? 1 : 0; //offhand is not a viable slot (some servers disable it) - List itemBlacklist = new ArrayList<>(slotBlacklist.size() + 1); - itemBlacklist.add(item); - for (int slot : slotBlacklist) { - ItemStack blacklistItem = inventory.getItem(slot); - if (blacklistItem != null) - itemBlacklist.add(blacklistItem); - } - for (int i = inventory.getSize() - (36 + offset); i < inventory.getSize() - offset; i++) { - ItemStack testItem = inventory.getItem(i); - boolean acceptable = true; - if (testItem != null) { - if (emptyOnly) { - continue; - } - for (ItemStack blacklistItem : itemBlacklist) { - if (InventoryUtils.canStack(testItem, blacklistItem)) { - acceptable = false; - break; - } - } - } - if (acceptable && !slotBlacklist.contains(i)) - return i; - } - //could not find a viable temp slot - return -1; - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/click/Click.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/click/Click.java new file mode 100644 index 000000000..d3666a9e9 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/click/Click.java @@ -0,0 +1,45 @@ +/* + * 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.inventory.click; + +import com.github.steveice10.mc.protocol.data.game.window.*; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum Click { + LEFT(WindowAction.CLICK_ITEM, ClickItemParam.LEFT_CLICK), + RIGHT(WindowAction.CLICK_ITEM, ClickItemParam.RIGHT_CLICK), + LEFT_SHIFT(WindowAction.SHIFT_CLICK_ITEM, ShiftClickItemParam.LEFT_CLICK), + DROP_ONE(WindowAction.DROP_ITEM, DropItemParam.DROP_FROM_SELECTED), + DROP_ALL(WindowAction.DROP_ITEM, DropItemParam.DROP_SELECTED_STACK), + LEFT_OUTSIDE(WindowAction.CLICK_ITEM, ClickItemParam.LEFT_CLICK), + RIGHT_OUTSIDE(WindowAction.CLICK_ITEM, ClickItemParam.RIGHT_CLICK); + + public static final int OUTSIDE_SLOT = -999; + + public final WindowAction windowAction; + public final WindowActionParam actionParam; +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/click/ClickPlan.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/click/ClickPlan.java new file mode 100644 index 000000000..c750baf51 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/click/ClickPlan.java @@ -0,0 +1,294 @@ +/* + * 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.inventory.click; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.window.WindowAction; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientConfirmTransactionPacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientWindowActionPacket; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import lombok.Value; +import org.geysermc.connector.inventory.GeyserItemStack; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; +import org.geysermc.connector.network.translators.inventory.SlotType; +import org.geysermc.connector.network.translators.inventory.translators.CraftingInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.PlayerInventoryTranslator; +import org.geysermc.connector.utils.InventoryUtils; + +import java.util.*; + +public class ClickPlan { + private final List plan = new ArrayList<>(); + private final Int2ObjectMap simulatedItems; + private GeyserItemStack simulatedCursor; + private boolean simulating; + + private final GeyserSession session; + private final InventoryTranslator translator; + private final Inventory inventory; + private final int gridSize; + + public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventory inventory) { + this.session = session; + this.translator = translator; + this.inventory = inventory; + + this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize()); + this.simulatedCursor = session.getPlayerInventory().getCursor().copy(); + this.simulating = true; + + if (translator instanceof PlayerInventoryTranslator) { + gridSize = 4; + } else if (translator instanceof CraftingInventoryTranslator) { + gridSize = 9; + } else { + gridSize = -1; + } + } + + private void resetSimulation() { + this.simulatedItems.clear(); + this.simulatedCursor = session.getPlayerInventory().getCursor().copy(); + } + + public void add(Click click, int slot) { + add(click, slot, false); + } + + public void add(Click click, int slot, boolean force) { + if (!simulating) + throw new UnsupportedOperationException("ClickPlan already executed"); + + if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) { + slot = Click.OUTSIDE_SLOT; + } + + ClickAction action = new ClickAction(click, slot, force); + plan.add(action); + simulateAction(action); + } + + public void execute(boolean refresh) { + //update geyser inventory after simulation to avoid net id desync + resetSimulation(); + ListIterator planIter = plan.listIterator(); + while (planIter.hasNext()) { + ClickAction action = planIter.next(); + + if (action.slot != Click.OUTSIDE_SLOT && translator.getSlotType(action.slot) != SlotType.NORMAL) { + refresh = true; + } + + ItemStack clickedItemStack; + if (!planIter.hasNext() && refresh) { + clickedItemStack = InventoryUtils.REFRESH_ITEM; + } else if (action.click.windowAction == WindowAction.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) { + clickedItemStack = null; + } else { + clickedItemStack = getItem(action.slot).getItemStack(); + } + + short actionId = inventory.getNextTransactionId(); + ClientWindowActionPacket clickPacket = new ClientWindowActionPacket( + inventory.getId(), + actionId, + action.slot, + clickedItemStack, + action.click.windowAction, + action.click.actionParam + ); + + simulateAction(action); + + session.sendDownstreamPacket(clickPacket); + if (clickedItemStack == InventoryUtils.REFRESH_ITEM || action.force) { + session.sendDownstreamPacket(new ClientConfirmTransactionPacket(inventory.getId(), actionId, true)); + } + } + + session.getPlayerInventory().setCursor(simulatedCursor, session); + for (Int2ObjectMap.Entry simulatedSlot : simulatedItems.int2ObjectEntrySet()) { + inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session); + } + simulating = false; + } + + public GeyserItemStack getItem(int slot) { + return getItem(slot, true); + } + + public GeyserItemStack getItem(int slot, boolean generate) { + if (generate) { + return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy()); + } else { + return simulatedItems.getOrDefault(slot, inventory.getItem(slot)); + } + } + + public GeyserItemStack getCursor() { + return simulatedCursor; + } + + private void setItem(int slot, GeyserItemStack item) { + if (simulating) { + simulatedItems.put(slot, item); + } else { + inventory.setItem(slot, item, session); + } + } + + private void setCursor(GeyserItemStack item) { + if (simulating) { + simulatedCursor = item; + } else { + session.getPlayerInventory().setCursor(item, session); + } + } + + private void simulateAction(ClickAction action) { + GeyserItemStack cursor = simulating ? getCursor() : session.getPlayerInventory().getCursor(); + switch (action.click) { + case LEFT_OUTSIDE: + setCursor(GeyserItemStack.EMPTY); + return; + case RIGHT_OUTSIDE: + if (!cursor.isEmpty()) { + cursor.sub(1); + } + return; + } + + GeyserItemStack clicked = simulating ? getItem(action.slot) : inventory.getItem(action.slot); + if (translator.getSlotType(action.slot) == SlotType.OUTPUT) { + switch (action.click) { + case LEFT: + case RIGHT: + if (cursor.isEmpty() && !clicked.isEmpty()) { + setCursor(clicked.copy()); + } else if (InventoryUtils.canStack(cursor, clicked)) { + cursor.add(clicked.getAmount()); + } + reduceCraftingGrid(false); + break; + case LEFT_SHIFT: + reduceCraftingGrid(true); + break; + } + } else { + switch (action.click) { + case LEFT: + if (!InventoryUtils.canStack(cursor, clicked)) { + setCursor(clicked); + setItem(action.slot, cursor); + } else { + setCursor(GeyserItemStack.EMPTY); + clicked.add(cursor.getAmount()); + } + break; + case RIGHT: + if (cursor.isEmpty() && !clicked.isEmpty()) { + int half = clicked.getAmount() / 2; //smaller half + setCursor(clicked.copy(clicked.getAmount() - half)); //larger half + clicked.setAmount(half); + } else if (!cursor.isEmpty() && clicked.isEmpty()) { + cursor.sub(1); + setItem(action.slot, cursor.copy(1)); + } else if (InventoryUtils.canStack(cursor, clicked)) { + cursor.sub(1); + clicked.add(1); + } + break; + case LEFT_SHIFT: + //TODO + break; + case DROP_ONE: + if (!clicked.isEmpty()) { + clicked.sub(1); + } + break; + case DROP_ALL: + setItem(action.slot, GeyserItemStack.EMPTY); + break; + } + } + } + + //TODO + private void reduceCraftingGrid(boolean makeAll) { + if (gridSize == -1) + return; + + int crafted; + if (!makeAll) { + crafted = 1; + } else { + crafted = 0; + for (int i = 0; i < gridSize; i++) { + GeyserItemStack item = getItem(i + 1); + if (!item.isEmpty()) { + if (crafted == 0) { + crafted = item.getAmount(); + } + crafted = Math.min(crafted, item.getAmount()); + } + } + } + + for (int i = 0; i < gridSize; i++) { + GeyserItemStack item = getItem(i + 1); + if (!item.isEmpty()) + item.sub(crafted); + } + } + + /** + * @return a new set of all affected slots. This isn't a constant variable; it's newly generated each time it is run. + */ + public IntSet getAffectedSlots() { + IntSet affectedSlots = new IntOpenHashSet(); + for (ClickAction action : plan) { + if (translator.getSlotType(action.slot) == SlotType.NORMAL && action.slot != Click.OUTSIDE_SLOT) { + affectedSlots.add(action.slot); + } + } + return affectedSlots; + } + + @Value + private static class ClickAction { + Click click; + /** + * Java slot + */ + int slot; + boolean force; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java index 6b47cf704..b7f67879b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java @@ -26,35 +26,99 @@ package org.geysermc.connector.network.translators.inventory.holder; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.google.common.collect.ImmutableSet; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.nbt.NbtMap; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; +import com.nukkitx.protocol.bedrock.packet.ContainerClosePacket; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; -import lombok.AllArgsConstructor; +import org.geysermc.connector.inventory.Container; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; import org.geysermc.connector.network.translators.world.block.BlockTranslator; -@AllArgsConstructor +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Manages the fake block we implement for each inventory, should we need to. + * This class will attempt to use a real block first, if possible. + */ public class BlockInventoryHolder extends InventoryHolder { - private final int blockId; + /** + * The default Java block ID to translate as a fake block + */ + private final int defaultJavaBlockState; private final ContainerType containerType; + private final Set validBlocks; + + public BlockInventoryHolder(String javaBlockIdentifier, ContainerType containerType, String... validBlocks) { + this.defaultJavaBlockState = BlockTranslator.getJavaBlockState(javaBlockIdentifier); + this.containerType = containerType; + if (validBlocks != null) { + Set validBlocksTemp = new HashSet<>(validBlocks.length + 1); + Collections.addAll(validBlocksTemp, validBlocks); + validBlocksTemp.add(javaBlockIdentifier.split("\\[")[0]); + this.validBlocks = ImmutableSet.copyOf(validBlocksTemp); + } else { + this.validBlocks = Collections.singleton(javaBlockIdentifier.split("\\[")[0]); + } + } @Override public void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + // Check to see if there is an existing block we can use that the player just selected. + // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range. + // (This could be a virtual inventory that the player is opening) + if (checkInteractionPosition(session)) { + // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid + int javaBlockId = session.getConnector().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); + String[] javaBlockString = BlockTranslator.getJavaIdBlockMap().inverse().getOrDefault(javaBlockId, "minecraft:air").split("\\["); + if (isValidBlock(javaBlockString)) { + // We can safely use this block + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); + setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId); + return; + } + } + + // Otherwise, time to conjure up a fake block! Vector3i position = session.getPlayerEntity().getPosition().toInt(); position = position.add(Vector3i.UP); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(position); - blockPacket.setRuntimeId(blockId); + blockPacket.setRuntimeId(session.getBlockTranslator().getBedrockBlockId(defaultJavaBlockState)); blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); inventory.setHolderPosition(position); + setCustomName(session, position, inventory, defaultJavaBlockState); + } + + /** + * Will be overwritten in the beacon inventory translator to remove the check, since virtual inventories can't exist. + * + * @return if the player's last interaction position and current position match. Used to ensure that we don't select + * a block to hold the inventory that's wildly out of range. + */ + protected boolean checkInteractionPosition(GeyserSession session) { + return session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition()); + } + + /** + * @return true if this Java block ID can be used for player inventory. + */ + protected boolean isValidBlock(String[] javaBlockString) { + return this.validBlocks.contains(javaBlockString[0]); + } + + protected void setCustomName(GeyserSession session, Vector3i position, Inventory inventory, int javaBlockState) { NbtMap tag = NbtMap.builder() .putInt("x", position.getX()) .putInt("y", position.getY()) @@ -78,13 +142,24 @@ public class BlockInventoryHolder extends InventoryHolder { @Override public void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + if (((Container) inventory).isUsingRealBlock()) { + // No need to reset a block since we didn't change any blocks + // But send a container close packet because we aren't destroying the original. + ContainerClosePacket packet = new ContainerClosePacket(); + packet.setId((byte) inventory.getId()); + packet.setUnknownBool0(true); //TODO needs to be changed in Protocol to "server-side" or something + session.sendUpstreamPacket(packet); + return; + } + Vector3i holderPos = inventory.getHolderPosition(); Position pos = new Position(holderPos.getX(), holderPos.getY(), holderPos.getZ()); int realBlock = session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(holderPos); - blockPacket.setRuntimeId(BlockTranslator.getBedrockBlockId(realBlock)); + blockPacket.setRuntimeId(session.getBlockTranslator().getBedrockBlockId(realBlock)); + blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/MerchantInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AbstractBlockInventoryTranslator.java similarity index 50% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/MerchantInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AbstractBlockInventoryTranslator.java index aa36a8a81..49caef13b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/MerchantInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AbstractBlockInventoryTranslator.java @@ -23,84 +23,60 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater; +import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder; +import org.geysermc.connector.network.translators.inventory.holder.InventoryHolder; import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; -import java.util.List; - -public class MerchantInventoryTranslator extends BaseInventoryTranslator { - +/** + * Provided as a base for any inventory that requires a block for opening it + */ +public abstract class AbstractBlockInventoryTranslator extends BaseInventoryTranslator { + private final InventoryHolder holder; private final InventoryUpdater updater; - public MerchantInventoryTranslator() { - super(3); - this.updater = new CursorInventoryUpdater(); + /** + * @param size the amount of slots that the inventory adds alongside the base inventory slots + * @param javaBlockIdentifier a Java block identifier that is used as a temporary block + * @param containerType the container type of this inventory + * @param updater updater + * @param additionalValidBlocks any other block identifiers that can safely use this inventory without a fake block + */ + public AbstractBlockInventoryTranslator(int size, String javaBlockIdentifier, ContainerType containerType, InventoryUpdater updater, + String... additionalValidBlocks) { + super(size); + this.holder = new BlockInventoryHolder(javaBlockIdentifier, containerType, additionalValidBlocks); + this.updater = updater; } - @Override - public int javaSlotToBedrock(int slot) { - switch (slot) { - case 0: - return 4; - case 1: - return 5; - case 2: - return 50; - } - return super.javaSlotToBedrock(slot); - } - - @Override - public int bedrockSlotToJava(InventoryActionData action) { - switch (action.getSource().getContainerId()) { - case ContainerId.UI: - switch (action.getSlot()) { - case 4: - return 0; - case 5: - return 1; - case 50: - return 2; - } - break; - case -28: // Trading 1? - return 0; - case -29: // Trading 2? - return 1; - case -30: // Trading Output? - return 2; - } - return super.bedrockSlotToJava(action); - } - - @Override - public SlotType getSlotType(int javaSlot) { - if (javaSlot == 2) { - return SlotType.OUTPUT; - } - return SlotType.NORMAL; + /** + * @param size the amount of slots that the inventory adds alongside the base inventory slots + * @param holder the custom block holder + * @param updater updater + */ + public AbstractBlockInventoryTranslator(int size, InventoryHolder holder, InventoryUpdater updater) { + super(size); + this.holder = holder; + this.updater = updater; } @Override public void prepareInventory(GeyserSession session, Inventory inventory) { - + holder.prepareInventory(this, session, inventory); } @Override public void openInventory(GeyserSession session, Inventory inventory) { - + holder.openInventory(this, session, inventory); } @Override public void closeInventory(GeyserSession session, Inventory inventory) { - session.setLastInteractedVillagerEid(-1); - session.setVillagerTrades(null); + holder.closeInventory(this, session, inventory); } @Override @@ -112,13 +88,4 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator { public void updateSlot(GeyserSession session, Inventory inventory, int slot) { updater.updateSlot(this, session, inventory, slot); } - - @Override - public void translateActions(GeyserSession session, Inventory inventory, List actions) { - if (actions.stream().anyMatch(a -> a.getSource().getContainerId() == -31)) { - return; - } - - super.translateActions(session, inventory, actions); - } } 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 new file mode 100644 index 000000000..38a0935e6 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java @@ -0,0 +1,144 @@ +/* + * 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.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.*; +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.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, + "minecraft:chipped_anvil", "minecraft:damaged_anvil"); + } + + /* 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, ItemStackRequest 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) { + switch (slotInfoData.getContainer()) { + case ANVIL_INPUT: + return 0; + case ANVIL_MATERIAL: + return 1; + case ANVIL_RESULT: + case CREATIVE_OUTPUT: + return 2; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + switch (slot) { + case 0: + return new BedrockContainerSlot(ContainerSlotType.ANVIL_INPUT, 1); + case 1: + return new BedrockContainerSlot(ContainerSlotType.ANVIL_MATERIAL, 2); + case 2: + return new BedrockContainerSlot(ContainerSlotType.ANVIL_RESULT, 50); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int javaSlotToBedrock(int slot) { + switch (slot) { + case 0: + return 1; + case 1: + return 2; + case 2: + return 50; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new AnvilContainer(name, windowId, this.size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BaseInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BaseInventoryTranslator.java similarity index 53% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/BaseInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BaseInventoryTranslator.java index ca241e299..5b3be5b27 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BaseInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BaseInventoryTranslator.java @@ -23,18 +23,21 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import org.geysermc.connector.inventory.Container; 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.action.InventoryActionDataTranslator; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; +import org.geysermc.connector.network.translators.inventory.SlotType; -import java.util.List; - -public abstract class BaseInventoryTranslator extends InventoryTranslator{ - BaseInventoryTranslator(int size) { +public abstract class BaseInventoryTranslator extends InventoryTranslator { + public BaseInventoryTranslator(int size) { super(size); } @@ -44,15 +47,18 @@ public abstract class BaseInventoryTranslator extends InventoryTranslator{ } @Override - public int bedrockSlotToJava(InventoryActionData action) { - int slotnum = action.getSlot(); - if (action.getSource().getContainerId() == ContainerId.INVENTORY) { - //hotbar - if (slotnum >= 9) { - return slotnum + this.size - 9; - } else { - return slotnum + this.size + 27; - } + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + int slotnum = slotInfoData.getSlot(); + switch (slotInfoData.getContainer()) { + case HOTBAR_AND_INVENTORY: + case HOTBAR: + case INVENTORY: + //hotbar + if (slotnum >= 9) { + return slotnum + this.size - 9; + } else { + return slotnum + this.size + 27; + } } return slotnum; } @@ -70,13 +76,26 @@ public abstract class BaseInventoryTranslator extends InventoryTranslator{ return slot; } + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot >= this.size) { + final int tmp = slot - this.size; + if (tmp < 27) { + return new BedrockContainerSlot(ContainerSlotType.INVENTORY, tmp + 9); + } else { + return new BedrockContainerSlot(ContainerSlotType.HOTBAR, tmp - 27); + } + } + throw new IllegalArgumentException("Unknown bedrock slot"); + } + @Override public SlotType getSlotType(int javaSlot) { return SlotType.NORMAL; } @Override - public void translateActions(GeyserSession session, Inventory inventory, List actions) { - InventoryActionDataTranslator.translate(this, session, inventory, actions); + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new Container(name, windowId, this.size, windowType, playerInventory); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BeaconInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BeaconInventoryTranslator.java new file mode 100644 index 000000000..5af921f2d --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BeaconInventoryTranslator.java @@ -0,0 +1,159 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientSetBeaconEffectPacket; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.BeaconPaymentStackRequestActionData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType; +import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; +import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; +import org.geysermc.connector.inventory.BeaconContainer; +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.InventoryTranslator; +import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder; +import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; +import org.geysermc.connector.utils.InventoryUtils; + +import java.util.Collections; + +public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator { + public BeaconInventoryTranslator() { + super(1, new BlockInventoryHolder("minecraft:beacon", ContainerType.BEACON) { + @Override + public void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + if (!session.getConnector().getConfig().isCacheChunks()) { + // Beacons cannot work without knowing their physical location + return; + } + super.prepareInventory(translator, session, inventory); + } + + @Override + protected boolean checkInteractionPosition(GeyserSession session) { + // Since we can't fall back to a virtual inventory, let's make opening one easier + return true; + } + + @Override + public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + if (!session.getConnector().getConfig().isCacheChunks() || !((BeaconContainer) inventory).isUsingRealBlock()) { + InventoryUtils.closeInventory(session, inventory.getId(), false); + return; + } + super.openInventory(translator, session, inventory); + } + }, UIInventoryUpdater.INSTANCE); + } + + @Override + public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { + //FIXME?: Beacon graphics look weird after inputting an item. This might be a Bedrock bug, since it resets to nothing + // on BDS + BeaconContainer beaconContainer = (BeaconContainer) inventory; + switch (key) { + case 0: + // Power - beacon doesn't use this, and uses the block position instead + break; + case 1: + beaconContainer.setPrimaryId(value == -1 ? 0 : value); + break; + case 2: + beaconContainer.setSecondaryId(value == -1 ? 0 : value); + break; + } + + // Send a block entity data packet update to the fake beacon inventory + Vector3i position = inventory.getHolderPosition(); + NbtMapBuilder builder = NbtMap.builder() + .putInt("x", position.getX()) + .putInt("y", position.getY()) + .putInt("z", position.getZ()) + .putString("CustomName", inventory.getTitle()) + .putString("id", "Beacon") + .putInt("primary", beaconContainer.getPrimaryId()) + .putInt("secondary", beaconContainer.getSecondaryId()); + + BlockEntityDataPacket packet = new BlockEntityDataPacket(); + packet.setBlockPosition(position); + packet.setData(builder.build()); + session.sendUpstreamPacket(packet); + } + + @Override + public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { + return action.getType() == StackRequestActionType.BEACON_PAYMENT; + } + + @Override + public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + // Input a beacon payment + BeaconPaymentStackRequestActionData beaconPayment = (BeaconPaymentStackRequestActionData) request.getActions()[0]; + ClientSetBeaconEffectPacket packet = new ClientSetBeaconEffectPacket(beaconPayment.getPrimaryEffect(), beaconPayment.getSecondaryEffect()); + session.sendDownstreamPacket(packet); + return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet())); + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + if (slotInfoData.getContainer() == ContainerSlotType.BEACON_PAYMENT) { + return 0; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.BEACON_PAYMENT, 27); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int javaSlotToBedrock(int slot) { + if (slot == 0) { + return 27; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new BeaconContainer(name, windowId, this.size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BrewingInventoryTranslator.java similarity index 71% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BrewingInventoryTranslator.java index 2242a979d..c54722849 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BrewingInventoryTranslator.java @@ -23,18 +23,20 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; import com.nukkitx.protocol.bedrock.packet.ContainerSetDataPacket; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater; -public class BrewingInventoryTranslator extends BlockInventoryTranslator { +public class BrewingInventoryTranslator extends AbstractBlockInventoryTranslator { public BrewingInventoryTranslator() { - super(5, "minecraft:brewing_stand[has_bottle_0=false,has_bottle_1=false,has_bottle_2=false]", ContainerType.BREWING_STAND, new ContainerInventoryUpdater()); + super(5, "minecraft:brewing_stand[has_bottle_0=false,has_bottle_1=false,has_bottle_2=false]", ContainerType.BREWING_STAND, ContainerInventoryUpdater.INSTANCE); } @Override @@ -66,20 +68,16 @@ public class BrewingInventoryTranslator extends BlockInventoryTranslator { } @Override - public int bedrockSlotToJava(InventoryActionData action) { - final int slot = super.bedrockSlotToJava(action); - switch (slot) { - case 0: - return 3; - case 1: - return 0; - case 2: - return 1; - case 3: - return 2; - default: - return slot; + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + if (slotInfoData.getContainer() == ContainerSlotType.BREWING_INPUT) { + // Ingredient + return 3; } + if (slotInfoData.getContainer() == ContainerSlotType.BREWING_RESULT) { + // Potions + return slotInfoData.getSlot() - 1; + } + return super.bedrockSlotToJava(slotInfoData); } @Override @@ -93,7 +91,24 @@ public class BrewingInventoryTranslator extends BlockInventoryTranslator { return 3; case 3: return 0; + case 4: + return 4; } return super.javaSlotToBedrock(slot); } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + switch (slot) { + case 0: + case 1: + case 2: + return new BedrockContainerSlot(ContainerSlotType.BREWING_RESULT, javaSlotToBedrock(slot)); + case 3: + return new BedrockContainerSlot(ContainerSlotType.BREWING_INPUT, 0); + case 4: + return new BedrockContainerSlot(ContainerSlotType.BREWING_FUEL, 4); + } + return super.javaSlotToBedrockContainer(slot); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CartographyInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CartographyInventoryTranslator.java new file mode 100644 index 000000000..a3b50dace --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CartographyInventoryTranslator.java @@ -0,0 +1,104 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import org.geysermc.connector.inventory.CartographyContainer; +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; + +public class CartographyInventoryTranslator extends AbstractBlockInventoryTranslator { + public CartographyInventoryTranslator() { + super(3, "minecraft:cartography_table", ContainerType.CARTOGRAPHY, UIInventoryUpdater.INSTANCE); + } + + @Override + public boolean shouldRejectItemPlace(GeyserSession session, Inventory inventory, ContainerSlotType bedrockSourceContainer, + int javaSourceSlot, ContainerSlotType bedrockDestinationContainer, int javaDestinationSlot) { + if (javaDestinationSlot == 0) { + // Bedrock Edition can use paper or an empty map in slot 0 + GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot); + return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:paper") || itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:map"); + } else if (javaDestinationSlot == 1) { + // Bedrock Edition can use a compass to create locator maps, or use a filled map, in the ADDITIONAL slot + GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot); + return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:compass") || itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:filled_map"); + } + return false; + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + switch (slotInfoData.getContainer()) { + case CARTOGRAPHY_INPUT: + return 0; + case CARTOGRAPHY_ADDITIONAL: + return 1; + case CARTOGRAPHY_RESULT: + case CREATIVE_OUTPUT: + return 2; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + switch (slot) { + case 0: + return new BedrockContainerSlot(ContainerSlotType.CARTOGRAPHY_INPUT, 12); + case 1: + return new BedrockContainerSlot(ContainerSlotType.CARTOGRAPHY_ADDITIONAL, 13); + case 2: + return new BedrockContainerSlot(ContainerSlotType.CARTOGRAPHY_RESULT, 50); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int javaSlotToBedrock(int slot) { + switch (slot) { + case 0: + return 12; + case 1: + return 13; + case 2: + return 50; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new CartographyContainer(name, windowId, this.size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CraftingInventoryTranslator.java similarity index 52% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CraftingInventoryTranslator.java index 18cbbae75..363c9b702 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CraftingInventoryTranslator.java @@ -23,39 +23,50 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators; -import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; -import com.nukkitx.protocol.bedrock.data.inventory.InventorySource; -import org.geysermc.connector.inventory.Inventory; -import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater; -import org.geysermc.connector.utils.InventoryUtils; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.SlotType; +import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; -import java.util.List; - -public class CraftingInventoryTranslator extends BlockInventoryTranslator { +public class CraftingInventoryTranslator extends AbstractBlockInventoryTranslator { public CraftingInventoryTranslator() { - super(10, "minecraft:crafting_table", ContainerType.WORKBENCH, new CursorInventoryUpdater()); + super(10, "minecraft:crafting_table", ContainerType.WORKBENCH, UIInventoryUpdater.INSTANCE); } @Override - public int bedrockSlotToJava(InventoryActionData action) { - if (action.getSlot() == 50) { - // Slot 50 is used for crafting with a controller. + public SlotType getSlotType(int javaSlot) { + if (javaSlot == 0) { + return SlotType.OUTPUT; + } + return SlotType.NORMAL; + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot >= 1 && slot <= 9) { + return new BedrockContainerSlot(ContainerSlotType.CRAFTING_INPUT, slot + 31); + } + if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.CRAFTING_OUTPUT, 0); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + if (slotInfoData.getContainer() == ContainerSlotType.CRAFTING_INPUT) { + // Java goes from 1 - 9, left to right then up to down + // Bedrock is the same, but it starts from 32. + return slotInfoData.getSlot() - 31; + } + if (slotInfoData.getContainer() == ContainerSlotType.CRAFTING_OUTPUT || slotInfoData.getContainer() == ContainerSlotType.CREATIVE_OUTPUT) { return 0; } - - if (action.getSource().getContainerId() == ContainerId.UI) { - int slotnum = action.getSlot(); - if (slotnum >= 32 && 42 >= slotnum) { - return slotnum - 31; - } - } - return super.bedrockSlotToJava(action); + return super.bedrockSlotToJava(slotInfoData); } @Override @@ -65,25 +76,4 @@ public class CraftingInventoryTranslator extends BlockInventoryTranslator { } return super.javaSlotToBedrock(slot); } - - @Override - public SlotType getSlotType(int javaSlot) { - if (javaSlot == 0) - return SlotType.OUTPUT; - return SlotType.NORMAL; - } - - @Override - public void translateActions(GeyserSession session, Inventory inventory, List actions) { - if (session.getGameMode() == GameMode.CREATIVE) { - for (InventoryActionData action : actions) { - if (action.getSource().getType() == InventorySource.Type.CREATIVE) { - updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - return; - } - } - } - super.translateActions(session, inventory, actions); - } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/EnchantingInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/EnchantingInventoryTranslator.java new file mode 100644 index 000000000..03f8bb104 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/EnchantingInventoryTranslator.java @@ -0,0 +1,213 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket; +import com.nukkitx.protocol.bedrock.data.inventory.*; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.CraftRecipeStackRequestActionData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType; +import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; +import com.nukkitx.protocol.bedrock.packet.PlayerEnchantOptionsPacket; +import org.geysermc.connector.inventory.EnchantingContainer; +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.Enchantment; + +import java.util.Arrays; +import java.util.Collections; + +public class EnchantingInventoryTranslator extends AbstractBlockInventoryTranslator { + public EnchantingInventoryTranslator() { + super(2, "minecraft:enchanting_table", ContainerType.ENCHANTMENT, UIInventoryUpdater.INSTANCE); + } + + @Override + public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { + int slotToUpdate; + EnchantingContainer enchantingInventory = (EnchantingContainer) inventory; + boolean shouldUpdate = false; + switch (key) { + case 0: + case 1: + case 2: + // Experience required + slotToUpdate = key; + enchantingInventory.getGeyserEnchantOptions()[slotToUpdate].setXpCost(value); + break; + case 4: + case 5: + case 6: + // Enchantment type + slotToUpdate = key - 4; + int index = value; + if (index != -1) { + Enchantment enchantment = Enchantment.getByJavaIdentifier("minecraft:" + JavaEnchantment.values()[index].name().toLowerCase()); + if (enchantment != null) { + // Convert the Java enchantment index to Bedrock's + index = enchantment.ordinal(); + } else { + index = -1; + } + } + enchantingInventory.getGeyserEnchantOptions()[slotToUpdate].setJavaEnchantIndex(value); + enchantingInventory.getGeyserEnchantOptions()[slotToUpdate].setBedrockEnchantIndex(index); + break; + case 7: + case 8: + case 9: + // Enchantment level + slotToUpdate = key - 7; + enchantingInventory.getGeyserEnchantOptions()[slotToUpdate].setEnchantLevel(value); + shouldUpdate = true; // Java sends each property as its own packet, so let's only update after all properties have been sent + break; + default: + return; + } + if (shouldUpdate) { + enchantingInventory.getEnchantOptions()[slotToUpdate] = enchantingInventory.getGeyserEnchantOptions()[slotToUpdate].build(session); + PlayerEnchantOptionsPacket packet = new PlayerEnchantOptionsPacket(); + packet.getOptions().addAll(Arrays.asList(enchantingInventory.getEnchantOptions())); + session.sendUpstreamPacket(packet); + } + } + + @Override + public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { + return action.getType() == StackRequestActionType.CRAFT_RECIPE; + } + + @Override + public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + // Client has requested an item to be enchanted + CraftRecipeStackRequestActionData craftRecipeData = (CraftRecipeStackRequestActionData) request.getActions()[0]; + EnchantingContainer enchantingInventory = (EnchantingContainer) inventory; + int javaSlot = -1; + for (int i = 0; i < enchantingInventory.getEnchantOptions().length; i++) { + EnchantOptionData enchantData = enchantingInventory.getEnchantOptions()[i]; + if (enchantData != null) { + if (craftRecipeData.getRecipeNetworkId() == enchantData.getEnchantNetId()) { + // Enchant net ID is how we differentiate between what item Bedrock wants + javaSlot = enchantingInventory.getGeyserEnchantOptions()[i].getJavaIndex(); + break; + } + } + } + if (javaSlot == -1) { + // Slot should be determined as 0, 1, or 2 + return rejectRequest(request); + } + ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), javaSlot); + session.sendDownstreamPacket(packet); + return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet())); + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + if (slotInfoData.getContainer() == ContainerSlotType.ENCHANTING_INPUT) { + return 0; + } + if (slotInfoData.getContainer() == ContainerSlotType.ENCHANTING_LAPIS) { + return 1; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.ENCHANTING_INPUT, 14); + } + if (slot == 1) { + return new BedrockContainerSlot(ContainerSlotType.ENCHANTING_LAPIS, 15); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int javaSlotToBedrock(int slot) { + if (slot == 0) { + return 14; + } + if (slot == 1) { + return 15; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new EnchantingContainer(name, windowId, this.size, windowType, playerInventory); + } + + /** + * Enchantments classified by their Java index + */ + public enum JavaEnchantment { + PROTECTION, + FIRE_PROTECTION, + FEATHER_FALLING, + BLAST_PROTECTION, + PROJECTILE_PROTECTION, + RESPIRATION, + AQUA_AFFINITY, + THORNS, + DEPTH_STRIDER, + FROST_WALKER, + BINDING_CURSE, + SOUL_SPEED, + SHARPNESS, + SMITE, + BANE_OF_ARTHROPODS, + KNOCKBACK, + FIRE_ASPECT, + LOOTING, + SWEEPING, + EFFICIENCY, + SILK_TOUCH, + UNBREAKING, + FORTUNE, + POWER, + PUNCH, + FLAME, + INFINITY, + LUCK_OF_THE_SEA, + LURE, + LOYALTY, + IMPALING, + RIPTIDE, + CHANNELING, + MULTISHOT, + QUICK_CHARGE, + PIERCING, + MENDING, + VANISHING_CURSE + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/Generic3X3InventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/Generic3X3InventoryTranslator.java new file mode 100644 index 000000000..ceac1b2c1 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/Generic3X3InventoryTranslator.java @@ -0,0 +1,71 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; +import org.geysermc.connector.inventory.Generic3X3Container; +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.ContainerInventoryUpdater; + +/** + * Droppers and dispensers + */ +public class Generic3X3InventoryTranslator extends AbstractBlockInventoryTranslator { + public Generic3X3InventoryTranslator() { + super(9, "minecraft:dispenser[facing=north,triggered=false]", ContainerType.DISPENSER, ContainerInventoryUpdater.INSTANCE, + "minecraft:dropper"); + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new Generic3X3Container(name, windowId, this.size, windowType, playerInventory); + } + + @Override + public void openInventory(GeyserSession session, Inventory inventory) { + ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket(); + containerOpenPacket.setId((byte) inventory.getId()); + // Required for opening the real block - otherwise, if the container type is incorrect, it refuses to open + containerOpenPacket.setType(((Generic3X3Container) inventory).isDropper() ? ContainerType.DROPPER : ContainerType.DISPENSER); + containerOpenPacket.setBlockPosition(inventory.getHolderPosition()); + containerOpenPacket.setUniqueEntityId(inventory.getHolderId()); + session.sendUpstreamPacket(containerOpenPacket); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot) { + if (javaSlot < this.size) { + return new BedrockContainerSlot(ContainerSlotType.CONTAINER, javaSlot); + } + return super.javaSlotToBedrockContainer(javaSlot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/GrindstoneInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/GrindstoneInventoryTranslator.java similarity index 55% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/GrindstoneInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/GrindstoneInventoryTranslator.java index 87448ff53..65364e147 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/GrindstoneInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/GrindstoneInventoryTranslator.java @@ -23,34 +23,44 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; -import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater; - -public class GrindstoneInventoryTranslator extends BlockInventoryTranslator { +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; +public class GrindstoneInventoryTranslator extends AbstractBlockInventoryTranslator { public GrindstoneInventoryTranslator() { - super(3, "minecraft:grindstone[face=floor,facing=north]", ContainerType.GRINDSTONE, new CursorInventoryUpdater()); + super(3, "minecraft:grindstone[face=floor,facing=north]", ContainerType.GRINDSTONE, UIInventoryUpdater.INSTANCE); } @Override - public int bedrockSlotToJava(InventoryActionData action) { - final int slot = super.bedrockSlotToJava(action); - if (action.getSource().getContainerId() == ContainerId.UI) { - switch (slot) { - case 16: - return 0; - case 17: - return 1; - case 50: - return 2; - default: - return slot; - } - } return slot; + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + switch (slotInfoData.getContainer()) { + case GRINDSTONE_INPUT: + return 0; + case GRINDSTONE_ADDITIONAL: + return 1; + case GRINDSTONE_RESULT: + case CREATIVE_OUTPUT: + return 2; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + switch (slot) { + case 0: + return new BedrockContainerSlot(ContainerSlotType.GRINDSTONE_INPUT, 16); + case 1: + return new BedrockContainerSlot(ContainerSlotType.GRINDSTONE_ADDITIONAL, 17); + case 2: + return new BedrockContainerSlot(ContainerSlotType.GRINDSTONE_RESULT, 50); + } + return super.javaSlotToBedrockContainer(slot); } @Override @@ -65,5 +75,4 @@ public class GrindstoneInventoryTranslator extends BlockInventoryTranslator { } return super.javaSlotToBedrock(slot); } - } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/HopperInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/HopperInventoryTranslator.java new file mode 100644 index 000000000..7f067d1c0 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/HopperInventoryTranslator.java @@ -0,0 +1,48 @@ +/* + * 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.inventory.translators; + +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater; + +/** + * Implemented on top of any block that does not have special properties implemented + */ +public class HopperInventoryTranslator extends AbstractBlockInventoryTranslator { + public HopperInventoryTranslator() { + super(5, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER, ContainerInventoryUpdater.INSTANCE); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot) { + if (javaSlot < this.size) { + return new BedrockContainerSlot(ContainerSlotType.CONTAINER, javaSlot); + } + return super.javaSlotToBedrockContainer(javaSlot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LecternInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LecternInventoryTranslator.java new file mode 100644 index 000000000..c08dfd995 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LecternInventoryTranslator.java @@ -0,0 +1,170 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCloseWindowPacket; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.nbt.NbtType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import org.geysermc.connector.inventory.GeyserItemStack; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.inventory.LecternContainer; +import org.geysermc.connector.inventory.PlayerInventory; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; +import org.geysermc.connector.utils.BlockEntityUtils; +import org.geysermc.connector.utils.InventoryUtils; + +import java.util.Collections; + +public class LecternInventoryTranslator extends BaseInventoryTranslator { + private final InventoryUpdater updater; + + public LecternInventoryTranslator() { + super(1); + this.updater = new InventoryUpdater(); + } + + @Override + public void prepareInventory(GeyserSession session, Inventory inventory) { + } + + @Override + public void openInventory(GeyserSession session, Inventory inventory) { + } + + @Override + public void closeInventory(GeyserSession session, Inventory inventory) { + } + + @Override + public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { + if (key == 0) { // Lectern page update + LecternContainer lecternContainer = (LecternContainer) inventory; + lecternContainer.setCurrentBedrockPage(value / 2); + lecternContainer.setBlockEntityTag(lecternContainer.getBlockEntityTag().toBuilder().putInt("page", lecternContainer.getCurrentBedrockPage()).build()); + BlockEntityUtils.updateBlockEntity(session, lecternContainer.getBlockEntityTag(), lecternContainer.getPosition()); + } + } + + @Override + public void updateInventory(GeyserSession session, Inventory inventory) { + } + + @Override + public void updateSlot(GeyserSession session, Inventory inventory, int slot) { + this.updater.updateSlot(this, session, inventory, slot); + if (slot == 0) { + LecternContainer lecternContainer = (LecternContainer) inventory; + if (session.isDroppingLecternBook()) { + // We have to enter the inventory GUI to eject the book + ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), 3); + session.sendDownstreamPacket(packet); + session.setDroppingLecternBook(false); + InventoryUtils.closeInventory(session, inventory.getId(), false); + } else if (lecternContainer.getBlockEntityTag() == null) { + // If the method returns true, this is already handled for us + GeyserItemStack geyserItemStack = inventory.getItem(0); + CompoundTag tag = geyserItemStack.getNbt(); + // Position has to be the last interacted position... right? + Vector3i position = session.getLastInteractionBlockPosition(); + // shouldRefresh means that we should boot out the client on our side because their lectern GUI isn't updated yet + boolean shouldRefresh = !session.getConnector().getWorldManager().shouldExpectLecternHandled() && !session.getLecternCache().contains(position); + + NbtMap blockEntityTag; + if (tag != null) { + int pagesSize = ((ListTag) tag.get("pages")).size(); + ItemData itemData = geyserItemStack.getItemData(session); + NbtMapBuilder lecternTag = getBaseLecternTag(position.getX(), position.getY(), position.getZ(), pagesSize); + lecternTag.putCompound("book", NbtMap.builder() + .putByte("Count", (byte) itemData.getCount()) + .putShort("Damage", (short) 0) + .putString("Name", "minecraft:written_book") + .putCompound("tag", itemData.getTag()) + .build()); + lecternTag.putInt("page", lecternContainer.getCurrentBedrockPage()); + blockEntityTag = lecternTag.build(); + } else { + // There is *a* book here, but... no NBT. + NbtMapBuilder lecternTag = getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 1); + NbtMapBuilder bookTag = NbtMap.builder() + .putByte("Count", (byte) 1) + .putShort("Damage", (short) 0) + .putString("Name", "minecraft:writable_book") + .putCompound("tag", NbtMap.builder().putList("pages", NbtType.COMPOUND, Collections.singletonList( + NbtMap.builder() + .putString("photoname", "") + .putString("text", "") + .build() + )).build()); + + blockEntityTag = lecternTag.putCompound("book", bookTag.build()).build(); + } + + // Even with serverside access to lecterns, we don't easily know which lectern this is, so we need to rebuild + // the block entity tag + lecternContainer.setBlockEntityTag(blockEntityTag); + lecternContainer.setPosition(position); + if (shouldRefresh) { + // Update the lectern because it's not updated client-side + BlockEntityUtils.updateBlockEntity(session, blockEntityTag, position); + session.getLecternCache().add(position); + // Close the window - we will reopen it once the client has this data synced + ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(lecternContainer.getId()); + session.sendDownstreamPacket(closeWindowPacket); + InventoryUtils.closeInventory(session, inventory.getId(), false); + } + } + } + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new LecternContainer(name, windowId, this.size, windowType, playerInventory); + } + + public static NbtMapBuilder getBaseLecternTag(int x, int y, int z, int totalPages) { + NbtMapBuilder builder = NbtMap.builder() + .putInt("x", x) + .putInt("y", y) + .putInt("z", z) + .putString("id", "Lectern"); + if (totalPages != 0) { + builder.putByte("hasBook", (byte) 1); + builder.putInt("totalPages", totalPages); + } else { + // Not usually needed, but helps with kicking out Bedrock players from reading the UI + builder.putByte("hasBook", (byte) 0); + } + return builder; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LoomInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LoomInventoryTranslator.java new file mode 100644 index 000000000..17c93c15b --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LoomInventoryTranslator.java @@ -0,0 +1,231 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest; +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.ItemStackResponsePacket; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import org.geysermc.connector.inventory.GeyserItemStack; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.SlotType; +import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; +import org.geysermc.connector.network.translators.item.translators.BannerTranslator; + +import java.util.Collections; +import java.util.List; + +public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator { + /** + * A map of Bedrock patterns to Java index. Used to request for a specific banner pattern. + */ + private static final Object2IntMap PATTERN_TO_INDEX = new Object2IntOpenHashMap<>(); + + static { + // Added from left-to-right then up-to-down in the order Java presents it + int index = 1; + PATTERN_TO_INDEX.put("bl", index++); + PATTERN_TO_INDEX.put("br", index++); + PATTERN_TO_INDEX.put("tl", index++); + PATTERN_TO_INDEX.put("tr", index++); + PATTERN_TO_INDEX.put("bs", index++); + PATTERN_TO_INDEX.put("ts", index++); + PATTERN_TO_INDEX.put("ls", index++); + PATTERN_TO_INDEX.put("rs", index++); + PATTERN_TO_INDEX.put("cs", index++); + PATTERN_TO_INDEX.put("ms", index++); + PATTERN_TO_INDEX.put("drs", index++); + PATTERN_TO_INDEX.put("dls", index++); + PATTERN_TO_INDEX.put("ss", index++); + PATTERN_TO_INDEX.put("cr", index++); + PATTERN_TO_INDEX.put("sc", index++); + PATTERN_TO_INDEX.put("bt", index++); + PATTERN_TO_INDEX.put("tt", index++); + PATTERN_TO_INDEX.put("bts", index++); + PATTERN_TO_INDEX.put("tts", index++); + PATTERN_TO_INDEX.put("ld", index++); + PATTERN_TO_INDEX.put("rd", index++); + PATTERN_TO_INDEX.put("lud", index++); + PATTERN_TO_INDEX.put("rud", index++); + PATTERN_TO_INDEX.put("mc", index++); + PATTERN_TO_INDEX.put("mr", index++); + PATTERN_TO_INDEX.put("vh", index++); + PATTERN_TO_INDEX.put("hh", index++); + PATTERN_TO_INDEX.put("vhr", index++); + PATTERN_TO_INDEX.put("hhb", index++); + PATTERN_TO_INDEX.put("bo", index++); + index++; // Bordure indented, does not appear to exist in Bedrock? + PATTERN_TO_INDEX.put("gra", index++); + PATTERN_TO_INDEX.put("gru", index); + // Bricks do not appear to be a pattern on Bedrock, either + } + + public LoomInventoryTranslator() { + super(4, "minecraft:loom[facing=north]", ContainerType.LOOM, UIInventoryUpdater.INSTANCE); + } + + @Override + public boolean shouldRejectItemPlace(GeyserSession session, Inventory inventory, ContainerSlotType bedrockSourceContainer, + int javaSourceSlot, ContainerSlotType bedrockDestinationContainer, int javaDestinationSlot) { + if (javaDestinationSlot != 1) { + return false; + } + GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot); + if (itemStack.isEmpty()) { + return false; + } + + // Reject the item if Bedrock is attempting to put in a dye that is not a dye in Java Edition + return !itemStack.getItemEntry().getJavaIdentifier().endsWith("_dye"); + } + + @Override + public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { + // If the LOOM_MATERIAL slot is not empty, we are crafting a pattern that does not come from an item + return action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED && inventory.getItem(2).isEmpty(); + } + + @Override + public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + // TODO: I anticipate this will be changed in the future to use something non-deprecated. Keep an eye out. + StackRequestActionData data = request.getActions()[1]; + if (!(data instanceof CraftResultsDeprecatedStackRequestActionData)) { + return rejectRequest(request); + } + CraftResultsDeprecatedStackRequestActionData craftData = (CraftResultsDeprecatedStackRequestActionData) data; + + // Get the patterns compound tag + List newBlockEntityTag = craftData.getResultItems()[0].getTag().getList("Patterns", NbtType.COMPOUND); + // Get the pattern that the Bedrock client requests - the last pattern in the Patterns list + NbtMap pattern = newBlockEntityTag.get(newBlockEntityTag.size() - 1); + // Get the Java index of this pattern + int index = PATTERN_TO_INDEX.getOrDefault(pattern.getString("Pattern"), -1); + if (index == -1) { + return rejectRequest(request); + } + // Java's formula: 4 * row + col + // And the Java loom window has a fixed row/width of four + // So... Number / 4 = row (so we don't have to bother there), and number % 4 is our column, which leads us back to our index. :) + ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), index); + session.sendDownstreamPacket(packet); + + GeyserItemStack inputCopy = inventory.getItem(0).copy(1); + inputCopy.setNetId(session.getNextItemNetId()); + // Add the pattern manually, for better item synchronization + if (inputCopy.getNbt() == null) { + inputCopy.setNbt(new CompoundTag("")); + } + CompoundTag blockEntityTag = inputCopy.getNbt().get("BlockEntityTag"); + CompoundTag javaBannerPattern = BannerTranslator.getJavaBannerPattern(pattern); + + if (blockEntityTag != null) { + ListTag patternsList = blockEntityTag.get("Patterns"); + if (patternsList != null) { + patternsList.add(javaBannerPattern); + } else { + patternsList = new ListTag("Patterns", Collections.singletonList(javaBannerPattern)); + blockEntityTag.put(patternsList); + } + } else { + blockEntityTag = new CompoundTag("BlockEntityTag"); + ListTag patternsList = new ListTag("Patterns", Collections.singletonList(javaBannerPattern)); + blockEntityTag.put(patternsList); + inputCopy.getNbt().put(blockEntityTag); + } + + // Set the new item as the output + inventory.setItem(3, inputCopy, session); + + return translateRequest(session, inventory, request); + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + switch (slotInfoData.getContainer()) { + case LOOM_INPUT: + return 0; + case LOOM_DYE: + return 1; + case LOOM_MATERIAL: + return 2; + case LOOM_RESULT: + case CREATIVE_OUTPUT: + return 3; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + switch (slot) { + case 0: + return new BedrockContainerSlot(ContainerSlotType.LOOM_INPUT, 9); + case 1: + return new BedrockContainerSlot(ContainerSlotType.LOOM_DYE, 10); + case 2: + return new BedrockContainerSlot(ContainerSlotType.LOOM_MATERIAL, 11); + case 3: + return new BedrockContainerSlot(ContainerSlotType.LOOM_RESULT, 50); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int javaSlotToBedrock(int slot) { + switch (slot) { + case 0: + return 9; + case 1: + return 10; + case 2: + return 11; + case 3: + return 50; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public SlotType getSlotType(int javaSlot) { + if (javaSlot == 3) { + return SlotType.OUTPUT; + } + return super.getSlotType(javaSlot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/MerchantInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/MerchantInventoryTranslator.java new file mode 100644 index 000000000..736568868 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/MerchantInventoryTranslator.java @@ -0,0 +1,165 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap; +import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; +import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket; +import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.type.EntityType; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.inventory.MerchantContainer; +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.SlotType; +import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; +import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; + +public class MerchantInventoryTranslator extends BaseInventoryTranslator { + private final InventoryUpdater updater; + + public MerchantInventoryTranslator() { + super(3); + this.updater = UIInventoryUpdater.INSTANCE; + } + + @Override + public int javaSlotToBedrock(int slot) { + switch (slot) { + case 0: + return 4; + case 1: + return 5; + case 2: + return 50; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + switch (slot) { + case 0: + return new BedrockContainerSlot(ContainerSlotType.TRADE2_INGREDIENT1, 4); + case 1: + return new BedrockContainerSlot(ContainerSlotType.TRADE2_INGREDIENT2, 5); + case 2: + return new BedrockContainerSlot(ContainerSlotType.TRADE2_RESULT, 50); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + switch (slotInfoData.getContainer()) { + case TRADE2_INGREDIENT1: + return 0; + case TRADE2_INGREDIENT2: + return 1; + case TRADE2_RESULT: + case CREATIVE_OUTPUT: + return 2; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public SlotType getSlotType(int javaSlot) { + if (javaSlot == 2) { + return SlotType.OUTPUT; + } + return SlotType.NORMAL; + } + + @Override + public void prepareInventory(GeyserSession session, Inventory inventory) { + MerchantContainer merchantInventory = (MerchantContainer) inventory; + if (merchantInventory.getVillager() == null) { + long geyserId = session.getEntityCache().getNextEntityId().incrementAndGet(); + Vector3f pos = session.getPlayerEntity().getPosition().sub(0, 3, 0); + + EntityDataMap metadata = new EntityDataMap(); + metadata.put(EntityData.SCALE, 0f); + metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0f); + metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0f); + + Entity villager = new Entity(0, geyserId, EntityType.VILLAGER, pos, Vector3f.ZERO, Vector3f.ZERO); + villager.setMetadata(metadata); + villager.spawnEntity(session); + + SetEntityLinkPacket linkPacket = new SetEntityLinkPacket(); + EntityLinkData.Type type = EntityLinkData.Type.PASSENGER; + linkPacket.setEntityLink(new EntityLinkData(session.getPlayerEntity().getGeyserId(), geyserId, type, true, false)); + session.sendUpstreamPacket(linkPacket); + + merchantInventory.setVillager(villager); + } + } + + @Override + public void openInventory(GeyserSession session, Inventory inventory) { + //Handled in JavaTradeListTranslator + //TODO: send a blank inventory here in case the villager doesn't send a TradeList packet + } + + @Override + public void closeInventory(GeyserSession session, Inventory inventory) { + MerchantContainer merchantInventory = (MerchantContainer) inventory; + if (merchantInventory.getVillager() != null) { + merchantInventory.getVillager().despawnEntity(session); + } + } + + @Override + public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + // We're not crafting here + // Called at least by consoles when pressing a trade option button + return translateRequest(session, inventory, request); + } + + @Override + public void updateInventory(GeyserSession session, Inventory inventory) { + updater.updateInventory(this, session, inventory); + } + + @Override + public void updateSlot(GeyserSession session, Inventory inventory, int slot) { + updater.updateSlot(this, session, inventory, slot); + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new MerchantContainer(name, windowId, this.size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/PlayerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/PlayerInventoryTranslator.java new file mode 100644 index 000000000..e3dbec507 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/PlayerInventoryTranslator.java @@ -0,0 +1,490 @@ +/* + * 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.inventory.translators; + +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.data.game.window.WindowType; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCreativeInventoryActionPacket; +import com.nukkitx.protocol.bedrock.data.inventory.*; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.*; +import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; +import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; +import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +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.InventoryTranslator; +import org.geysermc.connector.network.translators.inventory.SlotType; +import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.network.translators.item.ItemTranslator; +import org.geysermc.connector.utils.InventoryUtils; +import org.geysermc.connector.utils.LanguageUtils; + +import java.util.Arrays; +import java.util.Collections; + +public class PlayerInventoryTranslator extends InventoryTranslator { + private static final ItemData UNUSUABLE_CRAFTING_SPACE_BLOCK = InventoryUtils.createUnusableSpaceBlock(LanguageUtils.getLocaleStringLog("geyser.inventory.unusable_item.creative")); + + public PlayerInventoryTranslator() { + super(46); + } + + @Override + public void updateInventory(GeyserSession session, Inventory inventory) { + updateCraftingGrid(session, inventory); + + InventoryContentPacket inventoryContentPacket = new InventoryContentPacket(); + inventoryContentPacket.setContainerId(ContainerId.INVENTORY); + ItemData[] contents = new ItemData[36]; + // Inventory + for (int i = 9; i < 36; i++) { + contents[i] = inventory.getItem(i).getItemData(session); + } + // Hotbar + for (int i = 36; i < 45; i++) { + contents[i - 36] = inventory.getItem(i).getItemData(session); + } + inventoryContentPacket.setContents(Arrays.asList(contents)); + session.sendUpstreamPacket(inventoryContentPacket); + + // Armor + InventoryContentPacket armorContentPacket = new InventoryContentPacket(); + armorContentPacket.setContainerId(ContainerId.ARMOR); + contents = new ItemData[4]; + for (int i = 5; i < 9; i++) { + contents[i - 5] = inventory.getItem(i).getItemData(session); + } + armorContentPacket.setContents(Arrays.asList(contents)); + session.sendUpstreamPacket(armorContentPacket); + + // Offhand + InventoryContentPacket offhandPacket = new InventoryContentPacket(); + offhandPacket.setContainerId(ContainerId.OFFHAND); + offhandPacket.setContents(Collections.singletonList(inventory.getItem(45).getItemData(session))); + session.sendUpstreamPacket(offhandPacket); + } + + /** + * Update the crafting grid for the player to hide/show the barriers in the creative inventory + * @param session Session of the player + * @param inventory Inventory of the player + */ + public static void updateCraftingGrid(GeyserSession session, Inventory inventory) { + // Crafting grid + for (int i = 1; i < 5; i++) { + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(ContainerId.UI); + slotPacket.setSlot(i + 27); + + if (session.getGameMode() == GameMode.CREATIVE) { + slotPacket.setItem(UNUSUABLE_CRAFTING_SPACE_BLOCK); + } else { + slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(i).getItemStack())); + } + + session.sendUpstreamPacket(slotPacket); + } + } + + @Override + public void updateSlot(GeyserSession session, Inventory inventory, int slot) { + if (slot >= 1 && slot <= 44) { + InventorySlotPacket slotPacket = new InventorySlotPacket(); + if (slot >= 9) { + slotPacket.setContainerId(ContainerId.INVENTORY); + if (slot >= 36) { + slotPacket.setSlot(slot - 36); + } else { + slotPacket.setSlot(slot); + } + } else if (slot >= 5) { + slotPacket.setContainerId(ContainerId.ARMOR); + slotPacket.setSlot(slot - 5); + } else { + slotPacket.setContainerId(ContainerId.UI); + slotPacket.setSlot(slot + 27); + } + slotPacket.setItem(inventory.getItem(slot).getItemData(session)); + session.sendUpstreamPacket(slotPacket); + } else if (slot == 45) { + InventoryContentPacket offhandPacket = new InventoryContentPacket(); + offhandPacket.setContainerId(ContainerId.OFFHAND); + offhandPacket.setContents(Collections.singletonList(inventory.getItem(slot).getItemData(session))); + session.sendUpstreamPacket(offhandPacket); + } + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + int slotnum = slotInfoData.getSlot(); + switch (slotInfoData.getContainer()) { + case HOTBAR_AND_INVENTORY: + case HOTBAR: + case INVENTORY: + // Inventory + if (slotnum >= 9 && slotnum <= 35) { + return slotnum; + } + // Hotbar + if (slotnum >= 0 && slotnum <= 8) { + return slotnum + 36; + } + break; + case ARMOR: + if (slotnum >= 0 && slotnum <= 3) { + return slotnum + 5; + } + break; + case OFFHAND: + return 45; + case CRAFTING_INPUT: + if (slotnum >= 28 && 31 >= slotnum) { + return slotnum - 27; + } + break; + case CREATIVE_OUTPUT: + return 0; + } + return slotnum; + } + + @Override + public int javaSlotToBedrock(int slot) { + return -1; + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot >= 36 && slot <= 44) { + return new BedrockContainerSlot(ContainerSlotType.HOTBAR, slot - 36); + } else if (slot >= 9 && slot <= 35) { + return new BedrockContainerSlot(ContainerSlotType.INVENTORY, slot); + } else if (slot >= 5 && slot <= 8) { + return new BedrockContainerSlot(ContainerSlotType.ARMOR, slot - 5); + } else if (slot == 45) { + return new BedrockContainerSlot(ContainerSlotType.OFFHAND, 1); + } else if (slot >= 1 && slot <= 4) { + return new BedrockContainerSlot(ContainerSlotType.CRAFTING_INPUT, slot + 27); + } else if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.CRAFTING_OUTPUT, 0); + } else { + throw new IllegalArgumentException("Unknown bedrock slot"); + } + } + + @Override + public SlotType getSlotType(int javaSlot) { + if (javaSlot == 0) + return SlotType.OUTPUT; + return SlotType.NORMAL; + } + + @Override + public ItemStackResponsePacket.Response translateRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + if (session.getGameMode() != GameMode.CREATIVE) { + return super.translateRequest(session, inventory, request); + } + + PlayerInventory playerInv = session.getPlayerInventory(); + IntSet affectedSlots = new IntOpenHashSet(); + for (StackRequestActionData action : request.getActions()) { + switch (action.getType()) { + case TAKE: + case PLACE: { + TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action; + if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) { + return rejectRequest(request); + } + if (isCraftingGrid(transferAction.getSource()) || isCraftingGrid(transferAction.getDestination())) { + return rejectRequest(request, false); + } + + int transferAmount = transferAction.getCount(); + if (isCursor(transferAction.getDestination())) { + int sourceSlot = bedrockSlotToJava(transferAction.getSource()); + GeyserItemStack sourceItem = inventory.getItem(sourceSlot); + if (playerInv.getCursor().isEmpty()) { + playerInv.setCursor(sourceItem.copy(0), session); + } + + playerInv.getCursor().add(transferAmount); + sourceItem.sub(transferAmount); + + affectedSlots.add(sourceSlot); + } else if (isCursor(transferAction.getSource())) { + int destSlot = bedrockSlotToJava(transferAction.getDestination()); + GeyserItemStack sourceItem = playerInv.getCursor(); + if (inventory.getItem(destSlot).isEmpty()) { + inventory.setItem(destSlot, sourceItem.copy(0), session); + } + + inventory.getItem(destSlot).add(transferAmount); + sourceItem.sub(transferAmount); + + affectedSlots.add(destSlot); + } else { + int sourceSlot = bedrockSlotToJava(transferAction.getSource()); + int destSlot = bedrockSlotToJava(transferAction.getDestination()); + GeyserItemStack sourceItem = inventory.getItem(sourceSlot); + if (inventory.getItem(destSlot).isEmpty()) { + inventory.setItem(destSlot, sourceItem.copy(0), session); + } + + inventory.getItem(destSlot).add(transferAmount); + sourceItem.sub(transferAmount); + + affectedSlots.add(sourceSlot); + affectedSlots.add(destSlot); + } + break; + } + case SWAP: { + SwapStackRequestActionData swapAction = (SwapStackRequestActionData) action; + if (!(checkNetId(session, inventory, swapAction.getSource()) && checkNetId(session, inventory, swapAction.getDestination()))) { + return rejectRequest(request); + } + if (isCraftingGrid(swapAction.getSource()) || isCraftingGrid(swapAction.getDestination())) { + return rejectRequest(request, false); + } + + if (isCursor(swapAction.getDestination())) { + int sourceSlot = bedrockSlotToJava(swapAction.getSource()); + GeyserItemStack sourceItem = inventory.getItem(sourceSlot); + GeyserItemStack destItem = playerInv.getCursor(); + + playerInv.setCursor(sourceItem, session); + inventory.setItem(sourceSlot, destItem, session); + + affectedSlots.add(sourceSlot); + } else if (isCursor(swapAction.getSource())) { + int destSlot = bedrockSlotToJava(swapAction.getDestination()); + GeyserItemStack sourceItem = playerInv.getCursor(); + GeyserItemStack destItem = inventory.getItem(destSlot); + + inventory.setItem(destSlot, sourceItem, session); + playerInv.setCursor(destItem, session); + + affectedSlots.add(destSlot); + } else { + int sourceSlot = bedrockSlotToJava(swapAction.getSource()); + int destSlot = bedrockSlotToJava(swapAction.getDestination()); + GeyserItemStack sourceItem = inventory.getItem(sourceSlot); + GeyserItemStack destItem = inventory.getItem(destSlot); + + inventory.setItem(destSlot, sourceItem, session); + inventory.setItem(sourceSlot, destItem, session); + + affectedSlots.add(sourceSlot); + affectedSlots.add(destSlot); + } + break; + } + case DROP: { + DropStackRequestActionData dropAction = (DropStackRequestActionData) action; + if (!checkNetId(session, inventory, dropAction.getSource())) { + return rejectRequest(request); + } + if (isCraftingGrid(dropAction.getSource())) { + return rejectRequest(request, false); + } + + GeyserItemStack sourceItem; + if (isCursor(dropAction.getSource())) { + sourceItem = playerInv.getCursor(); + } else { + int sourceSlot = bedrockSlotToJava(dropAction.getSource()); + sourceItem = inventory.getItem(sourceSlot); + affectedSlots.add(sourceSlot); + } + + if (sourceItem.isEmpty()) { + return rejectRequest(request); + } + + ClientCreativeInventoryActionPacket creativeDropPacket = new ClientCreativeInventoryActionPacket(-1, sourceItem.getItemStack(dropAction.getCount())); + session.sendDownstreamPacket(creativeDropPacket); + + sourceItem.sub(dropAction.getCount()); + break; + } + case DESTROY: { + // Only called when a creative client wants to destroy an item... I think - Camotoy + DestroyStackRequestActionData destroyAction = (DestroyStackRequestActionData) action; + if (!checkNetId(session, inventory, destroyAction.getSource())) { + return rejectRequest(request); + } + if (isCraftingGrid(destroyAction.getSource())) { + return rejectRequest(request, false); + } + + if (!isCursor(destroyAction.getSource())) { + // Item exists; let's remove it from the inventory + int javaSlot = bedrockSlotToJava(destroyAction.getSource()); + GeyserItemStack existingItem = inventory.getItem(javaSlot); + existingItem.sub(destroyAction.getCount()); + affectedSlots.add(javaSlot); + } else { + // Just sync up the item on our end, since the server doesn't care what's in our cursor + playerInv.getCursor().sub(destroyAction.getCount()); + } + break; + } + default: + session.getConnector().getLogger().error("Unknown crafting state induced by " + session.getName()); + return rejectRequest(request); + } + } + for (int slot : affectedSlots) { + sendCreativeAction(session, inventory, slot); + } + return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); + } + + @Override + public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + ItemStack javaCreativeItem = null; + IntSet affectedSlots = new IntOpenHashSet(); + CraftState craftState = CraftState.START; + for (StackRequestActionData action : request.getActions()) { + switch (action.getType()) { + case CRAFT_CREATIVE: { + CraftCreativeStackRequestActionData creativeAction = (CraftCreativeStackRequestActionData) action; + if (craftState != CraftState.START) { + return rejectRequest(request); + } + craftState = CraftState.RECIPE_ID; + + int creativeId = creativeAction.getCreativeItemNetworkId() - 1; + if (creativeId < 0 || creativeId >= ItemRegistry.CREATIVE_ITEMS.length) { + return rejectRequest(request); + } + // Reference the creative items list we send to the client to know what it's asking of us + ItemData creativeItem = ItemRegistry.CREATIVE_ITEMS[creativeId]; + javaCreativeItem = ItemTranslator.translateToJava(creativeItem); + break; + } + case CRAFT_RESULTS_DEPRECATED: { + CraftResultsDeprecatedStackRequestActionData deprecatedCraftAction = (CraftResultsDeprecatedStackRequestActionData) action; + if (craftState != CraftState.RECIPE_ID) { + return rejectRequest(request); + } + craftState = CraftState.DEPRECATED; + break; + } + case DESTROY: { + DestroyStackRequestActionData destroyAction = (DestroyStackRequestActionData) action; + if (craftState != CraftState.DEPRECATED) { + return rejectRequest(request); + } + + int sourceSlot = bedrockSlotToJava(destroyAction.getSource()); + inventory.setItem(sourceSlot, GeyserItemStack.EMPTY, session); //assume all creative destroy requests will empty the slot + affectedSlots.add(sourceSlot); + break; + } + case TAKE: + case PLACE: { + TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action; + if (!(craftState == CraftState.DEPRECATED || craftState == CraftState.TRANSFER)) { + return rejectRequest(request); + } + craftState = CraftState.TRANSFER; + + if (transferAction.getSource().getContainer() != ContainerSlotType.CREATIVE_OUTPUT) { + return rejectRequest(request); + } + + if (isCursor(transferAction.getDestination())) { + if (session.getPlayerInventory().getCursor().isEmpty()) { + GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem); + newItemStack.setAmount(transferAction.getCount()); + session.getPlayerInventory().setCursor(newItemStack, session); + } else { + session.getPlayerInventory().getCursor().add(transferAction.getCount()); + } + //cursor is always included in response + } else { + int destSlot = bedrockSlotToJava(transferAction.getDestination()); + if (inventory.getItem(destSlot).isEmpty()) { + GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem); + newItemStack.setAmount(transferAction.getCount()); + inventory.setItem(destSlot, newItemStack, session); + } else { + inventory.getItem(destSlot).add(transferAction.getCount()); + } + affectedSlots.add(destSlot); + } + break; + } + default: + return rejectRequest(request); + } + } + for (int slot : affectedSlots) { + sendCreativeAction(session, inventory, slot); + } + return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); + } + + private static void sendCreativeAction(GeyserSession session, Inventory inventory, int slot) { + GeyserItemStack item = inventory.getItem(slot); + ItemStack itemStack = item.isEmpty() ? new ItemStack(-1, 0, null) : item.getItemStack(); + + ClientCreativeInventoryActionPacket creativePacket = new ClientCreativeInventoryActionPacket(slot, itemStack); + session.sendDownstreamPacket(creativePacket); + } + + private static boolean isCraftingGrid(StackRequestSlotInfoData slotInfoData) { + return slotInfoData.getContainer() == ContainerSlotType.CRAFTING_INPUT; + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + throw new UnsupportedOperationException(); + } + + @Override + public void prepareInventory(GeyserSession session, Inventory inventory) { + } + + @Override + public void openInventory(GeyserSession session, Inventory inventory) { + } + + @Override + public void closeInventory(GeyserSession session, Inventory inventory) { + } + + @Override + public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/ShulkerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/ShulkerInventoryTranslator.java new file mode 100644 index 000000000..76d1cb1cf --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/ShulkerInventoryTranslator.java @@ -0,0 +1,76 @@ +/* + * 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.inventory.translators; + +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder; +import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater; +import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; + +public class ShulkerInventoryTranslator extends AbstractBlockInventoryTranslator { + public ShulkerInventoryTranslator() { + super(27, new BlockInventoryHolder("minecraft:shulker_box[facing=north]", ContainerType.CONTAINER) { + private final BlockEntityTranslator shulkerBoxTranslator = BlockEntityTranslator.BLOCK_ENTITY_TRANSLATORS.get("ShulkerBox"); + + @Override + protected boolean isValidBlock(String[] javaBlockString) { + return javaBlockString[0].contains("shulker_box"); + } + + @Override + protected void setCustomName(GeyserSession session, Vector3i position, Inventory inventory, int javaBlockState) { + NbtMapBuilder tag = NbtMap.builder() + .putInt("x", position.getX()) + .putInt("y", position.getY()) + .putInt("z", position.getZ()) + .putString("CustomName", inventory.getTitle()); + // Don't reset facing property + shulkerBoxTranslator.translateTag(tag, null, javaBlockState); + + BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); + dataPacket.setData(tag.build()); + dataPacket.setBlockPosition(position); + session.sendUpstreamPacket(dataPacket); + } + }, ContainerInventoryUpdater.INSTANCE); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot) { + if (javaSlot < this.size) { + return new BedrockContainerSlot(ContainerSlotType.SHULKER, javaSlot); + } + return super.javaSlotToBedrockContainer(javaSlot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SmithingInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/SmithingInventoryTranslator.java similarity index 55% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/SmithingInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/SmithingInventoryTranslator.java index 19c2522ea..dbe04f68b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SmithingInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/SmithingInventoryTranslator.java @@ -23,34 +23,44 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; -import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater; - -public class SmithingInventoryTranslator extends BlockInventoryTranslator { +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; +public class SmithingInventoryTranslator extends AbstractBlockInventoryTranslator { public SmithingInventoryTranslator() { - super(3, "minecraft:smithing_table", ContainerType.SMITHING_TABLE, new CursorInventoryUpdater()); + super(3, "minecraft:smithing_table", ContainerType.SMITHING_TABLE, UIInventoryUpdater.INSTANCE); } @Override - public int bedrockSlotToJava(InventoryActionData action) { - final int slot = super.bedrockSlotToJava(action); - if (action.getSource().getContainerId() == ContainerId.UI) { - switch (slot) { - case 51: - return 0; - case 52: - return 1; - case 50: - return 2; - default: - return slot; - } - } return slot; + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + switch (slotInfoData.getContainer()) { + case SMITHING_TABLE_INPUT: + return 0; + case SMITHING_TABLE_MATERIAL: + return 1; + case SMITHING_TABLE_RESULT: + case CREATIVE_OUTPUT: + return 2; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + switch (slot) { + case 0: + return new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_INPUT, 51); + case 1: + return new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_MATERIAL, 52); + case 2: + return new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_RESULT, 50); + } + return super.javaSlotToBedrockContainer(slot); } @Override @@ -65,5 +75,4 @@ public class SmithingInventoryTranslator extends BlockInventoryTranslator { } return super.javaSlotToBedrock(slot); } - } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/StonecutterInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/StonecutterInventoryTranslator.java new file mode 100644 index 000000000..2acce3a9b --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/StonecutterInventoryTranslator.java @@ -0,0 +1,141 @@ +/* + * 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.inventory.translators; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest; +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.ItemStackResponsePacket; +import it.unimi.dsi.fastutil.ints.IntList; +import org.geysermc.connector.inventory.GeyserItemStack; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.inventory.PlayerInventory; +import org.geysermc.connector.inventory.StonecutterContainer; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.SlotType; +import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; +import org.geysermc.connector.network.translators.item.ItemTranslator; + +public class StonecutterInventoryTranslator extends AbstractBlockInventoryTranslator { + public StonecutterInventoryTranslator() { + super(2, "minecraft:stonecutter[facing=north]", ContainerType.STONECUTTER, UIInventoryUpdater.INSTANCE); + } + + @Override + public boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) { + return action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED; + } + + @Override + public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + // TODO: Also surely to change in the future + StackRequestActionData data = request.getActions()[1]; + if (!(data instanceof CraftResultsDeprecatedStackRequestActionData)) { + return rejectRequest(request); + } + CraftResultsDeprecatedStackRequestActionData craftData = (CraftResultsDeprecatedStackRequestActionData) data; + + StonecutterContainer container = (StonecutterContainer) inventory; + // Get the ID of the item we are cutting + int id = inventory.getItem(0).getJavaId(); + // Look up all possible options of cutting from this ID + IntList results = session.getStonecutterRecipes().get(id); + if (results == null) { + return rejectRequest(request); + } + + ItemStack javaOutput = ItemTranslator.translateToJava(craftData.getResultItems()[0]); + int button = results.indexOf(javaOutput.getId()); + // If we've already pressed the button with this item, no need to press it again! + if (container.getStonecutterButton() != button) { + // Getting the index of the item in the Java stonecutter list + ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), button); + session.sendDownstreamPacket(packet); + container.setStonecutterButton(button); + if (inventory.getItem(1).getJavaId() != javaOutput.getId()) { + // We don't know there is an output here, so we tell ourselves that there is + inventory.setItem(1, GeyserItemStack.from(javaOutput), session); + } + } + + return translateRequest(session, inventory, request); + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + switch (slotInfoData.getContainer()) { + case STONECUTTER_INPUT: + return 0; + case STONECUTTER_RESULT: + case CREATIVE_OUTPUT: + return 1; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.STONECUTTER_INPUT, 3); + } + if (slot == 1) { + return new BedrockContainerSlot(ContainerSlotType.STONECUTTER_RESULT, 50); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int javaSlotToBedrock(int slot) { + if (slot == 0) { + return 3; + } + if (slot == 1) { + return 50; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public SlotType getSlotType(int javaSlot) { + if (javaSlot == 1) { + return SlotType.OUTPUT; + } + return super.getSlotType(javaSlot); + } + + @Override + public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) { + return new StonecutterContainer(name, windowId, this.size, windowType, playerInventory); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ChestInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/ChestInventoryTranslator.java similarity index 64% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/ChestInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/ChestInventoryTranslator.java index 3bc587b1a..d54419b82 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ChestInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/ChestInventoryTranslator.java @@ -23,16 +23,15 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators.chest; -import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.translators.BaseInventoryTranslator; import org.geysermc.connector.network.translators.inventory.updater.ChestInventoryUpdater; import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; -import org.geysermc.connector.utils.InventoryUtils; - -import java.util.List; public abstract class ChestInventoryTranslator extends BaseInventoryTranslator { private final InventoryUpdater updater; @@ -42,6 +41,16 @@ public abstract class ChestInventoryTranslator extends BaseInventoryTranslator { this.updater = new ChestInventoryUpdater(paddedSize); } + @Override + public boolean shouldRejectItemPlace(GeyserSession session, Inventory inventory, ContainerSlotType bedrockSourceContainer, + int javaSourceSlot, ContainerSlotType bedrockDestinationContainer, int javaDestinationSlot) { + // Reject any item placements that occur in the unusable inventory space + if (bedrockSourceContainer == ContainerSlotType.CONTAINER && javaSourceSlot >= this.size) { + return true; + } + return bedrockDestinationContainer == ContainerSlotType.CONTAINER && javaDestinationSlot >= this.size; + } + @Override public void updateInventory(GeyserSession session, Inventory inventory) { updater.updateInventory(this, session, inventory); @@ -53,17 +62,10 @@ public abstract class ChestInventoryTranslator extends BaseInventoryTranslator { } @Override - public void translateActions(GeyserSession session, Inventory inventory, List actions) { - for (InventoryActionData action : actions) { - if (action.getSource().getContainerId() == inventory.getId()) { - if (action.getSlot() >= size) { - updateInventory(session, inventory); - InventoryUtils.updateCursor(session); - return; - } - } + public BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot) { + if (javaSlot < this.size) { + return new BedrockContainerSlot(ContainerSlotType.CONTAINER, javaSlot); } - - super.translateActions(session, inventory, actions); + return super.javaSlotToBedrockContainer(javaSlot); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/DoubleChestInventoryTranslator.java similarity index 58% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/DoubleChestInventoryTranslator.java index 14ccf745e..78ac0b609 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/DoubleChestInventoryTranslator.java @@ -23,37 +23,71 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators.chest; -import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; +import com.nukkitx.protocol.bedrock.packet.ContainerClosePacket; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; +import org.geysermc.connector.inventory.Container; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.network.translators.world.block.DoubleChestValue; +import org.geysermc.connector.network.translators.world.block.entity.DoubleChestBlockEntityTranslator; public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { - private final int blockId; + private final int defaultJavaBlockState; public DoubleChestInventoryTranslator(int size) { super(size, 54); - int javaBlockState = BlockTranslator.getJavaBlockState("minecraft:chest[facing=north,type=single,waterlogged=false]"); - this.blockId = BlockTranslator.getBedrockBlockId(javaBlockState); + this.defaultJavaBlockState = BlockTranslator.getJavaBlockState("minecraft:chest[facing=north,type=single,waterlogged=false]"); } @Override public void prepareInventory(GeyserSession session, Inventory inventory) { + // See BlockInventoryHolder - same concept there except we're also dealing with a specific block state + if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) { + int javaBlockId = session.getConnector().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition()); + String[] javaBlockString = BlockTranslator.getJavaIdBlockMap().inverse().getOrDefault(javaBlockId, "minecraft:air").split("\\["); + if (javaBlockString.length > 1 && (javaBlockString[0].equals("minecraft:chest") || javaBlockString[0].equals("minecraft:trapped_chest")) + && !javaBlockString[1].contains("type=single")) { + inventory.setHolderPosition(session.getLastInteractionBlockPosition()); + ((Container) inventory).setUsingRealBlock(true, javaBlockString[0]); + + NbtMapBuilder tag = NbtMap.builder() + .putString("id", "Chest") + .putInt("x", session.getLastInteractionBlockPosition().getX()) + .putInt("y", session.getLastInteractionBlockPosition().getY()) + .putInt("z", session.getLastInteractionBlockPosition().getZ()) + .putString("CustomName", inventory.getTitle()) + .putString("id", "Chest"); + + DoubleChestValue chestValue = BlockStateValues.getDoubleChestValues().get(javaBlockId); + DoubleChestBlockEntityTranslator.translateChestValue(tag, chestValue, + session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ()); + + BlockEntityDataPacket dataPacket = new BlockEntityDataPacket(); + dataPacket.setData(tag.build()); + dataPacket.setBlockPosition(session.getLastInteractionBlockPosition()); + session.sendUpstreamPacket(dataPacket); + return; + } + } + Vector3i position = session.getPlayerEntity().getPosition().toInt().add(Vector3i.UP); Vector3i pairPosition = position.add(Vector3i.UNIT_X); + int bedrockBlockId = session.getBlockTranslator().getBedrockBlockId(defaultJavaBlockState); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(position); - blockPacket.setRuntimeId(blockId); + blockPacket.setRuntimeId(bedrockBlockId); blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); @@ -73,7 +107,7 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(pairPosition); - blockPacket.setRuntimeId(blockId); + blockPacket.setRuntimeId(bedrockBlockId); blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY); session.sendUpstreamPacket(blockPacket); @@ -105,22 +139,30 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator { @Override public void closeInventory(GeyserSession session, Inventory inventory) { + if (((Container) inventory).isUsingRealBlock()) { + // No need to reset a block since we didn't change any blocks + // But send a container close packet because we aren't destroying the original. + ContainerClosePacket packet = new ContainerClosePacket(); + packet.setId((byte) inventory.getId()); + packet.setUnknownBool0(true); //TODO needs to be changed in Protocol to "server-side" or something + session.sendUpstreamPacket(packet); + return; + } + Vector3i holderPos = inventory.getHolderPosition(); - Position pos = new Position(holderPos.getX(), holderPos.getY(), holderPos.getZ()); - int realBlock = session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()); + int realBlock = session.getConnector().getWorldManager().getBlockAt(session, holderPos); UpdateBlockPacket blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(holderPos); - blockPacket.setRuntimeId(BlockTranslator.getBedrockBlockId(realBlock)); + blockPacket.setRuntimeId(session.getBlockTranslator().getBedrockBlockId(realBlock)); session.sendUpstreamPacket(blockPacket); holderPos = holderPos.add(Vector3i.UNIT_X); - pos = new Position(holderPos.getX(), holderPos.getY(), holderPos.getZ()); - realBlock = session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()); + realBlock = session.getConnector().getWorldManager().getBlockAt(session, holderPos); blockPacket = new UpdateBlockPacket(); blockPacket.setDataLayer(0); blockPacket.setBlockPosition(holderPos); - blockPacket.setRuntimeId(BlockTranslator.getBedrockBlockId(realBlock)); + blockPacket.setRuntimeId(session.getBlockTranslator().getBedrockBlockId(realBlock)); session.sendUpstreamPacket(blockPacket); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/SingleChestInventoryTranslator.java similarity index 73% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/SingleChestInventoryTranslator.java index 462762d03..42b23d5b4 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/chest/SingleChestInventoryTranslator.java @@ -23,22 +23,32 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators.chest; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder; import org.geysermc.connector.network.translators.inventory.holder.InventoryHolder; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; public class SingleChestInventoryTranslator extends ChestInventoryTranslator { private final InventoryHolder holder; public SingleChestInventoryTranslator(int size) { super(size, 27); - int javaBlockState = BlockTranslator.getJavaBlockState("minecraft:chest[facing=north,type=single,waterlogged=false]"); - this.holder = new BlockInventoryHolder(BlockTranslator.getBedrockBlockId(javaBlockState), ContainerType.CONTAINER); + this.holder = new BlockInventoryHolder("minecraft:chest[facing=north,type=single,waterlogged=false]", ContainerType.CONTAINER, + "minecraft:ender_chest", "minecraft:trapped_chest") { + @Override + protected boolean isValidBlock(String[] javaBlockString) { + if (javaBlockString[0].equals("minecraft:ender_chest")) { + // Can't have double ender chests + return true; + } + + // Add provision to ensure this isn't a double chest + return super.isValidBlock(javaBlockString) && (javaBlockString.length > 1 && javaBlockString[1].contains("type=single")); + } + }; } @Override diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/AbstractFurnaceInventoryTranslator.java similarity index 69% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/AbstractFurnaceInventoryTranslator.java index c7bc6acf2..dc9b00fd7 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/AbstractFurnaceInventoryTranslator.java @@ -23,18 +23,21 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators.furnace; -import com.github.steveice10.mc.protocol.data.game.window.WindowType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import com.nukkitx.protocol.bedrock.packet.ContainerSetDataPacket; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; +import org.geysermc.connector.network.translators.inventory.SlotType; +import org.geysermc.connector.network.translators.inventory.translators.AbstractBlockInventoryTranslator; import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater; -public class FurnaceInventoryTranslator extends BlockInventoryTranslator { - public FurnaceInventoryTranslator() { - super(3, "minecraft:furnace[facing=north,lit=false]", ContainerType.FURNACE, new ContainerInventoryUpdater()); +public abstract class AbstractFurnaceInventoryTranslator extends AbstractBlockInventoryTranslator { + AbstractFurnaceInventoryTranslator(String javaBlockIdentifier, ContainerType containerType) { + super(3, javaBlockIdentifier, containerType, ContainerInventoryUpdater.INSTANCE); } @Override @@ -50,9 +53,6 @@ public class FurnaceInventoryTranslator extends BlockInventoryTranslator { break; case 2: dataPacket.setProperty(ContainerSetDataPacket.FURNACE_TICK_COUNT); - if (inventory.getWindowType() == WindowType.BLAST_FURNACE || inventory.getWindowType() == WindowType.SMOKER) { - value *= 2; - } break; default: return; @@ -67,4 +67,15 @@ public class FurnaceInventoryTranslator extends BlockInventoryTranslator { return SlotType.FURNACE_OUTPUT; return SlotType.NORMAL; } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 1) { + return new BedrockContainerSlot(ContainerSlotType.FURNACE_FUEL, javaSlotToBedrock(slot)); + } + if (slot == 2) { + return new BedrockContainerSlot(ContainerSlotType.FURNACE_OUTPUT, javaSlotToBedrock(slot)); + } + return super.javaSlotToBedrockContainer(slot); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/BlastFurnaceInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/BlastFurnaceInventoryTranslator.java new file mode 100644 index 000000000..ed9a8a79c --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/BlastFurnaceInventoryTranslator.java @@ -0,0 +1,44 @@ +/* + * 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.inventory.translators.furnace; + +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; + +public class BlastFurnaceInventoryTranslator extends AbstractFurnaceInventoryTranslator { + public BlastFurnaceInventoryTranslator() { + super("minecraft:blast_furnace[facing=north,lit=false]", ContainerType.BLAST_FURNACE); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.BLAST_FURNACE_INGREDIENT, javaSlotToBedrock(slot)); + } + return super.javaSlotToBedrockContainer(slot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/FurnaceInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/FurnaceInventoryTranslator.java new file mode 100644 index 000000000..b41c9b03b --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/FurnaceInventoryTranslator.java @@ -0,0 +1,44 @@ +/* + * 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.inventory.translators.furnace; + +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; + +public class FurnaceInventoryTranslator extends AbstractFurnaceInventoryTranslator { + public FurnaceInventoryTranslator() { + super("minecraft:furnace[facing=north,lit=false]", ContainerType.FURNACE); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.FURNACE_INGREDIENT, javaSlotToBedrock(slot)); + } + return super.javaSlotToBedrockContainer(slot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/SmokerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/SmokerInventoryTranslator.java new file mode 100644 index 000000000..2b9a78c7d --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/furnace/SmokerInventoryTranslator.java @@ -0,0 +1,44 @@ +/* + * 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.inventory.translators.furnace; + +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; + +public class SmokerInventoryTranslator extends AbstractFurnaceInventoryTranslator { + public SmokerInventoryTranslator() { + super("minecraft:smoker[facing=north,lit=false]", ContainerType.SMOKER); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 0) { + return new BedrockContainerSlot(ContainerSlotType.SMOKER_INGREDIENT, javaSlotToBedrock(slot)); + } + return super.javaSlotToBedrockContainer(slot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BlockInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/AbstractHorseInventoryTranslator.java similarity index 66% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/BlockInventoryTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/AbstractHorseInventoryTranslator.java index 8f70189de..0e365aca1 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BlockInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/AbstractHorseInventoryTranslator.java @@ -23,41 +23,32 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.inventory; +package org.geysermc.connector.network.translators.inventory.translators.horse; -import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder; -import org.geysermc.connector.network.translators.inventory.holder.InventoryHolder; +import org.geysermc.connector.network.translators.inventory.translators.BaseInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.updater.HorseInventoryUpdater; import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; -public class BlockInventoryTranslator extends BaseInventoryTranslator { - private final InventoryHolder holder; +public abstract class AbstractHorseInventoryTranslator extends BaseInventoryTranslator { private final InventoryUpdater updater; - public BlockInventoryTranslator(int size, String javaBlockIdentifier, ContainerType containerType, InventoryUpdater updater) { + public AbstractHorseInventoryTranslator(int size) { super(size); - int javaBlockState = BlockTranslator.getJavaBlockState(javaBlockIdentifier); - int blockId = BlockTranslator.getBedrockBlockId(javaBlockState); - this.holder = new BlockInventoryHolder(blockId, containerType); - this.updater = updater; + this.updater = HorseInventoryUpdater.INSTANCE; } @Override public void prepareInventory(GeyserSession session, Inventory inventory) { - holder.prepareInventory(this, session, inventory); } @Override public void openInventory(GeyserSession session, Inventory inventory) { - holder.openInventory(this, session, inventory); } @Override public void closeInventory(GeyserSession session, Inventory inventory) { - holder.closeInventory(this, session, inventory); } @Override diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/ChestedHorseInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/ChestedHorseInventoryTranslator.java new file mode 100644 index 000000000..77a1976be --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/ChestedHorseInventoryTranslator.java @@ -0,0 +1,112 @@ +/* + * 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.inventory.translators.horse; + +import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; + +import java.util.Arrays; + +public abstract class ChestedHorseInventoryTranslator extends AbstractHorseInventoryTranslator { + private final int chestSize; + private final int equipSlot; + + /** + * @param size the total Java size of the inventory + * @param equipSlot the Java equipment slot. Java always has two slots - one for armor and one for saddle. Chested horses + * on Bedrock only acknowledge one slot. + */ + public ChestedHorseInventoryTranslator(int size, int equipSlot) { + super(size); + this.chestSize = size - 2; + this.equipSlot = equipSlot; + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + if (slotInfoData.getContainer() == ContainerSlotType.HORSE_EQUIP) { + return this.equipSlot; + } + if (slotInfoData.getContainer() == ContainerSlotType.CONTAINER) { + return slotInfoData.getSlot() + 1; + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == this.equipSlot) { + return new BedrockContainerSlot(ContainerSlotType.HORSE_EQUIP, 0); + } + if (slot <= this.size - 1) { // Accommodate for the lack of one slot (saddle or armor) + return new BedrockContainerSlot(ContainerSlotType.CONTAINER, slot - 1); + } + return super.javaSlotToBedrockContainer(slot); + } + + @Override + public int javaSlotToBedrock(int slot) { + if (slot == 0 && this.equipSlot == 0) { + return 0; + } + if (slot <= this.size - 1) { + return slot - 1; + } + return super.javaSlotToBedrock(slot); + } + + @Override + public void updateInventory(GeyserSession session, Inventory inventory) { + ItemData[] bedrockItems = new ItemData[36]; + for (int i = 0; i < 36; i++) { + final int offset = i < 9 ? 27 : -9; + bedrockItems[i] = inventory.getItem(this.size + i + offset).getItemData(session); + } + InventoryContentPacket contentPacket = new InventoryContentPacket(); + contentPacket.setContainerId(ContainerId.INVENTORY); + contentPacket.setContents(Arrays.asList(bedrockItems)); + session.sendUpstreamPacket(contentPacket); + + ItemData[] horseItems = new ItemData[chestSize + 1]; + // Manually specify the first slot - Java always has two slots (armor and saddle) and one is invisible. + // Bedrock doesn't have this invisible slot. + horseItems[0] = inventory.getItem(this.equipSlot).getItemData(session); + for (int i = 1; i < horseItems.length; i++) { + horseItems[i] = inventory.getItem(i + 1).getItemData(session); + } + + InventoryContentPacket horseContentsPacket = new InventoryContentPacket(); + horseContentsPacket.setContainerId(inventory.getId()); + horseContentsPacket.setContents(Arrays.asList(horseItems)); + session.sendUpstreamPacket(horseContentsPacket); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/DonkeyInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/DonkeyInventoryTranslator.java new file mode 100644 index 000000000..bf13bd6da --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/DonkeyInventoryTranslator.java @@ -0,0 +1,32 @@ +/* + * 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.inventory.translators.horse; + +public class DonkeyInventoryTranslator extends ChestedHorseInventoryTranslator { + public DonkeyInventoryTranslator(int size) { + super(size, 0); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/HorseInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/HorseInventoryTranslator.java new file mode 100644 index 000000000..09a8f5de3 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/HorseInventoryTranslator.java @@ -0,0 +1,52 @@ +/* + * 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.inventory.translators.horse; + +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData; +import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; + +public class HorseInventoryTranslator extends AbstractHorseInventoryTranslator { + public HorseInventoryTranslator(int size) { + super(size); + } + + @Override + public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) { + if (slotInfoData.getContainer() == ContainerSlotType.HORSE_EQUIP) { + return slotInfoData.getSlot(); + } + return super.bedrockSlotToJava(slotInfoData); + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + if (slot == 0 || slot == 1) { + return new BedrockContainerSlot(ContainerSlotType.HORSE_EQUIP, slot); + } + return super.javaSlotToBedrockContainer(slot); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/remote/RemoteServer.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/LlamaInventoryTranslator.java similarity index 83% rename from connector/src/main/java/org/geysermc/connector/network/remote/RemoteServer.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/LlamaInventoryTranslator.java index b957b90d6..cea605f83 100644 --- a/connector/src/main/java/org/geysermc/connector/network/remote/RemoteServer.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/LlamaInventoryTranslator.java @@ -23,15 +23,10 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.remote; +package org.geysermc.connector.network.translators.inventory.translators.horse; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class RemoteServer { - - private String address; - private int port; -} \ No newline at end of file +public class LlamaInventoryTranslator extends ChestedHorseInventoryTranslator { + public LlamaInventoryTranslator(int size) { + super(size, 1); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ChestInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ChestInventoryUpdater.java index 73c1f2ebc..f5029f749 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ChestInventoryUpdater.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ChestInventoryUpdater.java @@ -32,7 +32,6 @@ import lombok.AllArgsConstructor; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; -import org.geysermc.connector.network.translators.item.ItemTranslator; import org.geysermc.connector.utils.InventoryUtils; import org.geysermc.connector.utils.LanguageUtils; @@ -52,7 +51,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { List bedrockItems = new ArrayList<>(paddedSize); for (int i = 0; i < paddedSize; i++) { if (i < translator.size) { - bedrockItems.add(ItemTranslator.translateToBedrock(session, inventory.getItem(i))); + bedrockItems.add(inventory.getItem(i).getItemData(session)); } else { bedrockItems.add(UNUSUABLE_SPACE_BLOCK); } @@ -72,7 +71,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(inventory.getId()); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(javaSlot))); + slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ContainerInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ContainerInventoryUpdater.java index d7bdbde45..f77e687a0 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ContainerInventoryUpdater.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ContainerInventoryUpdater.java @@ -31,18 +31,19 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; -import org.geysermc.connector.network.translators.item.ItemTranslator; import java.util.Arrays; public class ContainerInventoryUpdater extends InventoryUpdater { + public static final ContainerInventoryUpdater INSTANCE = new ContainerInventoryUpdater(); + @Override public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); ItemData[] bedrockItems = new ItemData[translator.size]; for (int i = 0; i < bedrockItems.length; i++) { - bedrockItems[translator.javaSlotToBedrock(i)] = ItemTranslator.translateToBedrock(session, inventory.getItem(i)); + bedrockItems[translator.javaSlotToBedrock(i)] = inventory.getItem(i).getItemData(session); } InventoryContentPacket contentPacket = new InventoryContentPacket(); @@ -59,7 +60,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(inventory.getId()); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(javaSlot))); + slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/HorseInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/HorseInventoryUpdater.java new file mode 100644 index 000000000..db067a74c --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/HorseInventoryUpdater.java @@ -0,0 +1,67 @@ +/* + * 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.inventory.updater; + +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; +import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; +import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; + +import java.util.Arrays; + +public class HorseInventoryUpdater extends InventoryUpdater { + public static final HorseInventoryUpdater INSTANCE = new HorseInventoryUpdater(); + + @Override + public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { + super.updateInventory(translator, session, inventory); + + ItemData[] bedrockItems = new ItemData[translator.size]; + for (int i = 0; i < bedrockItems.length; i++) { + bedrockItems[translator.javaSlotToBedrock(i)] = inventory.getItem(i).getItemData(session); + } + + InventoryContentPacket contentPacket = new InventoryContentPacket(); + contentPacket.setContainerId(inventory.getId()); + contentPacket.setContents(Arrays.asList(bedrockItems)); + session.sendUpstreamPacket(contentPacket); + } + + @Override + public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) { + if (super.updateSlot(translator, session, inventory, javaSlot)) + return true; + + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(4); // Horse GUI? + slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); + slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); + session.sendUpstreamPacket(slotPacket); + return true; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java index 020f74671..e94c0944b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java @@ -32,16 +32,15 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; -import org.geysermc.connector.network.translators.item.ItemTranslator; import java.util.Arrays; -public abstract class InventoryUpdater { +public class InventoryUpdater { public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { ItemData[] bedrockItems = new ItemData[36]; for (int i = 0; i < 36; i++) { final int offset = i < 9 ? 27 : -9; - bedrockItems[i] = ItemTranslator.translateToBedrock(session, inventory.getItem(translator.size + i + offset)); + bedrockItems[i] = inventory.getItem(translator.size + i + offset).getItemData(session); } InventoryContentPacket contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(ContainerId.INVENTORY); @@ -54,7 +53,7 @@ public abstract class InventoryUpdater { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(ContainerId.INVENTORY); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(javaSlot))); + slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/CursorInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/UIInventoryUpdater.java similarity index 87% rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/CursorInventoryUpdater.java rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/UIInventoryUpdater.java index 89abdd847..5bb8ad5d2 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/CursorInventoryUpdater.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/UIInventoryUpdater.java @@ -30,11 +30,10 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; -import org.geysermc.connector.network.translators.item.ItemTranslator; -public class CursorInventoryUpdater extends InventoryUpdater { +public class UIInventoryUpdater extends InventoryUpdater { + public static final UIInventoryUpdater INSTANCE = new UIInventoryUpdater(); - //TODO: Consider renaming this? Since the Protocol enum updated @Override public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) { super.updateInventory(translator, session, inventory); @@ -46,7 +45,7 @@ public class CursorInventoryUpdater extends InventoryUpdater { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(bedrockSlot); - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(i))); + slotPacket.setItem(inventory.getItem(i).getItemData(session)); session.sendUpstreamPacket(slotPacket); } } @@ -59,7 +58,7 @@ public class CursorInventoryUpdater extends InventoryUpdater { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(javaSlot))); + slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java index 769cbd63a..a3b4b6c31 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java @@ -69,6 +69,18 @@ public enum Enchantment { QUICK_CHARGE, SOUL_SPEED; + /** + * A list of all enchantment Java identifiers for use with command suggestions. + */ + public static final String[] ALL_JAVA_IDENTIFIERS; + + static { + ALL_JAVA_IDENTIFIERS = new String[values().length]; + for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) { + ALL_JAVA_IDENTIFIERS[i] = values()[i].javaIdentifier; + } + } + private final String javaIdentifier; Enchantment() { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java index f61c3d709..278d708f9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemEntry.java @@ -34,7 +34,7 @@ import lombok.ToString; @ToString public class ItemEntry { - public static ItemEntry AIR = new ItemEntry("minecraft:air", "minecraft:air", 0, 0, 0, false); + public static ItemEntry AIR = new ItemEntry("minecraft:air", "minecraft:air", 0, 0, 0, false, 64); private final String javaIdentifier; private final String bedrockIdentifier; @@ -43,6 +43,7 @@ public class ItemEntry { private final int bedrockData; private final boolean block; + private final int stackSize; @Override public boolean equals(Object obj) { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java index e9b821588..c865a162a 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 @@ -56,13 +56,18 @@ public class ItemRegistry { * A list of all identifiers that only exist on Java. Used to prevent creative items from becoming these unintentionally. */ private static final List JAVA_ONLY_ITEMS = Arrays.asList("minecraft:spectral_arrow", "minecraft:debug_stick", - "minecraft:knowledge_book"); + "minecraft:knowledge_book", "minecraft:tipped_arrow", "minecraft:furnace_minecart"); public static final ItemData[] CREATIVE_ITEMS; public static final List ITEMS = new ArrayList<>(); public static final Int2ObjectMap ITEM_ENTRIES = new Int2ObjectOpenHashMap<>(); + /** + * A list of all Java item names. + */ + public static final String[] ITEM_NAMES; + /** * Bamboo item entry, used in PandaEntity.java */ @@ -116,6 +121,8 @@ public class ItemRegistry { // Used to get the Bedrock namespaced ID (in instances where there are small differences) Int2ObjectMap bedrockIdToIdentifier = new Int2ObjectOpenHashMap<>(); + List itemNames = new ArrayList<>(); + List itemEntries; try { itemEntries = GeyserConnector.JSON_MAPPER.readValue(stream, itemEntriesType); @@ -151,6 +158,8 @@ public class ItemRegistry { if (bedrockIdentifier == null) { throw new RuntimeException("Missing Bedrock ID in mappings!: " + bedrockId); } + JsonNode stackSizeNode = entry.getValue().get("stack_size"); + int stackSize = stackSizeNode == null ? 64 : stackSizeNode.intValue(); if (entry.getValue().has("tool_type")) { if (entry.getValue().has("tool_tier")) { ITEM_ENTRIES.put(itemIndex, new ToolItemEntry( @@ -158,19 +167,22 @@ public class ItemRegistry { entry.getValue().get("bedrock_data").intValue(), entry.getValue().get("tool_type").textValue(), entry.getValue().get("tool_tier").textValue(), - entry.getValue().get("is_block") != null && entry.getValue().get("is_block").booleanValue())); + entry.getValue().get("is_block").booleanValue(), + stackSize)); } else { ITEM_ENTRIES.put(itemIndex, new ToolItemEntry( entry.getKey(), bedrockIdentifier, itemIndex, bedrockId, entry.getValue().get("bedrock_data").intValue(), entry.getValue().get("tool_type").textValue(), - "", entry.getValue().get("is_block").booleanValue())); + "", entry.getValue().get("is_block").booleanValue(), + stackSize)); } } else { ITEM_ENTRIES.put(itemIndex, new ItemEntry( entry.getKey(), bedrockIdentifier, itemIndex, bedrockId, entry.getValue().get("bedrock_data").intValue(), - entry.getValue().get("is_block") != null && entry.getValue().get("is_block").booleanValue())); + entry.getValue().get("is_block").booleanValue(), + stackSize)); } switch (entry.getKey()) { case "minecraft:barrier": @@ -207,6 +219,8 @@ public class ItemRegistry { BUCKETS.add(entry.getValue().get("bedrock_id").intValue()); } + itemNames.add(entry.getKey()); + itemIndex++; } @@ -216,7 +230,7 @@ public class ItemRegistry { // Add the loadstone compass since it doesn't exist on java but we need it for item conversion ITEM_ENTRIES.put(itemIndex, new ItemEntry("minecraft:lodestone_compass", "minecraft:lodestone_compass", itemIndex, - lodestoneCompassId, 0, false)); + lodestoneCompassId, 0, false, 1)); /* Load creative items */ stream = FileUtils.getResource("bedrock/creative_items.json"); @@ -235,6 +249,8 @@ public class ItemRegistry { creativeItems.add(ItemData.fromNet(netId++, item.getId(), item.getDamage(), item.getCount(), item.getTag())); } CREATIVE_ITEMS = creativeItems.toArray(new ItemData[0]); + + ITEM_NAMES = itemNames.toArray(new String[0]); } /** diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java index 90acb781a..f1ae98faf 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java @@ -38,7 +38,6 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.ItemRemapper; import org.geysermc.connector.network.translators.chat.MessageTranslator; -import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; import org.reflections.Reflections; @@ -125,8 +124,12 @@ public abstract class ItemTranslator { } ItemEntry bedrockItem = ItemRegistry.getItem(stack); + if (bedrockItem == null) { + session.getConnector().getLogger().debug("No matching ItemEntry for " + stack); + return ItemData.AIR; + } - com.github.steveice10.opennbt.tag.builtin.CompoundTag nbt = stack.getNbt() != null ? stack.getNbt().clone() : null; + CompoundTag nbt = stack.getNbt() != null ? stack.getNbt().clone() : null; // This is a fallback for maps with no nbt if (nbt == null && bedrockItem.getJavaIdentifier().equals("minecraft:filled_map")) { @@ -160,8 +163,8 @@ public abstract class ItemTranslator { String[] canBreak = new String[0]; ListTag canPlaceOn = nbt.get("CanPlaceOn"); String[] canPlace = new String[0]; - canBreak = getCanModify(canDestroy, canBreak); - canPlace = getCanModify(canPlaceOn, canPlace); + canBreak = getCanModify(session, canDestroy, canBreak); + canPlace = getCanModify(session, canPlaceOn, canPlace); itemData = ItemData.of(itemData.getId(), itemData.getDamage(), itemData.getCount(), itemData.getTag(), canPlace, canBreak); } @@ -171,11 +174,12 @@ public abstract class ItemTranslator { /** * Translates the Java NBT of canDestroy and canPlaceOn to its Bedrock counterparts. * In Java, this is treated as normal NBT, but in Bedrock, these arguments are extra parts of the item data itself. + * * @param canModifyJava the list of items in Java * @param canModifyBedrock the empty list of items in Bedrock * @return the new list of items in Bedrock */ - private static String[] getCanModify(ListTag canModifyJava, String[] canModifyBedrock) { + private static String[] getCanModify(GeyserSession session, ListTag canModifyJava, String[] canModifyBedrock) { if (canModifyJava != null && canModifyJava.size() > 0) { canModifyBedrock = new String[canModifyJava.size()]; for (int i = 0; i < canModifyBedrock.length; i++) { @@ -185,7 +189,7 @@ public abstract class ItemTranslator { if (!block.startsWith("minecraft:")) block = "minecraft:" + block; // Get the Bedrock identifier of the item and replace it. // This will unfortunately be limited - for example, beds and banners will be translated weirdly - canModifyBedrock[i] = BlockTranslator.getBedrockBlockIdentifier(block).replace("minecraft:", ""); + canModifyBedrock[i] = session.getBlockTranslator().getBedrockBlockIdentifier(block).replace("minecraft:", ""); } } return canModifyBedrock; @@ -303,9 +307,8 @@ public abstract class ItemTranslator { CompoundTag javaTag = new CompoundTag(name); Map javaValue = javaTag.getValue(); if (tag != null && !tag.isEmpty()) { - for (String str : tag.keySet()) { - Object bedrockTag = tag.get(str); - Tag translatedTag = translateToJavaNBT(str, bedrockTag); + for (Map.Entry entry : tag.entrySet()) { + Tag translatedTag = translateToJavaNBT(entry.getKey(), entry.getValue()); if (translatedTag == null) continue; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/RecipeRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/RecipeRegistry.java index 7e307281e..110014bf5 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/RecipeRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/RecipeRegistry.java @@ -26,13 +26,25 @@ package org.geysermc.connector.network.translators.item; import com.fasterxml.jackson.databind.JsonNode; +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.Recipe; +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.nbt.NbtMap; +import com.nukkitx.nbt.NbtUtils; import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.utils.FileUtils; import org.geysermc.connector.utils.LanguageUtils; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; import java.util.*; @@ -47,6 +59,12 @@ public class RecipeRegistry { */ public static int LAST_RECIPE_NET_ID = 0; + /** + * A list of all the following crafting recipes, but in a format understood by Java servers. + * Used for console autocrafting. + */ + public static final Int2ObjectMap ALL_CRAFTING_RECIPES = new Int2ObjectOpenHashMap<>(); + /** * A list of all possible leather armor dyeing recipes. * Created manually. @@ -78,6 +96,12 @@ public class RecipeRegistry { */ public static final List TIPPED_ARROW_RECIPES = new ObjectArrayList<>(); + /** + * Recipe data that, when sent to the client, enables cartography features. + * This does not have a Java equivalent. + */ + public static final List CARTOGRAPHY_RECIPE_DATA = new ObjectArrayList<>(); + /** * Recipe data that, when sent to the client, enables book cloning */ @@ -106,6 +130,11 @@ public class RecipeRegistry { MAP_EXTENDING_RECIPE_DATA = CraftingData.fromMulti(UUID.fromString("d392b075-4ba1-40ae-8789-af868d56f6ce"), LAST_RECIPE_NET_ID++); MAP_CLONING_RECIPE_DATA = CraftingData.fromMulti(UUID.fromString("85939755-ba10-4d9d-a4cc-efb7a8e943c4"), LAST_RECIPE_NET_ID++); BANNER_DUPLICATING_RECIPE_DATA = CraftingData.fromMulti(UUID.fromString("b5c5d105-75a2-4076-af2b-923ea2bf4bf0"), LAST_RECIPE_NET_ID++); + + CARTOGRAPHY_RECIPE_DATA.add(CraftingData.fromMulti(UUID.fromString("8b36268c-1829-483c-a0f1-993b7156a8f2"), LAST_RECIPE_NET_ID++)); // Map extending + CARTOGRAPHY_RECIPE_DATA.add(CraftingData.fromMulti(UUID.fromString("442d85ed-8272-4543-a6f1-418f90ded05d"), LAST_RECIPE_NET_ID++)); // Map cloning + CARTOGRAPHY_RECIPE_DATA.add(CraftingData.fromMulti(UUID.fromString("98c84b38-1085-46bd-b1ce-dd38c159e6cc"), LAST_RECIPE_NET_ID++)); // Map upgrading + CARTOGRAPHY_RECIPE_DATA.add(CraftingData.fromMulti(UUID.fromString("602234e4-cac1-4353-8bb7-b1ebff70024b"), LAST_RECIPE_NET_ID++)); // Map locking // https://github.com/pmmp/PocketMine-MP/blob/stable/src/pocketmine/inventory/MultiRecipe.php // Get all recipes that are not directly sent from a Java server @@ -118,7 +147,7 @@ public class RecipeRegistry { throw new AssertionError(LanguageUtils.getLocaleStringLog("geyser.toolbox.fail.runtime_java"), e); } - for (JsonNode entry: items.get("leather_armor")) { + for (JsonNode entry : items.get("leather_armor")) { // This won't be perfect, as we can't possibly send every leather input for every kind of color // But it does display the correct output from a base leather armor, and besides visuals everything works fine LEATHER_DYEING_RECIPES.add(getCraftingDataFromJsonNode(entry)); @@ -146,9 +175,13 @@ public class RecipeRegistry { * @return the {@link CraftingData} to send to the Bedrock client. */ private static CraftingData getCraftingDataFromJsonNode(JsonNode node) { - ItemData output = ItemRegistry.getBedrockItemFromJson(node.get("output").get(0)); + int netId = LAST_RECIPE_NET_ID++; + int type = node.get("bedrockRecipeType").asInt(); + JsonNode outputNode = node.get("output"); + ItemEntry outputEntry = ItemRegistry.getItemEntry(outputNode.get("identifier").asText()); + ItemData output = getBedrockItemFromIdentifierJson(outputEntry, outputNode); UUID uuid = UUID.randomUUID(); - if (node.get("type").asInt() == 1) { + if (type == 1) { // Shaped recipe List shape = new ArrayList<>(); // Get the shape of the recipe @@ -158,10 +191,12 @@ public class RecipeRegistry { // In recipes.json each recipe is mapped by a letter Map letterToRecipe = new HashMap<>(); - Iterator> iterator = node.get("input").fields(); + Iterator> iterator = node.get("inputs").fields(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); - letterToRecipe.put(entry.getKey(), ItemRegistry.getBedrockItemFromJson(entry.getValue())); + JsonNode inputNode = entry.getValue(); + ItemEntry inputEntry = ItemRegistry.getItemEntry(inputNode.get("identifier").asText()); + letterToRecipe.put(entry.getKey(), getBedrockItemFromIdentifierJson(inputEntry, inputNode)); } List inputs = new ArrayList<>(shape.size() * shape.get(0).length()); @@ -175,20 +210,69 @@ public class RecipeRegistry { } } + /* Convert into a Java recipe class for autocrafting */ + List ingredients = new ArrayList<>(); + for (ItemData input : inputs) { + ingredients.add(new Ingredient(new ItemStack[]{ItemTranslator.translateToJava(input)})); + } + ShapedRecipeData data = new ShapedRecipeData(shape.get(0).length(), shape.size(), "crafting_table", + ingredients.toArray(new Ingredient[0]), ItemTranslator.translateToJava(output)); + Recipe recipe = new Recipe(RecipeType.CRAFTING_SHAPED, "", data); + ALL_CRAFTING_RECIPES.put(netId, recipe); + /* Convert end */ + return CraftingData.fromShaped(uuid.toString(), shape.get(0).length(), shape.size(), - inputs, Collections.singletonList(output), uuid, "crafting_table", 0, LAST_RECIPE_NET_ID++); + inputs, Collections.singletonList(output), uuid, "crafting_table", 0, netId); } List inputs = new ObjectArrayList<>(); - for (JsonNode entry : node.get("input")) { - inputs.add(ItemRegistry.getBedrockItemFromJson(entry)); + for (JsonNode entry : node.get("inputs")) { + ItemEntry inputEntry = ItemRegistry.getItemEntry(entry.get("identifier").asText()); + inputs.add(getBedrockItemFromIdentifierJson(inputEntry, entry)); } - if (node.get("type").asInt() == 5) { + + /* Convert into a Java Recipe class for autocrafting */ + List ingredients = new ArrayList<>(); + for (ItemData input : inputs) { + ingredients.add(new Ingredient(new ItemStack[]{ItemTranslator.translateToJava(input)})); + } + ShapelessRecipeData data = new ShapelessRecipeData("crafting_table", + ingredients.toArray(new Ingredient[0]), ItemTranslator.translateToJava(output)); + Recipe recipe = new Recipe(RecipeType.CRAFTING_SHAPELESS, "", data); + ALL_CRAFTING_RECIPES.put(netId, recipe); + /* Convert end */ + + if (type == 5) { // Shulker box return CraftingData.fromShulkerBox(uuid.toString(), - inputs, Collections.singletonList(output), uuid, "crafting_table", 0, LAST_RECIPE_NET_ID++); + inputs, Collections.singletonList(output), uuid, "crafting_table", 0, netId); } return CraftingData.fromShapeless(uuid.toString(), - inputs, Collections.singletonList(output), uuid, "crafting_table", 0, LAST_RECIPE_NET_ID++); + inputs, Collections.singletonList(output), uuid, "crafting_table", 0, netId); + } + + private static ItemData getBedrockItemFromIdentifierJson(ItemEntry itemEntry, JsonNode itemNode) { + int count = 1; + short damage = 0; + NbtMap tag = null; + JsonNode damageNode = itemNode.get("bedrockDamage"); + if (damageNode != null) { + damage = damageNode.numberValue().shortValue(); + } + JsonNode countNode = itemNode.get("count"); + if (countNode != null) { + count = countNode.asInt(); + } + JsonNode nbtNode = itemNode.get("bedrockNbt"); + if (nbtNode != null) { + byte[] bytes = Base64.getDecoder().decode(nbtNode.asText()); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + try { + tag = (NbtMap) NbtUtils.createReaderLE(bais).readTag(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return ItemData.of(itemEntry.getBedrockId(), damage, count, tag); } public static void init() { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ToolItemEntry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ToolItemEntry.java index 5352938c0..ba1753a35 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ToolItemEntry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ToolItemEntry.java @@ -32,8 +32,8 @@ public class ToolItemEntry extends ItemEntry { private final String toolType; private final String toolTier; - public ToolItemEntry(String javaIdentifier, String bedrockIdentifier, int javaId, int bedrockId, int bedrockData, String toolType, String toolTier, boolean isBlock) { - super(javaIdentifier, bedrockIdentifier, javaId, bedrockId, bedrockData, isBlock); + public ToolItemEntry(String javaIdentifier, String bedrockIdentifier, int javaId, int bedrockId, int bedrockData, String toolType, String toolTier, boolean isBlock, int stackSize) { + super(javaIdentifier, bedrockIdentifier, javaId, bedrockId, bedrockData, isBlock, stackSize); this.toolType = toolType; this.toolTier = toolTier; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/BannerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/BannerTranslator.java index 25bfe3d2e..a96c47f6a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/BannerTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/BannerTranslator.java @@ -195,13 +195,14 @@ public class BannerTranslator extends ItemTranslator { blockEntityTag.put(OMINOUS_BANNER_PATTERN); itemStack.getNbt().put(blockEntityTag); - } else if (nbtTag.containsKey("Patterns", NbtType.COMPOUND)) { + } else if (nbtTag.containsKey("Patterns", NbtType.LIST)) { List patterns = nbtTag.getList("Patterns", NbtType.COMPOUND); CompoundTag blockEntityTag = new CompoundTag("BlockEntityTag"); blockEntityTag.put(convertBannerPattern(patterns)); itemStack.getNbt().put(blockEntityTag); + itemStack.getNbt().remove("Patterns"); // Remove the old Bedrock patterns list } return itemStack; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkBaseTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkBaseTranslator.java new file mode 100644 index 000000000..dff40ea75 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkBaseTranslator.java @@ -0,0 +1,124 @@ +/* + * 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.item.translators.nbt; + +import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; +import org.geysermc.connector.utils.FireworkColor; +import org.geysermc.connector.utils.MathUtils; + +/** + * Stores common code for firework rockets and firework stars. + */ +public abstract class FireworkBaseTranslator extends NbtItemStackTranslator { + + protected CompoundTag translateExplosionToBedrock(CompoundTag explosion, String newName) { + CompoundTag newExplosionData = new CompoundTag(newName); + + if (explosion.get("Type") != null) { + newExplosionData.put(new ByteTag("FireworkType", MathUtils.convertByte(explosion.get("Type").getValue()))); + } + + if (explosion.get("Colors") != null) { + int[] oldColors = (int[]) explosion.get("Colors").getValue(); + byte[] colors = new byte[oldColors.length]; + + int i = 0; + for (int color : oldColors) { + colors[i++] = FireworkColor.fromJavaID(color).getBedrockID(); + } + + newExplosionData.put(new ByteArrayTag("FireworkColor", colors)); + } + + if (explosion.get("FadeColors") != null) { + int[] oldColors = (int[]) explosion.get("FadeColors").getValue(); + byte[] colors = new byte[oldColors.length]; + + int i = 0; + for (int color : oldColors) { + colors[i++] = FireworkColor.fromJavaID(color).getBedrockID(); + } + + newExplosionData.put(new ByteArrayTag("FireworkFade", colors)); + } + + if (explosion.get("Trail") != null) { + newExplosionData.put(new ByteTag("FireworkTrail", MathUtils.convertByte(explosion.get("Trail").getValue()))); + } + + if (explosion.get("Flicker") != null) { + newExplosionData.put(new ByteTag("FireworkFlicker", MathUtils.convertByte(explosion.get("Flicker").getValue()))); + } + + return newExplosionData; + } + + protected CompoundTag translateExplosionToJava(CompoundTag explosion, String newName) { + CompoundTag newExplosionData = new CompoundTag(newName); + + if (explosion.get("FireworkType") != null) { + newExplosionData.put(new ByteTag("Type", MathUtils.convertByte(explosion.get("FireworkType").getValue()))); + } + + if (explosion.get("FireworkColor") != null) { + byte[] oldColors = (byte[]) explosion.get("FireworkColor").getValue(); + int[] colors = new int[oldColors.length]; + + int i = 0; + for (byte color : oldColors) { + colors[i++] = FireworkColor.fromBedrockID(color).getJavaID(); + } + + newExplosionData.put(new IntArrayTag("Colors", colors)); + } + + if (explosion.get("FireworkFade") != null) { + byte[] oldColors = (byte[]) explosion.get("FireworkFade").getValue(); + int[] colors = new int[oldColors.length]; + + int i = 0; + for (byte color : oldColors) { + colors[i++] = FireworkColor.fromBedrockID(color).getJavaID(); + } + + newExplosionData.put(new IntArrayTag("FadeColors", colors)); + } + + if (explosion.get("FireworkTrail") != null) { + newExplosionData.put(new ByteTag("Trail", MathUtils.convertByte(explosion.get("FireworkTrail").getValue()))); + } + + if (explosion.get("FireworkFlicker") != null) { + newExplosionData.put(new ByteTag("Flicker", MathUtils.convertByte(explosion.get("FireworkFlicker").getValue()))); + } + + return newExplosionData; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkRocketTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkRocketTranslator.java new file mode 100644 index 000000000..f294315c8 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkRocketTranslator.java @@ -0,0 +1,92 @@ +/* + * 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.item.translators.nbt; + +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.ItemRemapper; +import org.geysermc.connector.network.translators.item.ItemEntry; +import org.geysermc.connector.utils.MathUtils; + +@ItemRemapper +public class FireworkRocketTranslator extends FireworkBaseTranslator { + + @Override + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { + CompoundTag fireworks = itemTag.get("Fireworks"); + if (fireworks == null) { + return; + } + + if (fireworks.get("Flight") != null) { + fireworks.put(new ByteTag("Flight", MathUtils.convertByte(fireworks.get("Flight").getValue()))); + } + + ListTag explosions = fireworks.get("Explosions"); + if (explosions == null) { + return; + } + for (Tag effect : explosions.getValue()) { + CompoundTag effectData = (CompoundTag) effect; + CompoundTag newEffectData = translateExplosionToBedrock(effectData, ""); + + explosions.remove(effectData); + explosions.add(newEffectData); + } + } + + @Override + public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) { + CompoundTag fireworks = itemTag.get("Fireworks"); + if (fireworks == null) { + return; + } + + if (fireworks.contains("Flight")) { + fireworks.put(new ByteTag("Flight", MathUtils.convertByte(fireworks.get("Flight").getValue()))); + } + + ListTag explosions = fireworks.get("Explosions"); + if (explosions == null) { + return; + } + for (Tag effect : explosions.getValue()) { + CompoundTag effectData = (CompoundTag) effect; + CompoundTag newEffectData = translateExplosionToJava(effectData, ""); + + explosions.remove(effect); + explosions.add(newEffectData); + } + } + + @Override + public boolean acceptItem(ItemEntry itemEntry) { + return "minecraft:firework_rocket".equals(itemEntry.getJavaIdentifier()); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkStarTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkStarTranslator.java new file mode 100644 index 000000000..686887b45 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkStarTranslator.java @@ -0,0 +1,96 @@ +/* + * 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.item.translators.nbt; + +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.ItemRemapper; +import org.geysermc.connector.network.translators.item.ItemEntry; + +@ItemRemapper +public class FireworkStarTranslator extends FireworkBaseTranslator { + + @Override + public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { + Tag explosion = itemTag.get("Explosion"); + if (explosion instanceof CompoundTag) { + CompoundTag newExplosion = translateExplosionToBedrock((CompoundTag) explosion, "FireworksItem"); + itemTag.remove("Explosion"); + itemTag.put(newExplosion); + Tag color = ((CompoundTag) explosion).get("Colors"); + if (color instanceof IntArrayTag) { + // Determine the custom color, if any. + // Mostly replicates Java's own rendering code, as Java determines the final firework star color client-side + // while Bedrock determines it server-side. + int[] colors = ((IntArrayTag) color).getValue(); + if (colors.length == 0) { + return; + } + int finalColor; + if (colors.length == 1) { + finalColor = colors[0]; + } else { + int r = 0; + int g = 0; + int b = 0; + + for (int fireworkColor : colors) { + r += (fireworkColor & (255 << 16)) >> 16; + g += (fireworkColor & (255 << 8)) >> 8; + b += fireworkColor & 255; + } + + r /= colors.length; + g /= colors.length; + b /= colors.length; + finalColor = r << 16 | g << 8 | b; + } + + itemTag.put(new IntTag("customColor", finalColor)); + } + } + } + + @Override + public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) { + Tag explosion = itemTag.get("FireworksItem"); + if (explosion instanceof CompoundTag) { + CompoundTag newExplosion = translateExplosionToJava((CompoundTag) explosion, "Explosion"); + itemTag.remove("FireworksItem"); + itemTag.put(newExplosion); + } + // Remove custom color, if any, since this only exists on Bedrock + itemTag.remove("customColor"); + } + + @Override + public boolean acceptItem(ItemEntry itemEntry) { + return "minecraft:firework_star".equals(itemEntry.getJavaIdentifier()); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkTranslator.java deleted file mode 100644 index 8c5b74f13..000000000 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/FireworkTranslator.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.item.translators.nbt; - -import com.github.steveice10.opennbt.tag.builtin.*; -import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.ItemRemapper; -import org.geysermc.connector.network.translators.item.ItemEntry; -import org.geysermc.connector.network.translators.item.NbtItemStackTranslator; -import org.geysermc.connector.utils.FireworkColor; -import org.geysermc.connector.utils.MathUtils; - -@ItemRemapper -public class FireworkTranslator extends NbtItemStackTranslator { - - @Override - public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { - if (!itemTag.contains("Fireworks")) { - return; - } - - CompoundTag fireworks = itemTag.get("Fireworks"); - if (fireworks.get("Flight") != null) { - fireworks.put(new ByteTag("Flight", MathUtils.convertByte(fireworks.get("Flight").getValue()))); - } - - ListTag explosions = fireworks.get("Explosions"); - if (explosions == null) { - return; - } - for (Tag effect : explosions.getValue()) { - CompoundTag effectData = (CompoundTag) effect; - - CompoundTag newEffectData = new CompoundTag(""); - - if (effectData.get("Type") != null) { - newEffectData.put(new ByteTag("FireworkType", MathUtils.convertByte(effectData.get("Type").getValue()))); - } - - if (effectData.get("Colors") != null) { - int[] oldColors = (int[]) effectData.get("Colors").getValue(); - byte[] colors = new byte[oldColors.length]; - - int i = 0; - for (int color : oldColors) { - colors[i++] = FireworkColor.fromJavaID(color).getBedrockID(); - } - - newEffectData.put(new ByteArrayTag("FireworkColor", colors)); - } - - if (effectData.get("FadeColors") != null) { - int[] oldColors = (int[]) effectData.get("FadeColors").getValue(); - byte[] colors = new byte[oldColors.length]; - - int i = 0; - for (int color : oldColors) { - colors[i++] = FireworkColor.fromJavaID(color).getBedrockID(); - } - - newEffectData.put(new ByteArrayTag("FireworkFade", colors)); - } - - if (effectData.get("Trail") != null) { - newEffectData.put(new ByteTag("FireworkTrail", MathUtils.convertByte(effectData.get("Trail").getValue()))); - } - - if (effectData.get("Flicker") != null) { - newEffectData.put(new ByteTag("FireworkFlicker", MathUtils.convertByte(effectData.get("Flicker").getValue()))); - } - - explosions.remove(effect); - explosions.add(newEffectData); - } - } - - @Override - public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) { - if (!itemTag.contains("Fireworks")) { - return; - } - CompoundTag fireworks = itemTag.get("Fireworks"); - if (fireworks.contains("Flight")) { - fireworks.put(new ByteTag("Flight", MathUtils.convertByte(fireworks.get("Flight").getValue()))); - } - - if (!itemTag.contains("Explosions")) { - return; - } - ListTag explosions = fireworks.get("Explosions"); - for (Tag effect : explosions.getValue()) { - CompoundTag effectData = (CompoundTag) effect; - - CompoundTag newEffectData = new CompoundTag(""); - - if (effectData.get("FireworkType") != null) { - newEffectData.put(new ByteTag("Type", MathUtils.convertByte(effectData.get("FireworkType").getValue()))); - } - - if (effectData.get("FireworkColor") != null) { - byte[] oldColors = (byte[]) effectData.get("FireworkColor").getValue(); - int[] colors = new int[oldColors.length]; - - int i = 0; - for (byte color : oldColors) { - colors[i++] = FireworkColor.fromBedrockID(color).getJavaID(); - } - - newEffectData.put(new IntArrayTag("Colors", colors)); - } - - if (effectData.get("FireworkFade") != null) { - byte[] oldColors = (byte[]) effectData.get("FireworkFade").getValue(); - int[] colors = new int[oldColors.length]; - - int i = 0; - for (byte color : oldColors) { - colors[i++] = FireworkColor.fromBedrockID(color).getJavaID(); - } - - newEffectData.put(new IntArrayTag("FadeColors", colors)); - } - - if (effectData.get("FireworkTrail") != null) { - newEffectData.put(new ByteTag("Trail", MathUtils.convertByte(effectData.get("FireworkTrail").getValue()))); - } - - if (effectData.get("FireworkFlicker") != null) { - newEffectData.put(new ByteTag("Flicker", MathUtils.convertByte(effectData.get("FireworkFlicker").getValue()))); - } - - explosions.remove(effect); - explosions.add(newEffectData); - } - } - - @Override - public boolean acceptItem(ItemEntry itemEntry) { - return "minecraft:firework_rocket".equals(itemEntry.getJavaIdentifier()); - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java index f78eadc25..c2305738d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java @@ -35,29 +35,28 @@ import org.geysermc.connector.network.translators.item.ItemEntry; @ItemRemapper public class LeatherArmorTranslator extends NbtItemStackTranslator { - private static final String[] ITEMS = new String[]{"minecraft:leather_helmet", "minecraft:leather_chestplate", "minecraft:leather_leggings", "minecraft:leather_boots"}; + private static final String[] ITEMS = new String[]{"minecraft:leather_helmet", "minecraft:leather_chestplate", + "minecraft:leather_leggings", "minecraft:leather_boots", "minecraft:leather_horse_armor"}; @Override public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) { - if (!itemTag.contains("display")) { + CompoundTag displayTag = itemTag.get("display"); + if (displayTag == null) { return; } - CompoundTag displayTag = itemTag.get("display"); - if (displayTag.contains("color")) { - IntTag color = displayTag.get("color"); - if (color != null) { - itemTag.put(new IntTag("customColor", color.getValue())); - displayTag.remove("color"); - } + IntTag color = displayTag.get("color"); + if (color != null) { + itemTag.put(new IntTag("customColor", color.getValue())); + displayTag.remove("color"); } } @Override public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) { - if (!itemTag.contains("customColor")) { + IntTag color = itemTag.get("customColor"); + if (color == null) { return; } - IntTag color = itemTag.get("customColor"); CompoundTag displayTag = itemTag.get("display"); if (displayTag == null) { displayTag = new CompoundTag("display"); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java index f6664c1a6..7de101811 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java @@ -33,21 +33,69 @@ import com.nukkitx.protocol.bedrock.data.command.CommandEnumData; import com.nukkitx.protocol.bedrock.data.command.CommandParamData; import com.nukkitx.protocol.bedrock.data.command.CommandParamType; import com.nukkitx.protocol.bedrock.packet.AvailableCommandsPacket; +import it.unimi.dsi.fastutil.Hash; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap; import lombok.Getter; +import lombok.ToString; +import net.kyori.adventure.text.format.NamedTextColor; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.item.Enchantment; +import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; @Translator(packet = ServerDeclareCommandsPacket.class) public class JavaDeclareCommandsTranslator extends PacketTranslator { + + private static final String[] ENUM_BOOLEAN = {"true", "false"}; + private static final String[] VALID_COLORS; + private static final String[] VALID_SCOREBOARD_SLOTS; + + private static final Hash.Strategy PARAM_STRATEGY = new Hash.Strategy() { + @Override + public int hashCode(CommandParamData[][] o) { + return Arrays.deepHashCode(o); + } + + @Override + public boolean equals(CommandParamData[][] a, CommandParamData[][] b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + CommandParamData[] a1 = a[i]; + CommandParamData[] b1 = b[i]; + if (a1.length != b1.length) return false; + + for (int j = 0; j < a1.length; j++) { + if (!a1[j].equals(b1[j])) return false; + } + } + return true; + } + }; + + static { + List validColors = new ArrayList<>(NamedTextColor.NAMES.keys()); + validColors.add("reset"); + VALID_COLORS = validColors.toArray(new String[0]); + + List teamOptions = new ArrayList<>(Arrays.asList("list", "sidebar", "belowName")); + for (String color : NamedTextColor.NAMES.keys()) { + teamOptions.add("sidebar.team." + color); + } + VALID_SCOREBOARD_SLOTS = teamOptions.toArray(new String[0]); + } + @Override public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) { // Don't send command suggestions if they are disabled @@ -60,48 +108,50 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator commandData = new ArrayList<>(); - Int2ObjectMap commands = new Int2ObjectOpenHashMap<>(); + IntSet commandNodes = new IntOpenHashSet(); + Set knownAliases = new HashSet<>(); + Map> commands = new Object2ObjectOpenCustomHashMap<>(PARAM_STRATEGY); Int2ObjectMap> commandArgs = new Int2ObjectOpenHashMap<>(); // Get the first node, it should be a root node - CommandNode rootNode = packet.getNodes()[packet.getFirstNodeIndex()]; + CommandNode rootNode = nodes[packet.getFirstNodeIndex()]; // Loop through the root nodes to get all commands for (int nodeIndex : rootNode.getChildIndices()) { - CommandNode node = packet.getNodes()[nodeIndex]; + CommandNode node = nodes[nodeIndex]; // Make sure we don't have duplicated commands (happens if there is more than 1 root node) - if (commands.containsKey(nodeIndex)) { continue; } - if (commands.containsValue(node.getName())) { continue; } + if (!commandNodes.add(nodeIndex) || !knownAliases.add(node.getName().toLowerCase())) continue; // Get and update the commandArgs list with the found arguments if (node.getChildIndices().length >= 1) { for (int childIndex : node.getChildIndices()) { - commandArgs.putIfAbsent(nodeIndex, new ArrayList<>()); - commandArgs.get(nodeIndex).add(packet.getNodes()[childIndex]); + commandArgs.computeIfAbsent(nodeIndex, ArrayList::new).add(nodes[childIndex]); } } - // Insert the command name into the list - commands.put(nodeIndex, node.getName()); + // Get and parse all params + CommandParamData[][] params = getParams(nodes[nodeIndex], nodes); + + // Insert the alias name into the command list + commands.computeIfAbsent(params, index -> new HashSet<>()).add(node.getName().toLowerCase()); } // The command flags, not sure what these do apart from break things List flags = Collections.emptyList(); // Loop through all the found commands - for (int commandID : commands.keySet()) { - String commandName = commands.get(commandID); + + for (Map.Entry> entry : commands.entrySet()) { + String commandName = entry.getValue().iterator().next(); // We know this has a value // Create a basic alias - CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", new String[] { commandName.toLowerCase() }, false); - - // Get and parse all params - CommandParamData[][] params = getParams(packet.getNodes()[commandID], packet.getNodes()); + CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", entry.getValue().toArray(new String[0]), false); // Build the completed command and add it to the final list - CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, params); + CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, entry.getKey()); commandData.add(data); } @@ -109,7 +159,7 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator treeData = rootParam.getTree(); - CommandParamData[][] params = new CommandParamData[treeData.size()][]; - // Fill the nested params array - int i = 0; - for (CommandParamData[] tree : treeData) { - params[i] = tree; - i++; - } - - return params; + return treeData.toArray(new CommandParamData[0][]); } return new CommandParamData[0][0]; @@ -155,14 +196,17 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator=", "==", etc + return CommandParamType.OPERATOR; + case BLOCK_STATE: - case BLOCK_PREDICATE: + return BlockTranslator.getAllBlockIdentifiers(); + case ITEM_STACK: - case ITEM_PREDICATE: - case COLOR: - case COMPONENT: - case OBJECTIVE: - case OBJECTIVE_CRITERIA: - case OPERATION: // Possibly OPERATOR - case PARTICLE: - case ROTATION: - case SCOREBOARD_SLOT: - case SCORE_HOLDER: - case SWIZZLE: - case TEAM: - case ITEM_SLOT: - case MOB_EFFECT: - case FUNCTION: - case ENTITY_ANCHOR: - case RANGE: - case FLOAT_RANGE: + return ItemRegistry.ITEM_NAMES; + case ITEM_ENCHANTMENT: + return Enchantment.ALL_JAVA_IDENTIFIERS; //TODO: inventory branch use Java enums + case ENTITY_SUMMON: - case DIMENSION: - case TIME: + return EntityType.ALL_JAVA_IDENTIFIERS; + + case COLOR: + return VALID_COLORS; + + case SCOREBOARD_SLOT: + return VALID_SCOREBOARD_SLOTS; + default: return CommandParamType.STRING; } } @Getter - private class ParamInfo { - private CommandNode paramNode; - private CommandParamData paramData; - private List children; + @ToString + private static class ParamInfo { + private final CommandNode paramNode; + private final CommandParamData paramData; + private final List children; /** * Create a new parameter info object @@ -252,33 +290,50 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator + // So if paramNode.getName() == "value" and enumData.getName() == "bool": + children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), this.paramNode.isExecutable(), enumData, type, null, Collections.emptyList()))); } } @@ -288,6 +343,64 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator + * Take the gamerule command, and let's present three "subcommands" you can perform: + * + *

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

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

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

+         *     overloads=[
+         *         [
+         *            CommandParamData(name=doDaylightCycle, optional=false, enumData=CommandEnumData(name=announceAdvancements, values=[announceAdvancements, doDaylightCycle], isSoft=false), type=STRING, postfix=null, options=[])
+         *            CommandParamData(name=value, optional=false, enumData=CommandEnumData(name=value, values=[true, false], isSoft=false), type=null, postfix=null, options=[])
+         *         ]
+         *         [
+         *            CommandParamData(name=randomTickSpeed, optional=false, enumData=CommandEnumData(name=randomTickSpeed, values=[randomTickSpeed], isSoft=false), type=STRING, postfix=null, options=[])
+         *            CommandParamData(name=value, optional=false, enumData=null, type=INT, postfix=null, options=[])
+         *         ]
+         *     ]
+         * 
+ * + * @return if these two can be merged into one overload. + */ + private boolean isCompatible(CommandNode[] allNodes, CommandNode a, CommandNode b) { + if (a == b) return true; + if (a.getParser() != b.getParser()) return false; + if (a.getChildIndices().length != b.getChildIndices().length) return false; + + for (int i = 0; i < a.getChildIndices().length; i++) { + boolean hasSimilarity = false; + CommandNode a1 = allNodes[a.getChildIndices()[i]]; + // Search "b" until we find a child that matches this one + for (int j = 0; j < b.getChildIndices().length; j++) { + if (isCompatible(allNodes, a1, allNodes[b.getChildIndices()[j]])) { + hasSimilarity = true; + break; + } + } + + if (!hasSimilarity) { + return false; + } + } + return true; + } + /** * Get the tree of every parameter node (recursive) * @@ -301,13 +414,10 @@ public class JavaDeclareCommandsTranslator extends PacketTranslator childTree = child.getTree(); // Un-pack the tree append the child node to it and push into the list - for (CommandParamData[] subchild : childTree) { - CommandParamData[] tmpTree = new ArrayList() { - { - add(child.getParamData()); - addAll(Arrays.asList(subchild)); - } - }.toArray(new CommandParamData[0]); + for (CommandParamData[] subChild : childTree) { + CommandParamData[] tmpTree = new CommandParamData[subChild.length + 1]; + tmpTree[0] = child.getParamData(); + System.arraycopy(subChild, 0, tmpTree, 1, subChild.length); treeParamData.add(tmpTree); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java index 33ebc7ea9..bf78d52c6 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 @@ -25,17 +25,18 @@ package org.geysermc.connector.network.translators.java; +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.Recipe; import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; +import com.github.steveice10.mc.protocol.data.game.recipe.data.StoneCuttingRecipeData; import com.github.steveice10.mc.protocol.packet.ingame.server.ServerDeclareRecipesPacket; import com.nukkitx.nbt.NbtMap; import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.ints.*; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import org.geysermc.connector.network.session.GeyserSession; @@ -46,13 +47,20 @@ import org.geysermc.connector.network.translators.item.*; import java.util.*; import java.util.stream.Collectors; +/** + * Used to send all valid recipes from Java to Bedrock. + * + * Bedrock REQUIRES a CraftingDataPacket to be sent in order to craft anything. + */ @Translator(packet = ServerDeclareRecipesPacket.class) public class JavaDeclareRecipesTranslator extends PacketTranslator { @Override public void translate(ServerDeclareRecipesPacket packet, GeyserSession session) { // Get the last known network ID (first used for the pregenerated recipes) and increment from there. - int networkId = RecipeRegistry.LAST_RECIPE_NET_ID; + int netId = RecipeRegistry.LAST_RECIPE_NET_ID + 1; + Int2ObjectMap recipeMap = new Int2ObjectOpenHashMap<>(RecipeRegistry.ALL_CRAFTING_RECIPES); + Int2ObjectMap> unsortedStonecutterData = new Int2ObjectOpenHashMap<>(); CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); craftingDataPacket.setCleanRecipes(true); for (Recipe recipe : packet.getRecipes()) { @@ -60,25 +68,29 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator data = unsortedStonecutterData.get(ingredient.getId()); + if (data == null) { + data = new ArrayList<>(); + unsortedStonecutterData.put(ingredient.getId(), data); + } + data.add(stoneCuttingData); + // Save for processing after all recipes have been received + } } } + // Add all cartography table recipe UUIDs, so we can use the cartography table + craftingDataPacket.getCraftingData().addAll(RecipeRegistry.CARTOGRAPHY_RECIPE_DATA); + craftingDataPacket.getPotionMixData().addAll(PotionMixRegistry.POTION_MIXES); + + Int2ObjectMap stonecutterRecipeMap = new Int2ObjectOpenHashMap<>(); + for (Int2ObjectMap.Entry> data : unsortedStonecutterData.int2ObjectEntrySet()) { + // Sort the list by each output item's Java identifier - this is how it's sorted on Java, and therefore + // We can get the correct order for button pressing + data.getValue().sort(Comparator.comparing((stoneCuttingRecipeData -> + ItemRegistry.getItem(stoneCuttingRecipeData.getResult()).getJavaIdentifier()))); + + // Now that it's sorted, let's translate these recipes + for (StoneCuttingRecipeData stoneCuttingData : data.getValue()) { + // As of 1.16.4, all stonecutter recipes have one ingredient option + ItemStack ingredient = stoneCuttingData.getIngredient().getOptions()[0]; + ItemData input = ItemTranslator.translateToBedrock(session, ingredient); + ItemData output = ItemTranslator.translateToBedrock(session, stoneCuttingData.getResult()); + UUID uuid = UUID.randomUUID(); + + // We need to register stonecutting recipes so they show up on Bedrock + craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(), + Collections.singletonList(input), Collections.singletonList(output), uuid, "stonecutter", 0, netId++)); + + // Save the recipe list for reference when crafting + IntList outputs = stonecutterRecipeMap.get(ingredient.getId()); + if (outputs == null) { + outputs = new IntArrayList(); + // Add the ingredient as the key and all possible values as the value + stonecutterRecipeMap.put(ingredient.getId(), outputs); + } + outputs.add(stoneCuttingData.getResult().getId()); + } + } + session.sendUpstreamPacket(craftingDataPacket); + session.setCraftingRecipes(recipeMap); + session.getUnlockedRecipes().clear(); + session.setStonecutterRecipes(stonecutterRecipeMap); + session.getLastRecipeNetId().set(netId); } //TODO: rewrite + /** + * The Java server sends an array of items for each ingredient you can use per slot in the crafting grid. + * Bedrock recipes take only one ingredient per crafting grid slot. + * + * @return the Java ingredient list as an array that Bedrock can understand + */ private ItemData[][] combinations(GeyserSession session, Ingredient[] ingredients) { Map, IntSet> squashedOptions = new HashMap<>(); for (int i = 0; i < ingredients.length; i++) { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java index 17b5087ec..3e37f9eae 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java @@ -97,7 +97,7 @@ public class JavaJoinGameTranslator extends PacketTranslator { +public class JavaLoginPluginRequestTranslator extends PacketTranslator { @Override public void translate(LoginPluginRequestPacket packet, GeyserSession session) { // A vanilla client doesn't know any PluginMessage in the Login state, so we don't know any either. + // Note: Fabric Networking API v1 will not let the client log in without sending this session.sendDownstreamPacket( new LoginPluginResponsePacket(packet.getMessageId(), null) ); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaPluginMessageTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaPluginMessageTranslator.java index 337dc0b74..cb80a68a4 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaPluginMessageTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaPluginMessageTranslator.java @@ -42,8 +42,8 @@ import java.nio.charset.StandardCharsets; public class JavaPluginMessageTranslator extends PacketTranslator { @Override public void translate(ServerPluginMessagePacket packet, GeyserSession session) { - // The only plugin messages to listen for are Floodgate plugin messages - if (session.getConnector().getAuthType() != AuthType.FLOODGATE) { + // The only plugin messages it has to listen for are Floodgate plugin messages + if (session.getConnector().getDefaultAuthType() != AuthType.FLOODGATE) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaRespawnTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaRespawnTranslator.java index 6c8faeb52..7c8cd0583 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaRespawnTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaRespawnTranslator.java @@ -35,6 +35,7 @@ import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; import org.geysermc.connector.utils.DimensionUtils; @Translator(packet = ServerRespawnPacket.class) @@ -48,7 +49,11 @@ public class JavaRespawnTranslator extends PacketTranslator // Max health must be divisible by two in bedrock entity.getAttributes().put(AttributeType.HEALTH, AttributeType.HEALTH.getAttribute(maxHealth, (maxHealth % 2 == 1 ? maxHealth + 1 : maxHealth))); - session.getInventoryCache().setOpenInventory(null); + session.addInventoryTask(() -> { + session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); + session.setOpenInventory(null); + session.setClosingInventory(false); + }); SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket(); playerGameTypePacket.setGamemode(packet.getGamemode().ordinal()); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaTitleTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaTitleTranslator.java index d3b93068a..ffda57826 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaTitleTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaTitleTranslator.java @@ -71,6 +71,7 @@ public class JavaTitleTranslator extends PacketTranslator { titlePacket.setFadeInTime(packet.getFadeIn()); titlePacket.setFadeOutTime(packet.getFadeOut()); titlePacket.setStayTime(packet.getStay()); + titlePacket.setText(""); break; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaUnlockRecipesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaUnlockRecipesTranslator.java new file mode 100644 index 000000000..0a0ba4d2d --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaUnlockRecipesTranslator.java @@ -0,0 +1,51 @@ +/* + * 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.java; + +import com.github.steveice10.mc.protocol.data.game.UnlockRecipesAction; +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerUnlockRecipesPacket; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; + +import java.util.Arrays; + +/** + * Used to list recipes that we can definitely use the recipe book for (and therefore save on packet usage) + */ +@Translator(packet = ServerUnlockRecipesPacket.class) +public class JavaUnlockRecipesTranslator extends PacketTranslator { + + @Override + public void translate(ServerUnlockRecipesPacket packet, GeyserSession session) { + if (packet.getAction() == UnlockRecipesAction.REMOVE) { + session.getUnlockedRecipes().removeAll(Arrays.asList(packet.getRecipes())); + } else { + session.getUnlockedRecipes().addAll(Arrays.asList(packet.getRecipes())); + } + } +} + diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java index 53c2864c8..735a5ea47 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityAnimationTranslator.java @@ -60,6 +60,9 @@ public class JavaEntityAnimationTranslator extends PacketTranslator { + PlayerHotbarPacket hotbarPacket = new PlayerHotbarPacket(); + hotbarPacket.setContainerId(0); + hotbarPacket.setSelectedHotbarSlot(packet.getSlot()); + hotbarPacket.setSelectHotbarSlot(true); + session.sendUpstreamPacket(hotbarPacket); - session.getInventory().setHeldItemSlot(packet.getSlot()); + session.getPlayerInventory().setHeldItemSlot(packet.getSlot()); + }); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnEntityTranslator.java index efbdfc5b2..b6a41fa42 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/spawn/JavaSpawnEntityTranslator.java @@ -32,6 +32,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnEntityPacket; import com.nukkitx.math.vector.Vector3f; import org.geysermc.connector.entity.*; +import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; @@ -69,8 +70,18 @@ public class JavaSpawnEntityTranslator extends PacketTranslator + // Sometimes the server can request a window close of ID 0... when the window isn't even open + // Don't confirm in this instance + InventoryUtils.closeInventory(session, packet.getWindowId(), (session.getOpenInventory() != null && session.getOpenInventory().getId() == packet.getWindowId()))); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java index cc0d153b4..3b55733bf 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.connector.network.translators.java.window; import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientConfirmTransactionPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerConfirmTransactionPacket; +import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; @@ -36,9 +37,11 @@ public class JavaConfirmTransactionTranslator extends PacketTranslator { + if (!packet.isAccepted()) { + ClientConfirmTransactionPacket confirmPacket = new ClientConfirmTransactionPacket(packet.getWindowId(), packet.getActionId(), true); + session.sendDownstreamPacket(confirmPacket); + } + }); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenHorseWindowTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenHorseWindowTranslator.java new file mode 100644 index 000000000..5016b6150 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenHorseWindowTranslator.java @@ -0,0 +1,137 @@ +/* + * 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.java.window; + +import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerOpenHorseWindowPacket; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.nbt.NbtType; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.packet.UpdateEquipPacket; +import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.living.animal.horse.ChestedHorseEntity; +import org.geysermc.connector.entity.living.animal.horse.LlamaEntity; +import org.geysermc.connector.inventory.Container; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.inventory.InventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.horse.DonkeyInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.horse.HorseInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.horse.LlamaInventoryTranslator; +import org.geysermc.connector.utils.InventoryUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Translator(packet = ServerOpenHorseWindowPacket.class) +public class JavaOpenHorseWindowTranslator extends PacketTranslator { + + private static final NbtMap ARMOR_SLOT; + private static final NbtMap CARPET_SLOT; + private static final NbtMap SADDLE_SLOT; + + static { + // Build the NBT mappings that Bedrock wants to lay out the GUI + String[] acceptedHorseArmorIdentifiers = new String[] {"minecraft:horsearmorleather", "minecraft:horsearmoriron", + "minecraft:horsearmorgold", "minecraft:horsearmordiamond"}; + NbtMapBuilder armorBuilder = NbtMap.builder(); + List acceptedArmors = new ArrayList<>(4); + for (String identifier : acceptedHorseArmorIdentifiers) { + NbtMapBuilder acceptedItemBuilder = NbtMap.builder() + .putShort("Aux", Short.MAX_VALUE) + .putString("Name", identifier); + acceptedArmors.add(NbtMap.builder().putCompound("slotItem", acceptedItemBuilder.build()).build()); + } + armorBuilder.putList("acceptedItems", NbtType.COMPOUND, acceptedArmors); + NbtMapBuilder armorItem = NbtMap.builder() + .putShort("Aux", Short.MAX_VALUE) + .putString("Name", "minecraft:horsearmoriron"); + armorBuilder.putCompound("item", armorItem.build()); + armorBuilder.putInt("slotNumber", 1); + ARMOR_SLOT = armorBuilder.build(); + + NbtMapBuilder carpetBuilder = NbtMap.builder(); + NbtMapBuilder carpetItem = NbtMap.builder() + .putShort("Aux", Short.MAX_VALUE) + .putString("Name", "minecraft:carpet"); + List acceptedCarpet = Collections.singletonList(NbtMap.builder().putCompound("slotItem", carpetItem.build()).build()); + carpetBuilder.putList("acceptedItems", NbtType.COMPOUND, acceptedCarpet); + carpetBuilder.putCompound("item", carpetItem.build()); + carpetBuilder.putInt("slotNumber", 1); + CARPET_SLOT = carpetBuilder.build(); + + NbtMapBuilder saddleBuilder = NbtMap.builder(); + NbtMapBuilder acceptedSaddle = NbtMap.builder() + .putShort("Aux", Short.MAX_VALUE) + .putString("Name", "minecraft:saddle"); + List acceptedItem = Collections.singletonList(NbtMap.builder().putCompound("slotItem", acceptedSaddle.build()).build()); + saddleBuilder.putList("acceptedItems", NbtType.COMPOUND, acceptedItem); + saddleBuilder.putCompound("item", acceptedSaddle.build()); + saddleBuilder.putInt("slotNumber", 0); + SADDLE_SLOT = saddleBuilder.build(); + } + + @Override + public void translate(ServerOpenHorseWindowPacket packet, GeyserSession session) { + Entity entity = session.getEntityCache().getEntityByJavaId(packet.getEntityId()); + if (entity == null) { + return; + } + + UpdateEquipPacket updateEquipPacket = new UpdateEquipPacket(); + updateEquipPacket.setWindowId((short) packet.getWindowId()); + updateEquipPacket.setWindowType((short) ContainerType.HORSE.getId()); + updateEquipPacket.setUniqueEntityId(entity.getGeyserId()); + + NbtMapBuilder builder = NbtMap.builder(); + List slots = new ArrayList<>(); + + InventoryTranslator inventoryTranslator; + if (entity instanceof LlamaEntity) { + inventoryTranslator = new LlamaInventoryTranslator(packet.getNumberOfSlots()); + slots.add(CARPET_SLOT); + } else if (entity instanceof ChestedHorseEntity) { + inventoryTranslator = new DonkeyInventoryTranslator(packet.getNumberOfSlots()); + slots.add(SADDLE_SLOT); + } else { + inventoryTranslator = new HorseInventoryTranslator(packet.getNumberOfSlots()); + slots.add(SADDLE_SLOT); + slots.add(ARMOR_SLOT); + } + + // Build the NbtMap that sets the icons for Bedrock (e.g. sets the saddle outline on the saddle slot) + builder.putList("slots", NbtType.COMPOUND, slots); + + updateEquipPacket.setTag(builder.build()); + session.sendUpstreamPacket(updateEquipPacket); + + session.setInventoryTranslator(inventoryTranslator); + InventoryUtils.openInventory(session, new Container(entity.getMetadata().getString(EntityData.NAMETAG), packet.getWindowId(), packet.getNumberOfSlots(), null, session.getPlayerInventory())); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java index 4c50b3131..79abcc957 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java @@ -41,35 +41,38 @@ public class JavaOpenWindowTranslator extends PacketTranslator { + if (packet.getWindowId() == 0) { + return; + } + + InventoryTranslator newTranslator = InventoryTranslator.INVENTORY_TRANSLATORS.get(packet.getType()); + Inventory openInventory = session.getOpenInventory(); + //No translator exists for this window type. Close all windows and return. + if (newTranslator == null) { + if (openInventory != null) { + InventoryUtils.closeInventory(session, openInventory.getId(), true); + } + ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(packet.getWindowId()); + session.sendDownstreamPacket(closeWindowPacket); + return; + } + + String name = MessageTranslator.convertMessageLenient(packet.getName(), session.getLocale()); + name = LocaleUtils.getLocaleString(name, session.getLocale()); + + Inventory newInventory = newTranslator.createInventory(name, packet.getWindowId(), packet.getType(), session.getPlayerInventory()); if (openInventory != null) { - InventoryUtils.closeWindow(session, openInventory.getId()); - InventoryUtils.closeInventory(session, openInventory.getId()); + // If the window type is the same, don't close. + // In rare cases, inventories can do funny things where it keeps the same window type up but change the contents. + if (openInventory.getWindowType() != packet.getType()) { + // Sometimes the server can double-open an inventory with the same ID - don't confirm in that instance. + InventoryUtils.closeInventory(session, openInventory.getId(), openInventory.getId() != packet.getWindowId()); + } } - ClientCloseWindowPacket closeWindowPacket = new ClientCloseWindowPacket(packet.getWindowId()); - session.sendDownstreamPacket(closeWindowPacket); - return; - } - String name = MessageTranslator.convertMessageLenient(packet.getName(), session.getLocale()); - - name = LocaleUtils.getLocaleString(name, session.getLocale()); - - Inventory newInventory = new Inventory(name, packet.getWindowId(), packet.getType(), newTranslator.size + 36); - session.getInventoryCache().cacheInventory(newInventory); - if (openInventory != null) { - InventoryTranslator openTranslator = InventoryTranslator.INVENTORY_TRANSLATORS.get(openInventory.getWindowType()); - if (!openTranslator.getClass().equals(newTranslator.getClass())) { - InventoryUtils.closeWindow(session, openInventory.getId()); - InventoryUtils.closeInventory(session, openInventory.getId()); - } - } - - InventoryUtils.openInventory(session, newInventory); + session.setInventoryTranslator(newTranslator); + InventoryUtils.openInventory(session, newInventory); + }); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java index 8caa25183..a0e9901f3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java @@ -25,36 +25,253 @@ package org.geysermc.connector.network.translators.java.window; +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.Recipe; +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.github.steveice10.mc.protocol.packet.ingame.server.window.ServerSetSlotPacket; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; +import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket; +import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; +import org.geysermc.connector.inventory.GeyserItemStack; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.CraftingInventoryTranslator; +import org.geysermc.connector.network.translators.inventory.translators.PlayerInventoryTranslator; +import org.geysermc.connector.network.translators.item.ItemTranslator; import org.geysermc.connector.utils.InventoryUtils; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + @Translator(packet = ServerSetSlotPacket.class) public class JavaSetSlotTranslator extends PacketTranslator { @Override public void translate(ServerSetSlotPacket packet, GeyserSession session) { - if (packet.getWindowId() == 255 && packet.getSlot() == -1) { //cursor - if (session.getCraftSlot() != 0) + session.addInventoryTask(() -> { + if (packet.getWindowId() == 255) { //cursor + GeyserItemStack newItem = GeyserItemStack.from(packet.getItem()); + session.getPlayerInventory().setCursor(newItem, session); + InventoryUtils.updateCursor(session); + return; + } + + //TODO: support window id -2, should update player inventory + Inventory inventory = InventoryUtils.getInventory(session, packet.getWindowId()); + if (inventory == null) return; - session.getInventory().setCursor(packet.getItem()); - InventoryUtils.updateCursor(session); - return; - } + InventoryTranslator translator = session.getInventoryTranslator(); + if (translator != null) { + if (session.getCraftingGridFuture() != null) { + session.getCraftingGridFuture().cancel(false); + } + session.setCraftingGridFuture(session.getConnector().getGeneralThreadPool().schedule(() -> session.addInventoryTask(() -> updateCraftingGrid(session, packet, inventory, translator)), 150, TimeUnit.MILLISECONDS)); - Inventory inventory = session.getInventoryCache().getInventories().get(packet.getWindowId()); - if (inventory == null || (packet.getWindowId() != 0 && inventory.getWindowType() == null)) - return; + GeyserItemStack newItem = GeyserItemStack.from(packet.getItem()); + if (packet.getWindowId() == 0 && !(translator instanceof PlayerInventoryTranslator)) { + // In rare cases, the window ID can still be 0 but Java treats it as valid + session.getPlayerInventory().setItem(packet.getSlot(), newItem, session); + InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), packet.getSlot()); + } else { + inventory.setItem(packet.getSlot(), newItem, session); + translator.updateSlot(session, inventory, packet.getSlot()); + } + } + }); + } - InventoryTranslator translator = InventoryTranslator.INVENTORY_TRANSLATORS.get(inventory.getWindowType()); - if (translator != null) { - inventory.setItem(packet.getSlot(), packet.getItem()); - 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; + + 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) || + !testShapedRecipe(mirroredIngredients, inventory, gridDimensions, firstRow, height, firstCol, width)) { + continue; + } + } + // 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; + } + } + + 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()); + // 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); + 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/window/JavaWindowItemsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowItemsTranslator.java index a50518e86..7d29f54b3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowItemsTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowItemsTranslator.java @@ -26,32 +26,33 @@ package org.geysermc.connector.network.translators.java.window; import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerWindowItemsPacket; +import org.geysermc.connector.inventory.GeyserItemStack; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; - -import java.util.Arrays; +import org.geysermc.connector.utils.InventoryUtils; @Translator(packet = ServerWindowItemsPacket.class) public class JavaWindowItemsTranslator extends PacketTranslator { @Override public void translate(ServerWindowItemsPacket packet, GeyserSession session) { - Inventory inventory = session.getInventoryCache().getInventories().get(packet.getWindowId()); - if (inventory == null || (packet.getWindowId() != 0 && inventory.getWindowType() == null)) - return; + session.addInventoryTask(() -> { + Inventory inventory = InventoryUtils.getInventory(session, packet.getWindowId()); + if (inventory == null) + return; - if (packet.getItems().length < inventory.getSize()) { - inventory.setItems(Arrays.copyOf(packet.getItems(), inventory.getSize())); - } else { - inventory.setItems(packet.getItems()); - } + for (int i = 0; i < packet.getItems().length; i++) { + GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]); + inventory.setItem(i, newItem, session); + } - InventoryTranslator translator = InventoryTranslator.INVENTORY_TRANSLATORS.get(inventory.getWindowType()); - if (translator != null) { - translator.updateInventory(session, inventory); - } + InventoryTranslator translator = session.getInventoryTranslator(); + if (translator != null) { + translator.updateInventory(session, inventory); + } + }); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java index d325f36d6..c31a39029 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java @@ -31,19 +31,22 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; +import org.geysermc.connector.utils.InventoryUtils; @Translator(packet = ServerWindowPropertyPacket.class) public class JavaWindowPropertyTranslator extends PacketTranslator { @Override public void translate(ServerWindowPropertyPacket packet, GeyserSession session) { - Inventory inventory = session.getInventoryCache().getInventories().get(packet.getWindowId()); - if (inventory == null || (packet.getWindowId() != 0 && inventory.getWindowType() == null)) - return; + session.addInventoryTask(() -> { + Inventory inventory = InventoryUtils.getInventory(session, packet.getWindowId()); + if (inventory == null) + return; - InventoryTranslator translator = InventoryTranslator.INVENTORY_TRANSLATORS.get(inventory.getWindowType()); - if (translator != null) { - translator.updateProperty(session, inventory, packet.getRawProperty(), packet.getValue()); - } + InventoryTranslator translator = session.getInventoryTranslator(); + if (translator != null) { + translator.updateProperty(session, inventory, packet.getRawProperty(), packet.getValue()); + } + }); } } 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 d74165b14..e362e335f 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 @@ -84,7 +84,7 @@ public class JavaBlockChangeTranslator extends PacketTranslator tags = new ArrayList<>(); - for (VillagerTrade trade : packet.getTrades()) { + boolean addExtraTrade = packet.isRegularVillager() && packet.getVillagerLevel() < 5; + List tags = new ArrayList<>(addExtraTrade ? packet.getTrades().length + 1 : packet.getTrades().length); + for (int i = 0; i < packet.getTrades().length; i++) { + VillagerTrade trade = packet.getTrades()[i]; NbtMapBuilder recipe = NbtMap.builder(); - recipe.putInt("maxUses", trade.getMaxUses()); + recipe.putInt("netId", i + 1); + recipe.putInt("maxUses", trade.isTradeDisabled() ? 0 : trade.getMaxUses()); recipe.putInt("traderExp", trade.getXp()); recipe.putFloat("priceMultiplierA", trade.getPriceMultiplier()); recipe.put("sell", getItemTag(session, trade.getOutput(), 0)); @@ -106,7 +105,7 @@ public class JavaTradeListTranslator extends PacketTranslator