From 12e93ba1d83d1300e2cf06005680479bee86d0c7 Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Mon, 11 Nov 2024 14:28:17 -0500
Subject: [PATCH 1/4] Bundle support
---
.../geyser/inventory/GeyserItemStack.java | 64 +-
.../geysermc/geyser/inventory/Inventory.java | 8 +-
.../geyser/inventory/click/Click.java | 3 +
.../geyser/inventory/click/ClickPlan.java | 108 ++-
.../geysermc/geyser/item/type/BundleItem.java | 32 +
.../geyser/session/GeyserSession.java | 3 +
.../geyser/session/cache/BundleCache.java | 350 +++++++
.../geyser/session/cache/tags/ItemTag.java | 1 +
.../inventory/BundleInventoryTranslator.java | 333 +++++++
.../inventory/InventoryTranslator.java | 27 +-
.../inventory/PlayerInventoryTranslator.java | 9 +
.../JavaContainerSetContentTranslator.java | 5 +-
.../JavaContainerSetSlotTranslator.java | 1 +
.../JavaSetCursorItemTranslator.java | 1 +
.../JavaSetPlayerInventoryTranslator.java | 4 +-
.../geysermc/geyser/util/InventoryUtils.java | 1 +
.../geyser/util/thirdparty/Fraction.java | 911 ++++++++++++++++++
17 files changed, 1840 insertions(+), 21 deletions(-)
create mode 100644 core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java
create mode 100644 core/src/main/java/org/geysermc/geyser/session/cache/BundleCache.java
create mode 100644 core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
create mode 100644 core/src/main/java/org/geysermc/geyser/util/thirdparty/Fraction.java
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
index 256de779942..5e1fb844762 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
@@ -25,7 +25,11 @@
package org.geysermc.geyser.inventory;
-import lombok.*;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
@@ -35,6 +39,7 @@
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.session.cache.BundleCache;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
@@ -49,25 +54,30 @@
@Data
public class GeyserItemStack {
public static final GeyserItemStack EMPTY = new GeyserItemStack(Items.AIR_ID, 0, null);
+ public static final int UNSET_NET_ID = 1;
private final int javaId;
private int amount;
private DataComponents components;
private int netId;
+ @EqualsAndHashCode.Exclude
+ private BundleCache.BundleData bundleData;
+
@Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private Item item;
private GeyserItemStack(int javaId, int amount, DataComponents components) {
- this(javaId, amount, components, 1);
+ this(javaId, amount, components, 1, null);
}
- private GeyserItemStack(int javaId, int amount, DataComponents components, int netId) {
+ private GeyserItemStack(int javaId, int amount, DataComponents components, int netId, BundleCache.BundleData bundleData) {
this.javaId = javaId;
this.amount = amount;
this.components = components;
this.netId = netId;
+ this.bundleData = bundleData;
}
public static @NonNull GeyserItemStack of(int javaId, int amount) {
@@ -148,10 +158,35 @@ public int getComponent(@NonNull DataComponentType type,
return def;
}
+// public boolean hasComponent(DataComponentType> type) {
+// if (components == null) {
+// return false;
+// }
+// return components.getDataComponents().containsKey(type);
+// }
+
public int getNetId() {
return isEmpty() ? 0 : netId;
}
+ public int getBundleId() {
+ if (isEmpty()) {
+ return -1;
+ }
+
+ return bundleData == null ? -1 : bundleData.bundleId();
+ }
+
+ public void mergeBundleData(GeyserSession session, BundleCache.BundleData oldBundleData) {
+ if (oldBundleData != null && this.bundleData != null) {
+ // Old bundle; re-use old IDs
+ this.bundleData.updateNetIds(session, oldBundleData);
+ } else if (this.bundleData != null) {
+ // New bundle; allocate new ID
+ session.getBundleCache().markNewBundle(this.bundleData);
+ }
+ }
+
public void add(int add) {
amount += add;
}
@@ -165,6 +200,24 @@ public ItemStack getItemStack() {
}
public @Nullable ItemStack getItemStack(int newAmount) {
+ if (isEmpty()) {
+ return null;
+ }
+ // Sync our updated bundle data to server, if applicable
+ // Not fresh from server? Then we have changes to apply!~
+ if (bundleData != null && !bundleData.freshFromServer()) {
+ if (!bundleData.contents().isEmpty()) {
+ if (components == null) {
+ components = new DataComponents(new HashMap<>());
+ }
+ components.put(DataComponentType.BUNDLE_CONTENTS, bundleData.toComponent());
+ } else {
+ if (components != null) {
+ // Empty list = no component = should delete
+ components.getDataComponents().remove(DataComponentType.BUNDLE_CONTENTS);
+ }
+ }
+ }
return isEmpty() ? null : new ItemStack(javaId, newAmount, components);
}
@@ -175,7 +228,8 @@ public ItemData getItemData(GeyserSession session) {
ItemData.Builder itemData = ItemTranslator.translateToBedrock(session, javaId, amount, components);
itemData.netId(getNetId());
itemData.usingNetId(true);
- return itemData.build();
+
+ return session.getBundleCache().checkForBundle(this, itemData);
}
public ItemMapping getMapping(GeyserSession session) {
@@ -208,6 +262,6 @@ public GeyserItemStack copy() {
}
public GeyserItemStack copy(int newAmount) {
- return isEmpty() ? EMPTY : new GeyserItemStack(javaId, newAmount, components == null ? null : components.clone(), netId);
+ return isEmpty() ? EMPTY : new GeyserItemStack(javaId, newAmount, components == null ? null : components.clone(), netId, bundleData == null ? null : bundleData.copy());
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
index 09d04f17c79..c960ed1a204 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
@@ -142,15 +142,21 @@ public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession se
}
}
- protected void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) {
+ public static void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) {
if (!newItem.isEmpty()) {
ItemDefinition oldMapping = ItemTranslator.getBedrockItemDefinition(session, oldItem);
ItemDefinition newMapping = ItemTranslator.getBedrockItemDefinition(session, newItem);
if (oldMapping.equals(newMapping)) {
newItem.setNetId(oldItem.getNetId());
+ newItem.mergeBundleData(session, oldItem.getBundleData());
} else {
newItem.setNetId(session.getNextItemNetId());
+ session.getBundleCache().markNewBundle(newItem.getBundleData());
+ session.getBundleCache().onOldItemDelete(oldItem);
}
+ } else {
+ // Empty item means no more bundle if one existed.
+ session.getBundleCache().onOldItemDelete(oldItem);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java b/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java
index 6897786c139..cf16d0b6fdf 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/click/Click.java
@@ -31,7 +31,10 @@
@AllArgsConstructor
public enum Click {
LEFT(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK),
+ LEFT_BUNDLE(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK),
+ LEFT_BUNDLE_FROM_CURSOR(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK),
RIGHT(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK),
+ RIGHT_BUNDLE(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK),
LEFT_SHIFT(ContainerActionType.SHIFT_CLICK_ITEM, ShiftClickItemAction.LEFT_CLICK),
DROP_ONE(ContainerActionType.DROP_ITEM, DropItemAction.DROP_FROM_SELECTED),
DROP_ALL(ContainerActionType.DROP_ITEM, DropItemAction.DROP_SELECTED_STACK),
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
index 9d6f4d3e3dc..15c46aed0e0 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
@@ -25,19 +25,26 @@
package org.geysermc.geyser.inventory.click;
-import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
-import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType;
-import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
-import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction;
-import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
-import it.unimi.dsi.fastutil.ints.*;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.SlotType;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.inventory.BundleInventoryTranslator;
import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.util.InventoryUtils;
+import org.geysermc.geyser.util.thirdparty.Fraction;
+import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType;
+import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
+import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction;
+import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
+import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
+import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSelectBundleItemPacket;
+import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
import org.jetbrains.annotations.Contract;
import java.util.ArrayList;
@@ -52,7 +59,8 @@ public final class ClickPlan {
*/
private Int2ObjectMap changedItems;
private GeyserItemStack simulatedCursor;
- private boolean finished;
+ private int desiredBundleSlot;
+ private boolean executionBegan;
private final GeyserSession session;
private final InventoryTranslator translator;
@@ -67,7 +75,7 @@ public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventor
this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
this.changedItems = null;
this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
- this.finished = false;
+ this.executionBegan = false;
gridSize = translator.getGridSize();
}
@@ -82,7 +90,7 @@ public void add(Click click, int slot) {
}
public void add(Click click, int slot, boolean force) {
- if (finished)
+ if (executionBegan)
throw new UnsupportedOperationException("ClickPlan already executed");
if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) {
@@ -97,6 +105,7 @@ public void add(Click click, int slot, boolean force) {
}
public void execute(boolean refresh) {
+ executionBegan = true;
//update geyser inventory after simulation to avoid net id desync
resetSimulation();
ListIterator planIter = plan.listIterator();
@@ -159,7 +168,25 @@ public void execute(boolean refresh) {
for (Int2ObjectMap.Entry simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
}
- finished = true;
+ }
+
+ public void executeForCreativeMode() {
+ executionBegan = true;
+ //update geyser inventory after simulation to avoid net id desync
+ resetSimulation();
+ changedItems = new Int2ObjectOpenHashMap<>();
+ for (ClickAction action : plan) {
+ simulateAction(action);
+ }
+ session.getPlayerInventory().setCursor(simulatedCursor, session);
+ for (Int2ObjectMap.Entry simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
+ inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
+ }
+ for (Int2ObjectMap.Entry changedSlot : changedItems.int2ObjectEntrySet()) {
+ session.sendDownstreamGamePacket(
+ new ServerboundSetCreativeModeSlotPacket((short) changedSlot.getIntKey(), changedSlot.getValue())
+ );
+ }
}
public Inventory getInventory() {
@@ -187,6 +214,10 @@ public GeyserItemStack getItem(int slot) {
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
}
+ public void setDesiredBundleSlot(int desiredBundleSlot) {
+ this.desiredBundleSlot = desiredBundleSlot;
+ }
+
public GeyserItemStack getCursor() {
return simulatedCursor;
}
@@ -275,7 +306,59 @@ private void simulateAction(ClickAction action) {
} else if (InventoryUtils.canStack(cursor, clicked)) {
cursor.sub(1);
add(action.slot, clicked, 1);
+ } else {
+ // Can't stack, but both the cursor and the slot have an item
+ // (Called for bundles)
+ setCursor(clicked);
+ setItem(action.slot, cursor);
+ }
+ break;
+ case LEFT_BUNDLE:
+ Fraction bundleWeight = BundleInventoryTranslator.calculateBundleWeight(clicked.getBundleData().contents());
+ int amountToAddInBundle = Math.min(BundleInventoryTranslator.capacityForItemStack(bundleWeight, cursor), cursor.getAmount());
+ GeyserItemStack toInsertInBundle = cursor.copy(amountToAddInBundle);
+ if (executionBegan) {
+ clicked.getBundleData().contents().add(0, toInsertInBundle);
+ session.getBundleCache().onItemAdded(clicked); // Must be run before onSlotItemChange as the latter exports an ItemStack from the bundle
}
+ onSlotItemChange(action.slot, clicked);
+ cursor.sub(amountToAddInBundle);
+ break;
+ case LEFT_BUNDLE_FROM_CURSOR:
+ List contents = cursor.getBundleData().contents();
+ bundleWeight = BundleInventoryTranslator.calculateBundleWeight(contents);
+ amountToAddInBundle = Math.min(BundleInventoryTranslator.capacityForItemStack(bundleWeight, clicked), clicked.getAmount());
+ toInsertInBundle = clicked.copy(amountToAddInBundle);
+ if (executionBegan) {
+ cursor.getBundleData().contents().add(0, toInsertInBundle);
+ session.getBundleCache().onItemAdded(cursor);
+ }
+ sub(action.slot, clicked, amountToAddInBundle);
+ break;
+ case RIGHT_BUNDLE:
+ if (!cursor.isEmpty()) {
+ // Bundle should be in player's hand.
+ GeyserItemStack itemStack = cursor.getBundleData()
+ .contents()
+ .remove(0);
+ if (executionBegan) {
+ session.getBundleCache().onItemRemoved(cursor, 0);
+ }
+ setItem(action.slot, itemStack);
+ break;
+ }
+
+ if (executionBegan) {
+ sendSelectedBundleSlot(action.slot);
+ }
+ GeyserItemStack itemStack = clicked.getBundleData()
+ .contents()
+ .remove(desiredBundleSlot);
+ if (executionBegan) {
+ session.getBundleCache().onItemRemoved(clicked, desiredBundleSlot);
+ }
+ onSlotItemChange(action.slot, clicked);
+ setCursor(itemStack);
break;
case SWAP_TO_HOTBAR_1:
swap(action.slot, inventory.getOffsetForHotbar(0), clicked);
@@ -319,6 +402,11 @@ private void simulateAction(ClickAction action) {
}
}
+ private void sendSelectedBundleSlot(int slot) {
+ // Looks like this is also technically sent in creative mode.
+ session.sendDownstreamGamePacket(new ServerboundSelectBundleItemPacket(slot, desiredBundleSlot));
+ }
+
/**
* Swap between two inventory slots without a cursor. This should only be used with {@link ContainerActionType#MOVE_TO_HOTBAR_SLOT}
*/
diff --git a/core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java b/core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java
new file mode 100644
index 00000000000..9c06768b3d5
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.item.type;
+
+public class BundleItem extends Item {
+ public BundleItem(String javaIdentifier, Builder builder) {
+ super(javaIdentifier, builder);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index ef6261ead56..08cfe7f347c 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -159,6 +159,7 @@
import org.geysermc.geyser.session.auth.BedrockClientData;
import org.geysermc.geyser.session.cache.AdvancementsCache;
import org.geysermc.geyser.session.cache.BookEditCache;
+import org.geysermc.geyser.session.cache.BundleCache;
import org.geysermc.geyser.session.cache.ChunkCache;
import org.geysermc.geyser.session.cache.EntityCache;
import org.geysermc.geyser.session.cache.EntityEffectCache;
@@ -275,6 +276,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private final AdvancementsCache advancementsCache;
private final BookEditCache bookEditCache;
+ private final BundleCache bundleCache;
private final ChunkCache chunkCache;
private final EntityCache entityCache;
private final EntityEffectCache effectCache;
@@ -662,6 +664,7 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio
this.advancementsCache = new AdvancementsCache(this);
this.bookEditCache = new BookEditCache(this);
+ this.bundleCache = new BundleCache(this);
this.chunkCache = new ChunkCache(this);
this.entityCache = new EntityCache(this);
this.effectCache = new EntityEffectCache();
diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/BundleCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/BundleCache.java
new file mode 100644
index 00000000000..a57c9bf1a17
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/BundleCache.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.session.cache;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.nbt.NbtMapBuilder;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
+import org.cloudburstmc.protocol.bedrock.packet.ContainerRegistryCleanupPacket;
+import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
+import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
+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.session.cache.tags.ItemTag;
+import org.geysermc.geyser.util.InventoryUtils;
+import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class BundleCache {
+ private static final int BUNDLE_CONTAINER_ID = 125; // BDS 1.21.44
+ private final GeyserSession session;
+ private int nextBundleId;
+
+ public BundleCache(GeyserSession session) {
+ this.session = session;
+ }
+
+ /**
+ * Checks to see if the given item from the server is a bundle.
+ * If so, we initialize our bundle cache.
+ */
+ public void initialize(GeyserItemStack itemStack) {
+ if (session.getTagCache().is(ItemTag.BUNDLES, itemStack)) { // Can't check for BUNDLE_CONTENTS, which may be missing if the bundle is empty.
+ if (itemStack.getBundleData() != null) {
+ session.getGeyser().getLogger().warning("Stack has bundle data already! It should not!");
+ }
+
+ BundleData bundleData;
+ List rawContents = itemStack.getComponent(DataComponentType.BUNDLE_CONTENTS);
+ if (rawContents != null) {
+ // Use existing list and transform it to support net IDs
+ bundleData = new BundleData(session, rawContents);
+ } else {
+ // This is valid behavior (as of vanilla 1.21.2) if the bundle is empty.
+ // Create new list
+ bundleData = new BundleData();
+ }
+ itemStack.setBundleData(bundleData);
+ }
+ }
+
+ public void markNewBundle(@Nullable BundleData bundleData) {
+ if (bundleData == null) {
+ return;
+ }
+ if (bundleData.bundleId != -1) {
+ return;
+ }
+ bundleData.bundleId = nextBundleId++;
+ for (GeyserItemStack stack : bundleData.contents()) {
+ stack.setNetId(session.getNextItemNetId());
+ session.getBundleCache().markNewBundle(stack.getBundleData());
+ }
+ }
+
+ public ItemData checkForBundle(GeyserItemStack itemStack, ItemData.Builder itemData) {
+ if (itemStack.getBundleData() == null) {
+ return itemData.build();
+ }
+ // Not ideal, since Cloudburst NBT is immutable, but there isn't another ideal intersection between
+ // item instance tracking and item translation
+ // (Java just reads the contents of each item, while Bedrock kind of wants its own ID for each bundle item stack)
+ List contents = itemStack.getBundleData().contents();
+ int containerId = itemStack.getBundleId();
+
+ if (containerId == -1) {
+ session.getGeyser().getLogger().warning("Bundle ID should not be -1!");
+ }
+
+ NbtMap nbt = itemData.build().getTag();
+ NbtMapBuilder builder = nbt == null ? NbtMap.builder() : nbt.toBuilder();
+ builder.putInt("bundle_id", containerId);
+ itemData.tag(builder.build());
+
+ // Now that the tag is updated...
+ ItemData finalItem = itemData.build();
+
+ if (!itemStack.getBundleData().triggerFullContentsUpdate) {
+ // We are probably in the middle of updating one slot. Let's save bandwidth! :)
+ return finalItem;
+ }
+
+ // This is how BDS does it, so while it isn't pretty, it is accurate.
+ // Ensure that all bundle slots are cleared when we re-send data.
+ // Otherwise, if we don't indicate an item for a slot, Bedrock will think
+ // the old item still exists.
+ ItemData[] array = new ItemData[64];
+ Arrays.fill(array, ItemData.AIR);
+ List bedrockItems = Arrays.asList(array);
+ // Reverse order to ensure contents line up with Java.
+ int j = 0;
+ for (int i = contents.size() - 1; i >= 0; i--) {
+ // Ensure item data can be tracked
+ bedrockItems.set(j++, contents.get(i).getItemData(session));
+ }
+ InventoryContentPacket packet = new InventoryContentPacket();
+ packet.setContainerId(BUNDLE_CONTAINER_ID);
+ packet.setContents(bedrockItems);
+ packet.setContainerNameData(BundleCache.createContainer(containerId));
+ packet.setStorageItem(finalItem);
+ session.sendUpstreamPacket(packet);
+
+ return finalItem;
+ }
+
+ /*
+ * We need to send an InventorySlotPacket to the Bedrock client so it updates its changes and doesn't desync.
+ */
+
+ public void onItemAdded(GeyserItemStack bundle) {
+ BundleData data = bundle.getBundleData();
+ data.freshFromServer = false;
+ data.triggerFullContentsUpdate = false;
+
+ List contents = data.contents();
+ int bedrockSlot = platformConvertSlot(contents.size(), 0);
+ ItemData bedrockContent = contents.get(0).getItemData(session);
+
+ sendInventoryPacket(data.bundleId(), bedrockSlot, bedrockContent, bundle.getItemData(session));
+
+ data.triggerFullContentsUpdate = true;
+ }
+
+ public void onItemRemoved(GeyserItemStack bundle, int slot) {
+ // Whatever item used to be in here should have been removed *before* this was triggered.
+ BundleData data = bundle.getBundleData();
+ data.freshFromServer = false;
+ data.triggerFullContentsUpdate = false;
+
+ List contents = data.contents();
+ ItemData baseBundle = bundle.getItemData(session);
+ // This first slot is now blank!
+ sendInventoryPacket(data.bundleId(), platformConvertSlot(contents.size() + 1, 0), ItemData.AIR, baseBundle);
+ // Adjust the index of every item that came before this item.
+ for (int i = 0; i < slot; i++) {
+ sendInventoryPacket(data.bundleId(), platformConvertSlot(contents.size(), i),
+ contents.get(i).getItemData(session), baseBundle);
+ }
+
+ data.triggerFullContentsUpdate = true;
+ }
+
+ private void sendInventoryPacket(int bundleId, int bedrockSlot, ItemData bedrockContent, ItemData baseBundle) {
+ InventorySlotPacket packet = new InventorySlotPacket();
+ packet.setContainerId(BUNDLE_CONTAINER_ID);
+ packet.setItem(bedrockContent);
+ packet.setSlot(bedrockSlot);
+ packet.setContainerNameData(createContainer(bundleId));
+ packet.setStorageItem(baseBundle);
+ session.sendUpstreamPacket(packet);
+ }
+
+ /**
+ * If a bundle is no longer present in the working inventory, delete the cache
+ * from the client.
+ */
+ public void onOldItemDelete(GeyserItemStack itemStack) {
+ if (itemStack.getBundleId() != -1) {
+ // Clean up old container ID, to match BDS behavior.
+ ContainerRegistryCleanupPacket packet = new ContainerRegistryCleanupPacket();
+ packet.getContainers().add(createContainer(itemStack.getBundleId()));
+ session.sendUpstreamPacket(packet);
+ }
+ }
+
+ public void onInventoryClose(Inventory inventory) {
+ if (inventory instanceof PlayerInventory) {
+ // Don't bother; items are still here.
+ return;
+ }
+
+ for (int i = 0; i < inventory.getSize(); i++) {
+ GeyserItemStack item = inventory.getItem(i);
+ onOldItemDelete(item);
+ }
+ }
+
+ /**
+ * Bidirectional; works for both Bedrock and Java.
+ */
+ public static int platformConvertSlot(int contentsSize, int rawSlot) {
+ return contentsSize - rawSlot - 1;
+ }
+
+ public static FullContainerName createContainer(int id) {
+ return new FullContainerName(ContainerSlotType.DYNAMIC_CONTAINER, id);
+ }
+
+ /**
+ * Primarily exists to support net IDs within bundles.
+ * Important to prevent accidental item deletion in creative mode.
+ */
+ public static final class BundleData {
+ private final List contents;
+ /**
+ * Will be set to a positive integer after checking for existing bundle data.
+ */
+ private int bundleId = -1;
+ /**
+ * If false, blocks a complete InventoryContentPacket being sent to the server.
+ */
+ private boolean triggerFullContentsUpdate = true;
+ /**
+ * Sets whether data is accurate from the server; if so, any old bundle contents
+ * will be overwritten.
+ * This will be set to false if we are the most recent change-makers.
+ */
+ private boolean freshFromServer = true;
+
+ BundleData(GeyserSession session, List contents) {
+ this();
+ for (ItemStack content : contents) {
+ GeyserItemStack itemStack = GeyserItemStack.from(content);
+ // Check recursively
+ session.getBundleCache().initialize(itemStack);
+ this.contents.add(itemStack);
+ }
+ }
+
+ BundleData() {
+ this.contents = new ArrayList<>();
+ }
+
+ public int bundleId() {
+ return bundleId;
+ }
+
+ public List contents() {
+ return contents;
+ }
+
+ public boolean freshFromServer() {
+ return freshFromServer;
+ }
+
+ public List toComponent() {
+ List component = new ArrayList<>(this.contents.size());
+ for (GeyserItemStack content : this.contents) {
+ component.add(content.getItemStack());
+ }
+ return component;
+ }
+
+ /**
+ * Merge in changes from the server and re-use net IDs where possible.
+ */
+ public void updateNetIds(GeyserSession session, BundleData oldData) {
+ List oldContents = oldData.contents();
+ // Items can't exactly be rearranged in a bundle; they can only be removed at an index, or inserted.
+ int oldIndex = 0;
+ for (int newIndex = 0; newIndex < this.contents.size(); newIndex++) {
+ GeyserItemStack itemStack = this.contents.get(newIndex);
+ if (oldIndex >= oldContents.size()) {
+ // Assume new item if it goes out of bounds of our existing stack
+ if (this.freshFromServer) {
+ // Only update net IDs for new items if the data is fresh from server.
+ // Otherwise, we can update net IDs for something that already has
+ // net IDs allocated, which can cause desyncs.
+ Inventory.updateItemNetId(GeyserItemStack.EMPTY, itemStack, session);
+ session.getBundleCache().markNewBundle(itemStack.getBundleData());
+ }
+ continue;
+ }
+
+ GeyserItemStack oldItem = oldContents.get(oldIndex);
+ // If it stacks with the old item at this index, then
+ if (!InventoryUtils.canStack(oldItem, itemStack)) {
+ // New item?
+ boolean found = false;
+ if (oldIndex + 1 < oldContents.size()) {
+ oldItem = oldContents.get(oldIndex + 1);
+ if (InventoryUtils.canStack(oldItem, itemStack)) {
+ // Permanently increment and assume all contents shifted here
+ oldIndex++;
+ found = true;
+ }
+ }
+ if (!found && oldIndex - 1 >= 0) {
+ oldItem = oldContents.get(oldIndex - 1);
+ if (InventoryUtils.canStack(oldItem, itemStack)) {
+ // Permanently decrement and assume all contents shifted here
+ oldIndex--;
+ found = true;
+ }
+ }
+ if (!found) {
+ oldItem = GeyserItemStack.EMPTY;
+ }
+ }
+
+ if (oldItem != GeyserItemStack.EMPTY || this.freshFromServer) {
+ Inventory.updateItemNetId(oldItem, itemStack, session);
+ }
+ oldIndex++;
+ }
+ this.bundleId = oldData.bundleId();
+ }
+
+ public BundleData copy() {
+ BundleData data = new BundleData();
+ data.bundleId = this.bundleId;
+ for (GeyserItemStack content : this.contents) {
+ data.contents.add(content.copy());
+ }
+ data.freshFromServer = this.freshFromServer;
+ return data;
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java b/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java
index 4f42f146a6d..5100625600a 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java
@@ -181,6 +181,7 @@ public final class ItemTag {
public static final Tag- ENCHANTABLE_CROSSBOW = create("enchantable/crossbow");
public static final Tag
- ENCHANTABLE_VANISHING = create("enchantable/vanishing");
public static final Tag
- ENCHANTABLE_MACE = create("enchantable/mace");
+ public static final Tag
- BUNDLES = create("bundles");
private ItemTag() {}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
new file mode 100644
index 00000000000..2d8e5770b33
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.inventory;
+
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.ints.IntSets;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest;
+import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
+import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.ItemStackRequestAction;
+import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.TransferItemStackRequestAction;
+import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemStackResponse;
+import org.geysermc.geyser.inventory.GeyserItemStack;
+import org.geysermc.geyser.inventory.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.session.cache.BundleCache;
+import org.geysermc.geyser.session.cache.tags.ItemTag;
+import org.geysermc.geyser.util.InventoryUtils;
+import org.geysermc.geyser.util.thirdparty.Fraction;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
+
+import java.util.List;
+
+import static org.geysermc.geyser.translator.inventory.InventoryTranslator.*;
+
+public final class BundleInventoryTranslator {
+ /**
+ * @return a processed bundle interaction, or null to resume normal transaction handling.
+ */
+ @Nullable
+ static ItemStackResponse handleBundle(GeyserSession session, InventoryTranslator translator, Inventory inventory, ItemStackRequest request, boolean sendCreativePackets) {
+ TransferItemStackRequestAction action = null;
+ for (ItemStackRequestAction requestAction : request.getActions()) {
+ if (!(requestAction instanceof TransferItemStackRequestAction transferAction)) {
+ // No known bundle action that does not use transfer actions
+ return null;
+ }
+ boolean sourceIsBundle = isBundle(transferAction.getSource());
+ boolean destIsBundle = isBundle(transferAction.getDestination());
+ if (sourceIsBundle && destIsBundle) {
+ // The client is rearranging the bundle inventory; we're going to ignore translating these actions.
+ continue;
+ }
+ if (sourceIsBundle || destIsBundle) {
+ // This action is moving to a bundle or moving out of a bundle. This is the one we want to track
+ action = transferAction;
+ } else {
+ // Neither location is a bundle location. We don't need to deal with this here.
+ return null;
+ }
+ }
+ if (action == null) {
+ return null;
+ }
+
+ ClickPlan plan = new ClickPlan(session, translator, inventory);
+ if (isBundle(action.getDestination())) {
+ // Placing into bundle
+ var bundleSlotData = action.getDestination();
+ var inventorySlotData = action.getSource();
+ int bundleId = bundleSlotData.getContainerName().getDynamicId();
+ GeyserItemStack cursor = session.getPlayerInventory().getCursor();
+
+ if (cursor.getBundleId() == bundleId) {
+ List contents = cursor.getBundleData().contents();
+ // Placing items into bundles can mean their contents are empty
+
+ // We are currently holding the bundle and trying to pick an item up.
+ int sourceSlot = translator.bedrockSlotToJava(inventorySlotData);
+ GeyserItemStack sourceItem = inventory.getItem(sourceSlot);
+ if (sourceItem.isEmpty()) {
+ // This would be treated as just... plumping the bundle down,
+ // and that should not be called here.
+ return rejectRequest(request);
+ }
+ if (inventorySlotData.getStackNetworkId() != sourceItem.getNetId()) {
+ return rejectRequest(request);
+ }
+
+ // Note that this is also called in ClickPlan. Not ideal...
+ Fraction bundleWeight = calculateBundleWeight(contents);
+ int allowedCapacity = Math.min(capacityForItemStack(bundleWeight, sourceItem), sourceItem.getAmount());
+
+ if (action.getCount() != allowedCapacity) {
+ // Might trigger if bundle weight is different between Java and Bedrock (see calculateBundleWeight)
+ return rejectRequest(request);
+ }
+
+ plan.add(Click.LEFT_BUNDLE_FROM_CURSOR, sourceSlot);
+ if (sendCreativePackets) {
+ plan.executeForCreativeMode();
+ } else {
+ plan.execute(false);
+ }
+ return acceptRequest(request, translator.makeContainerEntries(session, inventory, IntSets.singleton(sourceSlot)));
+ }
+
+ for (int javaSlot = 0; javaSlot < inventory.getSize(); javaSlot++) {
+ GeyserItemStack bundle = inventory.getItem(javaSlot);
+ if (bundle.getBundleId() != bundleId) {
+ continue;
+ }
+
+ if (!translator.checkNetId(session, inventory, inventorySlotData)) {
+ return rejectRequest(request);
+ }
+
+ // Placing items into bundles can mean their contents are empty
+ // Bundle slot does not matter; Java always appends an item to the beginning of a bundle inventory
+
+ IntSet affectedSlots = new IntOpenHashSet(2);
+ affectedSlots.add(javaSlot);
+
+ boolean slotIsInventory = !isCursor(inventorySlotData);
+ int sourceSlot;
+ // If source is cursor, logic lines up better with Java.
+ if (slotIsInventory) {
+ // Simulate picking up the item and adding it to our cursor,
+ // which is what Java would expect
+ sourceSlot = translator.bedrockSlotToJava(inventorySlotData);
+ plan.add(Click.LEFT, sourceSlot);
+ affectedSlots.add(sourceSlot);
+ } else {
+ sourceSlot = -1;
+ }
+
+ Fraction bundleWeight = calculateBundleWeight(bundle.getBundleData().contents());
+ // plan.getCursor() covers if we just picked up the item above from a slot
+ int allowedCapacity = Math.min(capacityForItemStack(bundleWeight, plan.getCursor()), plan.getCursor().getAmount());
+ if (action.getCount() != allowedCapacity) {
+ // Might trigger if bundle weight is different between Java and Bedrock (see calculateBundleWeight)
+ return rejectRequest(request);
+ }
+
+ plan.add(Click.LEFT_BUNDLE, javaSlot);
+
+ if (slotIsInventory && allowedCapacity != plan.getCursor().getAmount()) {
+ // We will need to place the item back in its original slot.
+ plan.add(Click.LEFT, sourceSlot);
+ }
+
+ if (sendCreativePackets) {
+ plan.executeForCreativeMode();
+ } else {
+ plan.execute(false);
+ }
+ return acceptRequest(request, translator.makeContainerEntries(session, inventory, affectedSlots));
+ }
+
+ // Could not find bundle in inventory
+
+ } else {
+ // Taking from bundle
+ var bundleSlotData = action.getSource();
+ var inventorySlotData = action.getDestination();
+ int bundleId = bundleSlotData.getContainerName().getDynamicId();
+ GeyserItemStack cursor = session.getPlayerInventory().getCursor();
+ if (cursor.getBundleId() == bundleId) {
+ // We are currently holding the bundle
+ List contents = cursor.getBundleData().contents();
+ if (contents.isEmpty()) {
+ // Nothing would be ejected?
+ return rejectRequest(request);
+ }
+
+ // Can't select bundle slots while holding bundle in any version; don't set desired bundle slot
+
+ if (bundleSlotData.getStackNetworkId() != contents.get(0).getNetId()) {
+ // We're pulling out the first item; if something mismatches, wuh oh.
+ System.out.println(bundleSlotData.getStackNetworkId() + " " + contents.get(0).getNetId());
+ return rejectRequest(request);
+ }
+
+ int destSlot = translator.bedrockSlotToJava(inventorySlotData);
+ if (!inventory.getItem(destSlot).isEmpty()) {
+ // Illegal action to place an item down on an existing stack, even if
+ // the bundle contains the item.
+ return rejectRequest(request);
+ }
+ plan.add(Click.RIGHT_BUNDLE, destSlot);
+ if (sendCreativePackets) {
+ plan.executeForCreativeMode();
+ } else {
+ plan.execute(false);
+ }
+ return acceptRequest(request, translator.makeContainerEntries(session, inventory, IntSets.singleton(destSlot)));
+ }
+
+ // We need context of what slot the bundle is in.
+ for (int javaSlot = 0; javaSlot < inventory.getSize(); javaSlot++) {
+ GeyserItemStack bundle = inventory.getItem(javaSlot);
+ if (bundle.getBundleId() != bundleId) {
+ continue;
+ }
+
+ List contents = bundle.getBundleData().contents();
+ int rawSelectedSlot = bundleSlotData.getSlot();
+ if (rawSelectedSlot >= contents.size()) {
+ // Illegal?
+ return rejectRequest(request);
+ }
+
+ // Bedrock's indexes are flipped around - first item shown to it is the last index.
+ int slot = BundleCache.platformConvertSlot(contents.size(), rawSelectedSlot);
+ plan.setDesiredBundleSlot(slot);
+
+ // We'll need it even if the final destination isn't the cursor.
+ // I can't think of a situation where we shouldn't reject it and use a temp slot,
+ // but we will see.
+ if (!cursor.isEmpty()) {
+ return rejectRequest(request);
+ }
+
+ IntSet affectedSlots = new IntOpenHashSet(2);
+ affectedSlots.add(javaSlot);
+ GeyserItemStack bundledItem = contents.get(slot);
+ if (bundledItem.getNetId() != bundleSlotData.getStackNetworkId()) {
+ // !!!
+ return rejectRequest(request);
+ }
+
+ plan.add(Click.RIGHT_BUNDLE, javaSlot);
+ // If false, simple logic that matches nicely with Java Edition
+ if (!isCursor(inventorySlotData)) {
+ // Alas, two-click time.
+ int destSlot = translator.bedrockSlotToJava(inventorySlotData);
+ GeyserItemStack existing = inventory.getItem(destSlot);
+
+ // Empty slot is good, but otherwise let's just check that
+ // the two can stack...
+ if (!existing.isEmpty()) {
+ if (!InventoryUtils.canStack(bundledItem, existing)) {
+ return rejectRequest(request);
+ }
+ }
+
+ // Copy the full stack to the new slot.
+ plan.add(Click.LEFT, destSlot);
+ affectedSlots.add(destSlot);
+ }
+
+ if (sendCreativePackets) {
+ plan.executeForCreativeMode();
+ } else {
+ plan.execute(false);
+ }
+ return acceptRequest(request, translator.makeContainerEntries(session, inventory, affectedSlots));
+ }
+
+ // Could not find bundle in inventory
+ }
+ return rejectRequest(request);
+ }
+
+ private static final Fraction BUNDLE_IN_BUNDLE_WEIGHT = Fraction.getFraction(1, 16);
+
+ public static Fraction calculateBundleWeight(List contents) {
+ Fraction fraction = Fraction.ZERO;
+
+ for (GeyserItemStack content : contents) {
+ fraction = fraction.add(calculateWeight(content)
+ .multiplyBy(Fraction.getFraction(content.getAmount(), 1)));
+ }
+
+ return fraction;
+ }
+
+ private static Fraction calculateWeight(GeyserItemStack itemStack) {
+ if (itemStack.getBundleData() != null) {
+ return BUNDLE_IN_BUNDLE_WEIGHT.add(calculateBundleWeight(itemStack.getBundleData().contents()));
+ }
+ DataComponents components = itemStack.getComponents();
+ if (components != null) {
+ // NOTE: this seems to be Java-only, so it can technically cause a bundle weight desync,
+ // but it'll be so rare we can probably ignore it.
+ List> bees = components.get(DataComponentType.BEES);
+ if (bees != null && !bees.isEmpty()) {
+ // Bees be heavy, I guess.
+ return Fraction.ONE;
+ }
+ }
+ return Fraction.getFraction(1, itemStack.asItem().maxStackSize());
+ }
+
+ public static int capacityForItemStack(Fraction bundleWeight, GeyserItemStack itemStack) {
+ Fraction inverse = Fraction.ONE.subtract(bundleWeight);
+ return Math.max(inverse.divideBy(calculateWeight(itemStack)).intValue(), 0);
+ }
+
+ static boolean isBundle(ItemStackRequestSlotData slotData) {
+ return slotData.getContainerName().getContainer() == ContainerSlotType.DYNAMIC_CONTAINER;
+ }
+
+ static boolean isBundle(GeyserSession session, ClickPlan plan, int slot) {
+ return isBundle(session, plan.getItem(slot));
+ }
+
+ static boolean isBundle(GeyserSession session, GeyserItemStack stack) {
+ // Client as of 1.21.3 does use this, and
+ return session.getTagCache().is(ItemTag.BUNDLES, stack);
+ }
+
+ private BundleInventoryTranslator() {
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
index 15c19c542ad..6cbc81957fe 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
@@ -87,6 +87,8 @@
import java.util.List;
import java.util.Map;
+import static org.geysermc.geyser.translator.inventory.BundleInventoryTranslator.isBundle;
+
@AllArgsConstructor
public abstract class InventoryTranslator {
@@ -242,6 +244,13 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
return rejectRequest(request);
}
+ // Might be a bundle action... let's check.
+ ItemStackResponse bundleResponse = BundleInventoryTranslator.handleBundle(session, this, inventory, request, false);
+ if (bundleResponse != null) {
+ // We can simplify a lot of logic because we aren't expecting multi-slot interactions.
+ return bundleResponse;
+ }
+
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
int destSlot = bedrockSlotToJava(transferAction.getDestination());
boolean isSourceCursor = isCursor(transferAction.getSource());
@@ -394,6 +403,7 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
break;
}
case SWAP: {
+ // TODO breaks with bundles
SwapAction swapAction = (SwapAction) action;
ItemStackRequestSlotData source = swapAction.getSource();
ItemStackRequestSlotData destination = swapAction.getDestination();
@@ -427,18 +437,24 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
}
}
+ // A note on all the bundle checks for clicks...
+ // Left clicking in these contexts can count as using the bundle
+ // and adding the stack to the contents of the bundle.
+ // In these cases, we can safely use right-clicking while holding the bundle
+ // as its stack size is 1.
+
if (isSourceCursor && isDestCursor) { //???
return rejectRequest(request);
} else if (isSourceCursor) { //swap cursor
if (InventoryUtils.canStack(cursor, plan.getItem(destSlot))) { //TODO: cannot simply swap if cursor stacks with slot (temp slot)
return rejectRequest(request);
}
- plan.add(Click.LEFT, destSlot);
+ plan.add(isBundle(session, plan, destSlot) || isBundle(session, cursor) ? Click.RIGHT : Click.LEFT, destSlot);
} else if (isDestCursor) { //swap cursor
if (InventoryUtils.canStack(cursor, plan.getItem(sourceSlot))) { //TODO
return rejectRequest(request);
}
- plan.add(Click.LEFT, sourceSlot);
+ plan.add(isBundle(session, plan, sourceSlot) || isBundle(session, cursor) ? Click.RIGHT : Click.LEFT, sourceSlot);
} else {
if (!cursor.isEmpty()) { //TODO: (temp slot)
return rejectRequest(request);
@@ -450,7 +466,7 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
return rejectRequest(request);
}
plan.add(Click.LEFT, sourceSlot); //pickup source into cursor
- plan.add(Click.LEFT, destSlot); //swap cursor with dest slot
+ plan.add(isBundle(session, plan, sourceSlot) || isBundle(session, plan, destSlot) ? Click.RIGHT : Click.LEFT, destSlot); //swap cursor with dest slot
plan.add(Click.LEFT, sourceSlot); //release cursor onto source
}
break;
@@ -916,6 +932,11 @@ protected void dumpStackRequestDetails(GeyserSession session, Inventory inventor
}
public boolean checkNetId(GeyserSession session, Inventory inventory, ItemStackRequestSlotData slotInfoData) {
+ if (BundleInventoryTranslator.isBundle(slotInfoData)) {
+ // Will thoroughly be investigated, if needed, in bundle checks.
+ return true;
+ }
+
int netId = slotInfoData.getStackNetworkId();
// "In my testing, sometimes the client thinks the netId of an item in the crafting grid is 1, even though we never said it was.
// I think it only happens when we manually set the grid but that was my quick fix"
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
index 8fd365d7f7f..7eefffa3e3e 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
@@ -264,6 +264,15 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
return rejectRequest(request, false);
}
+ // Might be a bundle action... let's check.
+ // If we're in creative mode, instead of replacing logic (more hassle for updates),
+ // let's just reuse as much logic as possible!!
+ ItemStackResponse bundleResponse = BundleInventoryTranslator.handleBundle(session, this, inventory, request, true);
+ if (bundleResponse != null) {
+ // We can simplify a lot of logic because we aren't expecting multi-slot interactions.
+ return bundleResponse;
+ }
+
int transferAmount = transferAction.getCount();
if (isCursor(transferAction.getDestination())) {
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java
index 36d382d69d3..54c92857c82 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java
@@ -63,6 +63,7 @@ public void translate(GeyserSession session, ClientboundContainerSetContentPacke
}
GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]);
+ session.getBundleCache().initialize(newItem);
inventory.setItem(i, newItem, session);
}
@@ -72,7 +73,9 @@ public void translate(GeyserSession session, ClientboundContainerSetContentPacke
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
inventory.setStateId(stateId);
- session.getPlayerInventory().setCursor(GeyserItemStack.from(packet.getCarriedItem()), session);
+ GeyserItemStack cursor = GeyserItemStack.from(packet.getCarriedItem());
+ session.getBundleCache().initialize(cursor);
+ session.getPlayerInventory().setCursor(cursor, session);
InventoryUtils.updateCursor(session);
}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
index fa2e3a54461..b3bd91c5b38 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
@@ -93,6 +93,7 @@ public void translate(GeyserSession session, ClientboundContainerSetSlotPacket p
}
GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
+ session.getBundleCache().initialize(newItem);
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
// In rare cases, the window ID can still be 0 but Java treats it as valid
// This behavior still exists as of Java Edition 1.21.2, despite the new packet
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaSetCursorItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaSetCursorItemTranslator.java
index df11bf1af81..649da8ea4f9 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaSetCursorItemTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaSetCursorItemTranslator.java
@@ -38,6 +38,7 @@ public class JavaSetCursorItemTranslator extends PacketTranslatorThis class is immutable, and interoperable with most methods that accept
+ * a {@link Number}.
+ *
+ * Note that this class is intended for common use cases, it is int
+ * based and thus suffers from various overflow issues. For a BigInteger based
+ * equivalent, please see the Commons Math BigFraction class.
+ *
+ * @since 2.0
+ */
+// Geyser: Java Edition uses this for 1.21.3 bundle calculation. Rather than
+// Reimplementing an open-source class or bringing in a whole library,
+// the single class is used to ensure accuracy.
+public final class Fraction extends Number implements Comparable {
+
+ /**
+ * Required for serialization support. Lang version 2.0.
+ *
+ * @see java.io.Serializable
+ */
+ private static final long serialVersionUID = 65382027393090L;
+
+ /**
+ * {@link Fraction} representation of 0.
+ */
+ public static final Fraction ZERO = new Fraction(0, 1);
+ /**
+ * {@link Fraction} representation of 1.
+ */
+ public static final Fraction ONE = new Fraction(1, 1);
+ /**
+ * {@link Fraction} representation of 1/2.
+ */
+ public static final Fraction ONE_HALF = new Fraction(1, 2);
+ /**
+ * {@link Fraction} representation of 1/3.
+ */
+ public static final Fraction ONE_THIRD = new Fraction(1, 3);
+ /**
+ * {@link Fraction} representation of 2/3.
+ */
+ public static final Fraction TWO_THIRDS = new Fraction(2, 3);
+ /**
+ * {@link Fraction} representation of 1/4.
+ */
+ public static final Fraction ONE_QUARTER = new Fraction(1, 4);
+ /**
+ * {@link Fraction} representation of 2/4.
+ */
+ public static final Fraction TWO_QUARTERS = new Fraction(2, 4);
+ /**
+ * {@link Fraction} representation of 3/4.
+ */
+ public static final Fraction THREE_QUARTERS = new Fraction(3, 4);
+ /**
+ * {@link Fraction} representation of 1/5.
+ */
+ public static final Fraction ONE_FIFTH = new Fraction(1, 5);
+ /**
+ * {@link Fraction} representation of 2/5.
+ */
+ public static final Fraction TWO_FIFTHS = new Fraction(2, 5);
+ /**
+ * {@link Fraction} representation of 3/5.
+ */
+ public static final Fraction THREE_FIFTHS = new Fraction(3, 5);
+ /**
+ * {@link Fraction} representation of 4/5.
+ */
+ public static final Fraction FOUR_FIFTHS = new Fraction(4, 5);
+
+
+ /**
+ * Add two integers, checking for overflow.
+ *
+ * @param x an addend
+ * @param y an addend
+ * @return the sum {@code x+y}
+ * @throws ArithmeticException if the result can not be represented as
+ * an int
+ */
+ private static int addAndCheck(final int x, final int y) {
+ final long s = (long) x + (long) y;
+ if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) {
+ throw new ArithmeticException("overflow: add");
+ }
+ return (int) s;
+ }
+ /**
+ * Creates a {@link Fraction} instance from a {@code double} value.
+ *
+ * This method uses the
+ * continued fraction algorithm, computing a maximum of
+ * 25 convergents and bounding the denominator by 10,000.
+ *
+ * @param value the double value to convert
+ * @return a new fraction instance that is close to the value
+ * @throws ArithmeticException if {@code |value| > Integer.MAX_VALUE}
+ * or {@code value = NaN}
+ * @throws ArithmeticException if the calculated denominator is {@code zero}
+ * @throws ArithmeticException if the algorithm does not converge
+ */
+ public static Fraction getFraction(double value) {
+ final int sign = value < 0 ? -1 : 1;
+ value = Math.abs(value);
+ if (value > Integer.MAX_VALUE || Double.isNaN(value)) {
+ throw new ArithmeticException("The value must not be greater than Integer.MAX_VALUE or NaN");
+ }
+ final int wholeNumber = (int) value;
+ value -= wholeNumber;
+
+ int numer0 = 0; // the pre-previous
+ int denom0 = 1; // the pre-previous
+ int numer1 = 1; // the previous
+ int denom1 = 0; // the previous
+ int numer2; // the current, setup in calculation
+ int denom2; // the current, setup in calculation
+ int a1 = (int) value;
+ int a2;
+ double x1 = 1;
+ double x2;
+ double y1 = value - a1;
+ double y2;
+ double delta1, delta2 = Double.MAX_VALUE;
+ double fraction;
+ int i = 1;
+ do {
+ delta1 = delta2;
+ a2 = (int) (x1 / y1);
+ x2 = y1;
+ y2 = x1 - a2 * y1;
+ numer2 = a1 * numer1 + numer0;
+ denom2 = a1 * denom1 + denom0;
+ fraction = (double) numer2 / (double) denom2;
+ delta2 = Math.abs(value - fraction);
+ a1 = a2;
+ x1 = x2;
+ y1 = y2;
+ numer0 = numer1;
+ denom0 = denom1;
+ numer1 = numer2;
+ denom1 = denom2;
+ i++;
+ } while (delta1 > delta2 && denom2 <= 10000 && denom2 > 0 && i < 25);
+ if (i == 25) {
+ throw new ArithmeticException("Unable to convert double to fraction");
+ }
+ return getReducedFraction((numer0 + wholeNumber * denom0) * sign, denom0);
+ }
+
+ /**
+ * Creates a {@link Fraction} instance with the 2 parts
+ * of a fraction Y/Z.
+ *
+ * Any negative signs are resolved to be on the numerator.
+ *
+ * @param numerator the numerator, for example the three in 'three sevenths'
+ * @param denominator the denominator, for example the seven in 'three sevenths'
+ * @return a new fraction instance
+ * @throws ArithmeticException if the denominator is {@code zero}
+ * or the denominator is {@code negative} and the numerator is {@code Integer#MIN_VALUE}
+ */
+ public static Fraction getFraction(int numerator, int denominator) {
+ if (denominator == 0) {
+ throw new ArithmeticException("The denominator must not be zero");
+ }
+ if (denominator < 0) {
+ if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) {
+ throw new ArithmeticException("overflow: can't negate");
+ }
+ numerator = -numerator;
+ denominator = -denominator;
+ }
+ return new Fraction(numerator, denominator);
+ }
+ /**
+ * Creates a {@link Fraction} instance with the 3 parts
+ * of a fraction X Y/Z.
+ *
+ * The negative sign must be passed in on the whole number part.
+ *
+ * @param whole the whole number, for example the one in 'one and three sevenths'
+ * @param numerator the numerator, for example the three in 'one and three sevenths'
+ * @param denominator the denominator, for example the seven in 'one and three sevenths'
+ * @return a new fraction instance
+ * @throws ArithmeticException if the denominator is {@code zero}
+ * @throws ArithmeticException if the denominator is negative
+ * @throws ArithmeticException if the numerator is negative
+ * @throws ArithmeticException if the resulting numerator exceeds
+ * {@code Integer.MAX_VALUE}
+ */
+ public static Fraction getFraction(final int whole, final int numerator, final int denominator) {
+ if (denominator == 0) {
+ throw new ArithmeticException("The denominator must not be zero");
+ }
+ if (denominator < 0) {
+ throw new ArithmeticException("The denominator must not be negative");
+ }
+ if (numerator < 0) {
+ throw new ArithmeticException("The numerator must not be negative");
+ }
+ final long numeratorValue;
+ if (whole < 0) {
+ numeratorValue = whole * (long) denominator - numerator;
+ } else {
+ numeratorValue = whole * (long) denominator + numerator;
+ }
+ if (numeratorValue < Integer.MIN_VALUE || numeratorValue > Integer.MAX_VALUE) {
+ throw new ArithmeticException("Numerator too large to represent as an Integer.");
+ }
+ return new Fraction((int) numeratorValue, denominator);
+ }
+ /**
+ * Creates a Fraction from a {@link String}.
+ *
+ * The formats accepted are:
+ *
+ *
+ * - {@code double} String containing a dot
+ * - 'X Y/Z'
+ * - 'Y/Z'
+ * - 'X' (a simple whole number)
+ *
+ * and a .
+ *
+ * @param str the string to parse, must not be {@code null}
+ * @return the new {@link Fraction} instance
+ * @throws NullPointerException if the string is {@code null}
+ * @throws NumberFormatException if the number format is invalid
+ */
+ public static Fraction getFraction(String str) {
+ Objects.requireNonNull(str, "str");
+ // parse double format
+ int pos = str.indexOf('.');
+ if (pos >= 0) {
+ return getFraction(Double.parseDouble(str));
+ }
+
+ // parse X Y/Z format
+ pos = str.indexOf(' ');
+ if (pos > 0) {
+ final int whole = Integer.parseInt(str.substring(0, pos));
+ str = str.substring(pos + 1);
+ pos = str.indexOf('/');
+ if (pos < 0) {
+ throw new NumberFormatException("The fraction could not be parsed as the format X Y/Z");
+ }
+ final int numer = Integer.parseInt(str.substring(0, pos));
+ final int denom = Integer.parseInt(str.substring(pos + 1));
+ return getFraction(whole, numer, denom);
+ }
+
+ // parse Y/Z format
+ pos = str.indexOf('/');
+ if (pos < 0) {
+ // simple whole number
+ return getFraction(Integer.parseInt(str), 1);
+ }
+ final int numer = Integer.parseInt(str.substring(0, pos));
+ final int denom = Integer.parseInt(str.substring(pos + 1));
+ return getFraction(numer, denom);
+ }
+
+ /**
+ * Creates a reduced {@link Fraction} instance with the 2 parts
+ * of a fraction Y/Z.
+ *
+ * For example, if the input parameters represent 2/4, then the created
+ * fraction will be 1/2.
+ *
+ * Any negative signs are resolved to be on the numerator.
+ *
+ * @param numerator the numerator, for example the three in 'three sevenths'
+ * @param denominator the denominator, for example the seven in 'three sevenths'
+ * @return a new fraction instance, with the numerator and denominator reduced
+ * @throws ArithmeticException if the denominator is {@code zero}
+ */
+ public static Fraction getReducedFraction(int numerator, int denominator) {
+ if (denominator == 0) {
+ throw new ArithmeticException("The denominator must not be zero");
+ }
+ if (numerator == 0) {
+ return ZERO; // normalize zero.
+ }
+ // allow 2^k/-2^31 as a valid fraction (where k>0)
+ if (denominator == Integer.MIN_VALUE && (numerator & 1) == 0) {
+ numerator /= 2;
+ denominator /= 2;
+ }
+ if (denominator < 0) {
+ if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) {
+ throw new ArithmeticException("overflow: can't negate");
+ }
+ numerator = -numerator;
+ denominator = -denominator;
+ }
+ // simplify fraction.
+ final int gcd = greatestCommonDivisor(numerator, denominator);
+ numerator /= gcd;
+ denominator /= gcd;
+ return new Fraction(numerator, denominator);
+ }
+
+ /**
+ * Gets the greatest common divisor of the absolute value of
+ * two numbers, using the "binary gcd" method which avoids
+ * division and modulo operations. See Knuth 4.5.2 algorithm B.
+ * This algorithm is due to Josef Stein (1961).
+ *
+ * @param u a non-zero number
+ * @param v a non-zero number
+ * @return the greatest common divisor, never zero
+ */
+ private static int greatestCommonDivisor(int u, int v) {
+ // From Commons Math:
+ if (u == 0 || v == 0) {
+ if (u == Integer.MIN_VALUE || v == Integer.MIN_VALUE) {
+ throw new ArithmeticException("overflow: gcd is 2^31");
+ }
+ return Math.abs(u) + Math.abs(v);
+ }
+ // if either operand is abs 1, return 1:
+ if (Math.abs(u) == 1 || Math.abs(v) == 1) {
+ return 1;
+ }
+ // keep u and v negative, as negative integers range down to
+ // -2^31, while positive numbers can only be as large as 2^31-1
+ // (i.e. we can't necessarily negate a negative number without
+ // overflow)
+ if (u > 0) {
+ u = -u;
+ } // make u negative
+ if (v > 0) {
+ v = -v;
+ } // make v negative
+ // B1. [Find power of 2]
+ int k = 0;
+ while ((u & 1) == 0 && (v & 1) == 0 && k < 31) { // while u and v are both even...
+ u /= 2;
+ v /= 2;
+ k++; // cast out twos.
+ }
+ if (k == 31) {
+ throw new ArithmeticException("overflow: gcd is 2^31");
+ }
+ // B2. Initialize: u and v have been divided by 2^k and at least
+ // one is odd.
+ int t = (u & 1) == 1 ? v : -(u / 2)/* B3 */;
+ // t negative: u was odd, v may be even (t replaces v)
+ // t positive: u was even, v is odd (t replaces u)
+ do {
+ /* assert u<0 && v<0; */
+ // B4/B3: cast out twos from t.
+ while ((t & 1) == 0) { // while t is even.
+ t /= 2; // cast out twos
+ }
+ // B5 [reset max(u,v)]
+ if (t > 0) {
+ u = -t;
+ } else {
+ v = t;
+ }
+ // B6/B3. at this point both u and v should be odd.
+ t = (v - u) / 2;
+ // |u| larger: t positive (replace u)
+ // |v| larger: t negative (replace v)
+ } while (t != 0);
+ return -u * (1 << k); // gcd is u*2^k
+ }
+
+ /**
+ * Multiply two integers, checking for overflow.
+ *
+ * @param x a factor
+ * @param y a factor
+ * @return the product {@code x*y}
+ * @throws ArithmeticException if the result can not be represented as
+ * an int
+ */
+ private static int mulAndCheck(final int x, final int y) {
+ final long m = (long) x * (long) y;
+ if (m < Integer.MIN_VALUE || m > Integer.MAX_VALUE) {
+ throw new ArithmeticException("overflow: mul");
+ }
+ return (int) m;
+ }
+
+ /**
+ * Multiply two non-negative integers, checking for overflow.
+ *
+ * @param x a non-negative factor
+ * @param y a non-negative factor
+ * @return the product {@code x*y}
+ * @throws ArithmeticException if the result can not be represented as
+ * an int
+ */
+ private static int mulPosAndCheck(final int x, final int y) {
+ /* assert x>=0 && y>=0; */
+ final long m = (long) x * (long) y;
+ if (m > Integer.MAX_VALUE) {
+ throw new ArithmeticException("overflow: mulPos");
+ }
+ return (int) m;
+ }
+
+ /**
+ * Subtract two integers, checking for overflow.
+ *
+ * @param x the minuend
+ * @param y the subtrahend
+ * @return the difference {@code x-y}
+ * @throws ArithmeticException if the result can not be represented as
+ * an int
+ */
+ private static int subAndCheck(final int x, final int y) {
+ final long s = (long) x - (long) y;
+ if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) {
+ throw new ArithmeticException("overflow: add");
+ }
+ return (int) s;
+ }
+
+ /**
+ * The numerator number part of the fraction (the three in three sevenths).
+ */
+ private final int numerator;
+
+ /**
+ * The denominator number part of the fraction (the seven in three sevenths).
+ */
+ private final int denominator;
+
+ /**
+ * Cached output hashCode (class is immutable).
+ */
+ private transient int hashCode;
+
+ /**
+ * Cached output toString (class is immutable).
+ */
+ private transient String toString;
+
+ /**
+ * Cached output toProperString (class is immutable).
+ */
+ private transient String toProperString;
+
+ /**
+ * Constructs a {@link Fraction} instance with the 2 parts
+ * of a fraction Y/Z.
+ *
+ * @param numerator the numerator, for example the three in 'three sevenths'
+ * @param denominator the denominator, for example the seven in 'three sevenths'
+ */
+ private Fraction(final int numerator, final int denominator) {
+ this.numerator = numerator;
+ this.denominator = denominator;
+ }
+
+ /**
+ * Gets a fraction that is the positive equivalent of this one.
+ * More precisely: {@code (fraction >= 0 ? this : -fraction)}
+ *
+ * The returned fraction is not reduced.
+ *
+ * @return {@code this} if it is positive, or a new positive fraction
+ * instance with the opposite signed numerator
+ */
+ public Fraction abs() {
+ if (numerator >= 0) {
+ return this;
+ }
+ return negate();
+ }
+
+ /**
+ * Adds the value of this fraction to another, returning the result in reduced form.
+ * The algorithm follows Knuth, 4.5.1.
+ *
+ * @param fraction the fraction to add, must not be {@code null}
+ * @return a {@link Fraction} instance with the resulting values
+ * @throws NullPointerException if the fraction is {@code null}
+ * @throws ArithmeticException if the resulting numerator or denominator exceeds
+ * {@code Integer.MAX_VALUE}
+ */
+ public Fraction add(final Fraction fraction) {
+ return addSub(fraction, true /* add */);
+ }
+
+ /**
+ * Implement add and subtract using algorithm described in Knuth 4.5.1.
+ *
+ * @param fraction the fraction to subtract, must not be {@code null}
+ * @param isAdd true to add, false to subtract
+ * @return a {@link Fraction} instance with the resulting values
+ * @throws IllegalArgumentException if the fraction is {@code null}
+ * @throws ArithmeticException if the resulting numerator or denominator
+ * cannot be represented in an {@code int}.
+ */
+ private Fraction addSub(final Fraction fraction, final boolean isAdd) {
+ Objects.requireNonNull(fraction, "fraction");
+ // zero is identity for addition.
+ if (numerator == 0) {
+ return isAdd ? fraction : fraction.negate();
+ }
+ if (fraction.numerator == 0) {
+ return this;
+ }
+ // if denominators are randomly distributed, d1 will be 1 about 61%
+ // of the time.
+ final int d1 = greatestCommonDivisor(denominator, fraction.denominator);
+ if (d1 == 1) {
+ // result is ( (u*v' +/- u'v) / u'v')
+ final int uvp = mulAndCheck(numerator, fraction.denominator);
+ final int upv = mulAndCheck(fraction.numerator, denominator);
+ return new Fraction(isAdd ? addAndCheck(uvp, upv) : subAndCheck(uvp, upv), mulPosAndCheck(denominator,
+ fraction.denominator));
+ }
+ // the quantity 't' requires 65 bits of precision; see knuth 4.5.1
+ // exercise 7. we're going to use a BigInteger.
+ // t = u(v'/d1) +/- v(u'/d1)
+ final BigInteger uvp = BigInteger.valueOf(numerator).multiply(BigInteger.valueOf(fraction.denominator / d1));
+ final BigInteger upv = BigInteger.valueOf(fraction.numerator).multiply(BigInteger.valueOf(denominator / d1));
+ final BigInteger t = isAdd ? uvp.add(upv) : uvp.subtract(upv);
+ // but d2 doesn't need extra precision because
+ // d2 = gcd(t,d1) = gcd(t mod d1, d1)
+ final int tmodd1 = t.mod(BigInteger.valueOf(d1)).intValue();
+ final int d2 = tmodd1 == 0 ? d1 : greatestCommonDivisor(tmodd1, d1);
+
+ // result is (t/d2) / (u'/d1)(v'/d2)
+ final BigInteger w = t.divide(BigInteger.valueOf(d2));
+ if (w.bitLength() > 31) {
+ throw new ArithmeticException("overflow: numerator too large after multiply");
+ }
+ return new Fraction(w.intValue(), mulPosAndCheck(denominator / d1, fraction.denominator / d2));
+ }
+
+ /**
+ * Compares this object to another based on size.
+ *
+ * Note: this class has a natural ordering that is inconsistent
+ * with equals, because, for example, equals treats 1/2 and 2/4 as
+ * different, whereas compareTo treats them as equal.
+ *
+ * @param other the object to compare to
+ * @return -1 if this is less, 0 if equal, +1 if greater
+ * @throws ClassCastException if the object is not a {@link Fraction}
+ * @throws NullPointerException if the object is {@code null}
+ */
+ @Override
+ public int compareTo(final Fraction other) {
+ if (this == other) {
+ return 0;
+ }
+ if (numerator == other.numerator && denominator == other.denominator) {
+ return 0;
+ }
+
+ // otherwise see which is less
+ final long first = (long) numerator * (long) other.denominator;
+ final long second = (long) other.numerator * (long) denominator;
+ return Long.compare(first, second);
+ }
+
+ /**
+ * Divide the value of this fraction by another.
+ *
+ * @param fraction the fraction to divide by, must not be {@code null}
+ * @return a {@link Fraction} instance with the resulting values
+ * @throws NullPointerException if the fraction is {@code null}
+ * @throws ArithmeticException if the fraction to divide by is zero
+ * @throws ArithmeticException if the resulting numerator or denominator exceeds
+ * {@code Integer.MAX_VALUE}
+ */
+ public Fraction divideBy(final Fraction fraction) {
+ Objects.requireNonNull(fraction, "fraction");
+ if (fraction.numerator == 0) {
+ throw new ArithmeticException("The fraction to divide by must not be zero");
+ }
+ return multiplyBy(fraction.invert());
+ }
+
+ /**
+ * Gets the fraction as a {@code double}. This calculates the fraction
+ * as the numerator divided by denominator.
+ *
+ * @return the fraction as a {@code double}
+ */
+ @Override
+ public double doubleValue() {
+ return (double) numerator / (double) denominator;
+ }
+
+ /**
+ * Compares this fraction to another object to test if they are equal..
+ *
+ *
To be equal, both values must be equal. Thus 2/4 is not equal to 1/2.
+ *
+ * @param obj the reference object with which to compare
+ * @return {@code true} if this object is equal
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof Fraction)) {
+ return false;
+ }
+ final Fraction other = (Fraction) obj;
+ return getNumerator() == other.getNumerator() && getDenominator() == other.getDenominator();
+ }
+
+ /**
+ * Gets the fraction as a {@code float}. This calculates the fraction
+ * as the numerator divided by denominator.
+ *
+ * @return the fraction as a {@code float}
+ */
+ @Override
+ public float floatValue() {
+ return (float) numerator / (float) denominator;
+ }
+
+ /**
+ * Gets the denominator part of the fraction.
+ *
+ * @return the denominator fraction part
+ */
+ public int getDenominator() {
+ return denominator;
+ }
+
+ /**
+ * Gets the numerator part of the fraction.
+ *
+ * This method may return a value greater than the denominator, an
+ * improper fraction, such as the seven in 7/4.
+ *
+ * @return the numerator fraction part
+ */
+ public int getNumerator() {
+ return numerator;
+ }
+
+ /**
+ * Gets the proper numerator, always positive.
+ *
+ * An improper fraction 7/4 can be resolved into a proper one, 1 3/4.
+ * This method returns the 3 from the proper fraction.
+ *
+ * If the fraction is negative such as -7/4, it can be resolved into
+ * -1 3/4, so this method returns the positive proper numerator, 3.
+ *
+ * @return the numerator fraction part of a proper fraction, always positive
+ */
+ public int getProperNumerator() {
+ return Math.abs(numerator % denominator);
+ }
+
+ /**
+ * Gets the proper whole part of the fraction.
+ *
+ * An improper fraction 7/4 can be resolved into a proper one, 1 3/4.
+ * This method returns the 1 from the proper fraction.
+ *
+ * If the fraction is negative such as -7/4, it can be resolved into
+ * -1 3/4, so this method returns the positive whole part -1.
+ *
+ * @return the whole fraction part of a proper fraction, that includes the sign
+ */
+ public int getProperWhole() {
+ return numerator / denominator;
+ }
+
+ /**
+ * Gets a hashCode for the fraction.
+ *
+ * @return a hash code value for this object
+ */
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ // hash code update should be atomic.
+ hashCode = 37 * (37 * 17 + getNumerator()) + getDenominator();
+ }
+ return hashCode;
+ }
+
+ /**
+ * Gets the fraction as an {@code int}. This returns the whole number
+ * part of the fraction.
+ *
+ * @return the whole number fraction part
+ */
+ @Override
+ public int intValue() {
+ return numerator / denominator;
+ }
+
+ /**
+ * Gets a fraction that is the inverse (1/fraction) of this one.
+ *
+ * The returned fraction is not reduced.
+ *
+ * @return a new fraction instance with the numerator and denominator
+ * inverted.
+ * @throws ArithmeticException if the fraction represents zero.
+ */
+ public Fraction invert() {
+ if (numerator == 0) {
+ throw new ArithmeticException("Unable to invert zero.");
+ }
+ if (numerator==Integer.MIN_VALUE) {
+ throw new ArithmeticException("overflow: can't negate numerator");
+ }
+ if (numerator<0) {
+ return new Fraction(-denominator, -numerator);
+ }
+ return new Fraction(denominator, numerator);
+ }
+
+ /**
+ * Gets the fraction as a {@code long}. This returns the whole number
+ * part of the fraction.
+ *
+ * @return the whole number fraction part
+ */
+ @Override
+ public long longValue() {
+ return (long) numerator / denominator;
+ }
+
+ /**
+ * Multiplies the value of this fraction by another, returning the
+ * result in reduced form.
+ *
+ * @param fraction the fraction to multiply by, must not be {@code null}
+ * @return a {@link Fraction} instance with the resulting values
+ * @throws NullPointerException if the fraction is {@code null}
+ * @throws ArithmeticException if the resulting numerator or denominator exceeds
+ * {@code Integer.MAX_VALUE}
+ */
+ public Fraction multiplyBy(final Fraction fraction) {
+ Objects.requireNonNull(fraction, "fraction");
+ if (numerator == 0 || fraction.numerator == 0) {
+ return ZERO;
+ }
+ // knuth 4.5.1
+ // make sure we don't overflow unless the result *must* overflow.
+ final int d1 = greatestCommonDivisor(numerator, fraction.denominator);
+ final int d2 = greatestCommonDivisor(fraction.numerator, denominator);
+ return getReducedFraction(mulAndCheck(numerator / d1, fraction.numerator / d2),
+ mulPosAndCheck(denominator / d2, fraction.denominator / d1));
+ }
+
+ /**
+ * Gets a fraction that is the negative (-fraction) of this one.
+ *
+ * The returned fraction is not reduced.
+ *
+ * @return a new fraction instance with the opposite signed numerator
+ */
+ public Fraction negate() {
+ // the positive range is one smaller than the negative range of an int.
+ if (numerator==Integer.MIN_VALUE) {
+ throw new ArithmeticException("overflow: too large to negate");
+ }
+ return new Fraction(-numerator, denominator);
+ }
+
+ /**
+ * Gets a fraction that is raised to the passed in power.
+ *
+ * The returned fraction is in reduced form.
+ *
+ * @param power the power to raise the fraction to
+ * @return {@code this} if the power is one, {@link #ONE} if the power
+ * is zero (even if the fraction equals ZERO) or a new fraction instance
+ * raised to the appropriate power
+ * @throws ArithmeticException if the resulting numerator or denominator exceeds
+ * {@code Integer.MAX_VALUE}
+ */
+ public Fraction pow(final int power) {
+ if (power == 1) {
+ return this;
+ }
+ if (power == 0) {
+ return ONE;
+ }
+ if (power < 0) {
+ if (power == Integer.MIN_VALUE) { // MIN_VALUE can't be negated.
+ return this.invert().pow(2).pow(-(power / 2));
+ }
+ return this.invert().pow(-power);
+ }
+ final Fraction f = this.multiplyBy(this);
+ if (power % 2 == 0) { // if even...
+ return f.pow(power / 2);
+ }
+ return f.pow(power / 2).multiplyBy(this);
+ }
+
+ /**
+ * Reduce the fraction to the smallest values for the numerator and
+ * denominator, returning the result.
+ *
+ * For example, if this fraction represents 2/4, then the result
+ * will be 1/2.
+ *
+ * @return a new reduced fraction instance, or this if no simplification possible
+ */
+ public Fraction reduce() {
+ if (numerator == 0) {
+ return equals(ZERO) ? this : ZERO;
+ }
+ final int gcd = greatestCommonDivisor(Math.abs(numerator), denominator);
+ if (gcd == 1) {
+ return this;
+ }
+ return getFraction(numerator / gcd, denominator / gcd);
+ }
+
+ /**
+ * Subtracts the value of another fraction from the value of this one,
+ * returning the result in reduced form.
+ *
+ * @param fraction the fraction to subtract, must not be {@code null}
+ * @return a {@link Fraction} instance with the resulting values
+ * @throws NullPointerException if the fraction is {@code null}
+ * @throws ArithmeticException if the resulting numerator or denominator
+ * cannot be represented in an {@code int}.
+ */
+ public Fraction subtract(final Fraction fraction) {
+ return addSub(fraction, false /* subtract */);
+ }
+
+ /**
+ * Gets the fraction as a proper {@link String} in the format X Y/Z.
+ *
+ * The format used in 'wholeNumber numerator/denominator'.
+ * If the whole number is zero it will be omitted. If the numerator is zero,
+ * only the whole number is returned.
+ *
+ * @return a {@link String} form of the fraction
+ */
+ public String toProperString() {
+ if (toProperString == null) {
+ if (numerator == 0) {
+ toProperString = "0";
+ } else if (numerator == denominator) {
+ toProperString = "1";
+ } else if (numerator == -1 * denominator) {
+ toProperString = "-1";
+ } else if ((numerator > 0 ? -numerator : numerator) < -denominator) {
+ // note that we do the magnitude comparison test above with
+ // NEGATIVE (not positive) numbers, since negative numbers
+ // have a larger range. otherwise numerator==Integer.MIN_VALUE
+ // is handled incorrectly.
+ final int properNumerator = getProperNumerator();
+ if (properNumerator == 0) {
+ toProperString = Integer.toString(getProperWhole());
+ } else {
+ toProperString = getProperWhole() + " " + properNumerator + "/" + getDenominator();
+ }
+ } else {
+ toProperString = getNumerator() + "/" + getDenominator();
+ }
+ }
+ return toProperString;
+ }
+
+ /**
+ * Gets the fraction as a {@link String}.
+ *
+ * The format used is 'numerator/denominator' always.
+ *
+ * @return a {@link String} form of the fraction
+ */
+ @Override
+ public String toString() {
+ if (toString == null) {
+ toString = getNumerator() + "/" + getDenominator();
+ }
+ return toString;
+ }
+}
From 415411e5fe88a70ce537862c7f3a9782def7249a Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Mon, 11 Nov 2024 14:36:49 -0500
Subject: [PATCH 2/4] Touchups
---
.../geyser/inventory/GeyserItemStack.java | 8 -----
.../geyser/inventory/click/ClickPlan.java | 4 ++-
.../geysermc/geyser/item/type/BundleItem.java | 32 -------------------
.../inventory/BundleInventoryTranslator.java | 3 +-
4 files changed, 4 insertions(+), 43 deletions(-)
delete mode 100644 core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
index 5e1fb844762..4c82773416c 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java
@@ -54,7 +54,6 @@
@Data
public class GeyserItemStack {
public static final GeyserItemStack EMPTY = new GeyserItemStack(Items.AIR_ID, 0, null);
- public static final int UNSET_NET_ID = 1;
private final int javaId;
private int amount;
@@ -158,13 +157,6 @@ public int getComponent(@NonNull DataComponentType type,
return def;
}
-// public boolean hasComponent(DataComponentType> type) {
-// if (components == null) {
-// return false;
-// }
-// return components.getDataComponents().containsKey(type);
-// }
-
public int getNetId() {
return isEmpty() ? 0 : netId;
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
index 15c46aed0e0..d4344f6e829 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
@@ -183,8 +183,10 @@ public void executeForCreativeMode() {
inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
}
for (Int2ObjectMap.Entry changedSlot : changedItems.int2ObjectEntrySet()) {
+ ItemStack value = changedSlot.getValue();
+ ItemStack toSend = InventoryUtils.isEmpty(value) ? new ItemStack(-1, 0, null) : value;
session.sendDownstreamGamePacket(
- new ServerboundSetCreativeModeSlotPacket((short) changedSlot.getIntKey(), changedSlot.getValue())
+ new ServerboundSetCreativeModeSlotPacket((short) changedSlot.getIntKey(), toSend)
);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java b/core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java
deleted file mode 100644
index 9c06768b3d5..00000000000
--- a/core/src/main/java/org/geysermc/geyser/item/type/BundleItem.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (c) 2024 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.item.type;
-
-public class BundleItem extends Item {
- public BundleItem(String javaIdentifier, Builder builder) {
- super(javaIdentifier, builder);
- }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
index 2d8e5770b33..b281945aaab 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
@@ -195,7 +195,6 @@ static ItemStackResponse handleBundle(GeyserSession session, InventoryTranslator
if (bundleSlotData.getStackNetworkId() != contents.get(0).getNetId()) {
// We're pulling out the first item; if something mismatches, wuh oh.
- System.out.println(bundleSlotData.getStackNetworkId() + " " + contents.get(0).getNetId());
return rejectRequest(request);
}
@@ -324,7 +323,7 @@ static boolean isBundle(GeyserSession session, ClickPlan plan, int slot) {
}
static boolean isBundle(GeyserSession session, GeyserItemStack stack) {
- // Client as of 1.21.3 does use this, and
+ // Client as of 1.21.3 does use this
return session.getTagCache().is(ItemTag.BUNDLES, stack);
}
From cde0cae77681106be139cb978f307208db919df5 Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Mon, 11 Nov 2024 14:53:00 -0500
Subject: [PATCH 3/4] Correct bundle mapping
---
core/src/main/resources/mappings | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings
index e1eafe2c530..57d2045675f 160000
--- a/core/src/main/resources/mappings
+++ b/core/src/main/resources/mappings
@@ -1 +1 @@
-Subproject commit e1eafe2c5304012d23acba80659459f7868fe2b1
+Subproject commit 57d2045675fc6f456729a89e39c0e676172510be
From 3236712e31bc9052bea2077778d68296aa499957 Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Tue, 12 Nov 2024 19:54:48 -0500
Subject: [PATCH 4/4] Grabbing a bundle from creative mode does work
---
.../inventory/BundleInventoryTranslator.java | 10 ++++------
.../translator/inventory/InventoryTranslator.java | 6 +++---
.../inventory/PlayerInventoryTranslator.java | 11 ++++++++++-
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
index b281945aaab..44477945f96 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BundleInventoryTranslator.java
@@ -41,7 +41,6 @@
import org.geysermc.geyser.inventory.click.ClickPlan;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.BundleCache;
-import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InventoryUtils;
import org.geysermc.geyser.util.thirdparty.Fraction;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
@@ -318,13 +317,12 @@ static boolean isBundle(ItemStackRequestSlotData slotData) {
return slotData.getContainerName().getContainer() == ContainerSlotType.DYNAMIC_CONTAINER;
}
- static boolean isBundle(GeyserSession session, ClickPlan plan, int slot) {
- return isBundle(session, plan.getItem(slot));
+ static boolean isBundle(ClickPlan plan, int slot) {
+ return isBundle(plan.getItem(slot));
}
- static boolean isBundle(GeyserSession session, GeyserItemStack stack) {
- // Client as of 1.21.3 does use this
- return session.getTagCache().is(ItemTag.BUNDLES, stack);
+ static boolean isBundle(GeyserItemStack stack) {
+ return stack.getBundleData() != null;
}
private BundleInventoryTranslator() {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
index 6cbc81957fe..16be81e121d 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
@@ -449,12 +449,12 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
if (InventoryUtils.canStack(cursor, plan.getItem(destSlot))) { //TODO: cannot simply swap if cursor stacks with slot (temp slot)
return rejectRequest(request);
}
- plan.add(isBundle(session, plan, destSlot) || isBundle(session, cursor) ? Click.RIGHT : Click.LEFT, destSlot);
+ plan.add(isBundle(plan, destSlot) || isBundle(cursor) ? Click.RIGHT : Click.LEFT, destSlot);
} else if (isDestCursor) { //swap cursor
if (InventoryUtils.canStack(cursor, plan.getItem(sourceSlot))) { //TODO
return rejectRequest(request);
}
- plan.add(isBundle(session, plan, sourceSlot) || isBundle(session, cursor) ? Click.RIGHT : Click.LEFT, sourceSlot);
+ plan.add(isBundle(plan, sourceSlot) || isBundle(cursor) ? Click.RIGHT : Click.LEFT, sourceSlot);
} else {
if (!cursor.isEmpty()) { //TODO: (temp slot)
return rejectRequest(request);
@@ -466,7 +466,7 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
return rejectRequest(request);
}
plan.add(Click.LEFT, sourceSlot); //pickup source into cursor
- plan.add(isBundle(session, plan, sourceSlot) || isBundle(session, plan, destSlot) ? Click.RIGHT : Click.LEFT, destSlot); //swap cursor with dest slot
+ plan.add(isBundle(plan, sourceSlot) || isBundle(plan, destSlot) ? Click.RIGHT : Click.LEFT, destSlot); //swap cursor with dest slot
plan.add(Click.LEFT, sourceSlot); //release cursor onto source
}
break;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
index 7eefffa3e3e..e8548745e8f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
@@ -423,6 +423,7 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
@Override
protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
ItemStack javaCreativeItem = null;
+ boolean bundle = false;
IntSet affectedSlots = new IntOpenHashSet();
CraftState craftState = CraftState.START;
for (ItemStackRequestAction action : request.getActions()) {
@@ -477,8 +478,10 @@ protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inve
if (isCursor(transferAction.getDestination())) {
if (session.getPlayerInventory().getCursor().isEmpty()) {
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
+ session.getBundleCache().initialize(newItemStack);
newItemStack.setAmount(transferAction.getCount());
session.getPlayerInventory().setCursor(newItemStack, session);
+ bundle = newItemStack.getBundleData() != null;
} else {
session.getPlayerInventory().getCursor().add(transferAction.getCount());
}
@@ -487,8 +490,10 @@ protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inve
int destSlot = bedrockSlotToJava(transferAction.getDestination());
if (inventory.getItem(destSlot).isEmpty()) {
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
+ session.getBundleCache().initialize(newItemStack);
newItemStack.setAmount(transferAction.getCount());
inventory.setItem(destSlot, newItemStack, session);
+ bundle = newItemStack.getBundleData() != null;
} else {
inventory.getItem(destSlot).add(transferAction.getCount());
}
@@ -528,7 +533,11 @@ protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inve
int slot = it.nextInt();
sendCreativeAction(session, inventory, slot);
}
- return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
+ // On the bundle check:
+ // We can also accept the request, but sending a bad request indicates to Geyser to refresh the inventory
+ // and we need to refresh the inventory to send the bundle ID/inventory to the client.
+ // It's not great, but I don't want to create a container class for request responses
+ return bundle ? rejectRequest(request, false) : acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
}
private static void sendCreativeAction(GeyserSession session, Inventory inventory, int slot) {