diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java index d17bc1267..af7da921e 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java @@ -20,6 +20,7 @@ import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerInventory; @@ -293,5 +294,6 @@ public interface ContainerItemContext { * * @return An unmodifiable list containing additional slots of this context. If no additional slot is available, the list is empty. */ + @UnmodifiableView List> getAdditionalSlots(); } diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java index 95af859ca..aee3a6e3e 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java @@ -21,6 +21,7 @@ import java.util.Objects; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.inventory.Inventory; @@ -28,7 +29,7 @@ import net.minecraft.inventory.SidedInventory; import net.minecraft.inventory.SimpleInventory; import net.minecraft.util.math.Direction; -import net.fabricmc.fabric.api.transfer.v1.storage.Storage; +import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage; import net.fabricmc.fabric.impl.transfer.item.InventoryStorageImpl; @@ -50,7 +51,7 @@ import net.fabricmc.fabric.impl.transfer.item.InventoryStorageImpl; */ @ApiStatus.Experimental @ApiStatus.NonExtendable -public interface InventoryStorage extends Storage { +public interface InventoryStorage extends SlottedStorage { /** * Return a wrapper around an {@link Inventory}. * @@ -69,11 +70,16 @@ public interface InventoryStorage extends Storage { * Retrieve an unmodifiable list of the wrappers for the slots in this inventory. * Each wrapper corresponds to a single slot in the inventory. */ + @Override + @UnmodifiableView List> getSlots(); - /** - * Retrieve a wrapper around a specific slot of the inventory. - */ + @Override + default int getSlotCount() { + return getSlots().size(); + } + + @Override default SingleSlotStorage getSlot(int slot) { return getSlots().get(slot); } diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java index 2b0c812c8..7a399f9f6 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java @@ -32,10 +32,12 @@ import net.minecraft.util.Identifier; import net.minecraft.util.math.Direction; import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; -import net.fabricmc.fabric.api.transfer.v1.storage.base.SidedStorageBlockEntity; import net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage; +import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.Storage; +import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedSlottedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage; +import net.fabricmc.fabric.api.transfer.v1.storage.base.SidedStorageBlockEntity; import net.fabricmc.fabric.impl.transfer.item.ComposterWrapper; import net.fabricmc.fabric.mixin.transfer.DoubleInventoryAccessor; @@ -119,10 +121,10 @@ public final class ItemStorage { // For double chests, we need to retrieve a wrapper for each part separately. if (inventoryToWrap instanceof DoubleInventoryAccessor accessor) { - Storage first = InventoryStorage.of(accessor.fabric_getFirst(), direction); - Storage second = InventoryStorage.of(accessor.fabric_getSecond(), direction); + SlottedStorage first = InventoryStorage.of(accessor.fabric_getFirst(), direction); + SlottedStorage second = InventoryStorage.of(accessor.fabric_getSecond(), direction); - return new CombinedStorage<>(List.of(first, second)); + return new CombinedSlottedStorage<>(List.of(first, second)); } } else { inventoryToWrap = inventory; diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/SlottedStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/SlottedStorage.java new file mode 100644 index 000000000..bb8cc60f9 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/SlottedStorage.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.transfer.v1.storage; + +import java.util.List; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnmodifiableView; + +import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage; +import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext; +import net.fabricmc.fabric.impl.transfer.TransferApiImpl; + +/** + * A {@link Storage} implementation made of indexed slots. + * + *

Please note that some storages may not implement this interface. + * It is up to the storage implementation to decide whether to implement this interface or not. + * Checking whether a storage is slotted can be done using {@code instanceof}. + * + * @param The type of the stored resources. + * + * Experimental feature, we reserve the right to remove or change it without further notice. + * The transfer API is a complex addition, and we want to be able to correct possible design mistakes. + */ +@ApiStatus.Experimental +public interface SlottedStorage extends Storage { + /** + * Retrieve the number of slots in this storage. + */ + int getSlotCount(); + + /** + * Retrieve a specific slot of this storage. + * + * @throws IndexOutOfBoundsException If the slot index is out of bounds. + */ + SingleSlotStorage getSlot(int slot); + + /** + * Retrieve a list containing all the slots of this storage. The list must not be modified. + * + *

This function can be used to interface with code that requires a slot list, + * for example {@link StorageUtil#insertStacking} or {@link ContainerItemContext#getAdditionalSlots()}. + * + *

It is guaranteed that calling this function is fast. + * The default implementation returns a view over the storage that delegates to {@link #getSlotCount} and {@link #getSlot}. + */ + @UnmodifiableView + default List> getSlots() { + return TransferApiImpl.makeListView(this); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java index 85e9b7493..5737d8c58 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java @@ -151,6 +151,39 @@ public interface Storage extends Iterable> { @Override Iterator> iterator(); + /** + * Same as {@link #iterator()}, but the iterator is guaranteed to skip over empty views, + * i.e. views that {@linkplain StorageView#isResourceBlank() contain blank resources} or have a zero {@linkplain StorageView#getAmount() amount}. + * + *

This can provide a large performance benefit over {@link #iterator()} if the caller is only interested in non-empty views, + * for example because it is trying to extract resources from the storage. + * + *

This function should only be overridden if the storage is able to provide an optimized iterator over non-empty views, + * for example because it is keeping an index of non-empty views. + * Otherwise, the default implementation simply calls {@link #iterator()} and filters out empty views. + * + *

When implementing this function, note that the guarantees of {@link #iterator()} still apply. + * In particular, {@link #insert} and {@link #extract} may be called safely during iteration. + * + * @return An iterator over the non-empty views of this storage. Calling remove on the iterator is not allowed. + */ + default Iterator> nonEmptyIterator() { + return TransferApiImpl.filterEmptyViews(iterator()); + } + + /** + * Convenient helper to get an {@link Iterable} over the {@linkplain #nonEmptyIterator() non-empty views} of this storage, for use in for-each loops. + * + *

{@code
+	 * for (StorageView view : storage.nonEmptyViews()) {
+	 *     // Do something with the view
+	 * }
+	 * }
+ */ + default Iterable> nonEmptyViews() { + return this::nonEmptyIterator; + } + /** * Return a view over this storage, for a specific resource, or {@code null} if none is quickly available. * diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java index 112d72c1c..5a7afe054 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java @@ -88,8 +88,7 @@ public final class StorageUtil { long totalMoved = 0; try (Transaction iterationTransaction = Transaction.openNested(transaction)) { - for (StorageView view : from) { - if (view.isResourceBlank()) continue; + for (StorageView view : from.nonEmptyViews()) { T resource = view.getResource(); if (!filter.test(resource)) continue; long maxExtracted; @@ -124,6 +123,32 @@ public final class StorageUtil { return totalMoved; } + /** + * Try to extract any resource from a storage, up to a maximum amount. + * + *

This function will only ever pull from one storage view of the storage, even if multiple storage views contain the same resource. + * + * @param storage The storage, may be null. + * @param maxAmount The maximum to extract. + * @param transaction The transaction this operation is part of. + * @return A non-blank resource and the strictly positive amount of it that was extracted from the storage, + * or {@code null} if none could be found. + */ + @Nullable + public static ResourceAmount extractAny(@Nullable Storage storage, long maxAmount, TransactionContext transaction) { + StoragePreconditions.notNegative(maxAmount); + + if (storage == null) return null; + + for (StorageView view : storage.nonEmptyViews()) { + T resource = view.getResource(); + long amount = view.extract(resource, maxAmount, transaction); + if (amount > 0) return new ResourceAmount<>(resource, amount); + } + + return null; + } + /** * Try to insert up to some amount of a resource into a list of storage slots, trying to "stack" first, * i.e. prioritizing slots that already contain the resource. @@ -131,7 +156,7 @@ public final class StorageUtil { * @return How much was inserted. * @see Storage#insert */ - public static long insertStacking(List> slots, T resource, long maxAmount, TransactionContext transaction) { + public static long insertStacking(List> slots, T resource, long maxAmount, TransactionContext transaction) { StoragePreconditions.notNegative(maxAmount); long amount = 0; @@ -150,6 +175,27 @@ public final class StorageUtil { return amount; } + /** + * Insert resources in a storage, attempting to stack them with existing resources first if possible. + * + * @param storage The storage, may be null. + * @param resource The resource to insert. May not be blank. + * @param maxAmount The maximum amount of resource to insert. May not be negative. + * @param transaction The transaction this operation is part of. + * @return A nonnegative integer not greater than maxAmount: the amount that was inserted. + */ + public static long tryInsertStacking(@Nullable Storage storage, T resource, long maxAmount, TransactionContext transaction) { + StoragePreconditions.notNegative(maxAmount); + + if (storage instanceof SlottedStorage slottedStorage) { + return insertStacking(slottedStorage.getSlots(), resource, maxAmount, transaction); + } else if (storage != null) { + return storage.insert(resource, maxAmount, transaction); + } else { + return 0; + } + } + /** * Attempt to find a resource stored in the passed storage. * @@ -174,8 +220,8 @@ public final class StorageUtil { Objects.requireNonNull(filter, "Filter may not be null"); if (storage == null) return null; - for (StorageView view : storage) { - if (!view.isResourceBlank() && filter.test(view.getResource())) { + for (StorageView view : storage.nonEmptyViews()) { + if (filter.test(view.getResource())) { return view.getResource(); } } @@ -209,11 +255,11 @@ public final class StorageUtil { if (storage == null) return null; try (Transaction nested = Transaction.openNested(transaction)) { - for (StorageView view : storage) { + for (StorageView view : storage.nonEmptyViews()) { // Extract below could change the resource, so we have to query it before extracting. T resource = view.getResource(); - if (!view.isResourceBlank() && filter.test(resource) && view.extract(resource, Long.MAX_VALUE, nested) > 0) { + if (filter.test(resource) && view.extract(resource, Long.MAX_VALUE, nested) > 0) { // Will abort the extraction. return resource; } diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedSlottedStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedSlottedStorage.java new file mode 100644 index 000000000..cce6841c6 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedSlottedStorage.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.transfer.v1.storage.base; + +import java.util.List; + +import org.jetbrains.annotations.ApiStatus; + +import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage; +import net.fabricmc.fabric.api.transfer.v1.storage.Storage; + +/** + * A {@link Storage} wrapping multiple slotted storages. + * Same as {@link CombinedStorage}, but for {@link SlottedStorage}s. + * + * @param The type of the stored resources. + * @param The class of every part. {@code ? extends Storage} can be used if the parts are of different types. + * + * Experimental feature, we reserve the right to remove or change it without further notice. + * The transfer API is a complex addition, and we want to be able to correct possible design mistakes. + */ +@ApiStatus.Experimental +public class CombinedSlottedStorage> extends CombinedStorage implements SlottedStorage { + public CombinedSlottedStorage(List parts) { + super(parts); + } + + @Override + public int getSlotCount() { + int count = 0; + + for (S part : parts) { + count += part.getSlotCount(); + } + + return count; + } + + @Override + public SingleSlotStorage getSlot(int slot) { + int updatedSlot = slot; + + for (SlottedStorage part : parts) { + if (updatedSlot < part.getSlotCount()) { + return part.getSlot(updatedSlot); + } + + updatedSlot -= part.getSlotCount(); + } + + throw new IndexOutOfBoundsException("Slot " + slot + " is out of bounds. This storage has size " + getSlotCount()); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleSlotStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleSlotStorage.java index 30e124745..1b53cf330 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleSlotStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleSlotStorage.java @@ -20,7 +20,7 @@ import java.util.Iterator; import org.jetbrains.annotations.ApiStatus; -import net.fabricmc.fabric.api.transfer.v1.storage.Storage; +import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.StorageView; import net.fabricmc.fabric.impl.transfer.TransferApiImpl; @@ -34,9 +34,23 @@ import net.fabricmc.fabric.impl.transfer.TransferApiImpl; * The transfer API is a complex addition, and we want to be able to correct possible design mistakes. */ @ApiStatus.Experimental -public interface SingleSlotStorage extends Storage, StorageView { +public interface SingleSlotStorage extends SlottedStorage, StorageView { @Override default Iterator> iterator() { return TransferApiImpl.singletonIterator(this); } + + @Override + default int getSlotCount() { + return 1; + } + + @Override + default SingleSlotStorage getSlot(int slot) { + if (slot != 0) { + throw new IndexOutOfBoundsException("Slot " + slot + " does not exist in a single-slot storage."); + } + + return this; + } } diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java index 759de3086..fcb98b4f2 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java @@ -47,6 +47,9 @@ public abstract class SingleVariantStorage> extends /** * Return the blank variant. + * + *

Note: this is called very early in the constructor. + * If fields need to be accessed from this function, make sure to re-initialize {@link #variant} yourself. */ protected abstract T getBlankVariant(); diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/TransferApiImpl.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/TransferApiImpl.java index 600c34917..148fcef85 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/TransferApiImpl.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/TransferApiImpl.java @@ -16,16 +16,20 @@ package net.fabricmc.fabric.impl.transfer; +import java.util.AbstractList; import java.util.Collections; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.LoggerFactory; import org.slf4j.Logger; +import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.Storage; import net.fabricmc.fabric.api.transfer.v1.storage.StorageView; +import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage; import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext; public class TransferApiImpl { @@ -64,11 +68,6 @@ public class TransferApiImpl { } }; - /** - * Not null when writing to an inventory in a transaction, null otherwise. - */ - public static final ThreadLocal SUPPRESS_SPECIAL_LOGIC = new ThreadLocal<>(); - public static Iterator singletonIterator(T it) { return new Iterator() { boolean hasNext = true; @@ -89,4 +88,56 @@ public class TransferApiImpl { } }; } + + public static Iterator> filterEmptyViews(Iterator> iterator) { + return new Iterator<>() { + StorageView next; + + { + findNext(); + } + + private void findNext() { + while (iterator.hasNext()) { + next = iterator.next(); + + if (next.getAmount() > 0 && !next.isResourceBlank()) { + return; + } + } + + next = null; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public StorageView next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + StorageView ret = next; + findNext(); + return ret; + } + }; + } + + public static List> makeListView(SlottedStorage storage) { + return new AbstractList<>() { + @Override + public SingleSlotStorage get(int index) { + return storage.getSlot(index); + } + + @Override + public int size() { + return storage.getSlotCount(); + } + }; + } } diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/gametests/VanillaStorageTests.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/gametests/VanillaStorageTests.java index d2cd9b779..b078b2a80 100644 --- a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/gametests/VanillaStorageTests.java +++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/gametests/VanillaStorageTests.java @@ -25,6 +25,7 @@ import net.minecraft.block.ComparatorBlock; import net.minecraft.block.entity.BrewingStandBlockEntity; import net.minecraft.block.entity.ChiseledBookshelfBlockEntity; import net.minecraft.block.entity.FurnaceBlockEntity; +import net.minecraft.block.entity.HopperBlockEntity; import net.minecraft.block.entity.ShulkerBoxBlockEntity; import net.minecraft.inventory.Inventory; import net.minecraft.item.ItemStack; @@ -41,6 +42,7 @@ import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant; +import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.Storage; import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; import net.fabricmc.fabric.test.transfer.mixin.AbstractFurnaceBlockEntityAccessor; @@ -289,6 +291,23 @@ public class VanillaStorageTests { tx.commit(); } + // Check that the inventory and slotted storages match + Inventory inventory = HopperBlockEntity.getInventoryAt(context.getWorld(), context.getAbsolutePos(chestPos)); + context.assertTrue(inventory != null, "Inventory must not be null"); + + if (!(storage instanceof SlottedStorage slottedStorage)) { + throw new GameTestException("Double chest storage must be a SlottedStorage"); + } + + for (int i = 0; i < inventory.size(); ++i) { + ItemStack stack = inventory.getStack(i); + ItemVariant variant = ItemVariant.of(stack.getItem()); + context.assertTrue(variant.matches(stack), "Item variant in slot " + i + " must match stack"); + long expectedCount = stack.getCount(); + long actualCount = slottedStorage.getSlot(i).getAmount(); + context.assertTrue(expectedCount == actualCount, "Slot " + i + " should have " + expectedCount + " items, but has " + actualCount); + } + // Check that an update is queued for every single comparator MutableInt comparatorCount = new MutableInt();