diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 000000000..598cab46a --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,36 @@ +name: SonarCloud +on: + push: + branches: + - master +jobs: + build: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + submodules: true + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: 17 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=GeyserMC_Geyser \ No newline at end of file diff --git a/.gitignore b/.gitignore index 85f8a6e9e..401002e1d 100644 --- a/.gitignore +++ b/.gitignore @@ -239,8 +239,9 @@ nbdist/ run/ config.yml logs/ -public-key.pem +key.pem locales/ /cache/ /packs/ -/dump.json \ No newline at end of file +/dump.json +/saved-refresh-tokens.json \ No newline at end of file diff --git a/README.md b/README.md index 593514e52..885ec920b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,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 joined us here! -### Currently supporting Minecraft Bedrock 1.17.41 + 1.18.0 - 1.18.10 and Minecraft Java 1.18/1.18.1. +### Currently supporting Minecraft Bedrock 1.17.41 + 1.18.0 - 1.18.10 and Minecraft Java 1.18.2. ## Setting Up Take a look [here](https://github.com/GeyserMC/Geyser/wiki/Setup) for how to set up Geyser. diff --git a/ap/pom.xml b/ap/pom.xml index dce28a7d7..75f98275c 100644 --- a/ap/pom.xml +++ b/ap/pom.xml @@ -6,9 +6,9 @@ org.geysermc geyser-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT ap - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT \ No newline at end of file diff --git a/api/base/pom.xml b/api/base/pom.xml index 17edb1a85..37e97ef7e 100644 --- a/api/base/pom.xml +++ b/api/base/pom.xml @@ -5,7 +5,7 @@ org.geysermc api-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/api/geyser/pom.xml b/api/geyser/pom.xml index de9c63e83..084b4e745 100644 --- a/api/geyser/pom.xml +++ b/api/geyser/pom.xml @@ -5,7 +5,7 @@ org.geysermc api-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT ../pom.xml 4.0.0 @@ -35,7 +35,7 @@ org.geysermc base-api - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile diff --git a/api/pom.xml b/api/pom.xml index b6d865cb4..6abbb4479 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT ../pom.xml diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 45a08c7db..f06a219bb 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT bootstrap-bungeecord @@ -14,7 +14,7 @@ org.geysermc core - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml index 58c651455..381f68bc2 100644 --- a/bootstrap/pom.xml +++ b/bootstrap/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT bootstrap-parent pom @@ -34,7 +34,7 @@ org.geysermc ap - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT provided diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index 6eda527f3..da8b184e9 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT bootstrap-spigot @@ -25,7 +25,7 @@ org.geysermc core - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile @@ -43,7 +43,7 @@ org.geysermc.geyser.adapters spigot-all - 1.3-SNAPSHOT + 1.4-SNAPSHOT diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java index e0ad866c8..7f1c4aa06 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java @@ -32,27 +32,26 @@ import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import org.geysermc.common.PlatformType; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserBootstrap; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.adapters.spigot.SpigotAdapters; import org.geysermc.geyser.command.GeyserCommandManager; -import org.geysermc.geyser.session.auth.AuthType; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; -import org.geysermc.geyser.network.MinecraftProtocol; import org.geysermc.geyser.level.WorldManager; +import org.geysermc.geyser.network.MinecraftProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.Constants; -import org.geysermc.geyser.util.FileUtils; -import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.adapters.spigot.SpigotAdapters; import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandExecutor; import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandManager; import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource; import org.geysermc.geyser.platform.spigot.world.GeyserPistonListener; -import org.geysermc.geyser.platform.spigot.world.GeyserSpigot1_11CraftingListener; import org.geysermc.geyser.platform.spigot.world.GeyserSpigotBlockPlaceListener; import org.geysermc.geyser.platform.spigot.world.manager.*; +import org.geysermc.geyser.session.auth.AuthType; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.util.FileUtils; import java.io.File; import java.io.IOException; @@ -236,11 +235,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this); - if (isPre1_12) { - // Register events needed to send all recipes to the client - Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigot1_11CraftingListener(geyser), this); - } - this.getCommand("geyser").setExecutor(new GeyserSpigotCommandExecutor(geyser)); // Check to ensure the current setup can support the protocol version Geyser uses diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigot1_11CraftingListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigot1_11CraftingListener.java deleted file mode 100644 index 78a64e47b..000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigot1_11CraftingListener.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.platform.spigot.world; - -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 com.viaversion.viaversion.api.Via; -import com.viaversion.viaversion.api.data.MappingData; -import com.viaversion.viaversion.api.protocol.ProtocolPathEntry; -import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; -import com.viaversion.viaversion.protocols.protocol1_13to1_12_2.Protocol1_13To1_12_2; -import com.viaversion.viaversion.util.Pair; -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.geyser.GeyserImpl; -import org.geysermc.geyser.network.MinecraftProtocol; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.inventory.item.ItemTranslator; -import org.geysermc.geyser.util.InventoryUtils; - -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 GeyserImpl geyser; - /** - * 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(GeyserImpl geyser) { - this.geyser = geyser; - this.mappingData1_12to1_13 = Via.getManager().getProtocolManager().getProtocol(Protocol1_13To1_12_2.class).getMappingData(); - this.protocolList = Via.getManager().getProtocolManager().getProtocolPath(MinecraftProtocol.getJavaProtocolVersion(), - ProtocolVersion.v1_13.getVersion()); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - GeyserSession session = null; - for (GeyserSession otherSession : geyser.getSessionManager().getSessions().values()) { - if (otherSession.name().equals(event.getPlayer().getName())) { - session = otherSession; - break; - } - } - if (session == null) { - return; - } - - sendServerRecipes(session); - } - - public void sendServerRecipes(GeyserSession session) { - int netId = InventoryUtils.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 == null || 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) { - 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) { - 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).getProtocol().getMappingData(); - if (mappingData != null) { - itemId = mappingData.getNewItemId(itemId); - } - } - - ItemStack mcItemStack = new ItemStack(itemId, itemStack.getAmount()); - ItemData finalData = ItemTranslator.translateToBedrock(session, mcItemStack); - return new Pair<>(mcItemStack, finalData); - } - - // Empty slot, most likely - return new Pair<>(null, ItemData.AIR); - } - -} diff --git a/bootstrap/sponge/pom.xml b/bootstrap/sponge/pom.xml index ab3b7d970..6285c6dbf 100644 --- a/bootstrap/sponge/pom.xml +++ b/bootstrap/sponge/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT bootstrap-sponge @@ -14,7 +14,7 @@ org.geysermc core - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile diff --git a/bootstrap/standalone/pom.xml b/bootstrap/standalone/pom.xml index 881c87e6c..6babc6933 100644 --- a/bootstrap/standalone/pom.xml +++ b/bootstrap/standalone/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT bootstrap-standalone @@ -18,7 +18,7 @@ org.geysermc core - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile @@ -47,17 +47,17 @@ org.jline jline-terminal - 3.20.0 + 3.21.0 org.jline jline-terminal-jna - 3.20.0 + 3.21.0 org.jline jline-reader - 3.20.0 + 3.21.0 org.apache.logging.log4j @@ -132,7 +132,6 @@ implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer"> - ${project.build.directory}/dependency-reduced-pom.xml diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index ca41d3c1d..3c69b4749 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -276,6 +276,12 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { return Paths.get(System.getProperty("user.dir")); } + @Override + public Path getSavedUserLoginsFolder() { + // Return the location of the config + return new File(configFilename).getAbsoluteFile().getParentFile().toPath(); + } + @Override public BootstrapDumpInfo getDumpInfo() { return new GeyserStandaloneDumpInfo(this); diff --git a/bootstrap/velocity/pom.xml b/bootstrap/velocity/pom.xml index ff052471d..1621d6ee6 100644 --- a/bootstrap/velocity/pom.xml +++ b/bootstrap/velocity/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT bootstrap-velocity @@ -14,7 +14,7 @@ org.geysermc core - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile diff --git a/common/pom.xml b/common/pom.xml index fde2605bc..a563b7aff 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT common diff --git a/core/pom.xml b/core/pom.xml index 31f9a075f..b606c3ef7 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT core @@ -20,19 +20,19 @@ org.geysermc ap - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT provided org.geysermc geyser-api - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile org.geysermc common - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT compile @@ -146,23 +146,23 @@ - com.github.RednedEpic + com.github.GeyserMC MCAuthLib - 6c99331 + d9d773e compile com.github.GeyserMC MCProtocolLib - 6a23a780 + 0771504 compile - com.github.steveice10 + com.github.GeyserMC packetlib - com.github.steveice10 + com.github.GeyserMC mcauthlib diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 49f8fa676..23fb76d16 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -37,6 +37,8 @@ public final class Constants { public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/"; + static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; + static { URI wsUri = null; try { diff --git a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java index bc6a07ae3..261c7416b 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java @@ -97,6 +97,13 @@ public interface GeyserBootstrap { */ Path getConfigFolder(); + /** + * @return the folder where user tokens are saved. This should always point to the location of the config. + */ + default Path getSavedUserLoginsFolder() { + return getConfigFolder(); + } + /** * Information used for the bootstrap section of the debug dump * diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 68bb69b46..3977bfde4 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -26,6 +26,7 @@ package org.geysermc.geyser; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.steveice10.packetlib.tcp.TcpSession; @@ -37,6 +38,7 @@ import io.netty.channel.kqueue.KQueue; import io.netty.util.NettyRuntime; import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.internal.SystemPropertyUtil; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; @@ -65,6 +67,7 @@ import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.session.SessionManager; import org.geysermc.geyser.session.auth.AuthType; import org.geysermc.geyser.skin.FloodgateSkinUploader; @@ -77,6 +80,9 @@ import org.geysermc.geyser.util.*; import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -84,6 +90,7 @@ import java.net.UnknownHostException; import java.security.Key; import java.text.DecimalFormat; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.regex.Matcher; @@ -134,6 +141,10 @@ public class GeyserImpl implements GeyserApi { private Metrics metrics; + private PendingMicrosoftAuthentication pendingMicrosoftAuthentication; + @Getter(AccessLevel.NONE) + private Map savedRefreshTokens; + private static GeyserImpl instance; private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) { @@ -286,6 +297,8 @@ public class GeyserImpl implements GeyserApi { logger.debug("Not getting git properties for the news handler as we are in a development environment."); } + pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.getPendingAuthenticationTimeout()); + this.newsHandler = new NewsHandler(branch, buildNumber); CooldownUtils.setDefaultShowCooldown(config.getShowCooldown()); @@ -338,7 +351,7 @@ public class GeyserImpl implements GeyserApi { metrics = new Metrics(this, "GeyserMC", config.getMetrics().getUniqueId(), false, java.util.logging.Logger.getLogger("")); metrics.addCustomChart(new Metrics.SingleLineChart("players", sessionManager::size)); // Prevent unwanted words best we can - metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> config.getRemote().getAuthType().toString().toLowerCase())); + metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> config.getRemote().getAuthType().toString().toLowerCase(Locale.ROOT))); metrics.addCustomChart(new Metrics.SimplePie("platform", platformType::getPlatformName)); metrics.addCustomChart(new Metrics.SimplePie("defaultLocale", GeyserLocale::getDefaultLocale)); metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserImpl.VERSION)); @@ -422,6 +435,47 @@ public class GeyserImpl implements GeyserApi { metrics = null; } + if (config.getRemote().getAuthType() == AuthType.ONLINE) { + if (config.getUserAuths() != null && !config.getUserAuths().isEmpty()) { + getLogger().warning("The 'userAuths' config section is now deprecated, and will be removed in the near future! " + + "Please migrate to the new 'saved-user-logins' config option: " + + "https://wiki.geysermc.org/geyser/understanding-the-config/"); + } + + // May be written/read to on multiple threads from each GeyserSession as well as writing the config + savedRefreshTokens = new ConcurrentHashMap<>(); + + File tokensFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); + if (tokensFile.exists()) { + TypeReference> type = new TypeReference<>() { }; + + Map refreshTokenFile = null; + try { + refreshTokenFile = JSON_MAPPER.readValue(tokensFile, type); + } catch (IOException e) { + logger.error("Cannot load saved user tokens!", e); + } + if (refreshTokenFile != null) { + List validUsers = config.getSavedUserLogins(); + boolean doWrite = false; + for (Map.Entry entry : refreshTokenFile.entrySet()) { + String user = entry.getKey(); + if (!validUsers.contains(user)) { + // Perform a write to this file to purge the now-unused name + doWrite = true; + continue; + } + savedRefreshTokens.put(user, entry.getValue()); + } + if (doWrite) { + scheduleRefreshTokensWrite(); + } + } + } + } else { + savedRefreshTokens = null; + } + newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED); this.eventBus.fire(new GeyserPostInitializeEvent(this.extensionManager, this.eventBus)); @@ -545,6 +599,39 @@ public class GeyserImpl implements GeyserApi { return bootstrap.getWorldManager(); } + @Nullable + public String refreshTokenFor(@NonNull String bedrockName) { + return savedRefreshTokens.get(bedrockName); + } + + public void saveRefreshToken(@NonNull String bedrockName, @NonNull String refreshToken) { + if (!getConfig().getSavedUserLogins().contains(bedrockName)) { + // Do not save this login + return; + } + + // We can safely overwrite old instances because MsaAuthenticationService#getLoginResponseFromRefreshToken + // refreshes the token for us + if (!Objects.equals(refreshToken, savedRefreshTokens.put(bedrockName, refreshToken))) { + scheduleRefreshTokensWrite(); + } + } + + private void scheduleRefreshTokensWrite() { + scheduledThread.execute(() -> { + // Ensure all writes are handled on the same thread + File savedTokens = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); + TypeReference> type = new TypeReference<>() { }; + try (FileWriter writer = new FileWriter(savedTokens)) { + JSON_MAPPER.writerFor(type) + .withDefaultPrettyPrinter() + .writeValue(writer, savedRefreshTokens); + } catch (IOException e) { + getLogger().error("Unable to write saved refresh tokens!", e); + } + }); + } + public static GeyserImpl getInstance() { return instance; } diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java index 3b7cad44c..7bb73a648 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -44,6 +44,9 @@ public interface GeyserConfiguration { IRemoteConfiguration getRemote(); + List getSavedUserLogins(); + + @Deprecated Map getUserAuths(); boolean isCommandSuggestions(); @@ -100,6 +103,8 @@ public interface GeyserConfiguration { IMetricsInfo getMetrics(); + int getPendingAuthenticationTimeout(); + interface IBedrockConfiguration { String getAddress(); diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java index 97c5bfea8..463350441 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -62,6 +62,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private BedrockConfiguration bedrock = new BedrockConfiguration(); private RemoteConfiguration remote = new RemoteConfiguration(); + @JsonProperty("saved-user-logins") + private List savedUserLogins = Collections.emptyList(); + @JsonProperty("floodgate-key-file") private String floodgateKeyFile = "key.pem"; @@ -141,6 +144,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private MetricsInfo metrics = new MetricsInfo(); + @JsonProperty("pending-authentication-timeout") + private int pendingAuthenticationTimeout = 120; + @Getter @JsonIgnoreProperties(ignoreUnknown = true) public static class BedrockConfiguration implements IBedrockConfiguration { diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 9e4124cdc..1de571c94 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -65,9 +65,9 @@ public final class EntityDefinitions { public static final EntityDefinition CHICKEN; public static final EntityDefinition COD; public static final EntityDefinition COMMAND_BLOCK_MINECART; - public static final EntityDefinition COW; + public static final EntityDefinition COW; public static final EntityDefinition CREEPER; - public static final EntityDefinition DOLPHIN; + public static final EntityDefinition DOLPHIN; public static final EntityDefinition DONKEY; public static final EntityDefinition DRAGON_FIREBALL; public static final EntityDefinition DROWNED; @@ -132,7 +132,7 @@ public final class EntityDefinitions { public static final EntityDefinition SHULKER_BULLET; public static final EntityDefinition SILVERFISH; public static final EntityDefinition SKELETON; - public static final EntityDefinition SKELETON_HORSE; + public static final EntityDefinition SKELETON_HORSE; public static final EntityDefinition SLIME; public static final EntityDefinition SMALL_FIREBALL; public static final EntityDefinition SNOWBALL; @@ -160,7 +160,7 @@ public final class EntityDefinitions { public static final EntityDefinition WOLF; public static final EntityDefinition ZOGLIN; public static final EntityDefinition ZOMBIE; - public static final EntityDefinition ZOMBIE_HORSE; + public static final EntityDefinition ZOMBIE_HORSE; public static final EntityDefinition ZOMBIE_VILLAGER; public static final EntityDefinition ZOMBIFIED_PIGLIN; @@ -459,7 +459,7 @@ public final class EntityDefinitions { .addTranslator(MetadataType.BOOLEAN, (entity, entityMetadata) -> entity.setFlag(EntityFlag.POWERED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .addTranslator(MetadataType.BOOLEAN, CreeperEntity::setIgnited) .build(); - DOLPHIN = EntityDefinition.inherited(WaterEntity::new, mobEntityBase) + DOLPHIN = EntityDefinition.inherited(DolphinEntity::new, mobEntityBase) .type(EntityType.DOLPHIN) .height(0.6f).width(0.9f) //TODO check @@ -723,7 +723,7 @@ public final class EntityDefinitions { .type(EntityType.CHICKEN) .height(0.7f).width(0.4f) .build(); - COW = EntityDefinition.inherited(AnimalEntity::new, ageableEntityBase) + COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase) .type(EntityType.COW) .height(1.4f).width(0.9f) .build(); @@ -745,14 +745,14 @@ public final class EntityDefinitions { .height(1.3f).width(0.9f) .addTranslator(MetadataType.BOOLEAN, GoatEntity::setScreamer) .build(); - MOOSHROOM = EntityDefinition.inherited(MooshroomEntity::new, ageableEntityBase) // TODO remove class + MOOSHROOM = EntityDefinition.inherited(MooshroomEntity::new, ageableEntityBase) .type(EntityType.MOOSHROOM) .height(1.4f).width(0.9f) - .addTranslator(MetadataType.STRING, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityData.VARIANT, entityMetadata.getValue().equals("brown") ? 1 : 0)) + .addTranslator(MetadataType.STRING, MooshroomEntity::setVariant) .build(); OCELOT = EntityDefinition.inherited(OcelotEntity::new, ageableEntityBase) .type(EntityType.OCELOT) - .height(0.35f).width(0.3f) + .height(0.7f).width(0.6f) .addTranslator(MetadataType.BOOLEAN, (ocelotEntity, entityMetadata) -> ocelotEntity.setFlag(EntityFlag.TRUSTING, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); PANDA = EntityDefinition.inherited(PandaEntity::new, ageableEntityBase) @@ -783,7 +783,7 @@ public final class EntityDefinitions { .build(); SHEEP = EntityDefinition.inherited(SheepEntity::new, ageableEntityBase) .type(EntityType.SHEEP) - .heightAndWidth(0.9f) + .height(1.3f).width(0.9f) .addTranslator(MetadataType.BYTE, SheepEntity::setSheepFlags) .build(); STRIDER = EntityDefinition.inherited(StriderEntity::new, ageableEntityBase) @@ -832,11 +832,11 @@ public final class EntityDefinitions { .height(1.6f).width(1.3965f) .addTranslator(MetadataType.INT, HorseEntity::setHorseVariant) .build(); - SKELETON_HORSE = EntityDefinition.inherited(abstractHorseEntityBase.factory(), abstractHorseEntityBase) + SKELETON_HORSE = EntityDefinition.inherited(SkeletonHorseEntity::new, abstractHorseEntityBase) .type(EntityType.SKELETON_HORSE) .height(1.6f).width(1.3965f) .build(); - ZOMBIE_HORSE = EntityDefinition.inherited(abstractHorseEntityBase.factory(), abstractHorseEntityBase) + ZOMBIE_HORSE = EntityDefinition.inherited(ZombieHorseEntity::new, abstractHorseEntityBase) .type(EntityType.ZOMBIE_HORSE) .height(1.6f).width(1.3965f) .build(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/InteractiveTagManager.java b/core/src/main/java/org/geysermc/geyser/entity/InteractiveTagManager.java deleted file mode 100644 index 0bc91cfcd..000000000 --- a/core/src/main/java/org/geysermc/geyser/entity/InteractiveTagManager.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.entity; - -import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; -import com.nukkitx.protocol.bedrock.data.entity.EntityData; -import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; -import lombok.Getter; -import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.entity.type.living.MobEntity; -import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; -import org.geysermc.geyser.entity.type.living.animal.horse.HorseEntity; -import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; -import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; -import org.geysermc.geyser.entity.type.living.merchant.VillagerEntity; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.registry.type.ItemMapping; - -import java.util.EnumSet; -import java.util.Set; - -public class InteractiveTagManager { - /** - * All entity types that can be leashed on Java Edition - */ - private static final Set LEASHABLE_MOB_TYPES = EnumSet.of(EntityType.AXOLOTL, EntityType.BEE, EntityType.CAT, EntityType.CHICKEN, - EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.GOAT, EntityType.GLOW_SQUID, 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.SQUID, EntityType.STRIDER, - EntityType.WOLF, EntityType.ZOGLIN); - - private static final Set SADDLEABLE_WHEN_TAMED_MOB_TYPES = EnumSet.of(EntityType.DONKEY, EntityType.HORSE, - EntityType.ZOMBIE_HORSE, EntityType.MULE); - - /** - * Update the suggestion that the client currently has on their screen for this entity (for example, "Feed" or "Ride") - * - * @param session the Bedrock client session - * @param interactEntity the entity that the client is currently facing. - */ - public static void updateTag(GeyserSession session, Entity interactEntity) { - ItemMapping mapping = session.getPlayerInventory().getItemInHand().getMapping(session); - String javaIdentifierStripped = mapping.getJavaIdentifier().replace("minecraft:", ""); - EntityType entityType = interactEntity.getDefinition().entityType(); - if (entityType == null) { - // Likely a technical entity; we don't need to worry about this - return; - } - - InteractiveTag interactiveTag = InteractiveTag.NONE; - - if (interactEntity instanceof MobEntity mobEntity && mobEntity.getLeashHolderBedrockId() == session.getPlayerEntity().getGeyserId()) { - // Unleash the entity - interactiveTag = InteractiveTag.REMOVE_LEASH; - } else if (javaIdentifierStripped.equals("saddle") && !interactEntity.getFlag(EntityFlag.SADDLED) && - ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(entityType) && interactEntity.getFlag(EntityFlag.TAMED) && !session.isSneaking()) || - entityType == EntityType.PIG || entityType == 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.getPlayerInventory().getItemInHand().getNbt() != null && - session.getPlayerInventory().getItemInHand().getNbt().contains("display")) { - // Holding a named name tag - interactiveTag = InteractiveTag.NAME; - } else if (interactEntity instanceof MobEntity mobEntity &&javaIdentifierStripped.equals("lead") - && LEASHABLE_MOB_TYPES.contains(entityType) && mobEntity.getLeashHolderBedrockId() == -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 if (interactEntity instanceof AnimalEntity && ((AnimalEntity) interactEntity).canEat(javaIdentifierStripped, mapping)) { - // This animal can be fed - interactiveTag = InteractiveTag.FEED; - } else { - switch (entityType) { - case BOAT: - if (interactEntity.getPassengers().size() < 2) { - interactiveTag = InteractiveTag.BOARD_BOAT; - } - break; - case CAT: - if (interactEntity.getFlag(EntityFlag.TAMED) && - ((CatEntity) interactEntity).getOwnerBedrockId() == session.getPlayerEntity().getGeyserId()) { - // Tamed and owned by player - can sit/stand - interactiveTag = interactEntity.getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; - break; - } - 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("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 (interactEntity.getFlag(EntityFlag.TAMED) && !interactEntity.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: - boolean tamed = interactEntity.getFlag(EntityFlag.TAMED); - if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || interactEntity.getFlag(EntityFlag.CHESTED))) { - interactiveTag = InteractiveTag.OPEN_CONTAINER; - break; - } - if (!interactEntity.getFlag(EntityFlag.BABY)) { - // Can't ride a baby - if (tamed) { - interactiveTag = InteractiveTag.RIDE_HORSE; - } else if (mapping.getJavaId() == 0) { - // Can't hide an untamed entity without having your hand empty - interactiveTag = InteractiveTag.MOUNT; - } - } - break; - case MINECART: - if (interactEntity.getPassengers().isEmpty()) { - interactiveTag = InteractiveTag.RIDE_MINECART; - } - break; - case CHEST_MINECART: - case COMMAND_BLOCK_MINECART: - case HOPPER_MINECART: - interactiveTag = InteractiveTag.OPEN_CONTAINER; - break; - case PIG: - if (interactEntity.getFlag(EntityFlag.SADDLED)) { - interactiveTag = InteractiveTag.MOUNT; - } - break; - case PIGLIN: - if (!interactEntity.getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) { - interactiveTag = InteractiveTag.BARTER; - } - break; - case SHEEP: - if (!interactEntity.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 (interactEntity.getFlag(EntityFlag.SADDLED)) { - interactiveTag = InteractiveTag.RIDE_STRIDER; - } - break; - case VILLAGER: - VillagerEntity villager = (VillagerEntity) interactEntity; - if (villager.isCanTradeWith() && !villager.isBaby()) { // 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") && !interactEntity.getFlag(EntityFlag.TAMED)) { - // Bone and untamed - can tame - interactiveTag = InteractiveTag.TAME; - } else if (interactEntity.getFlag(EntityFlag.TAMED) && - ((WolfEntity) interactEntity).getOwnerBedrockId() == session.getPlayerEntity().getGeyserId()) { - // Tamed and owned by player - can sit/stand - interactiveTag = interactEntity.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().getDirtyMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue()); - session.getPlayerEntity().updateBedrockMetadata(); - } - - /** - * All interactive tags in enum form. For potential API usage. - */ - public enum InteractiveTag { - NONE((Void) null), - 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(Void isNone) { - this.value = ""; - } - - InteractiveTag(String value) { - this.value = "action.interact." + value; - } - - InteractiveTag() { - this.value = "action.interact." + name().toLowerCase(); - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index ddff746d6..6ce490bc2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.entity.type; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.packet.AnimatePacket; @@ -35,6 +36,8 @@ import lombok.Getter; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -158,6 +161,27 @@ public class BoatEntity extends Entity { } } + @Override + protected InteractiveTag testInteraction(Hand hand) { + if (session.isSneaking()) { + return InteractiveTag.NONE; + } else if (passengers.size() < 2) { + return InteractiveTag.BOARD_BOAT; + } else { + return InteractiveTag.NONE; + } + } + + @Override + public InteractionResult interact(Hand hand) { + if (session.isSneaking()) { + return InteractionResult.PASS; + } else { + // TODO: the client also checks for "out of control" ticks + return InteractionResult.SUCCESS; + } + } + private void updateLeftPaddle(GeyserSession session, Entity rower) { if (isPaddlingLeft) { paddleTimeLeft += ROWING_SPEED; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java index 36c050d1b..251eb98a0 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/CommandBlockMinecartEntity.java @@ -25,10 +25,16 @@ package org.geysermc.geyser.entity.type; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityData; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; import java.util.UUID; @@ -55,4 +61,30 @@ public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity { dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getCommandBlockRuntimeId()); dirtyMetadata.put(EntityData.DISPLAY_OFFSET, 6); } + + @Override + protected InteractiveTag testInteraction(Hand hand) { + if (session.canUseCommandBlocks()) { + return InteractiveTag.OPEN_CONTAINER; + } else { + return InteractiveTag.NONE; + } + } + + @Override + public InteractionResult interact(Hand hand) { + if (session.canUseCommandBlocks()) { + // Client-side GUI required + ContainerOpenPacket openPacket = new ContainerOpenPacket(); + openPacket.setBlockPosition(Vector3i.ZERO); + openPacket.setId((byte) 1); + openPacket.setType(ContainerType.COMMAND_BLOCK); + openPacket.setUniqueEntityId(geyserId); + session.sendUpstreamPacket(openPacket); + + return InteractionResult.SUCCESS; + } else { + return InteractionResult.PASS; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index 77a1a199c..c0d031c87 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -30,15 +30,14 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; 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.entity.EntityFlags; -import com.nukkitx.protocol.bedrock.packet.AddEntityPacket; -import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket; -import com.nukkitx.protocol.bedrock.packet.RemoveEntityPacket; -import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; +import com.nukkitx.protocol.bedrock.packet.*; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -48,6 +47,8 @@ import org.geysermc.geyser.entity.GeyserDirtyMetadata; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.MathUtils; import java.util.Collections; @@ -467,12 +468,68 @@ public class Entity { } } + public boolean isAlive() { + return this.valid; + } + + /** + * Update the suggestion that the client currently has on their screen for this entity (for example, "Feed" or "Ride") + */ + public final void updateInteractiveTag() { + InteractiveTag tag = InteractiveTag.NONE; + for (Hand hand: EntityUtils.HANDS) { + tag = testInteraction(hand); + if (tag != InteractiveTag.NONE) { + break; + } + } + session.getPlayerEntity().getDirtyMetadata().put(EntityData.INTERACTIVE_TAG, tag.getValue()); + session.getPlayerEntity().updateBedrockMetadata(); + } + + /** + * Test interacting with the given hand to see if we should send a tag to the Bedrock client. + * Should usually mirror {@link #interact(Hand)} without any side effects. + */ + protected InteractiveTag testInteraction(Hand hand) { + return InteractiveTag.NONE; + } + + /** + * Simulates interacting with an entity. The code here should mirror Java Edition code to the best of its ability, + * to ensure packet parity as well as functionality parity (such as sound effect responses). + */ + public InteractionResult interact(Hand hand) { + return InteractionResult.PASS; + } + + /** + * Simulates interacting with this entity at a specific click point. As of Java Edition 1.18.1, this is only used for armor stands. + */ + public InteractionResult interactAt(Hand hand) { + return InteractionResult.PASS; + } + + /** + * Send an entity event of the specified type to the Bedrock player from this entity. + */ + public final void playEntityEvent(EntityEventType type) { + playEntityEvent(type, 0); + } + + /** + * Send an entity event of the specified type with the specified data to the Bedrock player from this entity. + */ + public final void playEntityEvent(EntityEventType type, int data) { + EntityEventPacket packet = new EntityEventPacket(); + packet.setRuntimeEntityId(geyserId); + packet.setType(type); + packet.setData(data); + session.sendUpstreamPacket(packet); + } + @SuppressWarnings("unchecked") public I as(Class entityClass) { return entityClass.isInstance(this) ? (I) this : null; } - - public boolean is(Class entityClass) { - return entityClass.isInstance(this); - } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java index 9b7c79de4..dbd9bf91f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FurnaceMinecartEntity.java @@ -26,11 +26,13 @@ package org.geysermc.geyser.entity.type; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.util.InteractionResult; import java.util.UUID; @@ -42,6 +44,7 @@ public class FurnaceMinecartEntity extends DefaultBlockMinecartEntity { } public void setHasFuel(BooleanEntityMetadata entityMetadata) { + // Note: Java ticks this entity and gives it particles if it has fuel hasFuel = entityMetadata.getPrimitiveValue(); updateDefaultBlockMetadata(); } @@ -51,4 +54,10 @@ public class FurnaceMinecartEntity extends DefaultBlockMinecartEntity { dirtyMetadata.put(EntityData.DISPLAY_ITEM, session.getBlockMappings().getBedrockBlockId(hasFuel ? BlockStateValues.JAVA_FURNACE_LIT_ID : BlockStateValues.JAVA_FURNACE_ID)); dirtyMetadata.put(EntityData.DISPLAY_OFFSET, 6); } + + @Override + public InteractionResult interact(Hand hand) { + // Always works since you can "push" it this way + return InteractionResult.SUCCESS; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java index 69aac5a26..9cfa22a1f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java @@ -29,6 +29,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadat import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; @@ -42,6 +43,8 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InventoryUtils; import java.util.UUID; @@ -205,6 +208,11 @@ public class ItemFrameEntity extends Entity { changed = false; } + @Override + public InteractionResult interact(Hand hand) { + return InventoryUtils.isEmpty(heldItem) && session.getPlayerInventory().getItemInHand(hand).isEmpty() ? InteractionResult.PASS : InteractionResult.SUCCESS; + } + /** * Finds the Java entity ID of an item frame from its Bedrock position. * @param position position of item frame in Bedrock. diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java index 28fe7d5bc..4ff1dfe7c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LeashKnotEntity.java @@ -25,9 +25,11 @@ package org.geysermc.geyser.entity.type; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.nukkitx.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; import java.util.UUID; @@ -38,4 +40,9 @@ public class LeashKnotEntity extends Entity { super(session, entityId, geyserId, uuid, definition, position.add(0.5f, 0.25f, 0.5f), motion, yaw, pitch, headYaw); } + @Override + public InteractionResult interact(Hand hand) { + // Un-leashing the knot + return InteractionResult.SUCCESS; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index bc553f56c..a5214854e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -33,6 +33,9 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.AttributeData; @@ -48,10 +51,12 @@ import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.ChunkUtils; +import org.geysermc.geyser.util.InteractionResult; import java.util.ArrayList; import java.util.Collections; @@ -169,6 +174,36 @@ public class LivingEntity extends Entity { return new AttributeData(GeyserAttributeType.HEALTH.getBedrockIdentifier(), 0f, this.maxHealth, (float) Math.ceil(this.health), this.maxHealth); } + @Override + public boolean isAlive() { + return this.valid && health > 0f; + } + + @Override + public InteractionResult interact(Hand hand) { + GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand); + if (itemStack.getJavaId() == session.getItemMappings().getStoredItems().nameTag()) { + InteractionResult result = checkInteractWithNameTag(itemStack); + if (result.consumesAction()) { + return result; + } + } + + return super.interact(hand); + } + + /** + * Checks to see if a nametag interaction would go through. + */ + protected final InteractionResult checkInteractWithNameTag(GeyserItemStack itemStack) { + CompoundTag nbt = itemStack.getNbt(); + if (nbt != null && nbt.get("display") instanceof CompoundTag displayTag && displayTag.get("Name") instanceof StringTag) { + // The mob shall be named + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + public void updateArmor(GeyserSession session) { if (!valid) return; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java index 80fc2a62e..a427d6a43 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java @@ -27,10 +27,14 @@ package org.geysermc.geyser.entity.type; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; import java.util.UUID; @@ -64,4 +68,39 @@ public class MinecartEntity extends Entity { // Note: minecart rotation on rails does not care about the actual rotation value return Vector3f.from(0, yaw, 0); } + + @Override + protected InteractiveTag testInteraction(Hand hand) { + if (definition == EntityDefinitions.CHEST_MINECART || definition == EntityDefinitions.HOPPER_MINECART) { + return InteractiveTag.OPEN_CONTAINER; + } else { + if (session.isSneaking()) { + return InteractiveTag.NONE; + } else if (!passengers.isEmpty()) { + // Can't enter if someone is inside + return InteractiveTag.NONE; + } else { + // Attempt to enter + return InteractiveTag.RIDE_MINECART; + } + } + } + + @Override + public InteractionResult interact(Hand hand) { + if (definition == EntityDefinitions.CHEST_MINECART || definition == EntityDefinitions.HOPPER_MINECART) { + // Opening the UI of this minecart + return InteractionResult.SUCCESS; + } else { + if (session.isSneaking()) { + return InteractionResult.PASS; + } else if (!passengers.isEmpty()) { + // Can't enter if someone is inside + return InteractionResult.PASS; + } else { + // Attempt to enter + return InteractionResult.SUCCESS; + } + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java index dae1c76e6..e6cd13f61 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AbstractFishEntity.java @@ -28,8 +28,12 @@ package org.geysermc.geyser.entity.type.living; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InteractionResult; +import javax.annotation.Nonnull; import java.util.UUID; public class AbstractFishEntity extends WaterEntity { @@ -42,4 +46,14 @@ public class AbstractFishEntity extends WaterEntity { setFlag(EntityFlag.CAN_CLIMB, false); setFlag(EntityFlag.HAS_GRAVITY, false); } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (EntityUtils.attemptToBucket(session, itemInHand)) { + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(itemInHand); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java index 9dc5dca07..d4c627a8e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/AmbientEntity.java @@ -36,4 +36,9 @@ public class AmbientEntity extends MobEntity { public AmbientEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + + @Override + protected boolean canBeLeashed() { + return false; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java index 10086be9c..9c7e6d107 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java @@ -28,6 +28,8 @@ package org.geysermc.geyser.entity.type.living; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Rotation; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; @@ -39,6 +41,7 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; import java.util.Optional; import java.util.UUID; @@ -237,6 +240,16 @@ public class ArmorStandEntity extends LivingEntity { } } + @Override + public InteractionResult interactAt(Hand hand) { + if (!isMarker && session.getPlayerInventory().getItemInHand(hand).getJavaId() != session.getItemMappings().getStoredItems().nameTag()) { + // Java Edition returns SUCCESS if in spectator mode, but this is overrided with an earlier check on the client + return InteractionResult.CONSUME; + } else { + return InteractionResult.PASS; + } + } + @Override public void setHelmet(ItemData helmet) { super.setHelmet(helmet); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java new file mode 100644 index 000000000..7085547f8 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2022 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.geyser.entity.type.living; + +import com.nukkitx.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; + +import javax.annotation.Nonnull; +import java.util.UUID; + +public class DolphinEntity extends WaterEntity { + public DolphinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + protected boolean canBeLeashed() { + return true; + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (!itemInHand.isEmpty() && session.getTagCache().isFish(itemInHand)) { + return InteractiveTag.FEED; + } + return super.testMobInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (!itemInHand.isEmpty() && session.getTagCache().isFish(itemInHand)) { + // Feed + return InteractionResult.SUCCESS; + } + return super.mobInteract(itemInHand); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java index 0acdb960f..4ab36b00e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java @@ -29,8 +29,11 @@ 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.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import javax.annotation.Nonnull; import java.util.UUID; public class IronGolemEntity extends GolemEntity { @@ -42,4 +45,18 @@ public class IronGolemEntity extends GolemEntity { // Required, or else the overlay is black dirtyMetadata.put(EntityData.COLOR_2, (byte) 0); } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().ironIngot()) { + if (health < maxHealth) { + // Healing the iron golem + return InteractionResult.SUCCESS; + } else { + return InteractionResult.PASS; + } + } + return super.mobInteract(itemInHand); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java index 54d652f32..8734f8bd1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java @@ -26,14 +26,21 @@ package org.geysermc.geyser.entity.type.living; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import lombok.Getter; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.inventory.item.StoredItemMappings; +import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class MobEntity extends LivingEntity { @@ -62,4 +69,95 @@ public class MobEntity extends LivingEntity { this.leashHolderBedrockId = bedrockId; dirtyMetadata.put(EntityData.LEASH_HOLDER_EID, bedrockId); } + + @Override + protected final InteractiveTag testInteraction(Hand hand) { + if (!isAlive()) { + // dead lol + return InteractiveTag.NONE; + } else if (leashHolderBedrockId == session.getPlayerEntity().getGeyserId()) { + return InteractiveTag.REMOVE_LEASH; + } else { + GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand(hand); + StoredItemMappings storedItems = session.getItemMappings().getStoredItems(); + if (itemStack.getJavaId() == storedItems.lead() && canBeLeashed()) { + // We shall leash + return InteractiveTag.LEASH; + } else if (itemStack.getJavaId() == storedItems.nameTag()) { + InteractionResult result = checkInteractWithNameTag(itemStack); + if (result.consumesAction()) { + return InteractiveTag.NAME; + } + } + + InteractiveTag tag = testMobInteraction(itemStack); + return tag != InteractiveTag.NONE ? tag : super.testInteraction(hand); + } + } + + @Override + public final InteractionResult interact(Hand hand) { + if (!isAlive()) { + // dead lol + return InteractionResult.PASS; + } else if (leashHolderBedrockId == session.getPlayerEntity().getGeyserId()) { + // TODO looks like the client assumes it will go through and removes the attachment itself? + return InteractionResult.SUCCESS; + } else { + GeyserItemStack itemInHand = session.getPlayerInventory().getItemInHand(hand); + InteractionResult result = checkPriorityInteractions(itemInHand); + if (result.consumesAction()) { + return result; + } else { + InteractionResult mobResult = mobInteract(itemInHand); + return mobResult.consumesAction() ? mobResult : super.interact(hand); + } + } + } + + private InteractionResult checkPriorityInteractions(GeyserItemStack itemInHand) { + StoredItemMappings storedItems = session.getItemMappings().getStoredItems(); + if (itemInHand.getJavaId() == storedItems.lead() && canBeLeashed()) { + // We shall leash + return InteractionResult.SUCCESS; + } else if (itemInHand.getJavaId() == storedItems.nameTag()) { + InteractionResult result = checkInteractWithNameTag(itemInHand); + if (result.consumesAction()) { + return result; + } + } else { + ItemMapping mapping = itemInHand.getMapping(session); + if (mapping.getJavaIdentifier().endsWith("_spawn_egg")) { + // Using the spawn egg on this entity to create a child + return InteractionResult.CONSUME; + } + } + + return InteractionResult.PASS; + } + + @Nonnull + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + return InteractiveTag.NONE; + } + + @Nonnull + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + return InteractionResult.PASS; + } + + protected boolean canBeLeashed() { + return isNotLeashed() && !isEnemy(); + } + + protected final boolean isNotLeashed() { + return leashHolderBedrockId == -1L; + } + + /** + * Returns if the entity is hostile. Used to determine if it can be leashed. + */ + protected boolean isEnemy() { + return false; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java index 60e639415..26cf2d627 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SlimeEntity.java @@ -42,4 +42,9 @@ public class SlimeEntity extends MobEntity { public void setScale(IntEntityMetadata entityMetadata) { dirtyMetadata.put(EntityData.SCALE, 0.10f + entityMetadata.getPrimitiveValue()); } + + @Override + protected boolean isEnemy() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java index 10ddb48f4..794f71c04 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SnowGolemEntity.java @@ -29,8 +29,12 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEnti import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class SnowGolemEntity extends GolemEntity { @@ -44,4 +48,24 @@ public class SnowGolemEntity extends GolemEntity { // Handle the visibility of the pumpkin setFlag(EntityFlag.SHEARED, (xd & 0x10) != 0x10); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (session.getItemMappings().getStoredItems().shears() == itemInHand.getJavaId() && isAlive() && !getFlag(EntityFlag.SHEARED)) { + // Shearing the snow golem + return InteractiveTag.SHEAR; + } + return InteractiveTag.NONE; + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (session.getItemMappings().getStoredItems().shears() == itemInHand.getJavaId() && isAlive() && !getFlag(EntityFlag.SHEARED)) { + // Shearing the snow golem + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java index 0f860ae60..c81cf68de 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java @@ -120,6 +120,11 @@ public class SquidEntity extends WaterEntity implements Tickable { return Vector3f.from(pitch, yaw, yaw); } + @Override + protected boolean canBeLeashed() { + return isNotLeashed(); + } + private void checkInWater() { if (getFlag(EntityFlag.RIDING)) { inWater = false; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java index 5adbd50a9..44275a7b1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/WaterEntity.java @@ -36,4 +36,9 @@ public class WaterEntity extends CreatureEntity { public WaterEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + + @Override + protected boolean canBeLeashed() { + return false; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java index 2d1787932..64f41c5ad 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AnimalEntity.java @@ -26,11 +26,17 @@ package org.geysermc.geyser.entity.type.living.animal; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.AgeableEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class AnimalEntity extends AgeableEntity { @@ -39,6 +45,12 @@ public class AnimalEntity extends AgeableEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + public final boolean canEat(GeyserItemStack itemStack) { + ItemMapping mapping = itemStack.getMapping(session); + String handIdentifier = mapping.getJavaIdentifier(); + return canEat(handIdentifier.replace("minecraft:", ""), mapping); + } + /** * @param javaIdentifierStripped the stripped Java identifier of the item that is potential breeding food. For example, * wheat. @@ -48,4 +60,28 @@ public class AnimalEntity extends AgeableEntity { // This is what it defaults to. OK. return javaIdentifierStripped.equals("wheat"); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (canEat(itemInHand)) { + return InteractiveTag.FEED; + } + return super.testMobInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (canEat(itemInHand)) { + // FEED + if (getFlag(EntityFlag.BABY)) { + playEntityEvent(EntityEventType.BABY_ANIMAL_FEED); + return InteractionResult.SUCCESS; + } else { + return InteractionResult.CONSUME; + } + } + return super.mobInteract(itemInHand); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java index ec919a5c4..aafa2b782 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/AxolotlEntity.java @@ -31,9 +31,13 @@ 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.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InteractionResult; +import javax.annotation.Nonnull; import java.util.UUID; public class AxolotlEntity extends AnimalEntity { @@ -56,11 +60,26 @@ public class AxolotlEntity extends AnimalEntity { @Override public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { - return javaIdentifierStripped.equals("tropical_fish_bucket"); + return session.getTagCache().isAxolotlTemptItem(mapping); } @Override protected int getMaxAir() { return 6000; } + + @Override + protected boolean canBeLeashed() { + return true; + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (EntityUtils.attemptToBucket(session, itemInHand)) { + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(itemInHand); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java new file mode 100644 index 000000000..b5ae48b23 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2022 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.geyser.entity.type.living.animal; + +import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.SoundEvent; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; + +import javax.annotation.Nonnull; +import java.util.UUID; + +public class CowEntity extends AnimalEntity { + public CowEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (getFlag(EntityFlag.BABY) || !itemInHand.getMapping(session).getJavaIdentifier().equals("minecraft:bucket")) { + return super.testMobInteraction(itemInHand); + } + + return InteractiveTag.MILK; + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (getFlag(EntityFlag.BABY) || !itemInHand.getMapping(session).getJavaIdentifier().equals("minecraft:bucket")) { + return super.mobInteract(itemInHand); + } + + session.playSoundEvent(SoundEvent.MILK, position); + return InteractionResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java index 7442a5417..817b466fa 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/GoatEntity.java @@ -28,17 +28,20 @@ package org.geysermc.geyser.entity.type.living.animal; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import com.nukkitx.math.vector.Vector3f; -import lombok.Getter; +import com.nukkitx.protocol.bedrock.data.SoundEvent; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import javax.annotation.Nonnull; import java.util.UUID; public class GoatEntity extends AnimalEntity { private static final float LONG_JUMPING_HEIGHT = 1.3f * 0.7f; private static final float LONG_JUMPING_WIDTH = 0.9f * 0.7f; - @Getter private boolean isScreamer; public GoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -59,4 +62,15 @@ public class GoatEntity extends AnimalEntity { super.setDimensions(pose); } } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (!getFlag(EntityFlag.BABY) && itemInHand.getMapping(session).getJavaIdentifier().equals("minecraft:bucket")) { + session.playSoundEvent(isScreamer ? SoundEvent.MILK_SCREAMER : SoundEvent.MILK, position); + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(itemInHand); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java index e96124250..362c25256 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java @@ -56,4 +56,14 @@ public class HoglinEntity extends AnimalEntity { public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { return javaIdentifierStripped.equals("crimson_fungus"); } + + @Override + protected boolean canBeLeashed() { + return isNotLeashed(); + } + + @Override + protected boolean isEnemy() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index e75d20f8d..c249663ac 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -25,15 +25,62 @@ package org.geysermc.geyser.entity.type.living.animal; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class MooshroomEntity extends AnimalEntity { + private boolean isBrown = false; public MooshroomEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + + public void setVariant(ObjectEntityMetadata entityMetadata) { + isBrown = entityMetadata.getValue().equals("brown"); + dirtyMetadata.put(EntityData.VARIANT, isBrown ? 1 : 0); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + StoredItemMappings storedItems = session.getItemMappings().getStoredItems(); + if (!isBaby()) { + if (itemInHand.getJavaId() == storedItems.bowl()) { + // Stew + return InteractiveTag.MOOSHROOM_MILK_STEW; + } else if (isAlive() && itemInHand.getJavaId() == storedItems.shears()) { + // Shear items + return InteractiveTag.MOOSHROOM_SHEAR; + } + } + return super.testMobInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + StoredItemMappings storedItems = session.getItemMappings().getStoredItems(); + boolean isBaby = isBaby(); + if (!isBaby && itemInHand.getJavaId() == storedItems.bowl()) { + // Stew + return InteractionResult.SUCCESS; + } else if (!isBaby && isAlive() && itemInHand.getJavaId() == storedItems.shears()) { + // Shear items + return InteractionResult.SUCCESS; + } else if (isBrown && session.getTagCache().isSmallFlower(itemInHand) && itemInHand.getMapping(session).isHasSuspiciousStewEffect()) { + // ? + return InteractionResult.SUCCESS; + } + return super.mobInteract(itemInHand); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java index ab7e9a053..4ed2bdce1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java @@ -26,10 +26,15 @@ package org.geysermc.geyser.entity.type.living.animal; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class OcelotEntity extends AnimalEntity { @@ -42,4 +47,26 @@ public class OcelotEntity extends AnimalEntity { public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { return javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon"); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (!getFlag(EntityFlag.TRUSTING) && canEat(itemInHand) && session.getPlayerEntity().getPosition().distanceSquared(position) < 9f) { + // Attempt to feed + return InteractiveTag.FEED; + } else { + return super.testMobInteraction(itemInHand); + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (!getFlag(EntityFlag.TRUSTING) && canEat(itemInHand) && session.getPlayerEntity().getPosition().distanceSquared(position) < 9f) { + // Attempt to feed + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(itemInHand); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java index bfe743bc1..d607f113b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PandaEntity.java @@ -33,14 +33,19 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.UUID; public class PandaEntity extends AnimalEntity { - private int mainGene; - private int hiddenGene; + private Gene mainGene = Gene.NORMAL; + private Gene hiddenGene = Gene.NORMAL; public PandaEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -61,12 +66,12 @@ public class PandaEntity extends AnimalEntity { } public void setMainGene(ByteEntityMetadata entityMetadata) { - mainGene = entityMetadata.getPrimitiveValue(); + mainGene = Gene.fromId(entityMetadata.getPrimitiveValue()); updateAppearance(); } public void setHiddenGene(ByteEntityMetadata entityMetadata) { - hiddenGene = entityMetadata.getPrimitiveValue(); + hiddenGene = Gene.fromId(entityMetadata.getPrimitiveValue()); updateAppearance(); } @@ -86,23 +91,81 @@ public class PandaEntity extends AnimalEntity { return javaIdentifierStripped.equals("bamboo"); } + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (mainGene == Gene.WORRIED && session.isThunder()) { + return InteractiveTag.NONE; + } + return super.testMobInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (mainGene == Gene.WORRIED && session.isThunder()) { + // Huh! + return InteractionResult.PASS; + } else if (getFlag(EntityFlag.LAYING_DOWN)) { + // Stop the panda from laying down + // TODO laying up is client-side? + return InteractionResult.SUCCESS; + } else if (canEat(itemInHand)) { + if (getFlag(EntityFlag.BABY)) { + playEntityEvent(EntityEventType.BABY_ANIMAL_FEED); + } + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + @Override + protected boolean canBeLeashed() { + return false; + } + /** * Update the panda's appearance, and take into consideration the recessive brown and weak traits that only show up * when both main and hidden genes match */ private void updateAppearance() { - if (mainGene == 4 || mainGene == 5) { - // Main gene is a recessive trait + if (mainGene.isRecessive) { if (mainGene == hiddenGene) { // Main and hidden genes match; this is what the panda looks like. - dirtyMetadata.put(EntityData.VARIANT, mainGene); + dirtyMetadata.put(EntityData.VARIANT, mainGene.ordinal()); } else { // Genes have no effect on appearance - dirtyMetadata.put(EntityData.VARIANT, 0); + dirtyMetadata.put(EntityData.VARIANT, Gene.NORMAL.ordinal()); } } else { // No need to worry about hidden gene - dirtyMetadata.put(EntityData.VARIANT, mainGene); + dirtyMetadata.put(EntityData.VARIANT, mainGene.ordinal()); + } + } + + enum Gene { + NORMAL(false), + LAZY(false), + WORRIED(false), + PLAYFUL(false), + BROWN(true), + WEAK(true), + AGGRESSIVE(false); + + private static final Gene[] VALUES = values(); + + private final boolean isRecessive; + + Gene(boolean isRecessive) { + this.isRecessive = isRecessive; + } + + @Nullable + private static Gene fromId(int id) { + if (id < 0 || id >= VALUES.length) { + return NORMAL; + } + return VALUES[id]; } } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java index a97193358..05f628f44 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java @@ -26,10 +26,16 @@ package org.geysermc.geyser.entity.type.living.animal; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class PigEntity extends AnimalEntity { @@ -42,4 +48,37 @@ public class PigEntity extends AnimalEntity { public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { return javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot"); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (!canEat(itemInHand) && getFlag(EntityFlag.SADDLED) && passengers.isEmpty() && !session.isSneaking()) { + // Mount + return InteractiveTag.MOUNT; + } else { + InteractiveTag superTag = super.testMobInteraction(itemInHand); + if (superTag != InteractiveTag.NONE) { + return superTag; + } else { + return EntityUtils.attemptToSaddle(session, this, itemInHand).consumesAction() + ? InteractiveTag.SADDLE : InteractiveTag.NONE; + } + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (!canEat(itemInHand) && getFlag(EntityFlag.SADDLED) && passengers.isEmpty() && !session.isSneaking()) { + // Mount + return InteractionResult.SUCCESS; + } else { + InteractionResult superResult = super.mobInteract(itemInHand); + if (superResult.consumesAction()) { + return superResult; + } else { + return EntityUtils.attemptToSaddle(session, this, itemInHand); + } + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java index 284b4aea4..9481944a7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SheepEntity.java @@ -30,19 +30,69 @@ 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.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.ItemUtils; +import javax.annotation.Nonnull; import java.util.UUID; public class SheepEntity extends AnimalEntity { + private int color; public SheepEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } public void setSheepFlags(ByteEntityMetadata entityMetadata) { - byte xd = ((ByteEntityMetadata) entityMetadata).getPrimitiveValue(); + byte xd = entityMetadata.getPrimitiveValue(); setFlag(EntityFlag.SHEARED, (xd & 0x10) == 0x10); - dirtyMetadata.put(EntityData.COLOR, xd); + color = xd & 15; + dirtyMetadata.put(EntityData.COLOR, (byte) color); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().shears()) { + return InteractiveTag.SHEAR; + } else { + InteractiveTag tag = super.testMobInteraction(itemInHand); + if (tag != InteractiveTag.NONE) { + return tag; + } else { + int color = ItemUtils.dyeColorFor(itemInHand.getJavaId()); + if (canDye(color)) { + return InteractiveTag.DYE; + } + return InteractiveTag.NONE; + } + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().shears()) { + return InteractionResult.CONSUME; + } else { + InteractionResult superResult = super.mobInteract(itemInHand); + if (superResult.consumesAction()) { + return superResult; + } else { + int color = ItemUtils.dyeColorFor(itemInHand.getJavaId()); + if (canDye(color)) { + // Dyeing the sheep + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + } + } + + private boolean canDye(int color) { + return color != -1 && color != this.color && !getFlag(EntityFlag.SHEARED); } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java index 27438544c..5f42b4b67 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java @@ -30,9 +30,14 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class StriderEntity extends AnimalEntity { @@ -90,4 +95,37 @@ public class StriderEntity extends AnimalEntity { public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { return javaIdentifierStripped.equals("warped_fungus"); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (!canEat(itemInHand) && getFlag(EntityFlag.SADDLED) && passengers.isEmpty() && !session.isSneaking()) { + // Mount Strider + return InteractiveTag.RIDE_STRIDER; + } else { + InteractiveTag tag = super.testMobInteraction(itemInHand); + if (tag != InteractiveTag.NONE) { + return tag; + } else { + return EntityUtils.attemptToSaddle(session, this, itemInHand).consumesAction() + ? InteractiveTag.SADDLE : InteractiveTag.NONE; + } + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (!canEat(itemInHand) && getFlag(EntityFlag.SADDLED) && passengers.isEmpty() && !session.isSneaking()) { + // Mount Strider + return InteractionResult.SUCCESS; + } else { + InteractionResult superResult = super.mobInteract(itemInHand); + if (superResult.consumesAction()) { + return superResult; + } else { + return EntityUtils.attemptToSaddle(session, this, itemInHand); + } + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java index f7d987300..79a7b8f50 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TurtleEntity.java @@ -52,4 +52,9 @@ public class TurtleEntity extends AnimalEntity { public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { return javaIdentifierStripped.equals("seagrass"); } + + @Override + protected boolean canBeLeashed() { + return false; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java index ef53f604f..9139495b8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java @@ -37,9 +37,13 @@ import com.nukkitx.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; -import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.Set; import java.util.UUID; @@ -122,4 +126,164 @@ public class AbstractHorseEntity extends AnimalEntity { public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { return DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + return testHorseInteraction(itemInHand); + } + + @Nonnull + protected final InteractiveTag testHorseInteraction(@Nonnull GeyserItemStack itemInHand) { + boolean isBaby = isBaby(); + if (!isBaby) { + if (getFlag(EntityFlag.TAMED) && session.isSneaking()) { + return InteractiveTag.OPEN_CONTAINER; + } + + if (!passengers.isEmpty()) { + return super.testMobInteraction(itemInHand); + } + } + + if (!itemInHand.isEmpty()) { + if (canEat(itemInHand)) { + return InteractiveTag.FEED; + } + + if (testSaddle(itemInHand)) { + return InteractiveTag.SADDLE; + } + + if (!getFlag(EntityFlag.TAMED)) { + // Horse will become mad + return InteractiveTag.NONE; + } + + if (testForChest(itemInHand)) { + return InteractiveTag.ATTACH_CHEST; + } + + if (additionalTestForInventoryOpen(itemInHand) || !isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.getJavaId() == session.getItemMappings().getStoredItems().saddle()) { + // Will open the inventory to be saddled + return InteractiveTag.OPEN_CONTAINER; + } + } + + if (isBaby) { + return super.testMobInteraction(itemInHand); + } else { + return InteractiveTag.MOUNT; + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + return mobHorseInteract(itemInHand); + } + + @Nonnull + protected final InteractionResult mobHorseInteract(@Nonnull GeyserItemStack itemInHand) { + boolean isBaby = isBaby(); + if (!isBaby) { + if (getFlag(EntityFlag.TAMED) && session.isSneaking()) { + // Will open the inventory + return InteractionResult.SUCCESS; + } + + if (!passengers.isEmpty()) { + return super.mobInteract(itemInHand); + } + } + + if (!itemInHand.isEmpty()) { + if (canEat(itemInHand)) { + if (isBaby) { + playEntityEvent(EntityEventType.BABY_ANIMAL_FEED); + } + return InteractionResult.CONSUME; + } + + if (testSaddle(itemInHand)) { + return InteractionResult.SUCCESS; + } + + if (!getFlag(EntityFlag.TAMED)) { + // Horse will become mad + return InteractionResult.SUCCESS; + } + + if (testForChest(itemInHand)) { + // TODO looks like chest is also handled client side + return InteractionResult.SUCCESS; + } + + // Note: yes, this code triggers for llamas too. lol (as of Java Edition 1.18.1) + if (additionalTestForInventoryOpen(itemInHand) || (!isBaby && !getFlag(EntityFlag.SADDLED) && itemInHand.getJavaId() == session.getItemMappings().getStoredItems().saddle())) { + // Will open the inventory to be saddled + return InteractionResult.SUCCESS; + } + } + + if (isBaby) { + return super.mobInteract(itemInHand); + } else { + // Attempt to mount + // TODO client-set flags sitting standing? + return InteractionResult.SUCCESS; + } + } + + protected boolean testSaddle(@Nonnull GeyserItemStack itemInHand) { + return isAlive() && !getFlag(EntityFlag.BABY) && getFlag(EntityFlag.TAMED); + } + + protected boolean testForChest(@Nonnull GeyserItemStack itemInHand) { + return false; + } + + protected boolean additionalTestForInventoryOpen(@Nonnull GeyserItemStack itemInHand) { + return itemInHand.getMapping(session).getJavaIdentifier().endsWith("_horse_armor"); + } + + /* Just a place to stuff common code for the undead variants without having duplicate code */ + + protected final InteractiveTag testUndeadHorseInteraction(@Nonnull GeyserItemStack itemInHand) { + if (!getFlag(EntityFlag.TAMED)) { + return InteractiveTag.NONE; + } else if (isBaby()) { + return testHorseInteraction(itemInHand); + } else if (session.isSneaking()) { + return InteractiveTag.OPEN_CONTAINER; + } else if (!passengers.isEmpty()) { + return testHorseInteraction(itemInHand); + } else { + if (session.getItemMappings().getStoredItems().saddle() == itemInHand.getJavaId()) { + return InteractiveTag.OPEN_CONTAINER; + } + + if (testSaddle(itemInHand)) { + return InteractiveTag.SADDLE; + } + + return InteractiveTag.RIDE_HORSE; + } + } + + protected final InteractionResult undeadHorseInteract(@Nonnull GeyserItemStack itemInHand) { + if (!getFlag(EntityFlag.TAMED)) { + return InteractionResult.PASS; + } else if (isBaby()) { + return mobHorseInteract(itemInHand); + } else if (session.isSneaking()) { + // Opens inventory + return InteractionResult.SUCCESS; + } else if (!passengers.isEmpty()) { + return mobHorseInteract(itemInHand); + } else { + // The client tests for saddle but it doesn't matter for us at this point. + return InteractionResult.SUCCESS; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java index fb907829a..7d59be713 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ChestedHorseEntity.java @@ -26,9 +26,12 @@ package org.geysermc.geyser.entity.type.living.animal.horse; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import javax.annotation.Nonnull; import java.util.UUID; public class ChestedHorseEntity extends AbstractHorseEntity { @@ -41,4 +44,21 @@ public class ChestedHorseEntity extends AbstractHorseEntity { protected int getContainerBaseSize() { return 16; } + + @Override + protected boolean testSaddle(@Nonnull GeyserItemStack itemInHand) { + // Not checked here + return false; + } + + @Override + protected boolean testForChest(@Nonnull GeyserItemStack itemInHand) { + return itemInHand.getJavaId() == session.getItemMappings().getStoredItems().chest() && !getFlag(EntityFlag.CHESTED); + } + + @Override + protected boolean additionalTestForInventoryOpen(@Nonnull GeyserItemStack itemInHand) { + // Armor won't work on these + return false; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java index 41ed74f5a..c2548daaf 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java @@ -31,8 +31,8 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.MobArmorEquipmentPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.session.GeyserSession; import java.util.UUID; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java new file mode 100644 index 000000000..c9f95f507 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2022 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.geyser.entity.type.living.animal.horse; + +import com.nukkitx.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; + +import javax.annotation.Nonnull; +import java.util.UUID; + +public class SkeletonHorseEntity extends AbstractHorseEntity { + public SkeletonHorseEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + return testUndeadHorseInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + return undeadHorseInteract(itemInHand); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java new file mode 100644 index 000000000..ddde11c5d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2022 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.geyser.entity.type.living.animal.horse; + +import com.nukkitx.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; + +import javax.annotation.Nonnull; +import java.util.UUID; + +public class ZombieHorseEntity extends AbstractHorseEntity { + public ZombieHorseEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + return testUndeadHorseInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + return undeadHorseInteract(itemInHand); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java index c38b15397..c17503606 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java @@ -32,9 +32,13 @@ 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.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class CatEntity extends TameableEntity { @@ -98,4 +102,28 @@ public class CatEntity extends TameableEntity { public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { return javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon"); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + boolean tamed = getFlag(EntityFlag.TAMED); + if (tamed && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { + // Toggle sitting + return getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; + } else { + return !canEat(itemInHand) || health >= maxHealth && tamed ? InteractiveTag.NONE : InteractiveTag.FEED; + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + boolean tamed = getFlag(EntityFlag.TAMED); + if (tamed && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { + return InteractionResult.SUCCESS; + } else { + // Attempt to feed + return !canEat(itemInHand) || health >= maxHealth && tamed ? InteractionResult.PASS : InteractionResult.SUCCESS; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java index 23f7696d4..b7aca99e5 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java @@ -26,10 +26,15 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class ParrotEntity extends TameableEntity { @@ -40,6 +45,46 @@ public class ParrotEntity extends TameableEntity { @Override public boolean canEat(String javaIdentifierStripped, ItemMapping mapping) { - return javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie"); + return false; + } + + private boolean isTameFood(String javaIdentifierStripped) { + return javaIdentifierStripped.contains("seeds"); + } + + private boolean isPoisonousFood(String javaIdentifierStripped) { + return javaIdentifierStripped.equals("cookie"); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + String javaIdentifierStripped = itemInHand.getMapping(session).getJavaIdentifier().replace("minecraft:", ""); + boolean tame = getFlag(EntityFlag.TAMED); + if (!tame && isTameFood(javaIdentifierStripped)) { + return InteractiveTag.FEED; + } else if (isPoisonousFood(javaIdentifierStripped)) { + return InteractiveTag.FEED; + } else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { + // Sitting/standing + return getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; + } + return super.testMobInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + String javaIdentifierStripped = itemInHand.getMapping(session).getJavaIdentifier().replace("minecraft:", ""); + boolean tame = getFlag(EntityFlag.TAMED); + if (!tame && isTameFood(javaIdentifierStripped)) { + return InteractionResult.SUCCESS; + } else if (isPoisonousFood(javaIdentifierStripped)) { + return InteractionResult.SUCCESS; + } else if (onGround && tame && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { + // Sitting/standing + return InteractionResult.SUCCESS; + } + return super.mobInteract(itemInHand); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java index 9bdb57368..50d17eaaa 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java @@ -64,14 +64,21 @@ public class TameableEntity extends AnimalEntity { Entity entity = session.getEntityCache().getPlayerEntity(entityMetadata.getValue().get()); // Used as both a check since the player isn't in the entity cache and a normal fallback if (entity == null) { - entity = session.getPlayerEntity(); + // Set to tame, but indicate that we are not the player that owns this + ownerBedrockId = Long.MAX_VALUE; + } else { + // Translate to entity ID + ownerBedrockId = entity.getGeyserId(); } - // Translate to entity ID - ownerBedrockId = entity.getGeyserId(); } else { // Reset ownerBedrockId = 0L; } dirtyMetadata.put(EntityData.OWNER_EID, ownerBedrockId); } + + @Override + protected boolean canBeLeashed() { + return isNotLeashed(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index 60a4a1993..8b900f071 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -32,9 +32,14 @@ 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.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.ItemUtils; +import javax.annotation.Nonnull; import java.util.Set; import java.util.UUID; @@ -90,4 +95,45 @@ public class WolfEntity extends TameableEntity { // Cannot be a baby to eat these foods return WOLF_FOODS.contains(javaIdentifierStripped) && !isBaby(); } + + @Override + protected boolean canBeLeashed() { + return !getFlag(EntityFlag.ANGRY) && super.canBeLeashed(); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (getFlag(EntityFlag.ANGRY)) { + return InteractiveTag.NONE; + } + if (itemInHand.getMapping(session).getJavaIdentifier().equals("minecraft:bone") && !getFlag(EntityFlag.TAMED)) { + // Bone and untamed - can tame + return InteractiveTag.TAME; + } else { + int color = ItemUtils.dyeColorFor(itemInHand.getJavaId()); + if (color != -1) { + // If this fails, as of Java Edition 1.18.1, you cannot toggle sit/stand + if (color != this.collarColor) { + return InteractiveTag.DYE; + } + } else if (getFlag(EntityFlag.TAMED) && ownerBedrockId == session.getPlayerEntity().getGeyserId()) { + // Tamed and owned by player - can sit/stand + return getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT; + } + } + return super.testMobInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (ownerBedrockId == session.getPlayerEntity().getGeyserId() || getFlag(EntityFlag.TAMED) + || itemInHand.getMapping(session).getJavaIdentifier().equals("minecraft:bone") && !getFlag(EntityFlag.ANGRY)) { + // Sitting toggle or feeding; not angry + return InteractionResult.CONSUME; + } else { + return InteractionResult.PASS; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java index 28a523f40..633ba707f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/AbstractMerchantEntity.java @@ -26,10 +26,16 @@ package org.geysermc.geyser.entity.type.living.merchant; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.living.AgeableEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class AbstractMerchantEntity extends AgeableEntity { @@ -37,4 +43,37 @@ public class AbstractMerchantEntity extends AgeableEntity { public AbstractMerchantEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + + @Override + protected boolean canBeLeashed() { + return false; + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + String javaIdentifier = itemInHand.getMapping(session).getJavaIdentifier(); + if (!javaIdentifier.equals("minecraft:villager_spawn_egg") + && (definition != EntityDefinitions.VILLAGER || !getFlag(EntityFlag.SLEEPING) && ((VillagerEntity) this).isCanTradeWith())) { + // An additional check we know cannot work + if (!isBaby()) { + return InteractiveTag.TRADE; + } + } + return super.testMobInteraction(itemInHand); + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + String javaIdentifier = itemInHand.getMapping(session).getJavaIdentifier(); + if (!javaIdentifier.equals("minecraft:villager_spawn_egg") + && (definition != EntityDefinitions.VILLAGER || !getFlag(EntityFlag.SLEEPING)) + && (definition != EntityDefinitions.WANDERING_TRADER || !getFlag(EntityFlag.BABY))) { + // Trading time + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(itemInHand); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java index 0f90e4d38..866ba36fc 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java @@ -33,52 +33,49 @@ import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket; -import it.unimi.dsi.fastutil.ints.Int2IntMap; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import lombok.Getter; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.session.GeyserSession; import java.util.Optional; import java.util.UUID; public class VillagerEntity extends AbstractMerchantEntity { - /** * A map of Java profession IDs to Bedrock IDs */ - public static final Int2IntMap VILLAGER_PROFESSIONS = new Int2IntOpenHashMap(); + private static final int[] VILLAGER_PROFESSIONS = new int[15]; /** * A map of all Java region IDs (plains, savanna...) to Bedrock */ - public static final Int2IntMap VILLAGER_REGIONS = new Int2IntOpenHashMap(); + private static final int[] VILLAGER_REGIONS = new int[7]; static { // Java villager profession IDs -> Bedrock - VILLAGER_PROFESSIONS.put(0, 0); - VILLAGER_PROFESSIONS.put(1, 8); - VILLAGER_PROFESSIONS.put(2, 11); - VILLAGER_PROFESSIONS.put(3, 6); - VILLAGER_PROFESSIONS.put(4, 7); - VILLAGER_PROFESSIONS.put(5, 1); - VILLAGER_PROFESSIONS.put(6, 2); - VILLAGER_PROFESSIONS.put(7, 4); - VILLAGER_PROFESSIONS.put(8, 12); - VILLAGER_PROFESSIONS.put(9, 5); - VILLAGER_PROFESSIONS.put(10, 13); - VILLAGER_PROFESSIONS.put(11, 14); - VILLAGER_PROFESSIONS.put(12, 3); - VILLAGER_PROFESSIONS.put(13, 10); - VILLAGER_PROFESSIONS.put(14, 9); + VILLAGER_PROFESSIONS[0] = 0; + VILLAGER_PROFESSIONS[1] = 8; + VILLAGER_PROFESSIONS[2] = 11; + VILLAGER_PROFESSIONS[3] = 6; + VILLAGER_PROFESSIONS[4] = 7; + VILLAGER_PROFESSIONS[5] = 1; + VILLAGER_PROFESSIONS[6] = 2; + VILLAGER_PROFESSIONS[7] = 4; + VILLAGER_PROFESSIONS[8] = 12; + VILLAGER_PROFESSIONS[9] = 5; + VILLAGER_PROFESSIONS[10] = 13; + VILLAGER_PROFESSIONS[11] = 14; + VILLAGER_PROFESSIONS[12] = 3; + VILLAGER_PROFESSIONS[13] = 10; + VILLAGER_PROFESSIONS[14] = 9; - VILLAGER_REGIONS.put(0, 1); - VILLAGER_REGIONS.put(1, 2); - VILLAGER_REGIONS.put(2, 0); - VILLAGER_REGIONS.put(3, 3); - VILLAGER_REGIONS.put(4, 4); - VILLAGER_REGIONS.put(5, 5); - VILLAGER_REGIONS.put(6, 6); + VILLAGER_REGIONS[0] = 1; + VILLAGER_REGIONS[1] = 2; + VILLAGER_REGIONS[2] = 0; + VILLAGER_REGIONS[3] = 3; + VILLAGER_REGIONS[4] = 4; + VILLAGER_REGIONS[5] = 5; + VILLAGER_REGIONS[6] = 6; } private Vector3i bedPosition; @@ -95,12 +92,12 @@ public class VillagerEntity extends AbstractMerchantEntity { public void setVillagerData(EntityMetadata entityMetadata) { VillagerData villagerData = entityMetadata.getValue(); // Profession - int profession = VILLAGER_PROFESSIONS.get(villagerData.getProfession()); + int profession = getBedrockProfession(villagerData.getProfession()); canTradeWith = profession != 14 && profession != 0; // Not a notwit and not professionless dirtyMetadata.put(EntityData.VARIANT, profession); //metadata.put(EntityData.SKIN_ID, villagerData.getType()); Looks like this is modified but for any reason? // Region - dirtyMetadata.put(EntityData.MARK_VARIANT, VILLAGER_REGIONS.get(villagerData.getType())); + dirtyMetadata.put(EntityData.MARK_VARIANT, getBedrockRegion(villagerData.getType())); // Trade tier - different indexing in Bedrock dirtyMetadata.put(EntityData.TRADE_TIER, villagerData.getLevel() - 1); } @@ -158,4 +155,12 @@ public class VillagerEntity extends AbstractMerchantEntity { moveEntityPacket.setTeleported(false); session.sendUpstreamPacket(moveEntityPacket); } + + public static int getBedrockProfession(int javaProfession) { + return javaProfession >= 0 && javaProfession < VILLAGER_PROFESSIONS.length ? VILLAGER_PROFESSIONS[javaProfession] : 0; + } + + public static int getBedrockRegion(int javaRegion) { + return javaRegion >= 0 && javaRegion < VILLAGER_REGIONS.length ? VILLAGER_REGIONS[javaRegion] : 0; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java index 12117d949..cf9393410 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreeperEntity.java @@ -28,10 +28,15 @@ package org.geysermc.geyser.entity.type.living.monster; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.SoundEvent; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class CreeperEntity extends MonsterEntity { @@ -55,4 +60,26 @@ public class CreeperEntity extends MonsterEntity { ignitedByFlintAndSteel = entityMetadata.getPrimitiveValue(); setFlag(EntityFlag.IGNITED, ignitedByFlintAndSteel); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().flintAndSteel()) { + return InteractiveTag.IGNITE_CREEPER; + } else { + return super.testMobInteraction(itemInHand); + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().flintAndSteel()) { + // Ignite creeper + session.playSoundEvent(SoundEvent.IGNITE, position); + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(itemInHand); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java index de1dab463..0069bfb5b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java @@ -150,6 +150,11 @@ public class EnderDragonEntity extends MobEntity implements Tickable { return super.despawnEntity(); } + @Override + protected boolean isEnemy() { + return true; + } + @Override public void tick() { effectTick(); @@ -288,10 +293,6 @@ public class EnderDragonEntity extends MobEntity implements Tickable { session.sendUpstreamPacket(playSoundPacket); } - private boolean isAlive() { - return health > 0; - } - private boolean isHovering() { return phase == 10; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java index 035d405a0..511c56ff7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/GhastEntity.java @@ -44,4 +44,9 @@ public class GhastEntity extends FlyingEntity { // If the ghast is attacking dirtyMetadata.put(EntityData.CHARGE_AMOUNT, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0)); } + + @Override + protected boolean isEnemy() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/MonsterEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/MonsterEntity.java index 885961326..92fbeee67 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/MonsterEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/MonsterEntity.java @@ -37,4 +37,9 @@ public class MonsterEntity extends CreatureEntity { public MonsterEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + + @Override + protected boolean isEnemy() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java index bdc461518..dff79104b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PhantomEntity.java @@ -48,4 +48,9 @@ public class PhantomEntity extends FlyingEntity { setBoundingBoxHeight(boundsScale * definition.height()); dirtyMetadata.put(EntityData.SCALE, modelScale); } + + @Override + protected boolean isEnemy() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java index 8d1c54a00..f0577ee20 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java @@ -30,8 +30,12 @@ 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.geyser.entity.EntityDefinition; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class PiglinEntity extends BasePiglinEntity { @@ -64,4 +68,30 @@ public class PiglinEntity extends BasePiglinEntity { super.updateOffHand(session); } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + InteractiveTag tag = super.testMobInteraction(itemInHand); + if (tag != InteractiveTag.NONE) { + return tag; + } else { + return canGiveGoldTo(itemInHand) ? InteractiveTag.BARTER : InteractiveTag.NONE; + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + InteractionResult superResult = super.mobInteract(itemInHand); + if (superResult.consumesAction()) { + return superResult; + } else { + return canGiveGoldTo(itemInHand) ? InteractionResult.SUCCESS : InteractionResult.PASS; + } + } + + private boolean canGiveGoldTo(@Nonnull GeyserItemStack itemInHand) { + return !getFlag(EntityFlag.BABY) && itemInHand.getJavaId() == session.getItemMappings().getStoredItems().goldIngot() && !getFlag(EntityFlag.ADMIRING); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java index 56719e902..ff1ba9ac3 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ShulkerEntity.java @@ -65,4 +65,9 @@ public class ShulkerEntity extends GolemEntity { dirtyMetadata.put(EntityData.VARIANT, Math.abs(color - 15)); } } + + @Override + protected boolean isEnemy() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java index f02031044..dd5acbfb1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java @@ -55,4 +55,14 @@ public class ZoglinEntity extends MonsterEntity { float scale = getFlag(EntityFlag.BABY) ? 0.55f : 1f; return scale * definition.height(); } + + @Override + protected boolean canBeLeashed() { + return isNotLeashed(); + } + + @Override + protected boolean isEnemy() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java index 15bcc9c6a..1ec0fc26b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZombieVillagerEntity.java @@ -33,33 +33,56 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.merchant.VillagerEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import javax.annotation.Nonnull; import java.util.UUID; public class ZombieVillagerEntity extends ZombieEntity { - private boolean isTransforming; public ZombieVillagerEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } public void setTransforming(BooleanEntityMetadata entityMetadata) { - isTransforming = entityMetadata.getPrimitiveValue(); - setFlag(EntityFlag.IS_TRANSFORMING, isTransforming); + setFlag(EntityFlag.IS_TRANSFORMING, entityMetadata.getPrimitiveValue()); setFlag(EntityFlag.SHAKING, isShaking()); } public void setZombieVillagerData(EntityMetadata entityMetadata) { VillagerData villagerData = entityMetadata.getValue(); - dirtyMetadata.put(EntityData.VARIANT, VillagerEntity.VILLAGER_PROFESSIONS.get(villagerData.getProfession())); // Actually works properly with the OptionalPack - dirtyMetadata.put(EntityData.MARK_VARIANT, VillagerEntity.VILLAGER_REGIONS.get(villagerData.getType())); + dirtyMetadata.put(EntityData.VARIANT, VillagerEntity.getBedrockProfession(villagerData.getProfession())); // Actually works properly with the OptionalPack + dirtyMetadata.put(EntityData.MARK_VARIANT, VillagerEntity.getBedrockRegion(villagerData.getType())); // Used with the OptionalPack dirtyMetadata.put(EntityData.TRADE_TIER, villagerData.getLevel() - 1); } @Override protected boolean isShaking() { - return isTransforming || super.isShaking(); + return getFlag(EntityFlag.IS_TRANSFORMING) || super.isShaking(); + } + + @Nonnull + @Override + protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().goldenApple()) { + return InteractiveTag.CURE; + } else { + return super.testMobInteraction(itemInHand); + } + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().goldenApple()) { + // The client doesn't know if the entity has weakness as that's not usually sent over the network + return InteractionResult.CONSUME; + } else { + return super.mobInteract(itemInHand); + } } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java index 7c0bcaf4d..315e6cb18 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java @@ -31,15 +31,27 @@ import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.Cli import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.session.GeyserSession; -@Getter -@Setter public class MerchantContainer extends Container { + @Getter @Setter private Entity villager; + @Setter private VillagerTrade[] villagerTrades; + @Getter @Setter private ClientboundMerchantOffersPacket pendingOffersPacket; public MerchantContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { super(title, id, size, containerType, playerInventory); } + + public void onTradeSelected(GeyserSession session, int slot) { + if (villagerTrades != null && slot >= 0 && slot < villagerTrades.length) { + VillagerTrade trade = villagerTrades[slot]; + setItem(2, GeyserItemStack.from(trade.getOutput()), session); + // TODO this logic doesn't add up + session.getPlayerEntity().addFakeTradeExperience(trade.getXp()); + session.getPlayerEntity().updateBedrockMetadata(); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java index 14c796a5f..7b1064c8f 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.inventory; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.GeyserImpl; @@ -61,6 +62,10 @@ public class PlayerInventory extends Inventory { cursor = newCursor; } + public GeyserItemStack getItemInHand(@Nonnull Hand hand) { + return hand == Hand.OFF_HAND ? getOffhand() : getItemInHand(); + } + public GeyserItemStack getItemInHand() { if (36 + heldItemSlot > this.size) { GeyserImpl.getInstance().getLogger().debug("Held item slot was larger than expected!"); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java index 2098e04a8..e4296c2d4 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java @@ -41,16 +41,27 @@ public class StoredItemMappings { private final ItemMapping bamboo; private final ItemMapping banner; private final ItemMapping barrier; + private final int bowl; + private final int chest; private final ItemMapping compass; private final ItemMapping crossbow; private final ItemMapping enchantedBook; private final ItemMapping fishingRod; + private final int flintAndSteel; + private final int goldenApple; + private final int goldIngot; + private final int ironIngot; + private final int lead; private final ItemMapping lodestoneCompass; private final ItemMapping milkBucket; + private final int nameTag; private final ItemMapping powderSnowBucket; private final ItemMapping playerHead; private final ItemMapping egg; + private final int saddle; + private final int shears; private final ItemMapping shield; + private final int waterBucket; private final ItemMapping wheat; private final ItemMapping writableBook; @@ -58,16 +69,27 @@ public class StoredItemMappings { this.bamboo = load(itemMappings, "bamboo"); this.banner = load(itemMappings, "white_banner"); // As of 1.17.10, all banners have the same Bedrock ID this.barrier = load(itemMappings, "barrier"); + this.bowl = load(itemMappings, "bowl").getJavaId(); + this.chest = load(itemMappings, "chest").getJavaId(); this.compass = load(itemMappings, "compass"); this.crossbow = load(itemMappings, "crossbow"); this.enchantedBook = load(itemMappings, "enchanted_book"); this.fishingRod = load(itemMappings, "fishing_rod"); + this.flintAndSteel = load(itemMappings, "flint_and_steel").getJavaId(); + this.goldenApple = load(itemMappings, "golden_apple").getJavaId(); + this.goldIngot = load(itemMappings, "gold_ingot").getJavaId(); + this.ironIngot = load(itemMappings, "iron_ingot").getJavaId(); + this.lead = load(itemMappings, "lead").getJavaId(); this.lodestoneCompass = load(itemMappings, "lodestone_compass"); this.milkBucket = load(itemMappings, "milk_bucket"); + this.nameTag = load(itemMappings, "name_tag").getJavaId(); this.powderSnowBucket = load(itemMappings, "powder_snow_bucket"); this.playerHead = load(itemMappings, "player_head"); this.egg = load(itemMappings, "egg"); + this.saddle = load(itemMappings, "saddle").getJavaId(); + this.shears = load(itemMappings, "shears").getJavaId(); this.shield = load(itemMappings, "shield"); + this.waterBucket = load(itemMappings, "water_bucket").getJavaId(); this.wheat = load(itemMappings, "wheat"); this.writableBook = load(itemMappings, "writable_book"); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java new file mode 100644 index 000000000..641d5ad94 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2022 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.geyser.inventory.recipe; + +/** + * A more compact version of {@link com.github.steveice10.mc.protocol.data.game.recipe.Recipe}. + */ +public interface GeyserRecipe { + /** + * Whether the recipe is flexible or not in which items can be placed where. + */ + boolean isShaped(); +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java new file mode 100644 index 000000000..a011fef6d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2022 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.geyser.inventory.recipe; + +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; + +public record GeyserShapedRecipe(int width, int height, Ingredient[] ingredients, ItemStack result) implements GeyserRecipe { + + public GeyserShapedRecipe(ShapedRecipeData data) { + this(data.getWidth(), data.getHeight(), data.getIngredients(), data.getResult()); + } + + @Override + public boolean isShaped() { + return true; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java new file mode 100644 index 000000000..6c7665bbb --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2022 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.geyser.inventory.recipe; + +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.data.ShapelessRecipeData; + +public record GeyserShapelessRecipe(Ingredient[] ingredients, ItemStack result) implements GeyserRecipe { + + public GeyserShapelessRecipe(ShapelessRecipeData data) { + this(data.getIngredients(), data.getResult()); + } + + @Override + public boolean isShaped() { + return false; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java index 3d43b066b..48d0e80e0 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java @@ -38,6 +38,8 @@ import org.geysermc.geyser.util.collection.FixedInt2ByteMap; import org.geysermc.geyser.util.collection.FixedInt2IntMap; import org.geysermc.geyser.util.collection.LecternHasBookMap; +import java.util.Locale; + /** * Used for block entities if the Java block state contains Bedrock block information. */ @@ -47,6 +49,7 @@ public final class BlockStateValues { private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap(); private static final Int2ObjectMap DOUBLE_CHEST_VALUES = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap FLOWER_POT_VALUES = new Int2ObjectOpenHashMap<>(); + private static final IntSet HORIZONTAL_FACING_JIGSAWS = new IntOpenHashSet(); private static final LecternHasBookMap LECTERN_BOOK_STATES = new LecternHasBookMap(); private static final Int2IntMap NOTEBLOCK_PITCHES = new FixedInt2IntMap(); private static final Int2BooleanMap PISTON_VALUES = new Int2BooleanOpenHashMap(); @@ -170,12 +173,22 @@ public final class BlockStateValues { JsonNode shulkerDirection = blockData.get("shulker_direction"); if (shulkerDirection != null) { BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue()); + return; } if (javaId.startsWith("minecraft:water")) { String strLevel = javaId.substring(javaId.lastIndexOf("level=") + 6, javaId.length() - 1); int level = Integer.parseInt(strLevel); WATER_LEVEL.put(javaBlockState, level); + return; + } + + if (javaId.startsWith("minecraft:jigsaw[orientation=")) { + String blockStateData = javaId.substring(javaId.indexOf("orientation=") + "orientation=".length(), javaId.lastIndexOf('_')); + Direction direction = Direction.valueOf(blockStateData.toUpperCase(Locale.ROOT)); + if (direction.isHorizontal()) { + HORIZONTAL_FACING_JIGSAWS.add(javaBlockState); + } } } @@ -230,6 +243,13 @@ public final class BlockStateValues { return FLOWER_POT_VALUES; } + /** + * @return a set of all forward-facing jigsaws, to use as a fallback if NBT is missing. + */ + public static IntSet getHorizontalFacingJigsaws() { + return HORIZONTAL_FACING_JIGSAWS; + } + /** * @return the lectern book state map pointing to book present state */ diff --git a/core/src/main/java/org/geysermc/geyser/network/MinecraftProtocol.java b/core/src/main/java/org/geysermc/geyser/network/MinecraftProtocol.java index c4bd05b13..7ab381375 100644 --- a/core/src/main/java/org/geysermc/geyser/network/MinecraftProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/MinecraftProtocol.java @@ -32,10 +32,7 @@ import com.nukkitx.protocol.bedrock.v471.Bedrock_v471; import com.nukkitx.protocol.bedrock.v475.Bedrock_v475; import com.nukkitx.protocol.bedrock.v486.Bedrock_v486; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.StringJoiner; +import java.util.*; /** * Contains information about the supported protocols in Geyser. @@ -60,7 +57,9 @@ public final class MinecraftProtocol { static { SUPPORTED_BEDROCK_CODECS.add(Bedrock_v471.V471_CODEC); SUPPORTED_BEDROCK_CODECS.add(Bedrock_v475.V475_CODEC.toBuilder().minecraftVersion("1.18.0/1.18.1/1.18.2").build()); - SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder() + .minecraftVersion("1.18.10/1.18.12") // 1.18.11 is also supported, but was only on Switch and since that auto-updates it's not needed + .build()); } /** @@ -92,7 +91,7 @@ public final class MinecraftProtocol { * @return the supported Minecraft: Java Edition version names */ public static List getJavaVersions() { - return Arrays.asList("1.18", "1.18.1"); + return Collections.singletonList(DEFAULT_JAVA_CODEC.getMinecraftVersion()); } /** diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 5686e3d98..f3ffaeeff 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -32,6 +32,7 @@ import com.nukkitx.protocol.bedrock.data.ResourcePackType; import com.nukkitx.protocol.bedrock.packet.*; import com.nukkitx.protocol.bedrock.v471.Bedrock_v471; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.session.auth.AuthType; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.session.GeyserSession; @@ -73,11 +74,9 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { String supportedVersions = MinecraftProtocol.getAllSupportedBedrockVersions(); if (loginPacket.getProtocolVersion() > MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) { // Too early to determine session locale - session.getGeyser().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.outdated.server", supportedVersions)); session.disconnect(GeyserLocale.getLocaleStringLog("geyser.network.outdated.server", supportedVersions)); return true; } else if (loginPacket.getProtocolVersion() < MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) { - session.getGeyser().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.outdated.client", supportedVersions)); session.disconnect(GeyserLocale.getLocaleStringLog("geyser.network.outdated.client", supportedVersions)); return true; } @@ -189,6 +188,14 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { } private boolean couldLoginUserByName(String bedrockUsername) { + if (geyser.getConfig().getSavedUserLogins().contains(bedrockUsername)) { + String refreshToken = geyser.refreshTokenFor(bedrockUsername); + if (refreshToken != null) { + geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().name())); + session.authenticateWithRefreshToken(refreshToken); + return true; + } + } if (geyser.getConfig().getUserAuths() != null) { GeyserConfiguration.IUserAuthenticationInfo info = geyser.getConfig().getUserAuths().get(bedrockUsername); @@ -199,6 +206,12 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { return true; } } + PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(session.getAuthData().xuid()); + if (task != null) { + if (task.getAuthentication().isDone() && session.onMicrosoftLoginComplete(task)) { + return true; + } + } return false; } diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index 791134aa7..e98066305 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -29,7 +29,6 @@ import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType; import com.github.steveice10.mc.protocol.data.game.level.event.SoundEvent; import com.github.steveice10.mc.protocol.data.game.level.particle.ParticleType; -import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType; import com.github.steveice10.packetlib.packet.Packet; import com.nukkitx.nbt.NbtMap; @@ -45,6 +44,7 @@ import net.kyori.adventure.key.Key; import org.geysermc.geyser.api.extension.ExtensionLoader; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.item.Enchantment.JavaEnchantment; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.registry.loader.*; import org.geysermc.geyser.registry.populator.ItemRegistryPopulator; import org.geysermc.geyser.registry.populator.PacketRegistryPopulator; @@ -146,9 +146,9 @@ public final class Registries { public static final SimpleRegistry> POTION_MIXES; /** - * A versioned registry holding all the recipes, with the net ID being the key, and {@link Recipe} as the value. + * A versioned registry holding all the recipes, with the net ID being the key, and {@link GeyserRecipe} as the value. */ - public static final VersionedRegistry> RECIPES = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); + public static final VersionedRegistry> RECIPES = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); /** * A mapped registry holding the available records, with the ID of the record being the key, and the {@link com.nukkitx.protocol.bedrock.data.SoundEvent} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index 1b56a83de..9614e9da8 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -38,10 +38,7 @@ import com.nukkitx.protocol.bedrock.packet.StartGamePacket; import com.nukkitx.protocol.bedrock.v471.Bedrock_v471; import com.nukkitx.protocol.bedrock.v475.Bedrock_v475; import com.nukkitx.protocol.bedrock.v486.Bedrock_v486; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.objects.*; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; @@ -49,6 +46,8 @@ import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.*; +import org.geysermc.geyser.util.ItemUtils; +import org.geysermc.geyser.util.collection.FixedInt2IntMap; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -84,6 +83,10 @@ public class ItemRegistryPopulator { throw new AssertionError("Unable to load Java runtime item IDs", e); } + // We can reduce some operations as Java information is the same across all palette versions + boolean firstMappingsPass = true; + Int2IntMap dyeColors = new FixedInt2IntMap(); + /* Load item palette */ for (Map.Entry palette : PALETTE_VERSIONS.entrySet()) { TypeReference> paletteEntriesType = new TypeReference<>() {}; @@ -224,8 +227,14 @@ public class ItemRegistryPopulator { // This items has a mapping specifically for this version of the game mappingItem = entry.getValue(); } + + String bedrockIdentifier; if (javaIdentifier.equals("minecraft:music_disc_otherside") && palette.getValue().protocolVersion() <= Bedrock_v471.V471_CODEC.getProtocolVersion()) { - mappingItem.setBedrockIdentifier("minecraft:music_disc_pigstep"); + bedrockIdentifier = "minecraft:music_disc_pigstep"; + } else if (javaIdentifier.equals("minecraft:globe_banner_pattern") && palette.getValue().protocolVersion() < Bedrock_v486.V486_CODEC.getProtocolVersion()) { + bedrockIdentifier = "minecraft:banner_pattern"; + } else { + bedrockIdentifier = mappingItem.getBedrockIdentifier(); } if (usingFurnaceMinecart && javaIdentifier.equals("minecraft:furnace_minecart")) { @@ -233,7 +242,7 @@ public class ItemRegistryPopulator { itemIndex++; continue; } - String bedrockIdentifier = mappingItem.getBedrockIdentifier().intern(); + int bedrockId = bedrockIdentifierToId.getInt(bedrockIdentifier); if (bedrockId == Short.MIN_VALUE) { throw new RuntimeException("Missing Bedrock ID in mappings: " + bedrockIdentifier); @@ -358,12 +367,13 @@ public class ItemRegistryPopulator { ItemMapping.ItemMappingBuilder mappingBuilder = ItemMapping.builder() .javaIdentifier(javaIdentifier) .javaId(itemIndex) - .bedrockIdentifier(bedrockIdentifier) + .bedrockIdentifier(bedrockIdentifier.intern()) .bedrockId(bedrockId) .bedrockData(mappingItem.getBedrockData()) .bedrockBlockId(bedrockBlockId) .stackSize(stackSize) - .maxDamage(mappingItem.getMaxDamage()); + .maxDamage(mappingItem.getMaxDamage()) + .hasSuspiciousStewEffect(mappingItem.isHasSuspiciousStewEffect()); if (mappingItem.getRepairMaterials() != null) { mappingBuilder = mappingBuilder.repairMaterials(new ObjectOpenHashSet<>(mappingItem.getRepairMaterials())); @@ -411,6 +421,10 @@ public class ItemRegistryPopulator { itemNames.add(javaIdentifier); + if (firstMappingsPass && mappingItem.getDyeColor() != -1) { + dyeColors.put(itemIndex, mappingItem.getDyeColor()); + } + itemIndex++; } @@ -506,6 +520,10 @@ public class ItemRegistryPopulator { .build(); Registries.ITEMS.register(palette.getValue().protocolVersion(), itemMappings); + + firstMappingsPass = false; } + + ItemUtils.setDyeColors(dyeColors); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/RecipeRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/RecipeRegistryPopulator.java index f32aeef51..f0a215f2a 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/RecipeRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/RecipeRegistryPopulator.java @@ -28,10 +28,7 @@ package org.geysermc.geyser.registry.populator; 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; @@ -40,6 +37,9 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; @@ -71,7 +71,7 @@ public class RecipeRegistryPopulator { // Make a bit of an assumption here that the last recipe net ID will be equivalent between all versions LAST_RECIPE_NET_ID = currentRecipeId; Map> craftingData = new EnumMap<>(RecipeType.class); - Int2ObjectMap recipes = new Int2ObjectOpenHashMap<>(); + Int2ObjectMap recipes = new Int2ObjectOpenHashMap<>(); craftingData.put(RecipeType.CRAFTING_SPECIAL_BOOKCLONING, Collections.singletonList(CraftingData.fromMulti(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"), ++LAST_RECIPE_NET_ID))); @@ -124,7 +124,7 @@ public class RecipeRegistryPopulator { * @param recipes a list of all the recipes * @return the {@link CraftingData} to send to the Bedrock client. */ - private static CraftingData getCraftingDataFromJsonNode(JsonNode node, Int2ObjectMap recipes, ItemMappings mappings) { + private static CraftingData getCraftingDataFromJsonNode(JsonNode node, Int2ObjectMap recipes, ItemMappings mappings) { int netId = ++LAST_RECIPE_NET_ID; int type = node.get("bedrockRecipeType").asInt(); JsonNode outputNode = node.get("output"); @@ -165,9 +165,8 @@ public class RecipeRegistryPopulator { for (ItemData input : inputs) { ingredients.add(new Ingredient(new ItemStack[]{ItemTranslator.translateToJava(input, mappings)})); } - ShapedRecipeData data = new ShapedRecipeData(shape.get(0).length(), shape.size(), "crafting_table", + GeyserRecipe recipe = new GeyserShapedRecipe(shape.get(0).length(), shape.size(), ingredients.toArray(new Ingredient[0]), ItemTranslator.translateToJava(output, mappings)); - Recipe recipe = new Recipe(RecipeType.CRAFTING_SHAPED, "", data); recipes.put(netId, recipe); /* Convert end */ @@ -185,9 +184,7 @@ public class RecipeRegistryPopulator { for (ItemData input : inputs) { ingredients.add(new Ingredient(new ItemStack[]{ItemTranslator.translateToJava(input, mappings)})); } - ShapelessRecipeData data = new ShapelessRecipeData("crafting_table", - ingredients.toArray(new Ingredient[0]), ItemTranslator.translateToJava(output, mappings)); - Recipe recipe = new Recipe(RecipeType.CRAFTING_SHAPELESS, "", data); + GeyserRecipe recipe = new GeyserShapelessRecipe(ingredients.toArray(new Ingredient[0]), ItemTranslator.translateToJava(output, mappings)); recipes.put(netId, recipe); /* Convert end */ diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java index a5b6c5ab8..9d06fd3a9 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/GeyserMappingItem.java @@ -44,4 +44,6 @@ public class GeyserMappingItem { @JsonProperty("tool_tier") String toolTier; @JsonProperty("max_damage") int maxDamage = 0; @JsonProperty("repair_materials") List repairMaterials; + @JsonProperty("has_suspicious_stew_effect") boolean hasSuspiciousStewEffect = false; + @JsonProperty("dye_color") int dyeColor = -1; } diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java index ff558c55f..28d41ba46 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java @@ -39,7 +39,7 @@ import java.util.Set; public class ItemMapping { public static final ItemMapping AIR = new ItemMapping("minecraft:air", "minecraft:air", 0, 0, 0, BlockRegistries.BLOCKS.forVersion(MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getBedrockAirId(), - 64, null, null, null, 0, null); + 64, null, null, null, 0, null, false); String javaIdentifier; String bedrockIdentifier; @@ -63,6 +63,8 @@ public class ItemMapping { Set repairMaterials; + boolean hasSuspiciousStewEffect; + /** * Gets if this item is a block. * diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index c79980f6f..201988347 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -26,7 +26,6 @@ package org.geysermc.geyser.session; import com.github.steveice10.mc.auth.data.GameProfile; -import com.github.steveice10.mc.auth.exception.request.AuthPendingException; import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException; import com.github.steveice10.mc.auth.exception.request.RequestException; import com.github.steveice10.mc.auth.service.AuthenticationService; @@ -39,7 +38,6 @@ import com.github.steveice10.mc.protocol.data.UnexpectedEncryptionException; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.entity.player.HandPreference; -import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; import com.github.steveice10.mc.protocol.data.game.setting.ChatVisibility; import com.github.steveice10.mc.protocol.data.game.setting.SkinPart; import com.github.steveice10.mc.protocol.data.game.statistic.CustomStatistic; @@ -85,7 +83,6 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption; -import org.geysermc.geyser.entity.InteractiveTagManager; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; @@ -94,6 +91,7 @@ import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.network.netty.LocalSession; @@ -114,13 +112,13 @@ import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MathUtils; +import javax.annotation.Nonnull; import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -128,13 +126,13 @@ import java.util.concurrent.atomic.AtomicInteger; @Getter public class GeyserSession implements GeyserConnection, GeyserCommandSource { - private final GeyserImpl geyser; - private final UpstreamSession upstream; + private final @Nonnull GeyserImpl geyser; + private final @Nonnull UpstreamSession upstream; /** * The loop where all packets and ticking is processed to prevent concurrency issues. * If this is manually called, ensure that any exceptions are properly handled. */ - private final EventLoop eventLoop; + private final @Nonnull EventLoop eventLoop; private TcpSession downstream; @Setter private AuthData authData; @@ -350,7 +348,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private Entity mouseoverEntity; @Setter - private Int2ObjectMap craftingRecipes; + private Int2ObjectMap craftingRecipes; private final Set unlockedRecipes; private final AtomicInteger lastRecipeNetId; @@ -361,6 +359,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private Int2ObjectMap stonecutterRecipes; + /** + * Whether to work around 1.13's different behavior in villager trading menus. + */ + @Setter + private boolean emulatePost1_14Logic = true; /** * Starting in 1.17, Java servers expect the carriedItem parameter of the serverbound click container * packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot @@ -444,6 +447,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { */ private boolean flying = false; + @Setter + private boolean instabuild = false; + /** * Caches current rain status. */ @@ -542,11 +548,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { } bedrockServerSession.addDisconnectHandler(disconnectReason -> { - InetAddress address = bedrockServerSession.getRealAddress().getAddress(); - geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.disconnect", address, disconnectReason)); + String message = switch (disconnectReason) { + // A generic message that just means the player quit normally. + case CLOSED_BY_REMOTE_PEER -> GeyserLocale.getLocaleStringLog("geyser.network.disconnect.closed_by_remote_peer"); + case TIMED_OUT -> GeyserLocale.getLocaleStringLog("geyser.network.disconnect.timed_out"); + default -> disconnectReason.name(); + }; - disconnect(disconnectReason.name()); - geyser.getSessionManager().removeSession(this); + disconnect(message); }); this.remoteAddress = geyser.getConfig().getRemote().getAddress(); @@ -632,7 +641,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { loggingIn = true; // Use a future to prevent timeouts as all the authentication is handled sync - // This will be changed with the new protocol library. CompletableFuture.supplyAsync(() -> { try { if (password != null && !password.isEmpty()) { @@ -689,10 +697,58 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { }); } + public void authenticateWithRefreshToken(String refreshToken) { + if (loggedIn) { + geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().name())); + return; + } + + loggingIn = true; + + CompletableFuture.supplyAsync(() -> { + MsaAuthenticationService service = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID); + service.setRefreshToken(refreshToken); + try { + service.login(); + } catch (RequestException e) { + geyser.getLogger().error("Error while attempting to use refresh token for " + name() + "!", e); + return Boolean.FALSE; + } + + GameProfile profile = service.getSelectedProfile(); + if (profile == null) { + // Java account is offline + disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); + return null; + } + + protocol = new MinecraftProtocol(profile, service.getAccessToken()); + geyser.saveRefreshToken(name(), service.getRefreshToken()); + return Boolean.TRUE; + }).whenComplete((successful, ex) -> { + if (this.closed) { + return; + } + if (successful == Boolean.FALSE) { + // The player is waiting for a spawn packet, so let's spawn them in now to show them forms + connect(); + // Will be cached for after login + LoginEncryptionUtils.buildAndShowTokenExpiredWindow(this); + return; + } + + connectDownstream(); + }); + } + + public void authenticateWithMicrosoftCode() { + authenticateWithMicrosoftCode(false); + } + /** * Present a form window to the user asking to log in with another web browser */ - public void authenticateWithMicrosoftCode() { + public void authenticateWithMicrosoftCode(boolean offlineAccess) { if (loggedIn) { geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().name())); return; @@ -705,65 +761,64 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { packet.setTime(16000); sendUpstreamPacket(packet); - // new thread so clients don't timeout - MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID); + final PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getOrCreateTask( + getAuthData().xuid() + ); + task.setOnline(true); + task.resetTimer(); - // Use a future to prevent timeouts as all the authentication is handled sync - // This will be changed with the new protocol library. - CompletableFuture.supplyAsync(() -> { - try { - return msaAuthenticationService.getAuthCode(); - } catch (RequestException e) { - throw new CompletionException(e); - } - }).whenComplete((response, ex) -> { - if (ex != null) { - ex.printStackTrace(); - disconnect(ex.toString()); - return; - } - LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response); - attemptCodeAuthentication(msaAuthenticationService); - }); + if (task.getAuthentication().isDone()) { + onMicrosoftLoginComplete(task); + } else { + task.getCode(offlineAccess).whenComplete((response, ex) -> { + boolean connected = !closed; + if (ex != null) { + if (connected) { + geyser.getLogger().error("Failed to get Microsoft auth code", ex); + disconnect(ex.toString()); + } + task.cleanup(); // error getting auth code -> clean up immediately + } else if (connected) { + LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response); + task.getAuthentication().whenComplete((r, $) -> onMicrosoftLoginComplete(task)); + } + }); + } } /** - * Poll every second to see if the user has successfully signed in + * If successful, also begins connecting to the Java server. */ - private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) { - if (loggedIn || closed) { - return; + public boolean onMicrosoftLoginComplete(PendingMicrosoftAuthentication.AuthenticationTask task) { + if (closed) { + return false; } - CompletableFuture.supplyAsync(() -> { - try { - msaAuthenticationService.login(); - GameProfile profile = msaAuthenticationService.getSelectedProfile(); - if (profile == null) { - // Java account is offline - disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); - return null; - } - - return new MinecraftProtocol(profile, msaAuthenticationService.getAccessToken()); - } catch (RequestException e) { - throw new CompletionException(e); - } - }).whenComplete((response, ex) -> { - if (ex != null) { - if (!(ex instanceof CompletionException completionException) || !(completionException.getCause() instanceof AuthPendingException)) { - geyser.getLogger().error("Failed to log in with Microsoft code!", ex); - disconnect(ex.toString()); - } else { - // Wait one second before trying again - geyser.getScheduledThread().schedule(() -> attemptCodeAuthentication(msaAuthenticationService), 1, TimeUnit.SECONDS); - } - return; - } - if (!closed) { - this.protocol = response; + task.cleanup(); // player is online -> remove pending authentication immediately + Throwable ex = task.getLoginException(); + if (ex != null) { + geyser.getLogger().error("Failed to log in with Microsoft code!", ex); + disconnect(ex.toString()); + } else { + MsaAuthenticationService service = task.getMsaAuthenticationService(); + GameProfile selectedProfile = service.getSelectedProfile(); + if (selectedProfile == null) { + disconnect(GeyserLocale.getPlayerLocaleString( + "geyser.network.remote.invalid_account", + clientData.getLanguageCode() + )); + } else { + this.protocol = new MinecraftProtocol( + selectedProfile, + service.getAccessToken() + ); connectDownstream(); + + // Save our refresh token for later use + geyser.saveRefreshToken(name(), service.getRefreshToken()); + return true; } - }); + } + return false; } /** @@ -958,11 +1013,22 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { loggedIn = false; if (downstream != null) { downstream.disconnect(reason); + } else { + // Downstream's disconnect will fire an event that prints a log message + // Otherwise, we print a message here + InetAddress address = upstream.getAddress().getAddress(); + geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.disconnect", address, reason)); } - if (upstream != null && !upstream.isClosed()) { - geyser.getSessionManager().removeSession(this); + if (!upstream.isClosed()) { upstream.disconnect(reason); } + geyser.getSessionManager().removeSession(this); + if (authData != null) { + PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid()); + if (task != null) { + task.setOnline(false); + } + } } if (tickThread != null) { @@ -1076,7 +1142,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { if (mouseoverEntity != null) { // Horses, etc can change their property depending on if you're sneaking - InteractiveTagManager.updateTag(this, mouseoverEntity); + mouseoverEntity.updateInteractiveTag(); } } @@ -1526,4 +1592,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { packet.getFogStack().addAll(this.fogNameSpaces); sendUpstreamPacket(packet); } + + public boolean canUseCommandBlocks() { + return instabuild && opPermissionLevel >= 2; + } + + public void playSoundEvent(SoundEvent sound, Vector3f position) { + LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet(); + packet.setPosition(position); + packet.setSound(sound); + packet.setIdentifier(":"); + packet.setExtraData(-1); + sendUpstreamPacket(packet); + } } diff --git a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java new file mode 100644 index 000000000..93200dcb6 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.session; + +import com.github.steveice10.mc.auth.exception.request.AuthPendingException; +import com.github.steveice10.mc.auth.exception.request.RequestException; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.SneakyThrows; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; + +import javax.annotation.Nonnull; +import java.util.concurrent.*; + +/** + * Pending Microsoft authentication task cache. + * It permits user to exit the server while they authorize Geyser to access their Microsoft account. + */ +public class PendingMicrosoftAuthentication { + /** + * For GeyserConnect usage. + */ + private boolean storeServerInformation = false; + private final LoadingCache authentications; + + public PendingMicrosoftAuthentication(int timeoutSeconds) { + this.authentications = CacheBuilder.newBuilder() + .build(new CacheLoader<>() { + @Override + public AuthenticationTask load(@NonNull String userKey) { + return storeServerInformation ? new ProxyAuthenticationTask(userKey, timeoutSeconds * 1000L) + : new AuthenticationTask(userKey, timeoutSeconds * 1000L); + } + }); + } + + public AuthenticationTask getTask(@Nonnull String userKey) { + return authentications.getIfPresent(userKey); + } + + @SneakyThrows(ExecutionException.class) + public AuthenticationTask getOrCreateTask(@Nonnull String userKey) { + return authentications.get(userKey); + } + + @SuppressWarnings("unused") // GeyserConnect + public void setStoreServerInformation() { + storeServerInformation = true; + } + + public class AuthenticationTask { + private static final Executor DELAYED_BY_ONE_SECOND = CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS); + + @Getter + private final MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID); + private final String userKey; + private final long timeoutMs; + + private long remainingTimeMs; + + @Setter + private boolean online = true; + + @Getter + private final CompletableFuture authentication; + + @Getter + private volatile Throwable loginException; + + private AuthenticationTask(String userKey, long timeoutMs) { + this.userKey = userKey; + this.timeoutMs = timeoutMs; + this.remainingTimeMs = timeoutMs; + + this.authentication = new CompletableFuture<>(); + this.authentication.whenComplete((r, ex) -> { + this.loginException = ex; + // avoid memory leak, in case player doesn't connect again + CompletableFuture.delayedExecutor(timeoutMs, TimeUnit.MILLISECONDS).execute(this::cleanup); + }); + } + + public void resetTimer() { + this.remainingTimeMs = this.timeoutMs; + } + + public void cleanup() { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); + if (logger.isDebug()) { + logger.debug("Cleaning up authentication task for " + userKey); + } + authentications.invalidate(userKey); + } + + public CompletableFuture getCode(boolean offlineAccess) { + // Request the code + CompletableFuture code = CompletableFuture.supplyAsync(() -> tryGetCode(offlineAccess)); + // Once the code is received, continuously try to request the access token, profile, etc + code.thenRun(() -> performLoginAttempt(System.currentTimeMillis())); + return code; + } + + /** + * @param offlineAccess whether we want a refresh token for later use. + */ + private MsaAuthenticationService.MsCodeResponse tryGetCode(boolean offlineAccess) throws CompletionException { + try { + return msaAuthenticationService.getAuthCode(offlineAccess); + } catch (RequestException e) { + throw new CompletionException(e); + } + } + + private void performLoginAttempt(long lastAttempt) { + CompletableFuture.runAsync(() -> { + try { + msaAuthenticationService.login(); + } catch (AuthPendingException e) { + long currentAttempt = System.currentTimeMillis(); + if (!online) { + // decrement timer only when player's offline + remainingTimeMs -= currentAttempt - lastAttempt; + if (remainingTimeMs <= 0L) { + // time's up + authentication.completeExceptionally(new TaskTimeoutException()); + cleanup(); + return; + } + } + // try again in 1 second + performLoginAttempt(currentAttempt); + return; + } catch (Exception e) { + authentication.completeExceptionally(e); + return; + } + // login successful + authentication.complete(msaAuthenticationService); + }, DELAYED_BY_ONE_SECOND); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{userKey='" + userKey + "'}"; + } + } + + @Getter + @Setter + public final class ProxyAuthenticationTask extends AuthenticationTask { + private String server; + private int port; + + private ProxyAuthenticationTask(String userKey, long timeoutMs) { + super(userKey, timeoutMs); + } + } + + /** + * @see PendingMicrosoftAuthentication + */ + public static class TaskTimeoutException extends Exception { + TaskTimeoutException() { + super("It took too long to authorize Geyser to access your Microsoft account. " + + "Please request new code and try again."); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index 549b2dbee..d46a39616 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java @@ -28,15 +28,19 @@ package org.geysermc.geyser.session.cache; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntLists; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.session.GeyserSession; +import javax.annotation.ParametersAreNonnullByDefault; import java.util.Map; /** * Manages information sent from the {@link ClientboundUpdateTagsPacket}. If that packet is not sent, all lists here * will remain empty, matching Java Edition behavior. */ +@ParametersAreNonnullByDefault public class TagCache { /* Blocks */ private IntList leaves; @@ -52,16 +56,19 @@ public class TagCache { private IntList requiresDiamondTool; /* Items */ + private IntList axolotlTemptItems; + private IntList fishes; private IntList flowers; private IntList foxFood; private IntList piglinLoved; + private IntList smallFlowers; public TagCache() { // Ensure all lists are non-null clear(); } - public void loadPacket(ClientboundUpdateTagsPacket packet) { + public void loadPacket(GeyserSession session, ClientboundUpdateTagsPacket packet) { Map blockTags = packet.getTags().get("minecraft:block"); this.leaves = IntList.of(blockTags.get("minecraft:leaves")); this.wool = IntList.of(blockTags.get("minecraft:wool")); @@ -76,9 +83,19 @@ public class TagCache { this.requiresDiamondTool = IntList.of(blockTags.get("minecraft:needs_diamond_tool")); Map itemTags = packet.getTags().get("minecraft:item"); + this.axolotlTemptItems = IntList.of(itemTags.get("minecraft:axolotl_tempt_items")); + this.fishes = IntList.of(itemTags.get("minecraft:fishes")); this.flowers = IntList.of(itemTags.get("minecraft:flowers")); this.foxFood = IntList.of(itemTags.get("minecraft:fox_food")); this.piglinLoved = IntList.of(itemTags.get("minecraft:piglin_loved")); + this.smallFlowers = IntList.of(itemTags.get("minecraft:small_flowers")); + + // Hack btw + boolean emulatePost1_14Logic = itemTags.get("minecraft:signs").length > 1; + session.setEmulatePost1_14Logic(emulatePost1_14Logic); + if (session.getGeyser().getLogger().isDebug()) { + session.getGeyser().getLogger().debug("Emulating post 1.14 villager logic for " + session.name() + "? " + emulatePost1_14Logic); + } } public void clear() { @@ -94,9 +111,20 @@ public class TagCache { this.requiresIronTool = IntLists.emptyList(); this.requiresDiamondTool = IntLists.emptyList(); + this.axolotlTemptItems = IntLists.emptyList(); + this.fishes = IntLists.emptyList(); this.flowers = IntLists.emptyList(); this.foxFood = IntLists.emptyList(); this.piglinLoved = IntLists.emptyList(); + this.smallFlowers = IntLists.emptyList(); + } + + public boolean isAxolotlTemptItem(ItemMapping itemMapping) { + return axolotlTemptItems.contains(itemMapping.getJavaId()); + } + + public boolean isFish(GeyserItemStack itemStack) { + return fishes.contains(itemStack.getJavaId()); } public boolean isFlower(ItemMapping mapping) { @@ -111,6 +139,10 @@ public class TagCache { return piglinLoved.contains(mapping.getJavaId()); } + public boolean isSmallFlower(GeyserItemStack itemStack) { + return smallFlowers.contains(itemStack.getJavaId()); + } + public boolean isAxeEffective(BlockMapping blockMapping) { return axeEffective.contains(blockMapping.getJavaBlockId()); } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java index 00a080d8b..66922ff0b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java @@ -30,7 +30,6 @@ import com.nukkitx.math.vector.Vector2d; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; -import com.nukkitx.protocol.bedrock.packet.PlayerFogPacket; import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.entity.EntityDefinitions; @@ -38,7 +37,6 @@ import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.session.GeyserSession; import javax.annotation.Nonnull; -import java.util.Collections; public class WorldBorder { private static final double DEFAULT_WORLD_BORDER_SIZE = 5.9999968E7D; @@ -131,11 +129,14 @@ public class WorldBorder { } /** - * @return true as long the entity is within the world limits. + * @return true as long as the player entity is within the world limits. */ public boolean isInsideBorderBoundaries() { - Vector3f entityPosition = session.getPlayerEntity().getPosition(); - return entityPosition.getX() > minX && entityPosition.getX() < maxX && entityPosition.getZ() > minZ && entityPosition.getZ() < maxZ; + return isInsideBorderBoundaries(session.getPlayerEntity().getPosition()); + } + + public boolean isInsideBorderBoundaries(Vector3f position) { + return position.getX() > minX && position.getX() < maxX && position.getZ() > minZ && position.getZ() < maxZ; } /** diff --git a/core/src/main/java/org/geysermc/geyser/text/DummyLegacyHoverEventSerializer.java b/core/src/main/java/org/geysermc/geyser/text/DummyLegacyHoverEventSerializer.java new file mode 100644 index 000000000..fdce1f879 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/text/DummyLegacyHoverEventSerializer.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2022 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.geyser.text; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.serializer.gson.LegacyHoverEventSerializer; +import net.kyori.adventure.util.Codec; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public final class DummyLegacyHoverEventSerializer implements LegacyHoverEventSerializer { + private final HoverEvent.ShowEntity dummyShowEntity; + private final HoverEvent.ShowItem dummyShowItem; + + public DummyLegacyHoverEventSerializer() { + dummyShowEntity = HoverEvent.ShowEntity.of(Key.key("geysermc", "dummyshowitem"), + UUID.nameUUIDFromBytes("entitiesareprettyneat".getBytes(StandardCharsets.UTF_8))); + dummyShowItem = HoverEvent.ShowItem.of(Key.key("geysermc", "dummyshowentity"), 0); + } + + @Override + public HoverEvent.@NotNull ShowItem deserializeShowItem(@NotNull Component input) { + return dummyShowItem; + } + + @Override + public HoverEvent.@NotNull ShowEntity deserializeShowEntity(@NotNull Component input, + Codec.Decoder componentDecoder) { + return dummyShowEntity; + } + + @Override + public @NotNull Component serializeShowItem(HoverEvent.@NotNull ShowItem input) { + return Component.empty(); + } + + @Override + public @NotNull Component serializeShowEntity(HoverEvent.@NotNull ShowEntity input, + Codec.Encoder componentEncoder) { + return Component.empty(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index e6a9faf74..b48709595 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -28,9 +28,6 @@ package org.geysermc.geyser.translator.inventory; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; 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.opennbt.tag.builtin.IntTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; @@ -45,6 +42,9 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.*; import org.geysermc.geyser.inventory.click.Click; import org.geysermc.geyser.inventory.click.ClickPlan; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTranslator; import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator; @@ -535,7 +535,6 @@ public abstract class InventoryTranslator { } int gridDimensions = gridSize == 4 ? 2 : 3; - Recipe recipe; Ingredient[] ingredients = new Ingredient[0]; ItemStack output = null; int recipeWidth = 0; @@ -564,7 +563,7 @@ public abstract class InventoryTranslator { craftState = CraftState.RECIPE_ID; int recipeId = autoCraftAction.getRecipeNetworkId(); - recipe = session.getCraftingRecipes().get(recipeId); + GeyserRecipe recipe = session.getCraftingRecipes().get(recipeId); if (recipe == null) { return rejectRequest(request); } @@ -578,24 +577,21 @@ public abstract class InventoryTranslator { } } - 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); - } + if (recipe.isShaped()) { + GeyserShapedRecipe shapedRecipe = (GeyserShapedRecipe) recipe; + ingredients = shapedRecipe.ingredients(); + recipeWidth = shapedRecipe.width(); + output = shapedRecipe.result(); + if (recipeWidth > gridDimensions || shapedRecipe.height() > gridDimensions) { + return rejectRequest(request); } - case CRAFTING_SHAPELESS -> { - ShapelessRecipeData shapelessData = (ShapelessRecipeData) recipe.getData(); - ingredients = shapelessData.getIngredients(); - recipeWidth = gridDimensions; - output = shapelessData.getResult(); - if (ingredients.length > gridSize) { - return rejectRequest(request); - } + } else { + GeyserShapelessRecipe shapelessRecipe = (GeyserShapelessRecipe) recipe; + ingredients = shapelessRecipe.ingredients(); + recipeWidth = gridDimensions; + output = shapelessRecipe.result(); + if (ingredients.length > gridSize) { + return rejectRequest(request); } } break; diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java index 6b63056a3..248bd35b7 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java @@ -26,14 +26,18 @@ package org.geysermc.geyser.translator.inventory; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; +import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSelectTradePacket; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.entity.EntityData; 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.data.inventory.stackrequestactions.AutoCraftRecipeStackRequestActionData; +import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.CraftRecipeStackRequestActionData; import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket; +import com.nukkitx.protocol.bedrock.v486.Bedrock_v486; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.inventory.Inventory; @@ -44,6 +48,9 @@ import org.geysermc.geyser.inventory.BedrockContainerSlot; import org.geysermc.geyser.inventory.SlotType; import org.geysermc.geyser.inventory.updater.InventoryUpdater; import org.geysermc.geyser.inventory.updater.UIInventoryUpdater; +import org.geysermc.geyser.util.InventoryUtils; + +import java.util.concurrent.TimeUnit; public class MerchantInventoryTranslator extends BaseInventoryTranslator { private final InventoryUpdater updater; @@ -131,11 +138,63 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator { } } + @Override + public ItemStackResponsePacket.Response translateCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + if (session.getUpstream().getProtocolVersion() < Bedrock_v486.V486_CODEC.getProtocolVersion()) { + return super.translateCraftingRequest(session, inventory, request); + } + + // Behavior as of 1.18.10. + // We set the net ID to the trade index + 1. This doesn't appear to cause issues and means we don't have to + // store a map of net ID to trade index on our end. + int tradeChoice = ((CraftRecipeStackRequestActionData) request.getActions()[0]).getRecipeNetworkId() - 1; + return handleTrade(session, inventory, request, tradeChoice); + } + @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); + if (session.getUpstream().getProtocolVersion() < Bedrock_v486.V486_CODEC.getProtocolVersion()) { + // We're not crafting here + // Called at least by consoles when pressing a trade option button + return translateRequest(session, inventory, request); + } + + // 1.18.10 update - seems impossible to call without consoles/controller input + // We set the net ID to the trade index + 1. This doesn't appear to cause issues and means we don't have to + // store a map of net ID to trade index on our end. + int tradeChoice = ((AutoCraftRecipeStackRequestActionData) request.getActions()[0]).getRecipeNetworkId() - 1; + return handleTrade(session, inventory, request, tradeChoice); + } + + private ItemStackResponsePacket.Response handleTrade(GeyserSession session, Inventory inventory, ItemStackRequest request, int tradeChoice) { + ServerboundSelectTradePacket packet = new ServerboundSelectTradePacket(tradeChoice); + session.sendDownstreamPacket(packet); + + if (session.isEmulatePost1_14Logic()) { + // 1.18 Java cooperates nicer than older versions + if (inventory instanceof MerchantContainer merchantInventory) { + merchantInventory.onTradeSelected(session, tradeChoice); + } + return translateRequest(session, inventory, request); + } else { + // 1.18 servers works fine without a workaround, but ViaVersion needs to work around 1.13 servers, + // so we need to work around that with the delay. Specifically they force a window refresh after a + // trade packet has been sent. + session.scheduleInEventLoop(() -> { + if (inventory instanceof MerchantContainer merchantInventory) { + merchantInventory.onTradeSelected(session, tradeChoice); + // Ignore output since we don't want to send a delayed response packet back to the client + translateRequest(session, inventory, request); + + // Resync items once more + updateInventory(session, inventory); + InventoryUtils.updateCursor(session); + } + }, 100, TimeUnit.MILLISECONDS); + + // Revert this request, for now + return rejectRequest(request); + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/BannerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/BannerTranslator.java index a5c3235a2..15f7c57ce 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/BannerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/BannerTranslator.java @@ -37,6 +37,7 @@ import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -96,10 +97,7 @@ public class BannerTranslator extends ItemTranslator { public static NbtList convertBannerPattern(ListTag patterns) { List tagsList = new ArrayList<>(); for (Tag patternTag : patterns.getValue()) { - NbtMap newPatternTag = getBedrockBannerPattern((CompoundTag) patternTag); - if (newPatternTag != null) { - tagsList.add(newPatternTag); - } + tagsList.add(getBedrockBannerPattern((CompoundTag) patternTag)); } return new NbtList<>(NbtType.COMPOUND, tagsList); @@ -111,17 +109,11 @@ public class BannerTranslator extends ItemTranslator { * @param pattern Java edition pattern nbt * @return The Bedrock edition format pattern nbt */ - public static NbtMap getBedrockBannerPattern(CompoundTag pattern) { - String patternName = (String) pattern.get("Pattern").getValue(); - - // Return null if its the globe pattern as it doesn't exist on bedrock - if (patternName.equals("glb")) { - return null; - } - + @Nonnull + private static NbtMap getBedrockBannerPattern(CompoundTag pattern) { return NbtMap.builder() .putInt("Color", 15 - (int) pattern.get("Color").getValue()) - .putString("Pattern", patternName) + .putString("Pattern", (String) pattern.get("Pattern").getValue()) .build(); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/JigsawBlockBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/JigsawBlockBlockEntityTranslator.java index a1e990138..bb036a1b0 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/JigsawBlockBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/JigsawBlockBlockEntityTranslator.java @@ -28,16 +28,25 @@ package org.geysermc.geyser.translator.level.block.entity; import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; import com.nukkitx.nbt.NbtMapBuilder; +import org.geysermc.geyser.level.block.BlockStateValues; @BlockEntity(type = BlockEntityType.JIGSAW) -public class JigsawBlockBlockEntityTranslator extends BlockEntityTranslator { +public class JigsawBlockBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState { @Override public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) { - builder.put("joint", ((StringTag) tag.get("joint")).getValue()); - builder.put("name", ((StringTag) tag.get("name")).getValue()); - builder.put("target_pool", ((StringTag) tag.get("pool")).getValue()); + Tag jointTag = tag.get("joint"); + if (jointTag instanceof StringTag) { + builder.put("joint", ((StringTag) jointTag).getValue()); + } else { + // Tag is not present in at least 1.14.4 Paper + // Minecraft 1.18.1 deliberately has a fallback here, but not for any other value + builder.put("joint", BlockStateValues.getHorizontalFacingJigsaws().contains(blockState) ? "aligned" : "rollable"); + } + builder.put("name", getOrDefault(tag.get("name"), "")); + builder.put("target_pool", getOrDefault(tag.get("pool"), "")); builder.put("final_state", ((StringTag) tag.get("final_state")).getValue()); - builder.put("target", ((StringTag) tag.get("target")).getValue()); + builder.put("target", getOrDefault(tag.get("target"), "")); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 869062da2..7129c1318 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -33,23 +33,21 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction; import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket; -import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket; +import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.inventory.*; import com.nukkitx.protocol.bedrock.packet.*; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.geysermc.geyser.entity.EntityDefinitions; -import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; +import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.click.Click; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.registry.BlockRegistries; @@ -58,8 +56,9 @@ import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; -import org.geysermc.geyser.translator.sound.EntitySoundInteractionTranslator; import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InventoryUtils; import java.util.List; @@ -91,18 +90,41 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator 1; + + if (session.getPlayerInventory().getHeldItemSlot() != containerAction.getSlot()) { + // Dropping an item that you don't have selected isn't supported in Java, but we can workaround it with an inventory hack + PlayerInventory inventory = session.getPlayerInventory(); + int hotbarSlot = inventory.getOffsetForHotbar(containerAction.getSlot()); + Click clickType = dropAll ? Click.DROP_ALL : Click.DROP_ONE; + Int2ObjectMap changedItem; + if (dropAll) { + inventory.setItem(hotbarSlot, GeyserItemStack.EMPTY, session); + changedItem = Int2ObjectMaps.singleton(hotbarSlot, null); + } else { + GeyserItemStack itemStack = inventory.getItem(hotbarSlot); + if (itemStack.isEmpty()) { + return; + } + itemStack.sub(1); + changedItem = Int2ObjectMaps.singleton(hotbarSlot, itemStack.getItemStack()); + } + ServerboundContainerClickPacket dropPacket = new ServerboundContainerClickPacket( + inventory.getId(), inventory.getStateId(), hotbarSlot, clickType.actionType, clickType.action, + inventory.getCursor().getItemStack(), changedItem); + session.sendDownstreamPacket(dropPacket); + return; + } + if (session.getPlayerInventory().getItemInHand().isEmpty()) { return; } - boolean dropAll = worldAction.getToItem().getCount() > 1; - ServerboundPlayerActionPacket dropAllPacket = new ServerboundPlayerActionPacket( + ServerboundPlayerActionPacket dropPacket = new ServerboundPlayerActionPacket( dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM, BlockUtils.POSITION_ZERO, Direction.DOWN ); - session.sendDownstreamPacket(dropAllPacket); + session.sendDownstreamPacket(dropPacket); if (dropAll) { session.getPlayerInventory().setItemInHand(GeyserItemStack.EMPTY); @@ -151,14 +173,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { + // Not sent as of 1.18.10 ServerboundSelectTradePacket selectTradePacket = new ServerboundSelectTradePacket(packet.getData()); session.sendDownstreamPacket(selectTradePacket); session.scheduleInEventLoop(() -> { - SessionPlayerEntity villager = session.getPlayerEntity(); Inventory openInventory = session.getOpenInventory(); if (openInventory instanceof MerchantContainer merchantInventory) { - 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); - // TODO this logic doesn't add up - villager.addFakeTradeExperience(trade.getXp()); - villager.updateBedrockMetadata(); - } + merchantInventory.onTradeSelected(session, packet.getData()); } }, 100, TimeUnit.MILLISECONDS); return; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java index 4df0ba048..471668492 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java @@ -40,7 +40,6 @@ import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; -import org.geysermc.geyser.entity.InteractiveTagManager; @Translator(packet = InteractPacket.class) public class BedrockInteractTranslator extends PacketTranslator { @@ -84,7 +83,7 @@ public class BedrockInteractTranslator extends PacketTranslator return; } - InteractiveTagManager.updateTag(session, interactEntity); + interactEntity.updateInteractiveTag(); } else { if (session.getMouseoverEntity() != null) { // No interactive tag should be sent diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index c3c8abfb4..4d7a1617a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -42,6 +42,9 @@ import com.nukkitx.protocol.bedrock.v486.Bedrock_v486; import it.unimi.dsi.fastutil.ints.*; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -80,7 +83,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator= Bedrock_v486.V486_CODEC.getProtocolVersion(); - Int2ObjectMap recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion())); + Int2ObjectMap recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion())); Int2ObjectMap> unsortedStonecutterData = new Int2ObjectOpenHashMap<>(); CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); craftingDataPacket.setCleanRecipes(true); @@ -100,7 +103,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { @@ -118,7 +121,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java index 3d5bfc43a..a899077f8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java @@ -35,6 +35,6 @@ public class JavaUpdateTagsTranslator extends PacketTranslator { @@ -57,8 +56,7 @@ public class JavaOpenScreenTranslator extends PacketTranslator { - - /** - * Handles the block interaction when a player - * right-clicks an entity. - * - * @param session the session interacting with the block - * @param position the position of the block - * @param entity the entity interacted with - */ - static void handleEntityInteraction(GeyserSession session, Vector3f position, Entity entity) { - // If we need to get the hand identifier, only get it once and save it to a variable - String handIdentifier = null; - - for (Map.Entry> interactionEntry : Registries.SOUND_TRANSLATORS.get().entrySet()) { - if (!(interactionEntry.getValue() instanceof EntitySoundInteractionTranslator)) { - continue; - } - if (interactionEntry.getKey().entities().length != 0) { - boolean contains = false; - for (String entityIdentifier : interactionEntry.getKey().entities()) { - if (entity.getDefinition().entityType().name().toLowerCase().contains(entityIdentifier)) { - contains = true; - break; - } - } - if (!contains) continue; - } - GeyserItemStack itemInHand = session.getPlayerInventory().getItemInHand(); - if (interactionEntry.getKey().items().length != 0) { - if (itemInHand.isEmpty()) { - continue; - } - if (handIdentifier == null) { - // Don't get the identifier unless we need it - handIdentifier = itemInHand.getMapping(session).getJavaIdentifier(); - } - boolean contains = false; - for (String itemIdentifier : interactionEntry.getKey().items()) { - if (handIdentifier.contains(itemIdentifier)) { - contains = true; - break; - } - } - if (!contains) continue; - } - if (session.isSneaking() && !interactionEntry.getKey().ignoreSneakingWhileHolding()) { - if (!itemInHand.isEmpty()) { - continue; - } - } - ((EntitySoundInteractionTranslator) interactionEntry.getValue()).translate(session, position, entity); - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/SoundTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/SoundTranslator.java index bb0e7c20a..0146c534e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/SoundTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/SoundTranslator.java @@ -54,17 +54,6 @@ public @interface SoundTranslator { */ String[] items() default {}; - /** - * The identifier(s) that the interacted entity must have. - * Leave empty to ignore. - * - * Only applies to interaction handlers that are an - * instance of {@link EntitySoundInteractionTranslator}. - * - * @return the value the item in the player's hand must contain - */ - String[] entities() default {}; - /** * Controls if the interaction should still be * called even if the player is sneaking while diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/entity/FeedBabySoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/entity/FeedBabySoundInteractionTranslator.java deleted file mode 100644 index b996dafee..000000000 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/entity/FeedBabySoundInteractionTranslator.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.translator.sound.entity; - -import com.nukkitx.math.vector.Vector3f; -import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; -import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; -import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; -import org.geysermc.geyser.entity.type.living.animal.OcelotEntity; -import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.sound.EntitySoundInteractionTranslator; -import org.geysermc.geyser.translator.sound.SoundTranslator; - -@SoundTranslator -public class FeedBabySoundInteractionTranslator implements EntitySoundInteractionTranslator { - - @Override - public void translate(GeyserSession session, Vector3f position, Entity entity) { - if (entity instanceof AnimalEntity animalEntity && !(entity instanceof CatEntity || entity instanceof OcelotEntity)) { - String handIdentifier = session.getPlayerInventory().getItemInHand().getMapping(session).getJavaIdentifier(); - boolean isBaby = animalEntity.isBaby(); - if (isBaby && animalEntity.canEat(handIdentifier.replace("minecraft:", ""), - session.getPlayerInventory().getItemInHand().getMapping(session))) { - // Play the "feed child" effect - EntityEventPacket feedEvent = new EntityEventPacket(); - feedEvent.setRuntimeEntityId(entity.getGeyserId()); - feedEvent.setType(EntityEventType.BABY_ANIMAL_FEED); - session.sendUpstreamPacket(feedEvent); - } - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/entity/MilkEntitySoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/entity/MilkEntitySoundInteractionTranslator.java deleted file mode 100644 index 49994f7e6..000000000 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/entity/MilkEntitySoundInteractionTranslator.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.translator.sound.entity; - -import com.nukkitx.math.vector.Vector3f; -import com.nukkitx.protocol.bedrock.data.SoundEvent; -import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; -import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket; -import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.entity.type.living.animal.GoatEntity; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.sound.EntitySoundInteractionTranslator; -import org.geysermc.geyser.translator.sound.SoundTranslator; - -@SoundTranslator(entities = {"cow", "goat"}, items = "bucket") -public class MilkEntitySoundInteractionTranslator implements EntitySoundInteractionTranslator { - - @Override - public void translate(GeyserSession session, Vector3f position, Entity value) { - if (!session.getPlayerInventory().getItemInHand().getMapping(session).getJavaIdentifier().equals("minecraft:bucket")) { - return; - } - if (value.getFlag(EntityFlag.BABY)) { - return; - } - - SoundEvent milkSound; - if (value instanceof GoatEntity && ((GoatEntity) value).isScreamer()) { - milkSound = SoundEvent.MILK_SCREAMER; - } else { - milkSound = SoundEvent.MILK; - } - LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); - levelSoundEventPacket.setPosition(position); - levelSoundEventPacket.setBabySound(false); - levelSoundEventPacket.setRelativeVolumeDisabled(false); - levelSoundEventPacket.setIdentifier(":"); - levelSoundEventPacket.setSound(milkSound); - levelSoundEventPacket.setExtraData(-1); - session.sendUpstreamPacket(levelSoundEventPacket); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java index a9fca9074..10b1bbc5a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java @@ -34,10 +34,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.ChatColor; -import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.text.GsonComponentSerializerWrapper; -import org.geysermc.geyser.text.MinecraftTranslationRegistry; +import org.geysermc.geyser.text.*; import java.util.EnumMap; import java.util.Map; @@ -85,8 +82,13 @@ public class MessageTranslator { TEAM_COLORS.put(TeamColor.STRIKETHROUGH, BASE + "m"); TEAM_COLORS.put(TeamColor.ITALIC, BASE + "o"); - // Temporary fix for https://github.com/KyoriPowered/adventure/issues/447 - GsonComponentSerializer source = DefaultComponentSerializer.get(); + // Temporary fix for https://github.com/KyoriPowered/adventure/issues/447 - TODO resolve properly + GsonComponentSerializer source = DefaultComponentSerializer.get() + .toBuilder() + // Use a custom legacy hover event deserializer since we don't use any of this data anyway, and + // fixes issues where legacy hover events throw deserialization errors + .legacyHoverEventSerializer(new DummyLegacyHoverEventSerializer()) + .build(); GSON_SERIALIZER = new GsonComponentSerializerWrapper(source); // Tell MCProtocolLib to use this serializer, too. DefaultComponentSerializer.set(GSON_SERIALIZER); diff --git a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java index 1c89d38c4..aafbbf66e 100644 --- a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java @@ -26,18 +26,25 @@ package org.geysermc.geyser.util; import com.github.steveice10.mc.protocol.data.game.entity.Effect; +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; 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.geyser.entity.type.Entity; import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.living.ArmorStandEntity; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.session.GeyserSession; import java.util.Locale; public final class EntityUtils { + /** + * A constant array of the two hands that a player can interact with an entity. + */ + public static final Hand[] HANDS = Hand.values(); /** * @return a new String array of all known effect identifiers @@ -197,6 +204,26 @@ public final class EntityUtils { } } + /** + * Determine if an action would result in a successful bucketing of the given entity. + */ + public static boolean attemptToBucket(GeyserSession session, GeyserItemStack itemInHand) { + return itemInHand.getJavaId() == session.getItemMappings().getStoredItems().waterBucket(); + } + + /** + * Attempt to determine the result of saddling the given entity. + */ + public static InteractionResult attemptToSaddle(GeyserSession session, Entity entityToSaddle, GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().saddle()) { + if (!entityToSaddle.getFlag(EntityFlag.SADDLED) && !entityToSaddle.getFlag(EntityFlag.BABY)) { + // Saddle + return InteractionResult.SUCCESS; + } + } + return InteractionResult.PASS; + } + private EntityUtils() { } } diff --git a/core/src/main/java/org/geysermc/geyser/util/InteractionResult.java b/core/src/main/java/org/geysermc/geyser/util/InteractionResult.java new file mode 100644 index 000000000..fd13dd743 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/util/InteractionResult.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019-2022 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.geyser.util; + +/** + * Used as a mirror of Java Edition's own interaction enum. + */ +public enum InteractionResult { + CONSUME(true), + /** + * Indicates that the action does nothing, or in rare cases is not a priority. + */ + PASS(false), + /** + * Indicates that the action does something, and don't try to find another action to process. + */ + SUCCESS(true); + + private final boolean consumesAction; + + InteractionResult(boolean consumesAction) { + this.consumesAction = consumesAction; + } + + public boolean consumesAction() { + return consumesAction; + } + + public boolean shouldSwing() { + return this == SUCCESS; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/util/InteractiveTag.java b/core/src/main/java/org/geysermc/geyser/util/InteractiveTag.java new file mode 100644 index 000000000..1e8795478 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/util/InteractiveTag.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019-2022 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.geyser.util; + +import lombok.Getter; + +import java.util.Locale; + +/** + * All interactive tags in enum form. For potential API usage. + */ +public enum InteractiveTag { + NONE((Void) null), + 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(Void isNone) { + this.value = ""; + } + + InteractiveTag(String value) { + this.value = "action.interact." + value; + } + + InteractiveTag() { + this.value = "action.interact." + name().toLowerCase(Locale.ROOT); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java index 4210ee6f8..5c2905d93 100644 --- a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java @@ -28,10 +28,6 @@ package org.geysermc.geyser.util; 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.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.serverbound.inventory.ServerboundPickItemPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; @@ -47,14 +43,17 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.click.Click; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator; import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTranslator; -import org.geysermc.geyser.registry.Registries; -import org.geysermc.geyser.registry.type.ItemMapping; import javax.annotation.Nullable; import java.util.Arrays; @@ -163,6 +162,13 @@ public class InventoryUtils { return item1.equals(item2, false, true, true); } + /** + * Checks to see if an item stack represents air or has no count. + */ + public static boolean isEmpty(@Nullable ItemStack itemStack) { + return itemStack == null || itemStack.getId() == ItemMapping.AIR.getJavaId() || itemStack.getAmount() <= 0; + } + /** * Returns a barrier block with custom name and lore to explain why * part of the inventory is unusable. @@ -361,7 +367,7 @@ public class InventoryUtils { * @param output if not null, the recipe has to output this item */ @Nullable - public static Recipe getValidRecipe(final GeyserSession session, final @Nullable ItemStack output, final IntFunction inventoryGetter, + public static GeyserRecipe getValidRecipe(final GeyserSession session, final @Nullable ItemStack output, final IntFunction inventoryGetter, final int gridDimensions, final int firstRow, final int height, final int firstCol, final int width) { int nonAirCount = 0; // Used for shapeless recipes for amount of items needed in recipe for (int row = firstRow; row < height + firstRow; row++) { @@ -373,14 +379,14 @@ public class InventoryUtils { } recipes: - for (Recipe recipe : session.getCraftingRecipes().values()) { - if (recipe.getType() == RecipeType.CRAFTING_SHAPED) { - ShapedRecipeData data = (ShapedRecipeData) recipe.getData(); - if (output != null && !data.getResult().equals(output)) { + for (GeyserRecipe recipe : session.getCraftingRecipes().values()) { + if (recipe.isShaped()) { + GeyserShapedRecipe shapedRecipe = (GeyserShapedRecipe) recipe; + if (output != null && !shapedRecipe.result().equals(output)) { continue; } - Ingredient[] ingredients = data.getIngredients(); - if (data.getWidth() != width || data.getHeight() != height || width * height != ingredients.length) { + Ingredient[] ingredients = shapedRecipe.ingredients(); + if (shapedRecipe.width() != width || shapedRecipe.height() != height || width * height != ingredients.length) { continue; } @@ -397,18 +403,17 @@ public class InventoryUtils { continue; } } - return recipe; - } else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) { - ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData(); - if (output != null && !data.getResult().equals(output)) { + } else { + GeyserShapelessRecipe data = (GeyserShapelessRecipe) recipe; + if (output != null && !data.result().equals(output)) { continue; } - if (nonAirCount != data.getIngredients().length) { + if (nonAirCount != data.ingredients().length) { // There is an amount of items on the crafting table that is not the same as the ingredient count so this is invalid continue; } - for (int i = 0; i < data.getIngredients().length; i++) { - Ingredient ingredient = data.getIngredients()[i]; + for (int i = 0; i < data.ingredients().length; i++) { + Ingredient ingredient = data.ingredients()[i]; for (ItemStack itemStack : ingredient.getOptions()) { boolean inventoryHasItem = false; // Iterate only over the crafting table to find this item @@ -432,8 +437,8 @@ public class InventoryUtils { } } } - return recipe; } + return recipe; } return null; } diff --git a/core/src/main/java/org/geysermc/geyser/util/ItemUtils.java b/core/src/main/java/org/geysermc/geyser/util/ItemUtils.java index be1731079..f05d702a0 100644 --- a/core/src/main/java/org/geysermc/geyser/util/ItemUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/ItemUtils.java @@ -26,9 +26,11 @@ package org.geysermc.geyser.util; import com.github.steveice10.opennbt.tag.builtin.*; +import it.unimi.dsi.fastutil.ints.Int2IntMap; import org.geysermc.geyser.session.GeyserSession; public class ItemUtils { + private static Int2IntMap DYE_COLORS = null; public static int getEnchantmentLevel(CompoundTag itemNBTData, String enchantmentId) { ListTag enchantments = (itemNBTData == null ? null : itemNBTData.get("Enchantments")); @@ -73,4 +75,19 @@ public class ItemUtils { } return null; } + + /** + * Return the dye color associated with this Java item ID, if any. Returns -1 if no dye color exists for this item. + */ + public static int dyeColorFor(int javaId) { + return DYE_COLORS.get(javaId); + } + + public static void setDyeColors(Int2IntMap dyeColors) { + if (DYE_COLORS != null) { + throw new RuntimeException(); + } + dyeColors.defaultReturnValue(-1); + DYE_COLORS = dyeColors; + } } diff --git a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java index 8fd079702..3488f713c 100644 --- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java @@ -48,6 +48,7 @@ import org.geysermc.cumulus.SimpleForm; import org.geysermc.cumulus.response.CustomFormResponse; import org.geysermc.cumulus.response.ModalFormResponse; import org.geysermc.cumulus.response.SimpleFormResponse; +import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import javax.crypto.SecretKey; @@ -261,6 +262,48 @@ public class LoginEncryptionUtils { })); } + /** + * Build a window that explains the user's credentials will be saved to the system. + */ + public static void buildAndShowConsentWindow(GeyserSession session) { + String locale = session.locale(); + session.sendForm( + SimpleForm.builder() + .title("%gui.signIn") + .content(GeyserLocale.getPlayerLocaleString("geyser.auth.login.save_token.warning", locale) + + "\n\n" + + GeyserLocale.getPlayerLocaleString("geyser.auth.login.save_token.proceed", locale)) + .button("%gui.ok") + .button("%gui.decline") + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (response.isCorrect() && response.getClickedButtonId() == 0) { + session.authenticateWithMicrosoftCode(true); + } else { + session.disconnect("%disconnect.quitting"); + } + })); + } + + public static void buildAndShowTokenExpiredWindow(GeyserSession session) { + String locale = session.locale(); + session.sendForm( + SimpleForm.builder() + .title(GeyserLocale.getPlayerLocaleString("geyser.auth.login.form.expired", locale)) + .content(GeyserLocale.getPlayerLocaleString("geyser.auth.login.save_token.expired", locale) + + "\n\n" + + GeyserLocale.getPlayerLocaleString("geyser.auth.login.save_token.proceed", locale)) + .button("%gui.ok") + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (response.isCorrect()) { + session.authenticateWithMicrosoftCode(true); + } else { + session.disconnect("%disconnect.quitting"); + } + })); + } + public static void buildAndShowLoginDetailsWindow(GeyserSession session) { session.sendForm( CustomForm.builder() @@ -312,10 +355,23 @@ public class LoginEncryptionUtils { * Shows the code that a user must input into their browser */ public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse msCode) { + StringBuilder message = new StringBuilder("%xbox.signin.website\n") + .append(ChatColor.AQUA) + .append("%xbox.signin.url") + .append(ChatColor.RESET) + .append("\n%xbox.signin.enterCode\n") + .append(ChatColor.GREEN) + .append(msCode.user_code); + int timeout = session.getGeyser().getConfig().getPendingAuthenticationTimeout(); + if (timeout != 0) { + message.append("\n\n") + .append(ChatColor.RESET) + .append(GeyserLocale.getPlayerLocaleString("geyser.auth.login.timeout", session.locale(), String.valueOf(timeout))); + } session.sendForm( ModalForm.builder() .title("%xbox.signin") - .content("%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + msCode.user_code) + .content(message.toString()) .button1("%gui.done") .button2("%menu.disconnect") .responseHandler((form, responseData) -> { diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 00e2521f3..2582e4d4d 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -66,20 +66,19 @@ remote: # If you're using a plugin version of Floodgate on the same server, the key will automatically be picked up from Floodgate. floodgate-key-file: key.pem -# The Xbox/Minecraft Bedrock username is the key for the Java server auth-info. -# This allows automatic configuration/login to the remote Java server. -# If you are brave enough to put your Mojang account info into a config file. -# Uncomment the lines below to enable this feature. -#userAuths: -# BedrockAccountUsername: # Your Minecraft: Bedrock Edition username -# email: javaccountemail@example.com # Your Minecraft: Java Edition email -# password: javaccountpassword123 # Your Minecraft: Java Edition password -# microsoft-account: true # Whether the account is a Mojang or Microsoft account. -# -# bluerkelp2: -# email: not_really_my_email_address_mr_minecrafter53267@gmail.com -# password: "this isn't really my password" -# microsoft-account: false +# For online mode authentication type only. +# Stores a list of Bedrock players that should have their Java Edition account saved after login. +# This saves a token that can be reused to authenticate the player later. This does not save emails or passwords, +# but you should still be cautious when adding to this list and giving others access to this Geyser instance's files. +# Removing a name from this list will delete its cached login information on the next Geyser startup. +# The file for this is in the same folder as this config, named "saved-refresh-tokens.json". +saved-user-logins: + - ThisExampleUsernameShouldBeLongEnoughToNeverBeAnXboxUsername + - ThisOtherExampleUsernameShouldAlsoBeLongEnough + +# Specify how many seconds to wait while user authorizes Geyser to access their Microsoft account. +# User is allowed to disconnect from the server during this period. +pending-authentication-timeout: 120 # Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands. # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index 94c185193..d2a01218d 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 94c1851931f2319a7e7f42c2fe9066b78235bc39 +Subproject commit d2a01218d43f5b60bd4512d5eb6ad7e03a097f8c diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index b60cfcdd4..f73b45844 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit b60cfcdd40cd58a93143b489fc9153a347e48c41 +Subproject commit f73b45844f1185c3898db3052ce4ea0d18246168 diff --git a/pom.xml b/pom.xml index 191a40704..d2d31a07b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.geysermc geyser-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT pom Geyser Allows for players from Minecraft Bedrock Edition to join Minecraft Java Edition servers. @@ -17,8 +17,9 @@ UTF-8 16 16 - 4.9.3 + geysermc + https://sonarcloud.io