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] 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<IntList> 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 <code>carriedItem</code> 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<String, int[]> 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<EntityEventPa
                 return;
             }
             case COMPLETE_TRADE -> {
+                // 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<ClientboundUpdateTagsPacket> {
+    private final Map<String, Map<String, int[]>> previous = new HashMap<>();
 
     @Override
     public void translate(GeyserSession session, ClientboundUpdateTagsPacket packet) {
-        session.getTagCache().loadPacket(packet);
+        for (Map.Entry<String, Map<String, int[]>> entry : packet.getTags().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
+            StringBuilder builder = new StringBuilder();
+            builder.append(entry.getKey()).append("={");
+            for (Map.Entry<String, int[]> 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<String, Map<String, int[]>> entry : packet.getTags().entrySet()) {
+                Map<String, int[]> oldTags = previous.get(entry.getKey());
+                for (Map.Entry<String, int[]> 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);
+
+
     }
 }