From 2d28ba0cb56b767e329a003ff1595f01fa5acedf Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sun, 30 Jan 2022 11:15:07 -0500 Subject: [PATCH] Several inventory and parity improvements These changes fix up things that were missed with Java Edition inventory changes in 1.17 and 1.17.1. Working with the inventory in modern versions should be much nicer. --- .../org/geysermc/geyser/GeyserLogger.java | 11 ++ .../geysermc/geyser/inventory/Container.java | 5 +- .../geysermc/geyser/inventory/Inventory.java | 8 +- .../geyser/inventory/click/ClickPlan.java | 178 +++++++++--------- .../geyser/session/GeyserSession.java | 9 + .../inventory/BeaconInventoryTranslator.java | 9 +- .../CraftingInventoryTranslator.java | 5 + .../EnchantingInventoryTranslator.java | 19 +- .../inventory/InventoryTranslator.java | 72 +++---- .../inventory/PlayerInventoryTranslator.java | 18 +- ...BedrockInventoryTransactionTranslator.java | 10 +- .../protocol/java/JavaRecipeTranslator.java | 8 +- .../JavaContainerSetContentTranslator.java | 26 ++- .../JavaContainerSetSlotTranslator.java | 134 +++---------- .../geysermc/geyser/util/InventoryUtils.java | 117 ++++++++++++ 15 files changed, 364 insertions(+), 265 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index a61c5db25..b47801cb5 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -25,6 +25,8 @@ package org.geysermc.geyser; +import javax.annotation.Nullable; + public interface GeyserLogger { /** @@ -78,6 +80,15 @@ public interface GeyserLogger { */ void debug(String message); + /** + * Logs an object to console if debug mode is enabled + * + * @param object the object to log + */ + default void debug(@Nullable Object object) { + debug(String.valueOf(object)); + } + /** * Sets if the logger should print debug messages * diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Container.java index 073887a64..569802a5a 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Container.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Container.java @@ -27,11 +27,12 @@ package org.geysermc.geyser.inventory; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import lombok.Getter; -import lombok.NonNull; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.jetbrains.annotations.Range; +import javax.annotation.Nonnull; + /** * Combination of {@link Inventory} and {@link PlayerInventory} */ @@ -66,7 +67,7 @@ public class Container extends Inventory { } @Override - public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { + public void setItem(int slot, @Nonnull GeyserItemStack newItem, GeyserSession session) { if (slot < this.size) { super.setItem(slot, newItem, session); } else { diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java index 26dc261a0..ca7e90a25 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java @@ -31,7 +31,6 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import com.nukkitx.math.vector.Vector3i; import lombok.Getter; -import lombok.NonNull; import lombok.Setter; import lombok.ToString; import org.geysermc.geyser.GeyserImpl; @@ -40,11 +39,11 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; import org.jetbrains.annotations.Range; +import javax.annotation.Nonnull; import java.util.Arrays; @ToString public abstract class Inventory { - @Getter protected final int id; @@ -72,8 +71,7 @@ public abstract class Inventory { protected final ContainerType containerType; @Getter - @Setter - protected String title; + protected final String title; protected final GeyserItemStack[] items; @@ -115,7 +113,7 @@ public abstract class Inventory { public abstract int getOffsetForHotbar(@Range(from = 0, to = 8) int slot); - public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) { + public void setItem(int slot, @Nonnull GeyserItemStack newItem, GeyserSession session) { if (slot > this.size) { session.getGeyser().getLogger().debug("Tried to set an item out of bounds! " + this); return; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java index e973beadc..e6eeea689 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java @@ -28,7 +28,6 @@ package org.geysermc.geyser.inventory.click; import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerActionType; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; -import com.github.steveice10.mc.protocol.data.game.inventory.MoveToHotbarAction; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -40,20 +39,22 @@ import org.geysermc.geyser.inventory.SlotType; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator; import org.geysermc.geyser.translator.inventory.InventoryTranslator; -import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator; import org.geysermc.geyser.util.InventoryUtils; import org.jetbrains.annotations.Contract; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.ListIterator; -public class ClickPlan { +public final class ClickPlan { private final List plan = new ArrayList<>(); private final Int2ObjectMap simulatedItems; + /** + * Used for 1.17.1+ proper packet translation - any non-cursor item that is changed in a single transaction gets sent here. + */ + private Int2ObjectMap changedItems; private GeyserItemStack simulatedCursor; - private boolean simulating; + private boolean finished; private final GeyserSession session; private final InventoryTranslator translator; @@ -66,21 +67,11 @@ public class ClickPlan { this.inventory = inventory; this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize()); + this.changedItems = null; this.simulatedCursor = session.getPlayerInventory().getCursor().copy(); - this.simulating = true; + this.finished = false; - if (translator instanceof PlayerInventoryTranslator) { - gridSize = 4; - } else if (translator instanceof CraftingInventoryTranslator) { - gridSize = 9; - } else { - gridSize = -1; - } - } - - private void resetSimulation() { - this.simulatedItems.clear(); - this.simulatedCursor = session.getPlayerInventory().getCursor().copy(); + gridSize = translator.getGridSize(); } public void add(Click click, int slot) { @@ -88,7 +79,7 @@ public class ClickPlan { } public void add(Click click, int slot, boolean force) { - if (!simulating) + if (finished) throw new UnsupportedOperationException("ClickPlan already executed"); if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) { @@ -97,12 +88,10 @@ public class ClickPlan { ClickAction action = new ClickAction(click, slot, force); plan.add(action); - simulateAction(action); } public void execute(boolean refresh) { //update geyser inventory after simulation to avoid net id desync - resetSimulation(); ListIterator planIter = plan.listIterator(); while (planIter.hasNext()) { ClickAction action = planIter.next(); @@ -112,33 +101,48 @@ public class ClickPlan { refresh = true; } - //int stateId = stateIdHack(action); + changedItems = new Int2ObjectOpenHashMap<>(); - //simulateAction(action); + boolean emulatePost1_16Logic = session.isEmulatePost1_16Logic(); + + int stateId; + if (emulatePost1_16Logic) { + stateId = stateIdHack(action); + simulateAction(action); + } else { + stateId = inventory.getStateId(); + } ItemStack clickedItemStack; if (!planIter.hasNext() && refresh) { clickedItemStack = InventoryUtils.REFRESH_ITEM; - } else if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) { - clickedItemStack = null; } else { - //// The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1) - //clickedItemStack = simulatedCursor.getItemStack(); TODO fix - this is the proper behavior but it terribly breaks 1.16.5 - clickedItemStack = getItem(action.slot).getItemStack(); + if (emulatePost1_16Logic) { + // The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1) + clickedItemStack = simulatedCursor.getItemStack(); + } else { + if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) { + clickedItemStack = null; + } else { + clickedItemStack = getItem(action.slot).getItemStack(); + } + } + } + + if (!emulatePost1_16Logic) { + simulateAction(action); } ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket( inventory.getId(), - inventory.getStateId(), + stateId, action.slot, action.click.actionType, action.click.action, clickedItemStack, - Collections.emptyMap() // Anything else we change, at this time, should have a packet sent to address + changedItems ); - simulateAction(action); - session.sendDownstreamPacket(clickPacket); } @@ -146,19 +150,11 @@ public class ClickPlan { for (Int2ObjectMap.Entry simulatedSlot : simulatedItems.int2ObjectEntrySet()) { inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session); } - simulating = false; + finished = true; } public GeyserItemStack getItem(int slot) { - return getItem(slot, true); - } - - public GeyserItemStack getItem(int slot, boolean generate) { - if (generate) { - return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy()); - } else { - return simulatedItems.getOrDefault(slot, inventory.getItem(slot)); - } + return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy()); } public GeyserItemStack getCursor() { @@ -166,23 +162,38 @@ public class ClickPlan { } private void setItem(int slot, GeyserItemStack item) { - if (simulating) { - simulatedItems.put(slot, item); - } else { - inventory.setItem(slot, item, session); - } + simulatedItems.put(slot, item); + onSlotItemChange(slot, item); } private void setCursor(GeyserItemStack item) { - if (simulating) { - simulatedCursor = item; - } else { - session.getPlayerInventory().setCursor(item, session); - } + simulatedCursor = item; + } + + private void add(int slot, GeyserItemStack itemStack, int amount) { + itemStack.add(amount); + onSlotItemChange(slot, itemStack); + } + + private void sub(int slot, GeyserItemStack itemStack, int amount) { + itemStack.sub(amount); + onSlotItemChange(slot, itemStack); + } + + private void setAmount(int slot, GeyserItemStack itemStack, int amount) { + itemStack.setAmount(amount); + onSlotItemChange(slot, itemStack); + } + + /** + * Does not need to be called for the cursor + */ + private void onSlotItemChange(int slot, GeyserItemStack itemStack) { + changedItems.put(slot, itemStack.getItemStack()); } private void simulateAction(ClickAction action) { - GeyserItemStack cursor = simulating ? getCursor() : session.getPlayerInventory().getCursor(); + GeyserItemStack cursor = getCursor(); switch (action.click) { case LEFT_OUTSIDE -> { setCursor(GeyserItemStack.EMPTY); @@ -196,7 +207,7 @@ public class ClickPlan { } } - GeyserItemStack clicked = simulating ? getItem(action.slot) : inventory.getItem(action.slot); + GeyserItemStack clicked = getItem(action.slot); if (translator.getSlotType(action.slot) == SlotType.OUTPUT) { switch (action.click) { case LEFT, RIGHT -> { @@ -206,6 +217,7 @@ public class ClickPlan { cursor.add(clicked.getAmount()); } reduceCraftingGrid(false); + setItem(action.slot, GeyserItemStack.EMPTY); // Matches Java Edition 1.18.1 } case LEFT_SHIFT -> reduceCraftingGrid(true); } @@ -217,20 +229,20 @@ public class ClickPlan { setItem(action.slot, cursor); } else { setCursor(GeyserItemStack.EMPTY); - clicked.add(cursor.getAmount()); + add(action.slot, clicked, cursor.getAmount()); } break; case RIGHT: if (cursor.isEmpty() && !clicked.isEmpty()) { int half = clicked.getAmount() / 2; //smaller half setCursor(clicked.copy(clicked.getAmount() - half)); //larger half - clicked.setAmount(half); + setAmount(action.slot, clicked, half); } else if (!cursor.isEmpty() && clicked.isEmpty()) { cursor.sub(1); setItem(action.slot, cursor.copy(1)); } else if (InventoryUtils.canStack(cursor, clicked)) { cursor.sub(1); - clicked.add(1); + add(action.slot, clicked, 1); } break; case SWAP_TO_HOTBAR_1: @@ -265,7 +277,7 @@ public class ClickPlan { break; case DROP_ONE: if (!clicked.isEmpty()) { - clicked.sub(1); + sub(action.slot, clicked, 1); } break; case DROP_ALL: @@ -279,7 +291,7 @@ public class ClickPlan { * Swap between two inventory slots without a cursor. This should only be used with {@link ContainerActionType#MOVE_TO_HOTBAR_SLOT} */ private void swap(int sourceSlot, int destSlot, GeyserItemStack sourceItem) { - GeyserItemStack destinationItem = simulating ? getItem(destSlot) : inventory.getItem(destSlot); + GeyserItemStack destinationItem = getItem(destSlot); setItem(sourceSlot, destinationItem); setItem(destSlot, sourceItem); } @@ -292,63 +304,44 @@ public class ClickPlan { stateId = inventory.getStateId(); } - // This is a hack. - // Java will never ever send more than one container click packet per set of actions. + // Java will never ever send more than one container click packet per set of actions*. + // *(exception being Java's "quick craft"/painting feature) // Bedrock might, and this would generally fall into one of two categories: // - Bedrock is sending an item directly from one slot to another, without picking it up, that cannot // be expressed with a shift click // - Bedrock wants to pick up or place an arbitrary amount of items that cannot be expressed from // one left/right click action. - // When Bedrock does one of these actions and sends multiple packets, a 1.17.1+ server will - // increment the state ID on each confirmation packet it sends back (I.E. set slot). Then when it - // reads our next packet, because we kept the same state ID but the server incremented it, it'll be - // desynced and send the entire inventory contents back at us. - // This hack therefore increments the state ID to what the server will presumably send back to us. - // (This won't be perfect, but should get us through most vanilla situations, and if this is wrong the - // server will just send a set content packet back at us) + // Java typically doesn't increment the state ID if you send a vanilla-accurate container click packet, + // but it will increment the state ID with a vanilla client in at least the crafting table if (inventory.getContainerType() == ContainerType.CRAFTING && CraftingInventoryTranslator.isCraftingGrid(action.slot)) { // 1.18.1 sends a second set slot update for any action in the crafting grid // And an additional packet if something is removed (Mojmap: CraftingContainer#removeItem) - //TODO this code kind of really sucks; it's potentially possible to see what Bedrock sends us and send a PlaceRecipePacket int stateIdIncrements; GeyserItemStack clicked = getItem(action.slot); if (action.click == Click.LEFT) { if (!clicked.isEmpty() && !InventoryUtils.canStack(simulatedCursor, clicked)) { // An item is removed from the crafting table; yes deletion - stateIdIncrements = 3; + stateIdIncrements = 2; } else { // We can stack and we add all the items to the crafting slot; no deletion - stateIdIncrements = 2; + stateIdIncrements = 1; } } else if (action.click == Click.RIGHT) { - if (simulatedCursor.isEmpty() && !clicked.isEmpty()) { - // Items are taken; yes deletion - stateIdIncrements = 3; - } else if ((!simulatedCursor.isEmpty() && clicked.isEmpty()) || InventoryUtils.canStack(simulatedCursor, clicked)) { - // Adding our cursor item to the slot; no deletion - stateIdIncrements = 2; - } else { - // ?? nothing I guess - stateIdIncrements = 2; - } + stateIdIncrements = 1; + } else if (action.click.actionType == ContainerActionType.MOVE_TO_HOTBAR_SLOT) { + stateIdIncrements = 1; } else { if (session.getGeyser().getConfig().isDebugMode()) { session.getGeyser().getLogger().debug("Not sure how to handle state ID hack in crafting table: " + plan); } - stateIdIncrements = 2; + stateIdIncrements = 1; } inventory.incrementStateId(stateIdIncrements); - } else if (action.click.action instanceof MoveToHotbarAction) { - // Two slot changes sent - inventory.incrementStateId(2); - } else { - inventory.incrementStateId(1); } return stateId; } - //TODO private void reduceCraftingGrid(boolean makeAll) { if (gridSize == -1) return; @@ -370,9 +363,12 @@ public class ClickPlan { } for (int i = 0; i < gridSize; i++) { - GeyserItemStack item = getItem(i + 1); - if (!item.isEmpty()) - item.sub(crafted); + final int slot = i + 1; + GeyserItemStack item = getItem(slot); + if (!item.isEmpty()) { + // These changes should be broadcasted to the server + sub(slot, item, crafted); + } } } 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 99c8c5cc4..3a097f732 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,15 @@ public class GeyserSession implements GeyserConnection, CommandSender { @Setter private Int2ObjectMap stonecutterRecipes; + /** + * Starting in 1.17, Java servers expect the carriedItem 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 + * contents before any transaction is done. With the current ViaVersion structure, if we do not send what 1.16 expects + * and send multiple click container packets, then successive transactions will be rejected. + */ + @Setter + private boolean emulatePost1_16Logic = true; + /** * The current attack speed of the player. Used for sending proper cooldown timings. * Setting a default fixes cooldowns not showing up on a fresh world. diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java index 19d9d6de5..f194d0d3f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java @@ -38,17 +38,16 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType; import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; +import it.unimi.dsi.fastutil.ints.IntSets; import org.geysermc.geyser.inventory.BeaconContainer; +import org.geysermc.geyser.inventory.BedrockContainerSlot; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.inventory.BedrockContainerSlot; import org.geysermc.geyser.inventory.holder.BlockInventoryHolder; import org.geysermc.geyser.inventory.updater.UIInventoryUpdater; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InventoryUtils; -import java.util.Collections; - public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator { public BeaconInventoryTranslator() { super(1, new BlockInventoryHolder("minecraft:beacon", com.nukkitx.protocol.bedrock.data.inventory.ContainerType.BEACON) { @@ -114,7 +113,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator BeaconPaymentStackRequestActionData beaconPayment = (BeaconPaymentStackRequestActionData) request.getActions()[0]; ServerboundSetBeaconPacket packet = new ServerboundSetBeaconPacket(beaconPayment.getPrimaryEffect(), beaconPayment.getSecondaryEffect()); session.sendDownstreamPacket(packet); - return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet())); + return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet())); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java index ec3335f3c..61e2258b6 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java @@ -37,6 +37,11 @@ public class CraftingInventoryTranslator extends AbstractBlockInventoryTranslato super(10, "minecraft:crafting_table", ContainerType.WORKBENCH, UIInventoryUpdater.INSTANCE); } + @Override + public int getGridSize() { + return 9; + } + @Override public SlotType getSlotType(int javaSlot) { if (javaSlot == 0) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java index 97ece79d8..800b35901 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java @@ -27,23 +27,22 @@ 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.ServerboundContainerButtonClickPacket; -import com.nukkitx.protocol.bedrock.data.inventory.*; +import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; +import com.nukkitx.protocol.bedrock.data.inventory.EnchantOptionData; +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.data.inventory.stackrequestactions.StackRequestActionData; import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType; import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; import com.nukkitx.protocol.bedrock.packet.PlayerEnchantOptionsPacket; -import org.geysermc.geyser.inventory.EnchantingContainer; -import org.geysermc.geyser.inventory.GeyserEnchantOption; -import org.geysermc.geyser.inventory.Inventory; -import org.geysermc.geyser.inventory.PlayerInventory; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.inventory.BedrockContainerSlot; -import org.geysermc.geyser.inventory.updater.UIInventoryUpdater; +import it.unimi.dsi.fastutil.ints.IntSets; +import org.geysermc.geyser.inventory.*; import org.geysermc.geyser.inventory.item.Enchantment; +import org.geysermc.geyser.inventory.updater.UIInventoryUpdater; +import org.geysermc.geyser.session.GeyserSession; import java.util.Arrays; -import java.util.Collections; public class EnchantingInventoryTranslator extends AbstractBlockInventoryTranslator { public EnchantingInventoryTranslator() { @@ -130,7 +129,7 @@ public class EnchantingInventoryTranslator extends AbstractBlockInventoryTransla } ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getId(), javaSlot); session.sendDownstreamPacket(packet); - return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet())); + return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet())); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index e0b90db02..e6a9faf74 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -26,12 +26,11 @@ package org.geysermc.geyser.translator.inventory; 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.inventory.ContainerType; import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient; import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; -import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import com.github.steveice10.opennbt.tag.builtin.IntTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; @@ -43,15 +42,10 @@ import it.unimi.dsi.fastutil.ints.*; import lombok.AllArgsConstructor; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.inventory.CartographyContainer; -import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.inventory.Inventory; -import org.geysermc.geyser.inventory.PlayerInventory; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.inventory.BedrockContainerSlot; -import org.geysermc.geyser.inventory.SlotType; +import org.geysermc.geyser.inventory.*; import org.geysermc.geyser.inventory.click.Click; import org.geysermc.geyser.inventory.click.ClickPlan; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTranslator; import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator; import org.geysermc.geyser.translator.inventory.furnace.BlastFurnaceInventoryTranslator; @@ -119,6 +113,13 @@ public abstract class InventoryTranslator { public abstract SlotType getSlotType(int javaSlot); public abstract Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory); + /** + * Used for crafting-related transactions. Will override in PlayerInventoryTranslator and CraftingInventoryTranslator. + */ + public int getGridSize() { + return -1; + } + /** * Should be overwritten in cases where specific inventories should reject an item being in a specific spot. * For examples, looms use this to reject items that are dyes in Bedrock but not in Java. @@ -147,7 +148,7 @@ public abstract class InventoryTranslator { return rejectRequest(request); } - public void translateRequests(GeyserSession session, Inventory inventory, List requests) { + public final void translateRequests(GeyserSession session, Inventory inventory, List requests) { boolean refresh = false; ItemStackResponsePacket responsePacket = new ItemStackResponsePacket(); for (ItemStackRequest request : requests) { @@ -199,10 +200,6 @@ public abstract class InventoryTranslator { case PLACE: { TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action; if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) { - if (session.getGameMode().equals(GameMode.CREATIVE) && transferAction.getSource().getContainer() == ContainerSlotType.CRAFTING_INPUT && - transferAction.getSource().getSlot() >= 28 && transferAction.getSource().getSlot() <= 31) { - return rejectRequest(request, false); - } if (session.getGeyser().getConfig().isDebugMode()) { session.getGeyser().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.name()); dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination()); @@ -212,17 +209,19 @@ public abstract class InventoryTranslator { int sourceSlot = bedrockSlotToJava(transferAction.getSource()); int destSlot = bedrockSlotToJava(transferAction.getDestination()); + boolean isSourceCursor = isCursor(transferAction.getSource()); + boolean isDestCursor = isCursor(transferAction.getDestination()); if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(), - isCursor(transferAction.getSource()) ? -1 : sourceSlot, - transferAction.getDestination().getContainer(), isCursor(transferAction.getDestination()) ? -1 : destSlot)) { + isSourceCursor ? -1 : sourceSlot, + transferAction.getDestination().getContainer(), isDestCursor ? -1 : destSlot)) { // This item would not be here in Java return rejectRequest(request, false); } - if (isCursor(transferAction.getSource()) && isCursor(transferAction.getDestination())) { //??? + if (isSourceCursor && isDestCursor) { //??? return rejectRequest(request); - } else if (isCursor(transferAction.getSource())) { //releasing cursor + } else if (isSourceCursor) { //releasing cursor int sourceAmount = cursor.getAmount(); if (transferAction.getCount() == sourceAmount) { //release all plan.add(Click.LEFT, destSlot); @@ -231,7 +230,7 @@ public abstract class InventoryTranslator { plan.add(Click.RIGHT, destSlot); } } - } else if (isCursor(transferAction.getDestination())) { //picking up into cursor + } else if (isDestCursor) { //picking up into cursor GeyserItemStack sourceItem = plan.getItem(sourceSlot); int sourceAmount = sourceItem.getAmount(); if (cursor.isEmpty()) { //picking up into empty cursor @@ -431,6 +430,8 @@ public abstract class InventoryTranslator { int leftover = 0; ClickPlan plan = new ClickPlan(session, this, inventory); + // Track all the crafting table slots to report back the contents of the slots after crafting + IntSet affectedSlots = new IntOpenHashSet(); for (StackRequestActionData action : request.getActions()) { switch (action.getType()) { case CRAFT_RECIPE: { @@ -462,6 +463,7 @@ public abstract class InventoryTranslator { return rejectRequest(request); } craftState = CraftState.INGREDIENTS; + affectedSlots.add(bedrockSlotToJava(((ConsumeStackRequestActionData) action).getSource())); break; } case TAKE: @@ -522,21 +524,16 @@ public abstract class InventoryTranslator { } } plan.execute(false); - return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots())); + affectedSlots.addAll(plan.getAffectedSlots()); + return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); } public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { - int gridSize; - int gridDimensions; - if (this instanceof PlayerInventoryTranslator) { - gridSize = 4; - gridDimensions = 2; - } else if (this instanceof CraftingInventoryTranslator) { - gridSize = 9; - gridDimensions = 3; - } else { + final int gridSize = getGridSize(); + if (gridSize == -1) { return rejectRequest(request); } + int gridDimensions = gridSize == 4 ? 2 : 3; Recipe recipe; Ingredient[] ingredients = new Ingredient[0]; @@ -722,7 +719,7 @@ public abstract class InventoryTranslator { /** * Handled in {@link PlayerInventoryTranslator} */ - public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + protected ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { return rejectRequest(request); } @@ -757,14 +754,14 @@ public abstract class InventoryTranslator { } } - public static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List containerEntries) { + protected static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List containerEntries) { return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.OK, request.getRequestId(), containerEntries); } /** * Reject an incorrect ItemStackRequest. */ - public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) { + protected static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) { return rejectRequest(request, true); } @@ -774,7 +771,7 @@ public abstract class InventoryTranslator { * @param throwError whether this request was truly erroneous (true), or known as an outcome and should not be treated * as bad (false). */ - public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) { + protected static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) { if (throwError && GeyserImpl.getInstance().getConfig().isDebugMode()) { new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace(); } @@ -849,9 +846,12 @@ public abstract class InventoryTranslator { return -1; } - public List makeContainerEntries(GeyserSession session, Inventory inventory, Set affectedSlots) { + protected final List makeContainerEntries(GeyserSession session, Inventory inventory, IntSet affectedSlots) { Map> containerMap = new HashMap<>(); - for (int slot : affectedSlots) { + // Manually call iterator to prevent Integer boxing + IntIterator it = affectedSlots.iterator(); + while (it.hasNext()) { + int slot = it.nextInt(); BedrockContainerSlot bedrockSlot = javaSlotToBedrockContainer(slot); List list = containerMap.computeIfAbsent(bedrockSlot.container(), k -> new ArrayList<>()); list.add(makeItemEntry(session, bedrockSlot.slot(), inventory.getItem(slot))); @@ -868,7 +868,7 @@ public abstract class InventoryTranslator { return containerEntries; } - public static ItemStackResponsePacket.ItemEntry makeItemEntry(GeyserSession session, int bedrockSlot, GeyserItemStack itemStack) { + private static ItemStackResponsePacket.ItemEntry makeItemEntry(GeyserSession session, int bedrockSlot, GeyserItemStack itemStack) { ItemStackResponsePacket.ItemEntry itemEntry; if (!itemStack.isEmpty()) { // As of 1.16.210: Bedrock needs confirmation on what the current item durability is. diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java index 04de68a1e..e2349e5a5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java @@ -35,6 +35,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.*; import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; +import it.unimi.dsi.fastutil.ints.IntIterator; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import org.geysermc.geyser.inventory.*; @@ -55,6 +56,11 @@ public class PlayerInventoryTranslator extends InventoryTranslator { super(46); } + @Override + public int getGridSize() { + return 4; + } + @Override public void updateInventory(GeyserSession session, Inventory inventory) { updateCraftingGrid(session, inventory); @@ -370,14 +376,17 @@ public class PlayerInventoryTranslator extends InventoryTranslator { } } } - for (int slot : affectedSlots) { + // Manually call iterator to prevent Integer boxing + IntIterator it = affectedSlots.iterator(); + while (it.hasNext()) { + int slot = it.nextInt(); sendCreativeAction(session, inventory, slot); } return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); } @Override - public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + protected ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { ItemStack javaCreativeItem = null; IntSet affectedSlots = new IntOpenHashSet(); CraftState craftState = CraftState.START; @@ -478,7 +487,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator { return rejectRequest(request); } } - for (int slot : affectedSlots) { + // Manually call iterator to prevent Integer boxing + IntIterator it = affectedSlots.iterator(); + while (it.hasNext()) { + int slot = it.nextInt(); sendCreativeAction(session, inventory, slot); } return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index be10452f4..869062da2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.translator.protocol.bedrock; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; @@ -41,6 +42,8 @@ import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.inventory.*; import com.nukkitx.protocol.bedrock.packet.*; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity; import org.geysermc.geyser.entity.type.Entity; @@ -59,7 +62,6 @@ import org.geysermc.geyser.translator.sound.EntitySoundInteractionTranslator; import org.geysermc.geyser.util.BlockUtils; import org.geysermc.geyser.util.InventoryUtils; -import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -316,9 +318,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator changedSlots = new Int2ObjectOpenHashMap<>(2); + changedSlots.put(armorSlot, hotbarItem.getItemStack()); + changedSlots.put(bedrockHotbarSlot, armorSlotItem.getItemStack()); + ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket( playerInventory.getId(), playerInventory.getStateId(), armorSlot, - click.actionType, click.action, null, Collections.emptyMap()); + click.actionType, click.action, null, changedSlots); session.sendDownstreamPacket(clickPacket); } } else { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java index b3a04e163..da35da60e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java @@ -31,7 +31,7 @@ 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.Collections; /** * Used to list recipes that we can definitely use the recipe book for (and therefore save on packet usage) @@ -42,9 +42,11 @@ public class JavaRecipeTranslator extends PacketTranslator inventorySize) { + GeyserImpl geyser = session.getGeyser(); + geyser.getLogger().warning("ClientboundContainerSetContentPacket sent to " + session.name() + + " that exceeds inventory size!"); + if (geyser.getConfig().isDebugMode()) { + geyser.getLogger().debug(packet); + geyser.getLogger().debug(inventory); + } + InventoryTranslator translator = session.getInventoryTranslator(); + if (translator != null) { + translator.updateInventory(session, inventory); + } + // 1.18.1 behavior: the previous items will be correctly set, but the state ID and carried item will not + // as this produces a stack trace on the client. + // If Java processes this correctly in the future, we can revert this behavior + return; + } + GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]); inventory.setItem(i, newItem, session); } @@ -55,6 +73,10 @@ public class JavaContainerSetContentTranslator extends PacketTranslator 0 || stateId != inventory.getStateId()); + inventory.setStateId(stateId); + session.getPlayerInventory().setCursor(GeyserItemStack.from(packet.getCarriedItem()), session); InventoryUtils.updateCursor(session); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java index 283d95fc4..4bb2a8e60 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java @@ -30,7 +30,6 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient; import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType; import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; -import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetSlotPacket; import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; @@ -40,17 +39,15 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.protocol.PacketTranslator; -import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.inventory.InventoryTranslator; -import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator; import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator; import org.geysermc.geyser.translator.inventory.item.ItemTranslator; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.InventoryUtils; import java.util.Arrays; import java.util.Collections; -import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -72,14 +69,16 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator 0 || stateId != inventory.getStateId()); + inventory.setStateId(stateId); InventoryTranslator translator = session.getInventoryTranslator(); if (translator != null) { if (session.getCraftingGridFuture() != null) { session.getCraftingGridFuture().cancel(false); } - session.setCraftingGridFuture(session.scheduleInEventLoop(() -> updateCraftingGrid(session, packet, inventory, translator), 150, TimeUnit.MILLISECONDS)); + updateCraftingGrid(session, packet.getSlot(), packet.getItem(), inventory, translator); GeyserItemStack newItem = GeyserItemStack.from(packet.getItem()); if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) { @@ -93,21 +92,23 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator { int offset = gridSize == 4 ? 28 : 32; int gridDimensions = gridSize == 4 ? 2 : 3; int firstRow = -1, height = -1; @@ -135,62 +136,10 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator null; }; } + + /** + * Test all known recipes to find a valid match + * + * @param output if not null, the recipe has to output this item + */ + @Nullable + public static Recipe getValidRecipe(final GeyserSession session, final @Nullable ItemStack output, final IntFunction inventoryGetter, + final int gridDimensions, final int firstRow, final int height, final int firstCol, final int width) { + int nonAirCount = 0; // Used for shapeless recipes for amount of items needed in recipe + for (int row = firstRow; row < height + firstRow; row++) { + for (int col = firstCol; col < width + firstCol; col++) { + if (!inventoryGetter.apply(col + (row * gridDimensions) + 1).isEmpty()) { + nonAirCount++; + } + } + } + + recipes: + for (Recipe recipe : session.getCraftingRecipes().values()) { + if (recipe.getType() == RecipeType.CRAFTING_SHAPED) { + ShapedRecipeData data = (ShapedRecipeData) recipe.getData(); + if (output != null && !data.getResult().equals(output)) { + continue; + } + Ingredient[] ingredients = data.getIngredients(); + if (data.getWidth() != width || data.getHeight() != height || width * height != ingredients.length) { + continue; + } + + if (!testShapedRecipe(ingredients, inventoryGetter, gridDimensions, firstRow, height, firstCol, width)) { + Ingredient[] mirroredIngredients = new Ingredient[ingredients.length]; + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + mirroredIngredients[col + (row * width)] = ingredients[(width - 1 - col) + (row * width)]; + } + } + + if (Arrays.equals(ingredients, mirroredIngredients) || + !testShapedRecipe(mirroredIngredients, inventoryGetter, gridDimensions, firstRow, height, firstCol, width)) { + continue; + } + } + return recipe; + } else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) { + ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData(); + if (output != null && !data.getResult().equals(output)) { + continue; + } + if (nonAirCount != data.getIngredients().length) { + // There is an amount of items on the crafting table that is not the same as the ingredient count so this is invalid + continue; + } + for (int i = 0; i < data.getIngredients().length; i++) { + Ingredient ingredient = data.getIngredients()[i]; + for (ItemStack itemStack : ingredient.getOptions()) { + boolean inventoryHasItem = false; + // Iterate only over the crafting table to find this item + crafting: + for (int row = firstRow; row < height + firstRow; row++) { + for (int col = firstCol; col < width + firstCol; col++) { + GeyserItemStack geyserItemStack = inventoryGetter.apply(col + (row * gridDimensions) + 1); + if (geyserItemStack.isEmpty()) { + inventoryHasItem = itemStack == null || itemStack.getId() == 0; + if (inventoryHasItem) { + break crafting; + } + } else if (itemStack.equals(geyserItemStack.getItemStack(1))) { + inventoryHasItem = true; + break crafting; + } + } + } + if (!inventoryHasItem) { + continue recipes; + } + } + } + return recipe; + } + } + return null; + } + + private static boolean testShapedRecipe(final Ingredient[] ingredients, final IntFunction inventoryGetter, + final int gridDimensions, final int firstRow, final int height, final int firstCol, final int width) { + int ingredientIndex = 0; + for (int row = firstRow; row < height + firstRow; row++) { + for (int col = firstCol; col < width + firstCol; col++) { + GeyserItemStack geyserItemStack = inventoryGetter.apply(col + (row * gridDimensions) + 1); + Ingredient ingredient = ingredients[ingredientIndex++]; + if (ingredient.getOptions().length == 0) { + if (!geyserItemStack.isEmpty()) { + return false; + } + } else { + boolean inventoryHasItem = false; + for (ItemStack item : ingredient.getOptions()) { + if (Objects.equals(geyserItemStack.getItemStack(1), item)) { + inventoryHasItem = true; + break; + } + } + if (!inventoryHasItem) { + return false; + } + } + } + } + return true; + } }