Fix villagers for 1.18.10

Includes working around pre-1.14 ONLY on pre-1.14 by checking the tags packet.

Fixes #2828
This commit is contained in:
Camotoy 2022-02-10 20:14:52 -05:00
parent 9ea59d616e
commit 746cd94dd1
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
6 changed files with 111 additions and 16 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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