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.
This commit is contained in:
Camotoy 2022-01-30 11:15:07 -05:00
parent d0fa2d2b05
commit 2d28ba0cb5
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
15 changed files with 364 additions and 265 deletions

View file

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

View file

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

View file

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

View file

@ -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<ClickAction> plan = new ArrayList<>();
private final Int2ObjectMap<GeyserItemStack> 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<ItemStack> 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<ClickAction> 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<GeyserItemStack> 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);
}
}
}

View file

@ -361,6 +361,15 @@ public class GeyserSession implements GeyserConnection, CommandSender {
@Setter
private Int2ObjectMap<IntList> stonecutterRecipes;
/**
* 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
* 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.

View file

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

View file

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

View file

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

View file

@ -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<ItemStackRequest> requests) {
public final void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> 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<ItemStackResponsePacket.ContainerEntry> containerEntries) {
protected static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List<ItemStackResponsePacket.ContainerEntry> 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<ItemStackResponsePacket.ContainerEntry> makeContainerEntries(GeyserSession session, Inventory inventory, Set<Integer> affectedSlots) {
protected final List<ItemStackResponsePacket.ContainerEntry> makeContainerEntries(GeyserSession session, Inventory inventory, IntSet affectedSlots) {
Map<ContainerSlotType, List<ItemStackResponsePacket.ItemEntry>> 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<ItemStackResponsePacket.ItemEntry> 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.

View file

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

View file

@ -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<Inve
playerInventory.setItem(armorSlot, hotbarItem, session);
playerInventory.setItem(bedrockHotbarSlot, armorSlotItem, session);
Int2ObjectMap<ItemStack> 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 {

View file

@ -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<ClientboundRecipePack
@Override
public void translate(GeyserSession session, ClientboundRecipePacket packet) {
if (packet.getAction() == UnlockRecipesAction.REMOVE) {
session.getUnlockedRecipes().removeAll(Arrays.asList(packet.getRecipes()));
for (String identifier : packet.getRecipes()) {
session.getUnlockedRecipes().remove(identifier);
}
} else {
session.getUnlockedRecipes().addAll(Arrays.asList(packet.getRecipes()));
Collections.addAll(session.getUnlockedRecipes(), packet.getRecipes());
}
}
}

View file

@ -26,6 +26,7 @@
package org.geysermc.geyser.translator.protocol.java.inventory;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetContentPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.session.GeyserSession;
@ -43,9 +44,26 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
if (inventory == null)
return;
inventory.setStateId(packet.getStateId());
int inventorySize = inventory.getSize();
for (int i = 0; i < packet.getItems().length; i++) {
if (i > 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<Clientbo
translator.updateInventory(session, inventory);
}
int stateId = packet.getStateId();
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
inventory.setStateId(stateId);
session.getPlayerInventory().setCursor(GeyserItemStack.from(packet.getCarriedItem()), session);
InventoryUtils.updateCursor(session);
}

View file

@ -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<Clientbound
return;
// Intentional behavior here below the cursor; Minecraft 1.18.1 also does this.
inventory.setStateId(packet.getStateId());
int stateId = packet.getStateId();
session.setEmulatePost1_16Logic(stateId > 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<Clientbound
}
}
private static void updateCraftingGrid(GeyserSession session, ClientboundContainerSetSlotPacket packet, Inventory inventory, InventoryTranslator translator) {
if (packet.getSlot() == 0) {
int gridSize;
if (translator instanceof PlayerInventoryTranslator) {
gridSize = 4;
} else if (translator instanceof CraftingInventoryTranslator) {
gridSize = 9;
} else {
return;
}
/**
* Checks for a changed output slot in the crafting grid, and ensures Bedrock sees the recipe.
*/
private static void updateCraftingGrid(GeyserSession session, int slot, ItemStack item, Inventory inventory, InventoryTranslator translator) {
if (slot != 0) {
return;
}
int gridSize = translator.getGridSize();
if (gridSize == -1) {
return;
}
if (packet.getItem() == null || packet.getItem().getId() == 0) {
return;
}
if (item == null || item.getId() == 0) {
return;
}
session.setCraftingGridFuture(session.scheduleInEventLoop(() -> {
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<Clientbound
height += -firstRow + 1;
width += -firstCol + 1;
recipes:
for (Recipe recipe : session.getCraftingRecipes().values()) {
if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {
ShapedRecipeData data = (ShapedRecipeData) recipe.getData();
if (!data.getResult().equals(packet.getItem())) {
continue;
}
if (data.getWidth() != width || data.getHeight() != height || width * height != data.getIngredients().length) {
continue;
}
Ingredient[] ingredients = data.getIngredients();
if (!testShapedRecipe(ingredients, inventory, gridDimensions, firstRow, height, firstCol, width)) {
Ingredient[] mirroredIngredients = new Ingredient[data.getIngredients().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, inventory, gridDimensions, firstRow, height, firstCol, width)) {
continue;
}
}
// Recipe is had, don't sent packet
return;
} else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) {
ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData();
if (!data.getResult().equals(packet.getItem())) {
continue;
}
for (int i = 0; i < data.getIngredients().length; i++) {
Ingredient ingredient = data.getIngredients()[i];
for (ItemStack itemStack : ingredient.getOptions()) {
boolean inventoryHasItem = false;
for (int j = 0; j < inventory.getSize(); j++) {
GeyserItemStack geyserItemStack = inventory.getItem(j);
if (geyserItemStack.isEmpty()) {
inventoryHasItem = itemStack == null || itemStack.getId() == 0;
if (inventoryHasItem) {
break;
}
} else if (itemStack.equals(geyserItemStack.getItemStack(1))) {
inventoryHasItem = true;
break;
}
}
if (!inventoryHasItem) {
continue recipes;
}
}
}
// Recipe is had, don't sent packet
return;
}
if (InventoryUtils.getValidRecipe(session, item, inventory::getItem, gridDimensions, firstRow,
height, firstCol, width) != null) {
// Recipe is already present on the client; don't send packet
return;
}
UUID uuid = UUID.randomUUID();
@ -216,7 +165,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
}
}
ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, packet.getItem());
ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, item);
// Cache this recipe so we know the client has received it
session.getCraftingRecipes().put(newRecipeId, new Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data));
@ -226,7 +175,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
width,
height,
Arrays.asList(ingredients),
Collections.singletonList(ItemTranslator.translateToBedrock(session, packet.getItem())),
Collections.singletonList(ItemTranslator.translateToBedrock(session, item)),
uuid,
"crafting_table",
0,
@ -246,33 +195,6 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
index++;
}
}
}
}
private static boolean testShapedRecipe(Ingredient[] ingredients, Inventory inventory, int gridDimensions, int firstRow, int height, int firstCol, int width) {
int ingredientIndex = 0;
for (int row = firstRow; row < height + firstRow; row++) {
for (int col = firstCol; col < width + firstCol; col++) {
GeyserItemStack geyserItemStack = inventory.getItem(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;
}, 150, TimeUnit.MILLISECONDS));
}
}

View file

@ -27,6 +27,11 @@ package org.geysermc.geyser.util;
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.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.serverbound.inventory.ServerboundPickItemPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
@ -52,6 +57,7 @@ import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.ItemMapping;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@ -348,4 +354,115 @@ public class InventoryUtils {
default -> 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<GeyserItemStack> 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<GeyserItemStack> 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;
}
}