Skip to content

Commit 7b3d111

Browse files
authored
Respect item MAX_STACK_SIZE in Storage.insert (#5220)
* Respect item MAX_STACK_SIZE in Storage.insert This fixes the issue described in #5219. When an item has a stack size specified via DataComponents.MAX_STACK_SIZE, if the stack size differs from the item's default stack size, the updated `transfer` methods could accidentally cause items to be deleted after transfers. This would occur if the target inventory already contained an identical item. Since getCapacity used the item type's default size (rather than the size specified by the item DataComponents), the insert logic would see plenty of room in a slot already containing items, merge the new item into that slot. After this occurs, the slot would claim to hold (e.g.) 2 of a max-stack-1 item. After the next inventory update, the extra item was silently lost. Changing the getCapacity logic to call toStack() on the variant correctly prevents the faulty merge and fixes the bug. * Add getMaxStackSize to ItemVariantImpl This is utilized in various implementations of getCapacity in the Storage API to ensure that getCapacity respects stack sizes that have been set on items via DataComponents. * Tweak getCapacity docstring * reorder imports in SingleStackStorage for linter
1 parent 796c762 commit 7b3d111

7 files changed

Lines changed: 97 additions & 6 deletions

File tree

fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/WrappedInventory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public ItemStack tryInsert(ItemStack input, boolean simulate) {
6161
ItemStack stack = inv.getStack(i);
6262

6363
if (stack.isEmpty() || ItemStack.areItemsAndComponentsEqual(stack, input)) {
64-
int remainingSpace = Math.min(inv.getMaxCountPerStack(), stack.getItem().getMaxCount()) - stack.getCount();
64+
int remainingSpace = Math.min(inv.getMaxCountPerStack(), stack.getMaxCount()) - stack.getCount();
6565
int inserted = Math.min(remainingSpace, input.getCount());
6666

6767
if (!simulate) {

fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleStackStorage.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
2424
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
2525
import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant;
26+
import net.fabricmc.fabric.impl.transfer.item.ItemVariantImpl;
2627

2728
/**
2829
* An item variant storage backed by an {@link ItemStack}.
@@ -69,12 +70,12 @@ protected boolean canExtract(ItemVariant itemVariant) {
6970
*
7071
* <p>If the capacity should be limited by the max count of the item, this function must take it into account.
7172
* For example, a storage with a maximum count of 4, or less for items that have a smaller max count,
72-
* should override this to return {@code Math.min(itemVariant.getItem().getMaxCount(), 4);}.
73+
* should override this to return {@code Math.min(super.getCapacity(itemVariant), 4);}.
7374
*
7475
* @return The maximum capacity of this storage for the passed item variant.
7576
*/
7677
protected int getCapacity(ItemVariant itemVariant) {
77-
return itemVariant.getItem().getMaxCount();
78+
return ItemVariantImpl.getMaxStackSize(itemVariant);
7879
}
7980

8081
@Override

fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ContainerComponentStorage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public long getAmount() {
166166

167167
@Override
168168
public long getCapacity() {
169-
return getStack().getItem().getMaxCount();
169+
return getStack().getMaxCount();
170170
}
171171

172172
@Override

fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventorySlotWrapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public int getCapacity(ItemVariant variant) {
121121
return 1;
122122
}
123123

124-
return Math.min(storage.inventory.getMaxCountPerStack(), variant.getItem().getMaxCount());
124+
return Math.min(storage.inventory.getMaxCountPerStack(), ItemVariantImpl.getMaxStackSize(variant));
125125
}
126126

127127
// We override updateSnapshots to also schedule a markDirty call for the backing inventory.

fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import net.minecraft.component.ComponentChanges;
2424
import net.minecraft.component.ComponentMap;
25+
import net.minecraft.component.DataComponentTypes;
2526
import net.minecraft.item.Item;
2627
import net.minecraft.item.ItemStack;
2728
import net.minecraft.item.Items;
@@ -108,6 +109,14 @@ public int hashCode() {
108109
return hashCode;
109110
}
110111

112+
/**
113+
* Return the max stack size for this variant, respecting component overrides such as
114+
* {@link DataComponentTypes#MAX_STACK_SIZE}.
115+
*/
116+
public static int getMaxStackSize(ItemVariant variant) {
117+
return variant.getComponentMap().getOrDefault(DataComponentTypes.MAX_STACK_SIZE, variant.getItem().getMaxCount());
118+
}
119+
111120
public ItemStack getCachedStack() {
112121
ItemStack ret = cachedStack;
113122

fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/PlayerInventoryStorageImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ protected void onFinalCommit() {
132132
long remainder = entry.amount;
133133

134134
while (remainder > 0) {
135-
int dropped = (int) Math.min(entry.key.getItem().getMaxCount(), remainder);
135+
int dropped = (int) Math.min(ItemVariantImpl.getMaxStackSize(entry.key), remainder);
136136
playerInventory.player.dropItem(entry.key.toStack(dropped), entry.throwRandomly, entry.retainOwnership);
137137
remainder -= dropped;
138138
}

fabric-transfer-api-v1/src/test/java/net/fabricmc/fabric/test/transfer/unittests/ItemTests.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,87 @@ public void testLimitedStackCountItem() {
245245
}
246246
}
247247

248+
/**
249+
* Test that {@link DataComponentTypes#MAX_STACK_SIZE} overrides on an item variant are respected.
250+
* Items that are normally stackable (e.g. diamonds, max 64) but have a component override
251+
* reducing their max stack size (e.g. to 1) should not be merged into existing stacks.
252+
*
253+
* <p>This is a regression test for a bug where {@code Item.getMaxCount()} was used instead of
254+
* {@code ItemStack.getMaxCount()}, causing the component override to be ignored.
255+
*/
256+
@Test
257+
public void testComponentMaxStackSizeOverride() {
258+
// Create a variant of a normally-stackable item (diamond, default max 64)
259+
// with MAX_STACK_SIZE overridden to 1 via components.
260+
ComponentChanges components = ComponentChanges.builder()
261+
.add(DataComponentTypes.MAX_STACK_SIZE, 1)
262+
.build();
263+
ItemVariant unstackableDiamond = ItemVariant.of(Items.DIAMOND, components);
264+
265+
// Also create a normal diamond variant for comparison.
266+
ItemVariant normalDiamond = ItemVariant.of(Items.DIAMOND);
267+
268+
// Test 1: Inserting into a fresh inventory should place 1 per slot.
269+
SimpleInventory inventory = new SimpleInventory(3);
270+
InventoryStorage wrapper = InventoryStorage.of(inventory, null);
271+
272+
try (Transaction transaction = Transaction.openOuter()) {
273+
// Try to insert 3 unstackable diamonds. Should go into 3 separate slots.
274+
long inserted = wrapper.insert(unstackableDiamond, 3, transaction);
275+
276+
if (inserted != 3) {
277+
throw new AssertionError("Should have inserted 3 unstackable diamonds, but inserted " + inserted);
278+
}
279+
280+
// Verify each slot has exactly 1.
281+
for (int i = 0; i < 3; i++) {
282+
ItemStack stack = inventory.getStack(i);
283+
284+
if (stack.getCount() != 1) {
285+
throw new AssertionError("Slot " + i + " should have count 1, but has " + stack.getCount());
286+
}
287+
}
288+
289+
transaction.commit();
290+
}
291+
292+
// Test 2: Inserting into a slot that already has one should NOT merge.
293+
SimpleInventory inventory2 = new SimpleInventory(1);
294+
InventoryStorage wrapper2 = InventoryStorage.of(inventory2, null);
295+
296+
try (Transaction transaction = Transaction.openOuter()) {
297+
// Insert one first.
298+
long first = wrapper2.insert(unstackableDiamond, 1, transaction);
299+
300+
if (first != 1) {
301+
throw new AssertionError("First insert should have succeeded.");
302+
}
303+
304+
// Try to insert another into the same single-slot inventory. Should fail.
305+
long second = wrapper2.insert(unstackableDiamond, 1, transaction);
306+
307+
if (second != 0) {
308+
throw new AssertionError("Second insert should have returned 0 (slot full), but inserted " + second);
309+
}
310+
311+
transaction.commit();
312+
}
313+
314+
// Test 3: Normal diamonds (no component override) should still stack to 64.
315+
SimpleInventory inventory3 = new SimpleInventory(1);
316+
InventoryStorage wrapper3 = InventoryStorage.of(inventory3, null);
317+
318+
try (Transaction transaction = Transaction.openOuter()) {
319+
long inserted = wrapper3.insert(normalDiamond, 100, transaction);
320+
321+
if (inserted != 64) {
322+
throw new AssertionError("Normal diamonds should stack to 64, but inserted " + inserted);
323+
}
324+
325+
transaction.commit();
326+
}
327+
}
328+
248329
private static class LimitedStackCountInventory extends SimpleInventory {
249330
LimitedStackCountInventory(int size) {
250331
super(size);

0 commit comments

Comments
 (0)