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 895f8cc1d..adb362e0d 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 fa9960f2e..ea34bef68 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 03042e3a4..376d0f439 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 71ba20e48..20b19432b 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 f59b82ba0..28c5345ab 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 000000000..b156ef839 --- /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); + } +}