From 9ea59d616e2dea9113693df872aff83812430250 Mon Sep 17 00:00:00 2001 From: rtm516 Date: Thu, 10 Feb 2022 18:52:14 +0000 Subject: [PATCH 01/27] Add sonarcloud analysis --- .github/workflows/sonarcloud.yml | 38 ++++++++++++++++++++++++++++++++ bootstrap/standalone/pom.xml | 1 - pom.xml | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sonarcloud.yml diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 000000000..64ac19621 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,38 @@ +name: SonarCloud +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +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/bootstrap/standalone/pom.xml b/bootstrap/standalone/pom.xml index 881c87e6c..0042a40d8 100644 --- a/bootstrap/standalone/pom.xml +++ b/bootstrap/standalone/pom.xml @@ -132,7 +132,6 @@ implementation="com.github.edwgiz.mavenShadePlugin.log4j2CacheTransformer.PluginsCacheFileTransformer"> - ${project.build.directory}/dependency-reduced-pom.xml diff --git a/pom.xml b/pom.xml index a528dcf98..f00da7fc8 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,8 @@ UTF-8 16 16 + geysermc + https://sonarcloud.io From 746cd94dd1c2e92ae8e12a6cd39b4d0790f28a2d Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 10 Feb 2022 20:14:52 -0500 Subject: [PATCH 02/27] Fix villagers for 1.18.10 Includes working around pre-1.14 ONLY on pre-1.14 by checking the tags packet. Fixes #2828 --- .../geyser/inventory/MerchantContainer.java | 16 ++++++- .../geyser/session/GeyserSession.java | 5 ++ .../geyser/session/cache/TagCache.java | 10 +++- .../MerchantInventoryTranslator.java | 46 +++++++++++++++++++ .../entity/BedrockEntityEventTranslator.java | 14 +----- .../java/JavaUpdateTagsTranslator.java | 36 ++++++++++++++- 6 files changed, 111 insertions(+), 16 deletions(-) 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/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 3a097f732..b886f8b20 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -361,6 +361,11 @@ public class GeyserSession implements GeyserConnection, CommandSender { @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 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..f2f1597fe 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 @@ -30,6 +30,7 @@ import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntLists; import org.geysermc.geyser.registry.type.BlockMapping; import org.geysermc.geyser.registry.type.ItemMapping; +import org.geysermc.geyser.session.GeyserSession; import java.util.Map; @@ -61,7 +62,7 @@ public class TagCache { 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")); @@ -79,6 +80,13 @@ public class TagCache { 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")); + + // 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() { 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..84f904d98 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,17 @@ 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.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 +47,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,6 +137,46 @@ 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; + 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 public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { // We're not crafting here diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java index a42184750..b693b7f3c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java @@ -25,11 +25,8 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity; -import com.github.steveice10.mc.protocol.data.game.inventory.VillagerTrade; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSelectTradePacket; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; -import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; -import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.MerchantContainer; import org.geysermc.geyser.session.GeyserSession; @@ -50,21 +47,14 @@ public class BedrockEntityEventTranslator 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/java/JavaUpdateTagsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java index 3d5bfc43a..9f1c24fd8 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 @@ -30,11 +30,45 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + @Translator(packet = ClientboundUpdateTagsPacket.class) public class JavaUpdateTagsTranslator extends PacketTranslator { + private final Map> previous = new HashMap<>(); @Override public void translate(GeyserSession session, ClientboundUpdateTagsPacket packet) { - session.getTagCache().loadPacket(packet); + for (Map.Entry> entry : packet.getTags().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) { + StringBuilder builder = new StringBuilder(); + builder.append(entry.getKey()).append("={"); + for (Map.Entry tag : entry.getValue().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) { + builder.append(tag.getKey()).append('=').append(Arrays.toString(tag.getValue())).append(", "); + } + System.out.println(builder.append("}").toString()); + } + + if (previous.isEmpty()) { + previous.putAll(packet.getTags()); + } else { + for (Map.Entry> entry : packet.getTags().entrySet()) { + Map oldTags = previous.get(entry.getKey()); + for (Map.Entry newTag : entry.getValue().entrySet()) { + int[] oldValue = oldTags.get(newTag.getKey()); + if (oldValue == null) { + System.out.println("Tag " + newTag.getKey() + " not found!!"); + continue; + } + if (!Arrays.equals(Arrays.stream(oldValue).sorted().toArray(), Arrays.stream(newTag.getValue()).sorted().toArray())) { + System.out.println(entry.getKey() + ": " + newTag.getKey() + " has different values! " + Arrays.toString(Arrays.stream(oldValue).sorted().toArray()) + " " + Arrays.toString(Arrays.stream(newTag.getValue()).sorted().toArray())); + } + } + } + } + + session.getTagCache().loadPacket(session, packet); + + } } From 0ca77233edfd85aff29dad28cea9001acdbc007a Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 10 Feb 2022 20:15:46 -0500 Subject: [PATCH 03/27] oh no my extensive debugging setup --- .../java/JavaUpdateTagsTranslator.java | 34 ------------------- 1 file changed, 34 deletions(-) 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 9f1c24fd8..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 @@ -30,45 +30,11 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - @Translator(packet = ClientboundUpdateTagsPacket.class) public class JavaUpdateTagsTranslator extends PacketTranslator { - private final Map> previous = new HashMap<>(); @Override public void translate(GeyserSession session, ClientboundUpdateTagsPacket packet) { - for (Map.Entry> entry : packet.getTags().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) { - StringBuilder builder = new StringBuilder(); - builder.append(entry.getKey()).append("={"); - for (Map.Entry tag : entry.getValue().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) { - builder.append(tag.getKey()).append('=').append(Arrays.toString(tag.getValue())).append(", "); - } - System.out.println(builder.append("}").toString()); - } - - if (previous.isEmpty()) { - previous.putAll(packet.getTags()); - } else { - for (Map.Entry> entry : packet.getTags().entrySet()) { - Map oldTags = previous.get(entry.getKey()); - for (Map.Entry newTag : entry.getValue().entrySet()) { - int[] oldValue = oldTags.get(newTag.getKey()); - if (oldValue == null) { - System.out.println("Tag " + newTag.getKey() + " not found!!"); - continue; - } - if (!Arrays.equals(Arrays.stream(oldValue).sorted().toArray(), Arrays.stream(newTag.getValue()).sorted().toArray())) { - System.out.println(entry.getKey() + ": " + newTag.getKey() + " has different values! " + Arrays.toString(Arrays.stream(oldValue).sorted().toArray()) + " " + Arrays.toString(Arrays.stream(newTag.getValue()).sorted().toArray())); - } - } - } - } - session.getTagCache().loadPacket(session, packet); - - } } From 44af9da01da3bbade88bdd56e6b884266e917f7c Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 11 Feb 2022 13:37:19 -0500 Subject: [PATCH 04/27] Use axolotl tempt tag to determine axolotl food --- .../geyser/entity/type/living/animal/AxolotlEntity.java | 2 +- .../java/org/geysermc/geyser/session/cache/TagCache.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) 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..2ada1fe09 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 @@ -56,7 +56,7 @@ 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 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 f2f1597fe..0f73737bb 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 @@ -53,6 +53,7 @@ public class TagCache { private IntList requiresDiamondTool; /* Items */ + private IntList axolotlTemptItems; private IntList flowers; private IntList foxFood; private IntList piglinLoved; @@ -77,6 +78,7 @@ 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.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")); @@ -102,11 +104,16 @@ public class TagCache { this.requiresIronTool = IntLists.emptyList(); this.requiresDiamondTool = IntLists.emptyList(); + this.axolotlTemptItems = IntLists.emptyList(); this.flowers = IntLists.emptyList(); this.foxFood = IntLists.emptyList(); this.piglinLoved = IntLists.emptyList(); } + public boolean isAxolotlTemptItem(ItemMapping itemMapping) { + return axolotlTemptItems.contains(itemMapping.getJavaId()); + } + public boolean isFlower(ItemMapping mapping) { return flowers.contains(mapping.getJavaId()); } From 31fd57a58d19829071859ef292fee706873d31fb Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 12 Feb 2022 21:44:51 -0500 Subject: [PATCH 05/27] Also apply villager fix for consoles --- .../MerchantInventoryTranslator.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) 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 84f904d98..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 @@ -33,6 +33,7 @@ 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; @@ -147,6 +148,25 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator { // 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) { + 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); @@ -177,13 +197,6 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator { } } - @Override - public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { - // We're not crafting here - // Called at least by consoles when pressing a trade option button - return translateRequest(session, inventory, request); - } - @Override public void updateInventory(GeyserSession session, Inventory inventory) { updater.updateInventory(this, session, inventory); From 252348ebd8fc0097a415dc4cbb165ae101f42fd5 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 19 Feb 2022 12:07:30 -0500 Subject: [PATCH 06/27] Implement globe banner pattern translation Co-Authored-By: Konicai <71294714+Konicai@users.noreply.github.com> --- .../populator/ItemRegistryPopulator.java | 12 +++++++++--- .../inventory/item/BannerTranslator.java | 18 +++++------------- core/src/main/resources/mappings | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) 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..209588d72 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 @@ -224,8 +224,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 +239,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,7 +364,7 @@ public class ItemRegistryPopulator { ItemMapping.ItemMappingBuilder mappingBuilder = ItemMapping.builder() .javaIdentifier(javaIdentifier) .javaId(itemIndex) - .bedrockIdentifier(bedrockIdentifier) + .bedrockIdentifier(bedrockIdentifier.intern()) .bedrockId(bedrockId) .bedrockData(mappingItem.getBedrockData()) .bedrockBlockId(bedrockBlockId) 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/resources/mappings b/core/src/main/resources/mappings index b60cfcdd4..8620c9c46 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit b60cfcdd40cd58a93143b489fc9153a347e48c41 +Subproject commit 8620c9c4603c16b74cbe1d6630695d243679896b From 811ae178c97bd6b6b9505516796ebb3bff368009 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 21 Feb 2022 16:11:51 -0500 Subject: [PATCH 07/27] Store recipes in a more compact GeyserRecipe type This prevents us from storing some unnecessary data. Also removes some 1.11 recipe compatibility code that is essentially unusable. --- .../platform/spigot/GeyserSpigotPlugin.java | 20 +- .../GeyserSpigot1_11CraftingListener.java | 203 ------------------ .../geyser/inventory/recipe/GeyserRecipe.java | 36 ++++ .../inventory/recipe/GeyserShapedRecipe.java | 43 ++++ .../recipe/GeyserShapelessRecipe.java | 42 ++++ .../geysermc/geyser/registry/Registries.java | 3 +- .../populator/RecipeRegistryPopulator.java | 17 +- .../geyser/session/GeyserSession.java | 4 +- .../inventory/InventoryTranslator.java | 40 ++-- .../java/JavaUpdateRecipesTranslator.java | 9 +- .../JavaContainerSetSlotTranslator.java | 7 +- .../geysermc/geyser/util/InventoryUtils.java | 40 ++-- 12 files changed, 184 insertions(+), 280 deletions(-) delete mode 100644 bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigot1_11CraftingListener.java create mode 100644 core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java create mode 100644 core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java create mode 100644 core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java 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 bdf28a203..aae6c599a 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.CommandManager; -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.SpigotCommandSender; 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; @@ -235,11 +234,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/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/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index 5a60351ce..20e9d3515 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -43,6 +43,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 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; @@ -141,7 +142,7 @@ public final class Registries { /** * A versioned registry holding all the recipes, with the net ID being the key, and {@link Recipe} 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/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/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index b886f8b20..c2e6ae6f6 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -39,7 +39,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; @@ -94,6 +93,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; @@ -350,7 +350,7 @@ public class GeyserSession implements GeyserConnection, CommandSender { private Entity mouseoverEntity; @Setter - private Int2ObjectMap craftingRecipes; + private Int2ObjectMap craftingRecipes; private final Set unlockedRecipes; private final AtomicInteger lastRecipeNetId; 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/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/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java index 4bb2a8e60..36307e7bd 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java @@ -27,9 +27,6 @@ package org.geysermc.geyser.translator.protocol.java.inventory; 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.packet.ingame.clientbound.inventory.ClientboundContainerSetSlotPacket; import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; @@ -38,6 +35,7 @@ import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket; import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; +import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator; @@ -165,9 +163,8 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator 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 +372,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 +396,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 +430,8 @@ public class InventoryUtils { } } } - return recipe; } + return recipe; } return null; } From 619ce53c50cba96f21a52aca85196087be366af9 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 21 Feb 2022 20:51:31 -0500 Subject: [PATCH 08/27] Javadoc fix for Registries.RECIPES --- .../src/main/java/org/geysermc/geyser/registry/Registries.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 20e9d3515..0b59492d3 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; @@ -140,7 +139,7 @@ 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)); From 7bd5b595650322d8915558731a282985235d1d07 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 21 Feb 2022 21:19:19 -0500 Subject: [PATCH 09/27] Allow the Jigsaw to operate correctly on 1.14.4 Fixes #2852 --- .../geyser/level/block/BlockStateValues.java | 20 +++++++++++++++++++ .../JigsawBlockBlockEntityTranslator.java | 19 +++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) 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/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"), "")); } } From 7d1ec5c41a16c176a579f5105121b15149b9b295 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 24 Feb 2022 22:49:10 -0500 Subject: [PATCH 10/27] Support offhand interactions with entities --- .../geyser/entity/EntityDefinitions.java | 24 +- .../geyser/entity/InteractiveTagManager.java | 293 ------------------ .../geyser/entity/type/BoatEntity.java | 24 ++ .../type/CommandBlockMinecartEntity.java | 32 ++ .../geysermc/geyser/entity/type/Entity.java | 73 ++++- .../entity/type/FurnaceMinecartEntity.java | 9 + .../geyser/entity/type/ItemFrameEntity.java | 8 + .../geyser/entity/type/LeashKnotEntity.java | 7 + .../geyser/entity/type/LivingEntity.java | 35 +++ .../geyser/entity/type/MinecartEntity.java | 39 +++ .../type/living/AbstractFishEntity.java | 14 + .../entity/type/living/AmbientEntity.java | 5 + .../entity/type/living/ArmorStandEntity.java | 13 + .../entity/type/living/DolphinEntity.java | 66 ++++ .../entity/type/living/IronGolemEntity.java | 17 + .../geyser/entity/type/living/MobEntity.java | 98 ++++++ .../entity/type/living/SlimeEntity.java | 5 + .../entity/type/living/SnowGolemEntity.java | 24 ++ .../entity/type/living/SquidEntity.java | 5 + .../entity/type/living/WaterEntity.java | 5 + .../type/living/animal/AnimalEntity.java | 36 +++ .../type/living/animal/AxolotlEntity.java | 19 ++ .../entity/type/living/animal/CowEntity.java | 65 ++++ .../entity/type/living/animal/GoatEntity.java | 18 +- .../type/living/animal/HoglinEntity.java | 10 + .../type/living/animal/MooshroomEntity.java | 47 +++ .../type/living/animal/OcelotEntity.java | 27 ++ .../type/living/animal/PandaEntity.java | 81 ++++- .../entity/type/living/animal/PigEntity.java | 39 +++ .../type/living/animal/SheepEntity.java | 54 +++- .../type/living/animal/StriderEntity.java | 38 +++ .../type/living/animal/TurtleEntity.java | 5 + .../animal/horse/AbstractHorseEntity.java | 156 +++++++++- .../animal/horse/ChestedHorseEntity.java | 20 ++ .../type/living/animal/horse/LlamaEntity.java | 2 +- .../animal/horse/SkeletonHorseEntity.java | 54 ++++ .../animal/horse/ZombieHorseEntity.java | 54 ++++ .../living/animal/tameable/CatEntity.java | 28 ++ .../living/animal/tameable/ParrotEntity.java | 47 ++- .../animal/tameable/TameableEntity.java | 13 +- .../living/animal/tameable/WolfEntity.java | 46 +++ .../merchant/AbstractMerchantEntity.java | 39 +++ .../type/living/merchant/VillagerEntity.java | 65 ++-- .../type/living/monster/CreeperEntity.java | 27 ++ .../living/monster/EnderDragonEntity.java | 9 +- .../type/living/monster/GhastEntity.java | 5 + .../type/living/monster/MonsterEntity.java | 5 + .../type/living/monster/PhantomEntity.java | 5 + .../type/living/monster/PiglinEntity.java | 30 ++ .../type/living/monster/ShulkerEntity.java | 5 + .../type/living/monster/ZoglinEntity.java | 10 + .../living/monster/ZombieVillagerEntity.java | 35 ++- .../geyser/inventory/PlayerInventory.java | 5 + .../inventory/item/StoredItemMappings.java | 22 ++ .../populator/ItemRegistryPopulator.java | 22 +- .../registry/type/GeyserMappingItem.java | 2 + .../geyser/registry/type/ItemMapping.java | 4 +- .../geyser/session/GeyserSession.java | 19 +- .../geyser/session/cache/TagCache.java | 17 + .../geyser/session/cache/WorldBorder.java | 11 +- ...BedrockInventoryTransactionTranslator.java | 80 ++--- .../BedrockMobEquipmentTranslator.java | 3 +- .../player/BedrockInteractTranslator.java | 3 +- .../entity/JavaSetEntityDataTranslator.java | 6 +- .../player/JavaPlayerAbilitiesTranslator.java | 1 + .../EntitySoundInteractionTranslator.java | 93 ------ .../translator/sound/SoundTranslator.java | 11 - .../FeedBabySoundInteractionTranslator.java | 57 ---- .../MilkEntitySoundInteractionTranslator.java | 65 ---- .../org/geysermc/geyser/util/EntityUtils.java | 33 +- .../geyser/util/InteractionResult.java | 55 ++++ .../geysermc/geyser/util/InteractiveTag.java | 91 ++++++ .../geysermc/geyser/util/InventoryUtils.java | 7 + .../org/geysermc/geyser/util/ItemUtils.java | 17 + 74 files changed, 1860 insertions(+), 654 deletions(-) delete mode 100644 core/src/main/java/org/geysermc/geyser/entity/InteractiveTagManager.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/living/DolphinEntity.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/SkeletonHorseEntity.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/ZombieHorseEntity.java delete mode 100644 core/src/main/java/org/geysermc/geyser/translator/sound/EntitySoundInteractionTranslator.java delete mode 100644 core/src/main/java/org/geysermc/geyser/translator/sound/entity/FeedBabySoundInteractionTranslator.java delete mode 100644 core/src/main/java/org/geysermc/geyser/translator/sound/entity/MilkEntitySoundInteractionTranslator.java create mode 100644 core/src/main/java/org/geysermc/geyser/util/InteractionResult.java create mode 100644 core/src/main/java/org/geysermc/geyser/util/InteractiveTag.java 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 adeccdd01..270f69ee0 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..f8e8c7091 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, this, 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 2ada1fe09..9f7e17194 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 { @@ -63,4 +67,19 @@ public class AxolotlEntity extends AnimalEntity { protected int getMaxAir() { return 6000; } + + @Override + protected boolean canBeLeashed() { + return true; + } + + @Nonnull + @Override + protected InteractionResult mobInteract(@Nonnull GeyserItemStack itemInHand) { + if (EntityUtils.attemptToBucket(session, this, 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..74e2ed368 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.getDyeColor(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.getDyeColor(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..de26e380e 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,154 @@ 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) { + 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) { + 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 testMobInteraction(itemInHand); + } else if (session.isSneaking()) { + return InteractiveTag.OPEN_CONTAINER; + } else if (!passengers.isEmpty()) { + return testMobInteraction(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 mobInteract(itemInHand); + } else if (session.isSneaking()) { + // Opens inventory + return InteractionResult.SUCCESS; + } else if (!passengers.isEmpty()) { + return mobInteract(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..b14b40dc3 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.getDyeColor(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/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/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index 209588d72..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<>() {}; @@ -369,7 +372,8 @@ public class ItemRegistryPopulator { .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())); @@ -417,6 +421,10 @@ public class ItemRegistryPopulator { itemNames.add(javaIdentifier); + if (firstMappingsPass && mappingItem.getDyeColor() != -1) { + dyeColors.put(itemIndex, mappingItem.getDyeColor()); + } + itemIndex++; } @@ -512,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/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 c2e6ae6f6..e76f8405a 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -84,7 +84,6 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.command.CommandSender; 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; @@ -449,6 +448,9 @@ public class GeyserSession implements GeyserConnection, CommandSender { */ private boolean flying = false; + @Setter + private boolean instabuild = false; + /** * Caches current rain status. */ @@ -1081,7 +1083,7 @@ public class GeyserSession implements GeyserConnection, CommandSender { if (mouseoverEntity != null) { // Horses, etc can change their property depending on if you're sneaking - InteractiveTagManager.updateTag(this, mouseoverEntity); + mouseoverEntity.updateInteractiveTag(); } } @@ -1531,4 +1533,17 @@ public class GeyserSession implements GeyserConnection, CommandSender { 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/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index 0f73737bb..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,16 +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; @@ -54,9 +57,11 @@ public class TagCache { /* 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 @@ -79,9 +84,11 @@ public class TagCache { 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; @@ -105,15 +112,21 @@ public class TagCache { 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) { return flowers.contains(mapping.getJavaId()); } @@ -126,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/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 869062da2..bd7b54def 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,10 +33,7 @@ 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; @@ -45,7 +42,6 @@ import com.nukkitx.protocol.bedrock.packet.*; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 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; @@ -58,8 +54,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; @@ -151,14 +148,7 @@ public class BedrockInventoryTransactionTranslator 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/entity/JavaSetEntityDataTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEntityDataTranslator.java index ed9129c26..54c14f7f0 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEntityDataTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEntityDataTranslator.java @@ -28,7 +28,6 @@ package org.geysermc.geyser.translator.protocol.java.entity; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityDataPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.InteractiveTagManager; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -60,8 +59,9 @@ public class JavaSetEntityDataTranslator 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/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java index 1c89d38c4..5500abbc8 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,30 @@ public final class EntityUtils { } } + /** + * Determine if an action would result in a successful bucketing of the given entity. + */ + public static boolean attemptToBucket(GeyserSession session, Entity entityToBucket, GeyserItemStack itemInHand) { + if (itemInHand.getJavaId() == session.getItemMappings().getStoredItems().waterBucket() && entityToBucket.isAlive()) { + //TODO check bucket sound + return true; + } + return false; + } + + /** + * 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.isAlive() && !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 29d8cd18c..5c2905d93 100644 --- a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java @@ -162,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. 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..d412247de 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 getDyeColor(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; + } } From 9b8e1372c1dd5ca5918f6a1a76b6e78fda422bd1 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 25 Feb 2022 10:31:00 -0500 Subject: [PATCH 11/27] Work around dropping items from other slots Fixes #2846 --- ...BedrockInventoryTransactionTranslator.java | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) 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..260e06a50 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 @@ -43,6 +43,7 @@ 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; @@ -50,6 +51,7 @@ 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; @@ -91,18 +93,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); From dfbb5897b972e023d28229e804cba0d9129d4f6b Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 25 Feb 2022 13:48:34 -0500 Subject: [PATCH 12/27] Update mappings --- core/src/main/resources/mappings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 8620c9c46..64f338a26 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 8620c9c4603c16b74cbe1d6630695d243679896b +Subproject commit 64f338a2670bb8d300a66975389fc7887df4c4de From 0251bb64b8a1355b4f46fc3d27b7b6b5026b5878 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 26 Feb 2022 14:26:13 -0500 Subject: [PATCH 13/27] Update JLine Fixes #2867 --- bootstrap/standalone/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bootstrap/standalone/pom.xml b/bootstrap/standalone/pom.xml index 0042a40d8..d0ef39bcf 100644 --- a/bootstrap/standalone/pom.xml +++ b/bootstrap/standalone/pom.xml @@ -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 From d0220a9b71f9c210d5a99a0e5e08583cf19de3f9 Mon Sep 17 00:00:00 2001 From: turikhay Date: Sun, 27 Feb 2022 01:45:56 +0500 Subject: [PATCH 14/27] Allow single-device Microsoft authentication (#2688) By default, there is a two-minute delay if you disconnect so you can authenticate your Microsoft account. Co-authored-by: rtm516 Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- .../java/org/geysermc/geyser/GeyserImpl.java | 5 + .../configuration/GeyserConfiguration.java | 2 + .../GeyserJacksonConfiguration.java | 3 + .../geyser/network/UpstreamPacketHandler.java | 7 + .../geyser/session/GeyserSession.java | 106 ++++++----- .../PendingMicrosoftAuthentication.java | 171 ++++++++++++++++++ .../geyser/util/LoginEncryptionUtils.java | 16 +- core/src/main/resources/config.yml | 4 + core/src/main/resources/languages | 2 +- 9 files changed, 259 insertions(+), 57 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 6226eec09..2577e6af1 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -59,6 +59,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; @@ -125,6 +126,8 @@ public class GeyserImpl implements GeyserApi { private Metrics metrics; + private PendingMicrosoftAuthentication pendingMicrosoftAuthentication; + private static GeyserImpl instance; private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) { @@ -268,6 +271,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()); 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..e2163675c 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java @@ -100,6 +100,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..e8be96138 100644 --- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java +++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java @@ -141,6 +141,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/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index f547c4dce..23542719a 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; @@ -199,6 +200,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/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index e76f8405a..d45276240 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; @@ -119,7 +118,6 @@ 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; @@ -712,65 +710,57 @@ public class GeyserSession implements GeyserConnection, CommandSender { 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().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 - */ - 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 { + GameProfile selectedProfile = task.getMsaAuthenticationService().getSelectedProfile(); + if (selectedProfile == null) { + disconnect(GeyserLocale.getPlayerLocaleString( + "geyser.network.remote.invalid_account", + clientData.getLanguageCode() + )); + } else { + this.protocol = new MinecraftProtocol( + selectedProfile, + task.getMsaAuthenticationService().getAccessToken() + ); connectDownstream(); + return true; } - }); + } + return false; } /** @@ -970,6 +960,12 @@ public class GeyserSession implements GeyserConnection, CommandSender { geyser.getSessionManager().removeSession(this); upstream.disconnect(reason); } + if (authData != null) { + PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid()); + if (task != null) { + task.setOnline(false); + } + } } if (tickThread != null) { 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..044389d24 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java @@ -0,0 +1,171 @@ +/* + * 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 { + private final LoadingCache authentications; + + public PendingMicrosoftAuthentication(int timeoutSeconds) { + this.authentications = CacheBuilder.newBuilder() + .build(new CacheLoader<>() { + @Override + public AuthenticationTask load(@NonNull String userKey) { + return 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); + } + + 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 code; + @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; + + // Request the code + this.code = CompletableFuture.supplyAsync(this::tryGetCode); + this.authentication = new CompletableFuture<>(); + // Once the code is received, continuously try to request the access token, profile, etc + this.code.thenRun(() -> performLoginAttempt(System.currentTimeMillis())); + 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); + } + + private MsaAuthenticationService.MsCodeResponse tryGetCode() throws CompletionException { + try { + return msaAuthenticationService.getAuthCode(); + } 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 + "'}"; + } + } + + /** + * @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/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java index 5a1063a10..dec138a3c 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; @@ -312,10 +313,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.getLocale(), 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..9692adbf3 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -81,6 +81,10 @@ floodgate-key-file: key.pem # password: "this isn't really my password" # microsoft-account: false +# 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. command-suggestions: true diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index bdee0d0f3..5db9d29ec 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit bdee0d0f3f8a1271cd001f0bd0d672d0010be1db +Subproject commit 5db9d29ece0b3d810ae42f6bdc9eeefd76e3d99d From a435288bdfa15be6d7dccf7278317134888c95c5 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 26 Feb 2022 20:10:07 -0500 Subject: [PATCH 15/27] Properly map lit deepslate redstone ore --- core/src/main/resources/mappings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 64f338a26..f73b45844 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 64f338a2670bb8d300a66975389fc7887df4c4de +Subproject commit f73b45844f1185c3898db3052ce4ea0d18246168 From 65b68087b84aaccc78fc3664ace7ed04a993adc6 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:07:45 -0500 Subject: [PATCH 16/27] Bump Geyser to 2.0.2 and Java to 1.18.2 --- README.md | 2 +- ap/pom.xml | 4 ++-- api/base/pom.xml | 2 +- api/geyser/pom.xml | 4 ++-- api/pom.xml | 2 +- bootstrap/bungeecord/pom.xml | 4 ++-- bootstrap/pom.xml | 4 ++-- bootstrap/spigot/pom.xml | 4 ++-- bootstrap/sponge/pom.xml | 4 ++-- bootstrap/standalone/pom.xml | 4 ++-- bootstrap/velocity/pom.xml | 4 ++-- common/pom.xml | 2 +- core/pom.xml | 10 +++++----- .../org/geysermc/geyser/network/MinecraftProtocol.java | 7 ++----- pom.xml | 2 +- 15 files changed, 28 insertions(+), 31 deletions(-) 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 26f4ddf30..9255bc579 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 4.0.0 @@ -26,7 +26,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 bc70be6c3..bae0da039 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 api-parent 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..3a8939ce3 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 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 d0ef39bcf..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 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 f2812fea0..4a6d8b5ec 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 @@ -21,19 +21,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 @@ -155,7 +155,7 @@ com.github.GeyserMC MCProtocolLib - 6a23a780 + 7efa636 compile 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..c35b744e1 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. @@ -92,7 +89,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/pom.xml b/pom.xml index f00da7fc8..0599716fe 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. From 0fd903e0a02a2f4d8db8775534b0b8ba89182f38 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:24:27 -0500 Subject: [PATCH 17/27] Pending Microsoft Authentication changes for GeyserConnect --- .../PendingMicrosoftAuthentication.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java index 044389d24..696d6b088 100644 --- a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java +++ b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java @@ -46,6 +46,10 @@ import java.util.concurrent.*; * 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) { @@ -53,7 +57,8 @@ public class PendingMicrosoftAuthentication { .build(new CacheLoader<>() { @Override public AuthenticationTask load(@NonNull String userKey) { - return new AuthenticationTask(userKey, timeoutSeconds * 1000L); + return storeServerInformation ? new ProxyAuthenticationTask(userKey, timeoutSeconds * 1000L) + : new AuthenticationTask(userKey, timeoutSeconds * 1000L); } }); } @@ -67,6 +72,11 @@ public class PendingMicrosoftAuthentication { 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); @@ -159,6 +169,17 @@ public class PendingMicrosoftAuthentication { } } + @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 */ From e163301d2315de337b98f335c550d2abe3bf217f Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 1 Mar 2022 15:44:13 -0500 Subject: [PATCH 18/27] Fix StackOverflow possibility with undead horses --- .../animal/horse/AbstractHorseEntity.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 de26e380e..f573840a6 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 @@ -130,6 +130,11 @@ public class AbstractHorseEntity extends AnimalEntity { @Nonnull @Override protected InteractiveTag testMobInteraction(@Nonnull GeyserItemStack itemInHand) { + return testHorseInteraction(itemInHand); + } + + @Nonnull + protected InteractiveTag testHorseInteraction(@Nonnull GeyserItemStack itemInHand) { boolean isBaby = isBaby(); if (!isBaby) { if (getFlag(EntityFlag.TAMED) && session.isSneaking()) { @@ -175,6 +180,11 @@ public class AbstractHorseEntity extends AnimalEntity { @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()) { @@ -243,11 +253,11 @@ public class AbstractHorseEntity extends AnimalEntity { if (!getFlag(EntityFlag.TAMED)) { return InteractiveTag.NONE; } else if (isBaby()) { - return testMobInteraction(itemInHand); + return testHorseInteraction(itemInHand); } else if (session.isSneaking()) { return InteractiveTag.OPEN_CONTAINER; } else if (!passengers.isEmpty()) { - return testMobInteraction(itemInHand); + return testHorseInteraction(itemInHand); } else { if (session.getItemMappings().getStoredItems().saddle() == itemInHand.getJavaId()) { return InteractiveTag.OPEN_CONTAINER; @@ -265,12 +275,12 @@ public class AbstractHorseEntity extends AnimalEntity { if (!getFlag(EntityFlag.TAMED)) { return InteractionResult.PASS; } else if (isBaby()) { - return mobInteract(itemInHand); + return mobHorseInteract(itemInHand); } else if (session.isSneaking()) { // Opens inventory return InteractionResult.SUCCESS; } else if (!passengers.isEmpty()) { - return mobInteract(itemInHand); + return mobHorseInteract(itemInHand); } else { // The client tests for saddle but it doesn't matter for us at this point. return InteractionResult.SUCCESS; From 17fb38e567aca5bca9b1a7b1d2012321c180f433 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:50:11 -0500 Subject: [PATCH 19/27] This method can be final --- .../entity/type/living/animal/horse/AbstractHorseEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f573840a6..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 @@ -134,7 +134,7 @@ public class AbstractHorseEntity extends AnimalEntity { } @Nonnull - protected InteractiveTag testHorseInteraction(@Nonnull GeyserItemStack itemInHand) { + protected final InteractiveTag testHorseInteraction(@Nonnull GeyserItemStack itemInHand) { boolean isBaby = isBaby(); if (!isBaby) { if (getFlag(EntityFlag.TAMED) && session.isSneaking()) { From 37c854b5ac68c7322d5d7ee9fb04185afb612d56 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:52:30 -0500 Subject: [PATCH 20/27] Fix our PacketLib selection not being used --- core/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 4a6d8b5ec..5b509e417 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -147,7 +147,7 @@ - com.github.RednedEpic + com.github.GeyserMC MCAuthLib 6c99331 compile @@ -159,11 +159,11 @@ compile - com.github.steveice10 + com.github.GeyserMC packetlib - com.github.steveice10 + com.github.GeyserMC mcauthlib From c977e36368a91484a13386bcd29094029e60c69d Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 3 Mar 2022 18:52:26 -0500 Subject: [PATCH 21/27] Deprecate userAuths in favor of a saved token system --- .gitignore | 5 +- .../standalone/GeyserStandaloneBootstrap.java | 6 ++ core/pom.xml | 2 +- .../java/org/geysermc/geyser/Constants.java | 2 + .../org/geysermc/geyser/GeyserBootstrap.java | 7 ++ .../java/org/geysermc/geyser/GeyserImpl.java | 84 ++++++++++++++++++- .../configuration/GeyserConfiguration.java | 3 + .../GeyserJacksonConfiguration.java | 3 + .../geyser/network/UpstreamPacketHandler.java | 8 ++ .../geyser/session/GeyserSession.java | 64 ++++++++++++-- .../PendingMicrosoftAuthentication.java | 21 +++-- ...SetLocalPlayerAsInitializedTranslator.java | 12 ++- .../geyser/util/LoginEncryptionUtils.java | 42 ++++++++++ core/src/main/resources/config.yml | 23 ++--- core/src/main/resources/languages | 2 +- 15 files changed, 251 insertions(+), 33 deletions(-) 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/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 43ab4b3fe..7c49585d5 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 @@ -275,6 +275,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/core/pom.xml b/core/pom.xml index 5b509e417..51fc149e3 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -149,7 +149,7 @@ com.github.GeyserMC MCAuthLib - 6c99331 + d9d773e compile 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 54ca36787..d40060310 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 2577e6af1..f3ebfa4a3 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; @@ -72,6 +74,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; @@ -79,6 +84,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; @@ -127,6 +133,8 @@ public class GeyserImpl implements GeyserApi { private Metrics metrics; private PendingMicrosoftAuthentication pendingMicrosoftAuthentication; + @Getter(AccessLevel.NONE) + private Map savedRefreshTokens; private static GeyserImpl instance; @@ -325,7 +333,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)); @@ -409,6 +417,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); } @@ -516,6 +565,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 e2163675c..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(); 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 e8be96138..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"; 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 23542719a..24ede03c1 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -190,6 +190,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); 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 d45276240..437044a6d 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -637,7 +637,6 @@ public class GeyserSession implements GeyserConnection, CommandSender { 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()) { @@ -694,10 +693,58 @@ public class GeyserSession implements GeyserConnection, CommandSender { }); } + 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; @@ -719,7 +766,7 @@ public class GeyserSession implements GeyserConnection, CommandSender { if (task.getAuthentication().isDone()) { onMicrosoftLoginComplete(task); } else { - task.getCode().whenComplete((response, ex) -> { + task.getCode(offlineAccess).whenComplete((response, ex) -> { boolean connected = !closed; if (ex != null) { if (connected) { @@ -735,6 +782,9 @@ public class GeyserSession implements GeyserConnection, CommandSender { } } + /** + * If successful, also begins connecting to the Java server. + */ public boolean onMicrosoftLoginComplete(PendingMicrosoftAuthentication.AuthenticationTask task) { if (closed) { return false; @@ -745,7 +795,8 @@ public class GeyserSession implements GeyserConnection, CommandSender { geyser.getLogger().error("Failed to log in with Microsoft code!", ex); disconnect(ex.toString()); } else { - GameProfile selectedProfile = task.getMsaAuthenticationService().getSelectedProfile(); + MsaAuthenticationService service = task.getMsaAuthenticationService(); + GameProfile selectedProfile = service.getSelectedProfile(); if (selectedProfile == null) { disconnect(GeyserLocale.getPlayerLocaleString( "geyser.network.remote.invalid_account", @@ -754,9 +805,12 @@ public class GeyserSession implements GeyserConnection, CommandSender { } else { this.protocol = new MinecraftProtocol( selectedProfile, - task.getMsaAuthenticationService().getAccessToken() + service.getAccessToken() ); connectDownstream(); + + // Save our refresh token for later use + geyser.saveRefreshToken(name(), service.getRefreshToken()); return true; } } diff --git a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java index 696d6b088..93200dcb6 100644 --- a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java +++ b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java @@ -90,8 +90,6 @@ public class PendingMicrosoftAuthentication { @Setter private boolean online = true; - @Getter - private final CompletableFuture code; @Getter private final CompletableFuture authentication; @@ -103,11 +101,7 @@ public class PendingMicrosoftAuthentication { this.timeoutMs = timeoutMs; this.remainingTimeMs = timeoutMs; - // Request the code - this.code = CompletableFuture.supplyAsync(this::tryGetCode); this.authentication = new CompletableFuture<>(); - // Once the code is received, continuously try to request the access token, profile, etc - this.code.thenRun(() -> performLoginAttempt(System.currentTimeMillis())); this.authentication.whenComplete((r, ex) -> { this.loginException = ex; // avoid memory leak, in case player doesn't connect again @@ -127,9 +121,20 @@ public class PendingMicrosoftAuthentication { authentications.invalidate(userKey); } - private MsaAuthenticationService.MsCodeResponse tryGetCode() throws CompletionException { + 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(); + return msaAuthenticationService.getAuthCode(offlineAccess); } catch (RequestException e) { throw new CompletionException(e); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java index e55b28602..8641a35ff 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java @@ -43,7 +43,17 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat if (session.getRemoteAuthType() == AuthType.ONLINE) { if (!session.isLoggedIn()) { - LoginEncryptionUtils.buildAndShowLoginWindow(session); + if (session.getGeyser().getConfig().getSavedUserLogins().contains(session.name())) { + if (session.getGeyser().refreshTokenFor(session.name()) == null) { + LoginEncryptionUtils.buildAndShowConsentWindow(session); + } else { + // If the refresh token is not null and we're here, then the refresh token expired + // and the expiration form has been cached + session.getFormCache().resendAllForms(); + } + } else { + LoginEncryptionUtils.buildAndShowLoginWindow(session); + } } // else we were able to log the user in } 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 dec138a3c..2ed754963 100644 --- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java @@ -262,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.getLocale(); + 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.getLocale(); + 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() diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 9692adbf3..2582e4d4d 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -66,20 +66,15 @@ 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. diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index 5db9d29ec..c03eea033 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 5db9d29ece0b3d810ae42f6bdc9eeefd76e3d99d +Subproject commit c03eea033cb61ece135cd795ce04b34dd39a02f8 From be93d0cc8141dfa92e2b6324efca8536cf9445c9 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 4 Mar 2022 15:25:18 -0500 Subject: [PATCH 22/27] Remove PR checks for Sonarcloud --- .github/workflows/sonarcloud.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 64ac19621..598cab46a 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -3,8 +3,6 @@ on: push: branches: - master - pull_request: - types: [opened, synchronize, reopened] jobs: build: name: SonarCloud From a58239f15b85f6f2ed87734f662059557297ca3b Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 4 Mar 2022 15:25:33 -0500 Subject: [PATCH 23/27] Update Adapters to support 1.18.2 --- bootstrap/spigot/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index 3a8939ce3..da8b184e9 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -43,7 +43,7 @@ org.geysermc.geyser.adapters spigot-all - 1.3-SNAPSHOT + 1.4-SNAPSHOT From edbb946d974fb33d7772dc6830e0ebbd231acf81 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 4 Mar 2022 23:24:38 -0500 Subject: [PATCH 24/27] Indicate Geyser supports 1.18.10 AND .12 --- .../java/org/geysermc/geyser/network/MinecraftProtocol.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 c35b744e1..7ab381375 100644 --- a/core/src/main/java/org/geysermc/geyser/network/MinecraftProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/MinecraftProtocol.java @@ -57,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()); } /** From 50bed6a2bef888b7a12621774f4e5f1fafad1886 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 5 Mar 2022 22:15:25 -0500 Subject: [PATCH 25/27] Use a dummy legacy event hover serializer This reduces computation processing needing, since Bedrock doesn't have any hover text ability. This also fixes a 1.8 bug where villager titles would not process correctly - by having a dummy serializer, a recent MCProtocolLib update would not stop the window packet from processing. --- core/pom.xml | 2 +- .../text/DummyLegacyHoverEventSerializer.java | 69 +++++++++++++++++++ .../inventory/JavaOpenScreenTranslator.java | 6 +- .../translator/text/MessageTranslator.java | 14 ++-- 4 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/text/DummyLegacyHoverEventSerializer.java diff --git a/core/pom.xml b/core/pom.xml index 51fc149e3..c5d80db8a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -155,7 +155,7 @@ com.github.GeyserMC MCProtocolLib - 7efa636 + 0771504 compile 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/protocol/java/inventory/JavaOpenScreenTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java index e68cae2e7..eedde6098 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java @@ -29,12 +29,11 @@ import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.Cli import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClosePacket; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.util.InventoryUtils; -import org.geysermc.geyser.text.MinecraftLocale; @Translator(packet = ClientboundOpenScreenPacket.class) public class JavaOpenScreenTranslator extends PacketTranslator { @@ -57,8 +56,7 @@ public class JavaOpenScreenTranslator extends PacketTranslator Date: Sat, 5 Mar 2022 22:32:38 -0500 Subject: [PATCH 26/27] Simplify disconnection logging A disconnect message will always be printed, but not more than once. CLOSED_BY_REMOTE_PEER -> Bedrock client disconnected (hopefully slightly less vague) If a message is sent from the server, the log will now indicate their disconnection reason. --- .../geyser/network/UpstreamPacketHandler.java | 2 -- .../geyser/session/GeyserSession.java | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) 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 24ede03c1..5ded35259 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -74,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; } 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 437044a6d..6452803b5 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -112,6 +112,7 @@ 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; @@ -125,13 +126,13 @@ import java.util.concurrent.atomic.AtomicInteger; @Getter public class GeyserSession implements GeyserConnection, CommandSender { - 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; @@ -547,11 +548,14 @@ public class GeyserSession implements GeyserConnection, CommandSender { } 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(); @@ -1009,11 +1013,16 @@ public class GeyserSession implements GeyserConnection, CommandSender { 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) { From a1bb4343a38ffdc59445090f91403c9bc287c93e Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 5 Mar 2022 22:33:43 -0500 Subject: [PATCH 27/27] Update languages submodule --- core/src/main/resources/languages | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index c03eea033..d2a01218d 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit c03eea033cb61ece135cd795ce04b34dd39a02f8 +Subproject commit d2a01218d43f5b60bd4512d5eb6ad7e03a097f8c