Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add villager trading support #740

Merged
merged 6 commits into from
Jun 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.github.steveice10.mc.protocol.MinecraftProtocol;
import com.github.steveice10.mc.protocol.data.SubProtocol;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade;
import com.github.steveice10.mc.protocol.data.game.world.block.BlockState;
import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket;
Expand Down Expand Up @@ -169,6 +170,11 @@ public class GeyserSession implements CommandSender {
@Setter
private long lastWindowCloseTime = 0;

@Setter
private VillagerTrade[] villagerTrades;
@Setter
private long lastInteractedVillagerEid;

private MinecraftProtocol protocol;

public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@

package org.geysermc.connector.network.translators.bedrock;

import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade;
import com.github.steveice10.mc.protocol.data.game.window.WindowType;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientSelectTradePacket;
import com.nukkitx.protocol.bedrock.data.EntityData;
import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
Expand All @@ -41,6 +47,22 @@ public void translate(EntityEventPacket packet, GeyserSession session) {
case EATING_ITEM:
session.sendUpstreamPacket(packet);
return;
case COMPLETE_TRADE:
ClientSelectTradePacket selectTradePacket = new ClientSelectTradePacket(packet.getData());
session.getDownstream().getSession().send(selectTradePacket);

Entity villager = session.getPlayerEntity();
Inventory openInventory = session.getInventoryCache().getOpenInventory();
if (openInventory != null && openInventory.getWindowType() == WindowType.MERCHANT) {
VillagerTrade[] trades = session.getVillagerTrades();
if (trades != null && packet.getData() >= 0 && packet.getData() < trades.length) {
VillagerTrade trade = session.getVillagerTrades()[packet.getData()];
openInventory.setItem(2, trade.getOutput());
villager.getMetadata().put(EntityData.TRADE_XP, trade.getXp() + villager.getMetadata().getInt(EntityData.TRADE_XP));
villager.updateBedrockMetadata(session);
}
}
return;
}
session.getConnector().getLogger().debug("Did not translate incoming EntityEventPacket: " + packet.toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
Expand Down Expand Up @@ -198,6 +199,10 @@ public void translate(InventoryTransactionPacket packet, GeyserSession session)
session.sendDownstreamPacket(interactAtPacket);

EntitySoundInteractionHandler.handleEntityInteraction(session, vector, entity);

if (entity instanceof AbstractMerchantEntity) {
session.setLastInteractedVillagerEid(packet.getRuntimeEntityId());
}
break;
case 1: //Attack
ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public abstract class InventoryTranslator {
put(WindowType.ANVIL, new AnvilInventoryTranslator());
put(WindowType.CRAFTING, new CraftingInventoryTranslator());
put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator());
put(WindowType.MERCHANT, new MerchantInventoryTranslator());
//put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator()); //TODO

InventoryTranslator furnace = new FurnaceInventoryTranslator();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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.inventory;

import com.nukkitx.protocol.bedrock.data.ContainerId;
import com.nukkitx.protocol.bedrock.data.InventoryActionData;
import com.nukkitx.protocol.bedrock.data.InventorySource;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater;
import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater;

import java.util.List;

public class MerchantInventoryTranslator extends BaseInventoryTranslator {

private final InventoryUpdater updater;

public MerchantInventoryTranslator() {
super(3);
this.updater = new CursorInventoryUpdater();
}

@Override
public int javaSlotToBedrock(int slot) {
switch (slot) {
case 0:
return 4;
case 1:
return 5;
case 2:
return 50;
}
return super.javaSlotToBedrock(slot);
}

@Override
public int bedrockSlotToJava(InventoryActionData action) {
if (action.getSource().getContainerId() == ContainerId.CURSOR) {
switch (action.getSlot()) {
case 4:
return 0;
case 5:
return 1;
case 50:
return 2;
}
}
return super.bedrockSlotToJava(action);
}

@Override
public SlotType getSlotType(int javaSlot) {
if (javaSlot == 2) {
return SlotType.OUTPUT;
}
return SlotType.NORMAL;
}

@Override
public void prepareInventory(GeyserSession session, Inventory inventory) {

}

@Override
public void openInventory(GeyserSession session, Inventory inventory) {

}

@Override
public void closeInventory(GeyserSession session, Inventory inventory) {
session.setLastInteractedVillagerEid(-1);
session.setVillagerTrades(null);
}

@Override
public void updateInventory(GeyserSession session, Inventory inventory) {
updater.updateInventory(this, session, inventory);
}

@Override
public void updateSlot(GeyserSession session, Inventory inventory, int slot) {
updater.updateSlot(this, session, inventory, slot);
}

@Override
public void translateActions(GeyserSession session, Inventory inventory, List<InventoryActionData> actions) {
for (InventoryActionData action : actions) {
if (action.getSource().getType() == InventorySource.Type.NON_IMPLEMENTED_TODO) {
return;
}
}

super.translateActions(session, inventory, actions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -385,5 +385,49 @@ private com.github.steveice10.opennbt.tag.builtin.Tag translateToJavaNBT(com.nuk
return 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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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.Entity;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;

import java.util.ArrayList;
import java.util.List;

@Translator(packet = ServerTradeListPacket.class)
public class JavaTradeListTranslator extends PacketTranslator<ServerTradeListPacket> {

@Override
public void translate(ServerTradeListPacket packet, GeyserSession session) {
Entity villager = session.getPlayerEntity();
session.setVillagerTrades(packet.getTrades());
villager.getMetadata().put(EntityData.TRADE_TIER, packet.getVillagerLevel() - 1);
villager.getMetadata().put(EntityData.MAX_TRADE_TIER, 4);
villager.getMetadata().put(EntityData.TRADE_XP, packet.getExperience());
villager.updateBedrockMetadata(session);

UpdateTradePacket updateTradePacket = new UpdateTradePacket();
updateTradePacket.setTradeTier(packet.getVillagerLevel() - 1);
updateTradePacket.setWindowId((short) packet.getWindowId());
updateTradePacket.setWindowType((short) ContainerType.TRADING.id());
String displayName;
Entity realVillager = session.getEntityCache().getEntityByGeyserId(session.getLastInteractedVillagerEid());
if (realVillager != null && realVillager.getMetadata().containsKey(EntityData.NAMETAG) && realVillager.getMetadata().getString(EntityData.NAMETAG) != null) {
displayName = realVillager.getMetadata().getString(EntityData.NAMETAG);
} else {
displayName = packet.isRegularVillager() ? "Villager" : "Wandering Trader";
}
updateTradePacket.setDisplayName(displayName);
updateTradePacket.setUnknownInt(0);
updateTradePacket.setScreen2(true);
updateTradePacket.setWilling(true);
updateTradePacket.setPlayerUniqueEntityId(session.getPlayerEntity().getGeyserId());
updateTradePacket.setTraderUniqueEntityId(session.getPlayerEntity().getGeyserId());
CompoundTagBuilder builder = CompoundTagBuilder.builder();
List<CompoundTag> tags = new ArrayList<>();
for (VillagerTrade trade : packet.getTrades()) {
CompoundTagBuilder recipe = CompoundTagBuilder.builder();
recipe.intTag("maxUses", trade.getMaxUses());
recipe.intTag("traderExp", trade.getXp());
recipe.floatTag("priceMultiplierA", trade.getPriceMultiplier());
recipe.tag(getItemTag(session, trade.getOutput(), "sell", 0));
recipe.floatTag("priceMultiplierB", 0.0f);
recipe.intTag("buyCountB", trade.getSecondInput() != null ? trade.getSecondInput().getAmount() : 0);
recipe.intTag("buyCountA", trade.getFirstInput().getAmount());
recipe.intTag("demand", trade.getDemand());
recipe.intTag("tier", packet.getVillagerLevel() - 1);
recipe.tag(getItemTag(session, trade.getFirstInput(), "buyA", trade.getSpecialPrice()));
if (trade.getSecondInput() != null) {
recipe.tag(getItemTag(session, trade.getSecondInput(), "buyB", 0));
}
recipe.intTag("uses", trade.getNumUses());
recipe.byteTag("rewardExp", (byte) 1);
tags.add(recipe.buildRootTag());
}

//Hidden trade to fix visual experience bug
if (packet.isRegularVillager() && packet.getVillagerLevel() < 5) {
tags.add(CompoundTagBuilder.builder()
.intTag("maxUses", 0)
.intTag("traderExp", 0)
.floatTag("priceMultiplierA", 0.0f)
.floatTag("priceMultiplierB", 0.0f)
.intTag("buyCountB", 0)
.intTag("buyCountA", 0)
.intTag("demand", 0)
.intTag("tier", 5)
.intTag("uses", 0)
.byteTag("rewardExp", (byte) 0)
.buildRootTag());
}

builder.listTag("Recipes", CompoundTag.class, tags);
List<CompoundTag> 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", 70).buildRootTag());
expTags.add(CompoundTagBuilder.builder().intTag("3", 150).buildRootTag());
expTags.add(CompoundTagBuilder.builder().intTag("4", 250).buildRootTag());
builder.listTag("TierExpRequirements", CompoundTag.class, expTags);
updateTradePacket.setOffers(builder.buildRootTag());
session.sendUpstreamPacket(updateTradePacket);
}

private CompoundTag getItemTag(GeyserSession session, ItemStack stack, String name, int specialPrice) {
ItemData itemData = ItemTranslator.translateToBedrock(session, stack);
ItemEntry itemEntry = ItemRegistry.getItem(stack);
CompoundTagBuilder builder = CompoundTagBuilder.builder();
builder.byteTag("Count", (byte) (Math.max(itemData.getCount() + specialPrice, 1)));
builder.shortTag("Damage", itemData.getDamage());
builder.shortTag("id", (short) itemEntry.getBedrockId());
if (itemData.getTag() != null) {
CompoundTag tag = itemData.getTag().toBuilder().build("tag");
builder.tag(tag);
}
return builder.build(name);
}
}