From 30e38b3a2f30ebd61dbd70e6cb9e1b5769eb41c4 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 16 May 2020 23:52:39 -0500 Subject: [PATCH] Add basic villager trading support (incomplete) This commit implements basic functionality for villager trading. This is still incomplete and is buggy in areas such as with villager trades that have more than one input and trade inputs and outputs containing NBT. Co-authored-by: DoctorMacc --- .../living/merchant/VillagerEntity.java | 7 + .../network/session/GeyserSession.java | 10 ++ .../network/translators/Translators.java | 1 + ...BedrockInventoryTransactionTranslator.java | 5 + .../MerchantInventoryTranslator.java | 170 ++++++++++++++++++ .../translators/item/ItemTranslator.java | 45 +++++ .../java/world/JavaTradeListTranslator.java | 115 ++++++++++++ 7 files changed, 353 insertions(+) create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/inventory/MerchantInventoryTranslator.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java index 895f8cc1..adb362e0 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java @@ -27,10 +27,13 @@ package org.geysermc.connector.entity.living.merchant; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData; +import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.EntityData; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import lombok.Getter; +import lombok.Setter; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; @@ -66,6 +69,10 @@ public class VillagerEntity extends AbstractMerchantEntity { VILLAGER_REGIONS.put(6, 6); } + @Getter + @Setter + private VillagerTrade[] villagerTrades; + public VillagerEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index fa9960f2..ea34bef6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -30,6 +30,7 @@ import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsExcepti import com.github.steveice10.mc.auth.exception.request.RequestException; import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.world.block.BlockState; import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket; @@ -149,9 +150,18 @@ public class GeyserSession implements CommandSender { @Setter private boolean interacting; + @Setter + private long lastInteractedVillagerEid; + @Setter private Vector3i lastInteractionPosition; + @Setter + private ItemStack firstTradeSlot; + + @Setter + private ItemStack secondTradeSlot; + @Setter private boolean switchingDimension = false; private boolean manyDimPackets = false; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/Translators.java b/connector/src/main/java/org/geysermc/connector/network/translators/Translators.java index 03042e3a..376d0f43 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/Translators.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/Translators.java @@ -159,6 +159,7 @@ public class Translators { inventoryTranslators.put(WindowType.ANVIL, new AnvilInventoryTranslator()); inventoryTranslators.put(WindowType.CRAFTING, new CraftingInventoryTranslator()); inventoryTranslators.put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator()); + inventoryTranslators.put(WindowType.MERCHANT, new MerchantInventoryTranslator()); //inventoryTranslators.put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator()); //TODO InventoryTranslator furnace = new FurnaceInventoryTranslator(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index 71ba20e4..20b19432 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -44,6 +44,7 @@ import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.ItemFrameEntity; +import org.geysermc.connector.entity.living.merchant.VillagerEntity; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; @@ -187,6 +188,10 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator actions) { + InventoryActionData result = null; + + VillagerEntity villager = (VillagerEntity) session.getEntityCache().getEntityByGeyserId(session.getLastInteractedVillagerEid()); + if (villager == null) { + session.getConnector().getLogger().debug("Could not find villager with entity id: " + session.getLastInteractedVillagerEid()); + return; + } + + // We need to store the trade slot data in the session itself as data + // needs to persist beyond this translateActions method since the client + // sends multiple packets for this + for (InventoryActionData data : actions) { + if (data.getSlot() == 4 && session.getFirstTradeSlot() == null && data.getSource().getContainerId() == ContainerId.CURSOR) { + session.setFirstTradeSlot(Translators.getItemTranslator().translateToJava(session, data.getToItem())); + } + + if (data.getSlot() == 5 && session.getSecondTradeSlot() == null && data.getToItem() != null && data.getSource().getContainerId() == ContainerId.CURSOR) { + session.setSecondTradeSlot(Translators.getItemTranslator().translateToJava(session, data.getToItem())); + } + if (data.getSlot() == 50 && result == null) { + result = data; + } + } + + if (result == null || session.getFirstTradeSlot() == null) { + super.translateActions(session, inventory, actions); + return; + } + + ItemStack resultSlot = Translators.getItemTranslator().translateToJava(session, result.getToItem()); + for (int i = 0; i < villager.getVillagerTrades().length; i++) { + VillagerTrade trade = villager.getVillagerTrades()[i]; + if (!Translators.getItemTranslator().equals(session.getFirstTradeSlot(), trade.getFirstInput(), true, true, false) || !Translators.getItemTranslator().equals(resultSlot, trade.getOutput(), true, false, false)) { + continue; + } + + if (session.getSecondTradeSlot() != null && trade.getSecondInput() != null && !Translators.getItemTranslator().equals(session.getSecondTradeSlot(), trade.getSecondInput(), true, false, false)) { + continue; + } + + ClientSelectTradePacket selectTradePacket = new ClientSelectTradePacket(i); + session.sendDownstreamPacket(selectTradePacket); + + ClientWindowActionPacket tradeAction = new ClientWindowActionPacket( + inventory.getId(), + inventory.getTransactionId().getAndIncrement(), + this.bedrockSlotToJava(result), + null, + WindowAction.CLICK_ITEM, + ClickItemParam.LEFT_CLICK + ); + session.sendDownstreamPacket(tradeAction); + break; + + } + + super.translateActions(session, inventory, actions); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java index f59b82ba..28c5345a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java @@ -160,6 +160,51 @@ public class ItemTranslator { .stream().filter(itemEntry -> itemEntry.getJavaIdentifier().equals(key)).findFirst().orElse(null)); } + /** + * Checks if an {@link ItemStack} is equal to another item stack + * + * @param itemStack the item stack to check + * @param equalsItemStack the item stack to check if equal to + * @param checkAmount if the amount should be taken into account + * @param trueIfAmountIsGreater if this should return true if the amount of the + * first item stack is greater than that of the second + * @param checkNbt if NBT data should be checked + * @return if an item stack is equal to another item stack + */ + public boolean equals(ItemStack itemStack, ItemStack equalsItemStack, boolean checkAmount, boolean trueIfAmountIsGreater, boolean checkNbt) { + if (itemStack.getId() != equalsItemStack.getId()) { + return false; + } + if (checkAmount) { + if (trueIfAmountIsGreater) { + if (itemStack.getAmount() < equalsItemStack.getAmount()) { + return false; + } + } else { + if (itemStack.getAmount() != equalsItemStack.getAmount()) { + return false; + } + } + } + + if (!checkNbt) { + return true; + } + if ((itemStack.getNbt() == null || itemStack.getNbt().isEmpty()) && (equalsItemStack.getNbt() != null && !equalsItemStack.getNbt().isEmpty())) { + return false; + } + + if ((itemStack.getNbt() != null && !itemStack.getNbt().isEmpty() && (equalsItemStack.getNbt() == null || !equalsItemStack.getNbt().isEmpty()))) { + return false; + } + + if (itemStack.getNbt() != null && equalsItemStack.getNbt() != null) { + return itemStack.getNbt().equals(equalsItemStack.getNbt()); + } + + return true; + } + private static final ItemStackTranslator DEFAULT_TRANSLATOR = new ItemStackTranslator() { @Override public List getAppliedItems() { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java new file mode 100644 index 00000000..b156ef83 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + * + */ + +package org.geysermc.connector.network.translators.java.world; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; +import com.github.steveice10.mc.protocol.packet.ingame.server.window.ServerTradeListPacket; +import com.nukkitx.nbt.CompoundTagBuilder; +import com.nukkitx.nbt.tag.CompoundTag; +import com.nukkitx.protocol.bedrock.data.ContainerType; +import com.nukkitx.protocol.bedrock.data.EntityData; +import com.nukkitx.protocol.bedrock.data.ItemData; +import com.nukkitx.protocol.bedrock.packet.UpdateTradePacket; +import org.geysermc.connector.entity.living.merchant.VillagerEntity; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.Translators; +import org.geysermc.connector.network.translators.item.ItemEntry; + +import java.util.ArrayList; +import java.util.List; + +@Translator(packet = ServerTradeListPacket.class) +public class JavaTradeListTranslator extends PacketTranslator { + + @Override + public void translate(ServerTradeListPacket packet, GeyserSession session) { + VillagerEntity villager = (VillagerEntity) session.getEntityCache().getEntityByGeyserId(session.getLastInteractedVillagerEid()); + if (villager == null) { + session.getConnector().getLogger().debug("Could not find villager with entity id: " + session.getLastInteractedVillagerEid()); + return; + } + villager.setVillagerTrades(packet.getTrades()); + villager.getMetadata().put(EntityData.TRADE_XP, packet.getExperience()); + villager.getMetadata().put(EntityData.TRADE_TIER, packet.getVillagerLevel() - 1); + villager.updateBedrockMetadata(session); + + UpdateTradePacket updateTradePacket = new UpdateTradePacket(); + updateTradePacket.setTradeTier(packet.getVillagerLevel() + 1); + updateTradePacket.setWindowId((short) packet.getWindowId()); + updateTradePacket.setWindowType((short) ContainerType.TRADING.id()); + updateTradePacket.setDisplayName("Villager"); + updateTradePacket.setUnknownInt(0); + updateTradePacket.setScreen2(true); + updateTradePacket.setWilling(true); + updateTradePacket.setPlayerUniqueEntityId(session.getPlayerEntity().getGeyserId()); + updateTradePacket.setTraderUniqueEntityId(session.getLastInteractedVillagerEid()); + CompoundTagBuilder builder = CompoundTagBuilder.builder(); + List tags = new ArrayList<>(); + for (VillagerTrade trade : packet.getTrades()) { + CompoundTagBuilder recipe = CompoundTagBuilder.builder(); + recipe.intTag("maxUses", trade.getMaxUses()); + recipe.intTag("traderExp", packet.getExperience()); + recipe.floatTag("priceMultiplierA", trade.getPriceMultiplier()); + recipe.tag(getItemTag(session, trade.getOutput(), "sell")); + recipe.floatTag("priceMultiplierB", 0.0f); + recipe.intTag("buyCountB", 0); + recipe.intTag("buyCountA", trade.getOutput().getAmount()); + recipe.intTag("demand", trade.getDemand()); + recipe.intTag("tier", packet.getVillagerLevel() - 1); + recipe.tag(getItemTag(session, trade.getFirstInput(), "buyA")); + if (trade.getSecondInput() != null) { + recipe.tag(getItemTag(session, trade.getSecondInput(), "buyB")); + } + recipe.intTag("uses", trade.getNumUses()); + recipe.byteTag("rewardExp", (byte) trade.getXp()); + tags.add(recipe.buildRootTag()); + } + builder.listTag("Recipes", CompoundTag.class, tags); + List expTags = new ArrayList<>(); + expTags.add(CompoundTagBuilder.builder().intTag("0", 0).buildRootTag()); + expTags.add(CompoundTagBuilder.builder().intTag("1", 10).buildRootTag()); + expTags.add(CompoundTagBuilder.builder().intTag("2", 60).buildRootTag()); + expTags.add(CompoundTagBuilder.builder().intTag("3", 160).buildRootTag()); + expTags.add(CompoundTagBuilder.builder().intTag("4", 310).buildRootTag()); + builder.listTag("TierExpRequirements", CompoundTag.class, expTags); + updateTradePacket.setOffers(builder.buildRootTag()); + session.sendUpstreamPacket(updateTradePacket); + } + + private CompoundTag getItemTag(GeyserSession session, ItemStack stack, String name) { + ItemData itemData = Translators.getItemTranslator().translateToBedrock(session, stack); + ItemEntry itemEntry = Translators.getItemTranslator().getItem(stack); + CompoundTagBuilder builder = CompoundTagBuilder.builder(); + builder.byteTag("Count", (byte) itemData.getCount()); + builder.shortTag("Damage", itemData.getDamage()); + builder.stringTag("Name", itemEntry.getJavaIdentifier()); + return builder.build(name); + } +}