3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-12-25 07:40:10 +01:00

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.
Dieser Commit ist enthalten in:
Camotoy 2022-01-30 11:15:07 -05:00
Ursprung d0fa2d2b05
Commit 2d28ba0cb5
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 7EEFB66FE798081F
15 geänderte Dateien mit 364 neuen und 265 gelöschten Zeilen

Datei anzeigen

@ -25,6 +25,8 @@
package org.geysermc.geyser; package org.geysermc.geyser;
import javax.annotation.Nullable;
public interface GeyserLogger { public interface GeyserLogger {
/** /**
@ -78,6 +80,15 @@ public interface GeyserLogger {
*/ */
void debug(String message); 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 * Sets if the logger should print debug messages
* *

Datei anzeigen

@ -27,11 +27,12 @@ package org.geysermc.geyser.inventory;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.jetbrains.annotations.Range; import org.jetbrains.annotations.Range;
import javax.annotation.Nonnull;
/** /**
* Combination of {@link Inventory} and {@link PlayerInventory} * Combination of {@link Inventory} and {@link PlayerInventory}
*/ */
@ -66,7 +67,7 @@ public class Container extends Inventory {
} }
@Override @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) { if (slot < this.size) {
super.setItem(slot, newItem, session); super.setItem(slot, newItem, session);
} else { } else {

Datei anzeigen

@ -31,7 +31,6 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.Tag; import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.math.vector.Vector3i; import com.nukkitx.math.vector.Vector3i;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.geysermc.geyser.GeyserImpl; 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.geysermc.geyser.translator.inventory.item.ItemTranslator;
import org.jetbrains.annotations.Range; import org.jetbrains.annotations.Range;
import javax.annotation.Nonnull;
import java.util.Arrays; import java.util.Arrays;
@ToString @ToString
public abstract class Inventory { public abstract class Inventory {
@Getter @Getter
protected final int id; protected final int id;
@ -72,8 +71,7 @@ public abstract class Inventory {
protected final ContainerType containerType; protected final ContainerType containerType;
@Getter @Getter
@Setter protected final String title;
protected String title;
protected final GeyserItemStack[] items; protected final GeyserItemStack[] items;
@ -115,7 +113,7 @@ public abstract class Inventory {
public abstract int getOffsetForHotbar(@Range(from = 0, to = 8) int slot); 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) { if (slot > this.size) {
session.getGeyser().getLogger().debug("Tried to set an item out of bounds! " + this); session.getGeyser().getLogger().debug("Tried to set an item out of bounds! " + this);
return; return;

Datei anzeigen

@ -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.entity.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerActionType; 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.ContainerType;
import com.github.steveice10.mc.protocol.data.game.inventory.MoveToHotbarAction;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 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.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator; import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator;
import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.geyser.util.InventoryUtils;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
public class ClickPlan { public final class ClickPlan {
private final List<ClickAction> plan = new ArrayList<>(); private final List<ClickAction> plan = new ArrayList<>();
private final Int2ObjectMap<GeyserItemStack> simulatedItems; 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 GeyserItemStack simulatedCursor;
private boolean simulating; private boolean finished;
private final GeyserSession session; private final GeyserSession session;
private final InventoryTranslator translator; private final InventoryTranslator translator;
@ -66,21 +67,11 @@ public class ClickPlan {
this.inventory = inventory; this.inventory = inventory;
this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize()); this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
this.changedItems = null;
this.simulatedCursor = session.getPlayerInventory().getCursor().copy(); this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
this.simulating = true; this.finished = false;
if (translator instanceof PlayerInventoryTranslator) { gridSize = translator.getGridSize();
gridSize = 4;
} else if (translator instanceof CraftingInventoryTranslator) {
gridSize = 9;
} else {
gridSize = -1;
}
}
private void resetSimulation() {
this.simulatedItems.clear();
this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
} }
public void add(Click click, int slot) { public void add(Click click, int slot) {
@ -88,7 +79,7 @@ public class ClickPlan {
} }
public void add(Click click, int slot, boolean force) { public void add(Click click, int slot, boolean force) {
if (!simulating) if (finished)
throw new UnsupportedOperationException("ClickPlan already executed"); throw new UnsupportedOperationException("ClickPlan already executed");
if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) { if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) {
@ -97,12 +88,10 @@ public class ClickPlan {
ClickAction action = new ClickAction(click, slot, force); ClickAction action = new ClickAction(click, slot, force);
plan.add(action); plan.add(action);
simulateAction(action);
} }
public void execute(boolean refresh) { public void execute(boolean refresh) {
//update geyser inventory after simulation to avoid net id desync //update geyser inventory after simulation to avoid net id desync
resetSimulation();
ListIterator<ClickAction> planIter = plan.listIterator(); ListIterator<ClickAction> planIter = plan.listIterator();
while (planIter.hasNext()) { while (planIter.hasNext()) {
ClickAction action = planIter.next(); ClickAction action = planIter.next();
@ -112,33 +101,48 @@ public class ClickPlan {
refresh = true; 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; ItemStack clickedItemStack;
if (!planIter.hasNext() && refresh) { if (!planIter.hasNext() && refresh) {
clickedItemStack = InventoryUtils.REFRESH_ITEM; clickedItemStack = InventoryUtils.REFRESH_ITEM;
} else if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) { } else {
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; clickedItemStack = null;
} else { } 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(); clickedItemStack = getItem(action.slot).getItemStack();
} }
}
}
if (!emulatePost1_16Logic) {
simulateAction(action);
}
ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket( ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket(
inventory.getId(), inventory.getId(),
inventory.getStateId(), stateId,
action.slot, action.slot,
action.click.actionType, action.click.actionType,
action.click.action, action.click.action,
clickedItemStack, clickedItemStack,
Collections.emptyMap() // Anything else we change, at this time, should have a packet sent to address changedItems
); );
simulateAction(action);
session.sendDownstreamPacket(clickPacket); session.sendDownstreamPacket(clickPacket);
} }
@ -146,19 +150,11 @@ public class ClickPlan {
for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) { for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session); inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
} }
simulating = false; finished = true;
} }
public GeyserItemStack getItem(int slot) { 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()); return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
} else {
return simulatedItems.getOrDefault(slot, inventory.getItem(slot));
}
} }
public GeyserItemStack getCursor() { public GeyserItemStack getCursor() {
@ -166,23 +162,38 @@ public class ClickPlan {
} }
private void setItem(int slot, GeyserItemStack item) { private void setItem(int slot, GeyserItemStack item) {
if (simulating) {
simulatedItems.put(slot, item); simulatedItems.put(slot, item);
} else { onSlotItemChange(slot, item);
inventory.setItem(slot, item, session);
}
} }
private void setCursor(GeyserItemStack item) { private void setCursor(GeyserItemStack item) {
if (simulating) {
simulatedCursor = item; simulatedCursor = item;
} else {
session.getPlayerInventory().setCursor(item, session);
} }
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) { private void simulateAction(ClickAction action) {
GeyserItemStack cursor = simulating ? getCursor() : session.getPlayerInventory().getCursor(); GeyserItemStack cursor = getCursor();
switch (action.click) { switch (action.click) {
case LEFT_OUTSIDE -> { case LEFT_OUTSIDE -> {
setCursor(GeyserItemStack.EMPTY); 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) { if (translator.getSlotType(action.slot) == SlotType.OUTPUT) {
switch (action.click) { switch (action.click) {
case LEFT, RIGHT -> { case LEFT, RIGHT -> {
@ -206,6 +217,7 @@ public class ClickPlan {
cursor.add(clicked.getAmount()); cursor.add(clicked.getAmount());
} }
reduceCraftingGrid(false); reduceCraftingGrid(false);
setItem(action.slot, GeyserItemStack.EMPTY); // Matches Java Edition 1.18.1
} }
case LEFT_SHIFT -> reduceCraftingGrid(true); case LEFT_SHIFT -> reduceCraftingGrid(true);
} }
@ -217,20 +229,20 @@ public class ClickPlan {
setItem(action.slot, cursor); setItem(action.slot, cursor);
} else { } else {
setCursor(GeyserItemStack.EMPTY); setCursor(GeyserItemStack.EMPTY);
clicked.add(cursor.getAmount()); add(action.slot, clicked, cursor.getAmount());
} }
break; break;
case RIGHT: case RIGHT:
if (cursor.isEmpty() && !clicked.isEmpty()) { if (cursor.isEmpty() && !clicked.isEmpty()) {
int half = clicked.getAmount() / 2; //smaller half int half = clicked.getAmount() / 2; //smaller half
setCursor(clicked.copy(clicked.getAmount() - half)); //larger half setCursor(clicked.copy(clicked.getAmount() - half)); //larger half
clicked.setAmount(half); setAmount(action.slot, clicked, half);
} else if (!cursor.isEmpty() && clicked.isEmpty()) { } else if (!cursor.isEmpty() && clicked.isEmpty()) {
cursor.sub(1); cursor.sub(1);
setItem(action.slot, cursor.copy(1)); setItem(action.slot, cursor.copy(1));
} else if (InventoryUtils.canStack(cursor, clicked)) { } else if (InventoryUtils.canStack(cursor, clicked)) {
cursor.sub(1); cursor.sub(1);
clicked.add(1); add(action.slot, clicked, 1);
} }
break; break;
case SWAP_TO_HOTBAR_1: case SWAP_TO_HOTBAR_1:
@ -265,7 +277,7 @@ public class ClickPlan {
break; break;
case DROP_ONE: case DROP_ONE:
if (!clicked.isEmpty()) { if (!clicked.isEmpty()) {
clicked.sub(1); sub(action.slot, clicked, 1);
} }
break; break;
case DROP_ALL: 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} * 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) { 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(sourceSlot, destinationItem);
setItem(destSlot, sourceItem); setItem(destSlot, sourceItem);
} }
@ -292,63 +304,44 @@ public class ClickPlan {
stateId = inventory.getStateId(); 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 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 // - Bedrock is sending an item directly from one slot to another, without picking it up, that cannot
// be expressed with a shift click // be expressed with a shift click
// - Bedrock wants to pick up or place an arbitrary amount of items that cannot be expressed from // - Bedrock wants to pick up or place an arbitrary amount of items that cannot be expressed from
// one left/right click action. // one left/right click action.
// When Bedrock does one of these actions and sends multiple packets, a 1.17.1+ server will // Java typically doesn't increment the state ID if you send a vanilla-accurate container click packet,
// increment the state ID on each confirmation packet it sends back (I.E. set slot). Then when it // but it will increment the state ID with a vanilla client in at least the crafting table
// 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)
if (inventory.getContainerType() == ContainerType.CRAFTING && CraftingInventoryTranslator.isCraftingGrid(action.slot)) { 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 // 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) // 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; int stateIdIncrements;
GeyserItemStack clicked = getItem(action.slot); GeyserItemStack clicked = getItem(action.slot);
if (action.click == Click.LEFT) { if (action.click == Click.LEFT) {
if (!clicked.isEmpty() && !InventoryUtils.canStack(simulatedCursor, clicked)) { if (!clicked.isEmpty() && !InventoryUtils.canStack(simulatedCursor, clicked)) {
// An item is removed from the crafting table; yes deletion // An item is removed from the crafting table; yes deletion
stateIdIncrements = 3; stateIdIncrements = 2;
} else { } else {
// We can stack and we add all the items to the crafting slot; no deletion // 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) { } else if (action.click == Click.RIGHT) {
if (simulatedCursor.isEmpty() && !clicked.isEmpty()) { stateIdIncrements = 1;
// Items are taken; yes deletion } else if (action.click.actionType == ContainerActionType.MOVE_TO_HOTBAR_SLOT) {
stateIdIncrements = 3; stateIdIncrements = 1;
} 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;
}
} else { } else {
if (session.getGeyser().getConfig().isDebugMode()) { if (session.getGeyser().getConfig().isDebugMode()) {
session.getGeyser().getLogger().debug("Not sure how to handle state ID hack in crafting table: " + plan); session.getGeyser().getLogger().debug("Not sure how to handle state ID hack in crafting table: " + plan);
} }
stateIdIncrements = 2; stateIdIncrements = 1;
} }
inventory.incrementStateId(stateIdIncrements); inventory.incrementStateId(stateIdIncrements);
} else if (action.click.action instanceof MoveToHotbarAction) {
// Two slot changes sent
inventory.incrementStateId(2);
} else {
inventory.incrementStateId(1);
} }
return stateId; return stateId;
} }
//TODO
private void reduceCraftingGrid(boolean makeAll) { private void reduceCraftingGrid(boolean makeAll) {
if (gridSize == -1) if (gridSize == -1)
return; return;
@ -370,9 +363,12 @@ public class ClickPlan {
} }
for (int i = 0; i < gridSize; i++) { for (int i = 0; i < gridSize; i++) {
GeyserItemStack item = getItem(i + 1); final int slot = i + 1;
if (!item.isEmpty()) GeyserItemStack item = getItem(slot);
item.sub(crafted); if (!item.isEmpty()) {
// These changes should be broadcasted to the server
sub(slot, item, crafted);
}
} }
} }

Datei anzeigen

@ -361,6 +361,15 @@ public class GeyserSession implements GeyserConnection, CommandSender {
@Setter @Setter
private Int2ObjectMap<IntList> stonecutterRecipes; 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. * 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. * Setting a default fixes cooldowns not showing up on a fresh world.

Datei anzeigen

@ -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.data.inventory.stackrequestactions.StackRequestActionType;
import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket; import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; 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.BeaconContainer;
import org.geysermc.geyser.inventory.BedrockContainerSlot;
import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory; 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.holder.BlockInventoryHolder;
import org.geysermc.geyser.inventory.updater.UIInventoryUpdater; import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.geyser.util.InventoryUtils;
import java.util.Collections;
public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator { public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator {
public BeaconInventoryTranslator() { public BeaconInventoryTranslator() {
super(1, new BlockInventoryHolder("minecraft:beacon", com.nukkitx.protocol.bedrock.data.inventory.ContainerType.BEACON) { 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]; BeaconPaymentStackRequestActionData beaconPayment = (BeaconPaymentStackRequestActionData) request.getActions()[0];
ServerboundSetBeaconPacket packet = new ServerboundSetBeaconPacket(beaconPayment.getPrimaryEffect(), beaconPayment.getSecondaryEffect()); ServerboundSetBeaconPacket packet = new ServerboundSetBeaconPacket(beaconPayment.getPrimaryEffect(), beaconPayment.getSecondaryEffect());
session.sendDownstreamPacket(packet); session.sendDownstreamPacket(packet);
return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet())); return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
} }
@Override @Override

Datei anzeigen

@ -37,6 +37,11 @@ public class CraftingInventoryTranslator extends AbstractBlockInventoryTranslato
super(10, "minecraft:crafting_table", ContainerType.WORKBENCH, UIInventoryUpdater.INSTANCE); super(10, "minecraft:crafting_table", ContainerType.WORKBENCH, UIInventoryUpdater.INSTANCE);
} }
@Override
public int getGridSize() {
return 9;
}
@Override @Override
public SlotType getSlotType(int javaSlot) { public SlotType getSlotType(int javaSlot) {
if (javaSlot == 0) { if (javaSlot == 0) {

Datei anzeigen

@ -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.data.game.inventory.ContainerType;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerButtonClickPacket; 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.CraftRecipeStackRequestActionData;
import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData; import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData;
import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType; import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType;
import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
import com.nukkitx.protocol.bedrock.packet.PlayerEnchantOptionsPacket; import com.nukkitx.protocol.bedrock.packet.PlayerEnchantOptionsPacket;
import org.geysermc.geyser.inventory.EnchantingContainer; import it.unimi.dsi.fastutil.ints.IntSets;
import org.geysermc.geyser.inventory.GeyserEnchantOption; import org.geysermc.geyser.inventory.*;
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 org.geysermc.geyser.inventory.item.Enchantment; 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.Arrays;
import java.util.Collections;
public class EnchantingInventoryTranslator extends AbstractBlockInventoryTranslator { public class EnchantingInventoryTranslator extends AbstractBlockInventoryTranslator {
public EnchantingInventoryTranslator() { public EnchantingInventoryTranslator() {
@ -130,7 +129,7 @@ public class EnchantingInventoryTranslator extends AbstractBlockInventoryTransla
} }
ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getId(), javaSlot); ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getId(), javaSlot);
session.sendDownstreamPacket(packet); session.sendDownstreamPacket(packet);
return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet())); return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
} }
@Override @Override

Datei anzeigen

@ -26,12 +26,11 @@
package org.geysermc.geyser.translator.inventory; 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.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.Ingredient;
import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; 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.ShapedRecipeData;
import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; 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.IntTag;
import com.github.steveice10.opennbt.tag.builtin.Tag; import com.github.steveice10.opennbt.tag.builtin.Tag;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
@ -43,15 +42,10 @@ import it.unimi.dsi.fastutil.ints.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.CartographyContainer; import org.geysermc.geyser.inventory.*;
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.click.Click; import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.inventory.click.ClickPlan; 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.DoubleChestInventoryTranslator;
import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator; import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator;
import org.geysermc.geyser.translator.inventory.furnace.BlastFurnaceInventoryTranslator; import org.geysermc.geyser.translator.inventory.furnace.BlastFurnaceInventoryTranslator;
@ -119,6 +113,13 @@ public abstract class InventoryTranslator {
public abstract SlotType getSlotType(int javaSlot); public abstract SlotType getSlotType(int javaSlot);
public abstract Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory); 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. * 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. * 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); 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; boolean refresh = false;
ItemStackResponsePacket responsePacket = new ItemStackResponsePacket(); ItemStackResponsePacket responsePacket = new ItemStackResponsePacket();
for (ItemStackRequest request : requests) { for (ItemStackRequest request : requests) {
@ -199,10 +200,6 @@ public abstract class InventoryTranslator {
case PLACE: { case PLACE: {
TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action; TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action;
if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) { 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()) { if (session.getGeyser().getConfig().isDebugMode()) {
session.getGeyser().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.name()); session.getGeyser().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.name());
dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination()); dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination());
@ -212,17 +209,19 @@ public abstract class InventoryTranslator {
int sourceSlot = bedrockSlotToJava(transferAction.getSource()); int sourceSlot = bedrockSlotToJava(transferAction.getSource());
int destSlot = bedrockSlotToJava(transferAction.getDestination()); int destSlot = bedrockSlotToJava(transferAction.getDestination());
boolean isSourceCursor = isCursor(transferAction.getSource());
boolean isDestCursor = isCursor(transferAction.getDestination());
if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(), if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(),
isCursor(transferAction.getSource()) ? -1 : sourceSlot, isSourceCursor ? -1 : sourceSlot,
transferAction.getDestination().getContainer(), isCursor(transferAction.getDestination()) ? -1 : destSlot)) { transferAction.getDestination().getContainer(), isDestCursor ? -1 : destSlot)) {
// This item would not be here in Java // This item would not be here in Java
return rejectRequest(request, false); return rejectRequest(request, false);
} }
if (isCursor(transferAction.getSource()) && isCursor(transferAction.getDestination())) { //??? if (isSourceCursor && isDestCursor) { //???
return rejectRequest(request); return rejectRequest(request);
} else if (isCursor(transferAction.getSource())) { //releasing cursor } else if (isSourceCursor) { //releasing cursor
int sourceAmount = cursor.getAmount(); int sourceAmount = cursor.getAmount();
if (transferAction.getCount() == sourceAmount) { //release all if (transferAction.getCount() == sourceAmount) { //release all
plan.add(Click.LEFT, destSlot); plan.add(Click.LEFT, destSlot);
@ -231,7 +230,7 @@ public abstract class InventoryTranslator {
plan.add(Click.RIGHT, destSlot); 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); GeyserItemStack sourceItem = plan.getItem(sourceSlot);
int sourceAmount = sourceItem.getAmount(); int sourceAmount = sourceItem.getAmount();
if (cursor.isEmpty()) { //picking up into empty cursor if (cursor.isEmpty()) { //picking up into empty cursor
@ -431,6 +430,8 @@ public abstract class InventoryTranslator {
int leftover = 0; int leftover = 0;
ClickPlan plan = new ClickPlan(session, this, inventory); 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()) { for (StackRequestActionData action : request.getActions()) {
switch (action.getType()) { switch (action.getType()) {
case CRAFT_RECIPE: { case CRAFT_RECIPE: {
@ -462,6 +463,7 @@ public abstract class InventoryTranslator {
return rejectRequest(request); return rejectRequest(request);
} }
craftState = CraftState.INGREDIENTS; craftState = CraftState.INGREDIENTS;
affectedSlots.add(bedrockSlotToJava(((ConsumeStackRequestActionData) action).getSource()));
break; break;
} }
case TAKE: case TAKE:
@ -522,21 +524,16 @@ public abstract class InventoryTranslator {
} }
} }
plan.execute(false); 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) { public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
int gridSize; final int gridSize = getGridSize();
int gridDimensions; if (gridSize == -1) {
if (this instanceof PlayerInventoryTranslator) {
gridSize = 4;
gridDimensions = 2;
} else if (this instanceof CraftingInventoryTranslator) {
gridSize = 9;
gridDimensions = 3;
} else {
return rejectRequest(request); return rejectRequest(request);
} }
int gridDimensions = gridSize == 4 ? 2 : 3;
Recipe recipe; Recipe recipe;
Ingredient[] ingredients = new Ingredient[0]; Ingredient[] ingredients = new Ingredient[0];
@ -722,7 +719,7 @@ public abstract class InventoryTranslator {
/** /**
* Handled in {@link PlayerInventoryTranslator} * 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); 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); return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.OK, request.getRequestId(), containerEntries);
} }
/** /**
* Reject an incorrect ItemStackRequest. * Reject an incorrect ItemStackRequest.
*/ */
public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) { protected static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) {
return rejectRequest(request, true); 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 * @param throwError whether this request was truly erroneous (true), or known as an outcome and should not be treated
* as bad (false). * 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()) { if (throwError && GeyserImpl.getInstance().getConfig().isDebugMode()) {
new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace(); new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace();
} }
@ -849,9 +846,12 @@ public abstract class InventoryTranslator {
return -1; 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<>(); 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); BedrockContainerSlot bedrockSlot = javaSlotToBedrockContainer(slot);
List<ItemStackResponsePacket.ItemEntry> list = containerMap.computeIfAbsent(bedrockSlot.container(), k -> new ArrayList<>()); List<ItemStackResponsePacket.ItemEntry> list = containerMap.computeIfAbsent(bedrockSlot.container(), k -> new ArrayList<>());
list.add(makeItemEntry(session, bedrockSlot.slot(), inventory.getItem(slot))); list.add(makeItemEntry(session, bedrockSlot.slot(), inventory.getItem(slot)));
@ -868,7 +868,7 @@ public abstract class InventoryTranslator {
return containerEntries; 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; ItemStackResponsePacket.ItemEntry itemEntry;
if (!itemStack.isEmpty()) { if (!itemStack.isEmpty()) {
// As of 1.16.210: Bedrock needs confirmation on what the current item durability is. // As of 1.16.210: Bedrock needs confirmation on what the current item durability is.

Datei anzeigen

@ -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.InventoryContentPacket;
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket; 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.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
import org.geysermc.geyser.inventory.*; import org.geysermc.geyser.inventory.*;
@ -55,6 +56,11 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
super(46); super(46);
} }
@Override
public int getGridSize() {
return 4;
}
@Override @Override
public void updateInventory(GeyserSession session, Inventory inventory) { public void updateInventory(GeyserSession session, Inventory inventory) {
updateCraftingGrid(session, 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); sendCreativeAction(session, inventory, slot);
} }
return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
} }
@Override @Override
public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { protected ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
ItemStack javaCreativeItem = null; ItemStack javaCreativeItem = null;
IntSet affectedSlots = new IntOpenHashSet(); IntSet affectedSlots = new IntOpenHashSet();
CraftState craftState = CraftState.START; CraftState craftState = CraftState.START;
@ -478,7 +487,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
return rejectRequest(request); 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); sendCreativeAction(session, inventory, slot);
} }
return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));

Datei anzeigen

@ -25,6 +25,7 @@
package org.geysermc.geyser.translator.protocol.bedrock; 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.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; 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.LevelEventType;
import com.nukkitx.protocol.bedrock.data.inventory.*; import com.nukkitx.protocol.bedrock.data.inventory.*;
import com.nukkitx.protocol.bedrock.packet.*; 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.EntityDefinitions;
import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity; import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity;
import org.geysermc.geyser.entity.type.Entity; 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.BlockUtils;
import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.geyser.util.InventoryUtils;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -316,9 +318,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
playerInventory.setItem(armorSlot, hotbarItem, session); playerInventory.setItem(armorSlot, hotbarItem, session);
playerInventory.setItem(bedrockHotbarSlot, armorSlotItem, 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( ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket(
playerInventory.getId(), playerInventory.getStateId(), armorSlot, playerInventory.getId(), playerInventory.getStateId(), armorSlot,
click.actionType, click.action, null, Collections.emptyMap()); click.actionType, click.action, null, changedSlots);
session.sendDownstreamPacket(clickPacket); session.sendDownstreamPacket(clickPacket);
} }
} else { } else {

Datei anzeigen

@ -31,7 +31,7 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator; 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) * 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 @Override
public void translate(GeyserSession session, ClientboundRecipePacket packet) { public void translate(GeyserSession session, ClientboundRecipePacket packet) {
if (packet.getAction() == UnlockRecipesAction.REMOVE) { if (packet.getAction() == UnlockRecipesAction.REMOVE) {
session.getUnlockedRecipes().removeAll(Arrays.asList(packet.getRecipes())); for (String identifier : packet.getRecipes()) {
session.getUnlockedRecipes().remove(identifier);
}
} else { } else {
session.getUnlockedRecipes().addAll(Arrays.asList(packet.getRecipes())); Collections.addAll(session.getUnlockedRecipes(), packet.getRecipes());
} }
} }
} }

Datei anzeigen

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

Datei anzeigen

@ -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.Recipe;
import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType; 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.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.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetSlotPacket;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.data.inventory.CraftingData; 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.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.session.GeyserSession; 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.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator; import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator; 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 org.geysermc.geyser.util.InventoryUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -72,14 +69,16 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
return; return;
// Intentional behavior here below the cursor; Minecraft 1.18.1 also does this. // 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(); InventoryTranslator translator = session.getInventoryTranslator();
if (translator != null) { if (translator != null) {
if (session.getCraftingGridFuture() != null) { if (session.getCraftingGridFuture() != null) {
session.getCraftingGridFuture().cancel(false); 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()); GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) { 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) { * Checks for a changed output slot in the crafting grid, and ensures Bedrock sees the recipe.
int gridSize; */
if (translator instanceof PlayerInventoryTranslator) { private static void updateCraftingGrid(GeyserSession session, int slot, ItemStack item, Inventory inventory, InventoryTranslator translator) {
gridSize = 4; if (slot != 0) {
} else if (translator instanceof CraftingInventoryTranslator) { return;
gridSize = 9; }
} else { int gridSize = translator.getGridSize();
if (gridSize == -1) {
return; return;
} }
if (packet.getItem() == null || packet.getItem().getId() == 0) { if (item == null || item.getId() == 0) {
return; return;
} }
session.setCraftingGridFuture(session.scheduleInEventLoop(() -> {
int offset = gridSize == 4 ? 28 : 32; int offset = gridSize == 4 ? 28 : 32;
int gridDimensions = gridSize == 4 ? 2 : 3; int gridDimensions = gridSize == 4 ? 2 : 3;
int firstRow = -1, height = -1; int firstRow = -1, height = -1;
@ -135,62 +136,10 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
height += -firstRow + 1; height += -firstRow + 1;
width += -firstCol + 1; width += -firstCol + 1;
recipes: if (InventoryUtils.getValidRecipe(session, item, inventory::getItem, gridDimensions, firstRow,
for (Recipe recipe : session.getCraftingRecipes().values()) { height, firstCol, width) != null) {
if (recipe.getType() == RecipeType.CRAFTING_SHAPED) { // Recipe is already present on the client; don't send packet
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; 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;
}
} }
UUID uuid = UUID.randomUUID(); 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 // Cache this recipe so we know the client has received it
session.getCraftingRecipes().put(newRecipeId, new Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data)); session.getCraftingRecipes().put(newRecipeId, new Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data));
@ -226,7 +175,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
width, width,
height, height,
Arrays.asList(ingredients), Arrays.asList(ingredients),
Collections.singletonList(ItemTranslator.translateToBedrock(session, packet.getItem())), Collections.singletonList(ItemTranslator.translateToBedrock(session, item)),
uuid, uuid,
"crafting_table", "crafting_table",
0, 0,
@ -246,33 +195,6 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
index++; index++;
} }
} }
} }, 150, TimeUnit.MILLISECONDS));
}
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;
} }
} }

Datei anzeigen

@ -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.metadata.ItemStack;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; 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.ServerboundPickItemPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag; 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 org.geysermc.geyser.registry.type.ItemMapping;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -348,4 +354,115 @@ public class InventoryUtils {
default -> null; 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;
}
} }