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 <toy.fighter1@gmail.com>
This commit is contained in:
RednedEpic 2020-05-16 23:52:39 -05:00
parent c6527fa723
commit 30e38b3a2f
7 changed files with 353 additions and 0 deletions

View File

@ -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);
}

View File

@ -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;

View File

@ -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();

View File

@ -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<Inve
session.sendDownstreamPacket(interactAtPacket);
EntitySoundInteractionHandler.handleEntityInteraction(session, vector, entity);
if (entity instanceof VillagerEntity) {
session.setLastInteractedVillagerEid(packet.getRuntimeEntityId());
}
break;
case 1: //Attack
ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) entity.getEntityId(),

View File

@ -0,0 +1,170 @@
/*
* 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.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.window.ClickItemParam;
import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade;
import com.github.steveice10.mc.protocol.data.game.window.WindowAction;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientSelectTradePacket;
import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientWindowActionPacket;
import com.nukkitx.protocol.bedrock.data.ContainerId;
import com.nukkitx.protocol.bedrock.data.InventoryActionData;
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.Translators;
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 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.setFirstTradeSlot(null);
session.setSecondTradeSlot(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) {
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);
}
}

View File

@ -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<ItemEntry> getAppliedItems() {

View File

@ -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<ServerTradeListPacket> {
@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<CompoundTag> 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<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", 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);
}
}