diff --git a/patches/net/minecraft/world/item/component/ItemContainerContents.java.patch b/patches/net/minecraft/world/item/component/ItemContainerContents.java.patch new file mode 100644 index 0000000000..8a1eea6328 --- /dev/null +++ b/patches/net/minecraft/world/item/component/ItemContainerContents.java.patch @@ -0,0 +1,41 @@ +--- a/net/minecraft/world/item/component/ItemContainerContents.java ++++ b/net/minecraft/world/item/component/ItemContainerContents.java +@@ -146,6 +_,38 @@ + return this.hashCode; + } + ++ // Neo Start ++ ++ /** ++ * {@return the number of slots in this container} ++ */ ++ public int getSlots() { ++ return this.items.size(); ++ } ++ ++ /** ++ * Gets a copy of the stack at a particular slot. ++ * ++ * @param slot The slot to check. Must be within [0, {@link #getSlots()}] ++ * @return A copy of the stack in that slot ++ * @throws UnsupportedOperationException if the provided slot index is out-of-bounds. ++ */ ++ public ItemStack getStackInSlot(int slot) { ++ validateSlotIndex(slot); ++ return this.items.get(slot).copy(); ++ } ++ ++ /** ++ * Throws {@link UnsupportedOperationException} if the provided slot index is invalid. ++ */ ++ private void validateSlotIndex(int slot) { ++ if (slot < 0 || slot >= getSlots()) { ++ throw new UnsupportedOperationException("Slot " + slot + " not in valid range - [0," + getSlots() + ")"); ++ } ++ } ++ ++ // Neo End ++ + static record Slot(int index, ItemStack item) { + public static final Codec CODEC = RecordCodecBuilder.create( + p_331695_ -> p_331695_.group( diff --git a/src/main/java/net/neoforged/neoforge/capabilities/CapabilityHooks.java b/src/main/java/net/neoforged/neoforge/capabilities/CapabilityHooks.java index 7d53bf075c..b0a0a9b1f2 100644 --- a/src/main/java/net/neoforged/neoforge/capabilities/CapabilityHooks.java +++ b/src/main/java/net/neoforged/neoforge/capabilities/CapabilityHooks.java @@ -6,6 +6,7 @@ package net.neoforged.neoforge.capabilities; import java.util.List; +import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.WorldlyContainerHolder; @@ -23,6 +24,7 @@ import net.neoforged.neoforge.event.level.ChunkEvent; import net.neoforged.neoforge.event.tick.LevelTickEvent; import net.neoforged.neoforge.fluids.capability.wrappers.FluidBucketWrapper; +import net.neoforged.neoforge.items.ComponentItemHandler; import net.neoforged.neoforge.items.VanillaHopperItemHandler; import net.neoforged.neoforge.items.wrapper.CombinedInvWrapper; import net.neoforged.neoforge.items.wrapper.EntityArmorInvWrapper; @@ -30,7 +32,6 @@ import net.neoforged.neoforge.items.wrapper.ForwardingItemHandler; import net.neoforged.neoforge.items.wrapper.InvWrapper; import net.neoforged.neoforge.items.wrapper.PlayerInvWrapper; -import net.neoforged.neoforge.items.wrapper.ShulkerItemStackInvWrapper; import net.neoforged.neoforge.items.wrapper.SidedInvWrapper; import org.jetbrains.annotations.ApiStatus; @@ -131,7 +132,7 @@ else if (entity instanceof LivingEntity livingEntity) if (NeoForgeMod.MILK.isBound()) { event.registerItem(Capabilities.FluidHandler.ITEM, (stack, ctx) -> new FluidBucketWrapper(stack), Items.MILK_BUCKET); } - event.registerItem(Capabilities.ItemHandler.ITEM, (stack, ctx) -> new ShulkerItemStackInvWrapper(stack), + event.registerItem(Capabilities.ItemHandler.ITEM, (stack, ctx) -> new ComponentItemHandler(stack, DataComponents.CONTAINER, 27), Items.SHULKER_BOX, Items.BLACK_SHULKER_BOX, Items.BLUE_SHULKER_BOX, diff --git a/src/main/java/net/neoforged/neoforge/items/ComponentItemHandler.java b/src/main/java/net/neoforged/neoforge/items/ComponentItemHandler.java new file mode 100644 index 0000000000..b20b7a0161 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/items/ComponentItemHandler.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.items; + +import com.google.common.base.Preconditions; +import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.ItemContainerContents; +import net.neoforged.neoforge.capabilities.ICapabilityProvider; +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; +import net.neoforged.neoforge.common.MutableDataComponentHolder; + +/** + * Variant of {@link ItemStackHandler} for use with data components. + *

+ * The actual data storage is managed by a data component, and all changes will write back to that component. + *

+ * To use this class, register a new {@link DataComponentType} which holds an {@link ItemContainerContents} for your item. + * Then reference that component from your {@link ICapabilityProvider} passed to {@link RegisterCapabilitiesEvent#registerItem} to create an instance of this class. + * + * @implNote All functions in this class should attempt to minimize component read/writes to avoid unnecessary churn, noting that the component can never be cached. + */ +public class ComponentItemHandler implements IItemHandlerModifiable { + protected final MutableDataComponentHolder parent; + protected final DataComponentType component; + protected final int size; + + /** + * Creates a new {@link ComponentItemHandler} with target size. If the existing component is smaller than the given size, it will be expanded on write. + * + * @param parent The parent component holder, such as an {@link ItemStack} + * @param component The data component referencing the stored inventory of the item stack + * @param size The number of slots. Must be less than 256 due to limitations of {@link ItemContainerContents} + */ + public ComponentItemHandler(MutableDataComponentHolder parent, DataComponentType component, int size) { + this.parent = parent; + this.component = component; + this.size = size; + Preconditions.checkArgument(size <= 256, "The max size of ItemContainerContents is 256 slots."); + } + + @Override + public int getSlots() { + return this.size; + } + + @Override + public ItemStack getStackInSlot(int slot) { + ItemContainerContents contents = this.getContents(); + return this.getStackFromContents(contents, slot); + } + + @Override + public void setStackInSlot(int slot, ItemStack stack) { + this.validateSlotIndex(slot); + if (!this.isItemValid(slot, stack)) { + throw new RuntimeException("Invalid stack " + stack + " for slot " + slot + ")"); + } + ItemContainerContents contents = this.getContents(); + ItemStack existing = this.getStackFromContents(contents, slot); + if (!ItemStack.matches(stack, existing)) { + this.updateContents(contents, stack, slot); + } + } + + @Override + public ItemStack insertItem(int slot, ItemStack toInsert, boolean simulate) { + this.validateSlotIndex(slot); + + if (toInsert.isEmpty()) { + return ItemStack.EMPTY; + } + + if (!this.isItemValid(slot, toInsert)) { + return toInsert; + } + + ItemContainerContents contents = this.getContents(); + ItemStack existing = this.getStackFromContents(contents, slot); + // Max amount of the stack that could be inserted + int insertLimit = Math.min(this.getSlotLimit(slot), toInsert.getMaxStackSize()); + + if (!existing.isEmpty()) { + if (!ItemStack.isSameItemSameComponents(toInsert, existing)) { + return toInsert; + } + + insertLimit -= existing.getCount(); + } + + if (insertLimit <= 0) { + return toInsert; + } + + int inserted = Math.min(insertLimit, toInsert.getCount()); + + if (!simulate) { + this.updateContents(contents, toInsert.copyWithCount(existing.getCount() + inserted), slot); + } + + return toInsert.copyWithCount(toInsert.getCount() - inserted); + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + this.validateSlotIndex(slot); + + if (amount == 0) { + return ItemStack.EMPTY; + } + + ItemContainerContents contents = this.getContents(); + ItemStack existing = this.getStackFromContents(contents, slot); + + if (existing.isEmpty()) { + return ItemStack.EMPTY; + } + + int toExtract = Math.min(amount, existing.getMaxStackSize()); + + if (!simulate) { + this.updateContents(contents, existing.copyWithCount(existing.getCount() - toExtract), slot); + } + + return existing.copyWithCount(toExtract); + } + + @Override + public int getSlotLimit(int slot) { + return Item.ABSOLUTE_MAX_STACK_SIZE; + } + + @Override + public boolean isItemValid(int slot, ItemStack stack) { + return stack.getItem().canFitInsideContainerItems(); + } + + /** + * Called from {@link #updateContents} after the stack stored in a slot has been updated. + *

+ * Modifications to the stacks used as parameters here will not write-back to the stored data. + * + * @param slot The slot that changed + * @param oldStack The old stack that was present in the slot + * @param newStack The new stack that is now present in the slot + */ + protected void onContentsChanged(int slot, ItemStack oldStack, ItemStack newStack) {} + + /** + * Retrieves the {@link ItemContainerContents} from the parent object's data component map. + */ + protected ItemContainerContents getContents() { + return this.parent.getOrDefault(this.component, ItemContainerContents.EMPTY); + } + + /** + * Retrieves a copy of a single stack from the underlying data component, returning {@link ItemStack#EMPTY} if the component does not have a slot present. + *

+ * Throws an exception if the slot is out-of-bounds for this capability. + * + * @param contents The existing contents from {@link #getContents()} + * @param slot The target slot + * @return A copy of the stack in the target slot + */ + protected ItemStack getStackFromContents(ItemContainerContents contents, int slot) { + this.validateSlotIndex(slot); + return contents.getSlots() <= slot ? ItemStack.EMPTY : contents.getStackInSlot(slot); + } + + /** + * Performs a copy and write operation on the underlying data component, changing the stack in the target slot. + *

+ * If the existing component is larger than {@link #getSlots()}, additional slots will not be truncated. + * + * @param contents The existing contents from {@link #getContents()} + * @param stack The new stack to set to the slot + * @param slot The target slot + */ + protected void updateContents(ItemContainerContents contents, ItemStack stack, int slot) { + this.validateSlotIndex(slot); + // Use the max of the contents slots and the capability slots to avoid truncating + NonNullList list = NonNullList.withSize(Math.max(contents.getSlots(), this.getSlots()), ItemStack.EMPTY); + contents.copyInto(list); + ItemStack oldStack = list.get(slot); + list.set(slot, stack); + this.parent.set(this.component, ItemContainerContents.fromItems(list)); + this.onContentsChanged(slot, oldStack, stack); + } + + /** + * Throws {@link UnsupportedOperationException} if the provided slot index is invalid. + */ + protected final void validateSlotIndex(int slot) { + if (slot < 0 || slot >= getSlots()) { + throw new RuntimeException("Slot " + slot + " not in valid range - [0," + getSlots() + ")"); + } + } +} diff --git a/src/main/java/net/neoforged/neoforge/items/ItemHandlerHelper.java b/src/main/java/net/neoforged/neoforge/items/ItemHandlerHelper.java index b9d2474657..2b743a769c 100644 --- a/src/main/java/net/neoforged/neoforge/items/ItemHandlerHelper.java +++ b/src/main/java/net/neoforged/neoforge/items/ItemHandlerHelper.java @@ -30,18 +30,20 @@ public static ItemStack insertItem(IItemHandler dest, ItemStack stack, boolean s return stack; } + /** + * @deprecated Use {@link ItemStack#isSameItemSameComponents(ItemStack, ItemStack)} + */ @Deprecated(forRemoval = true, since = "1.20.5") public static boolean canItemStacksStack(ItemStack a, ItemStack b) { return ItemStack.isSameItemSameComponents(a, b); } + /** + * @deprecated Use {@link ItemStack#copyWithCount(int)} + */ @Deprecated(forRemoval = true, since = "1.20.5") - public static ItemStack copyStackWithSize(ItemStack itemStack, int size) { - if (size == 0) - return ItemStack.EMPTY; - ItemStack copy = itemStack.copy(); - copy.setCount(size); - return copy; + public static ItemStack copyStackWithSize(ItemStack stack, int count) { + return stack.copyWithCount(count); } /** diff --git a/src/main/java/net/neoforged/neoforge/items/ItemStackHandler.java b/src/main/java/net/neoforged/neoforge/items/ItemStackHandler.java index a389f05508..4c38a3d74b 100644 --- a/src/main/java/net/neoforged/neoforge/items/ItemStackHandler.java +++ b/src/main/java/net/neoforged/neoforge/items/ItemStackHandler.java @@ -10,6 +10,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.neoforged.neoforge.common.util.INBTSerializable; @@ -122,7 +123,7 @@ public ItemStack extractItem(int slot, int amount, boolean simulate) { @Override public int getSlotLimit(int slot) { - return 64; + return Item.ABSOLUTE_MAX_STACK_SIZE; } protected int getStackLimit(int slot, ItemStack stack) { diff --git a/src/main/java/net/neoforged/neoforge/items/wrapper/CombinedInvWrapper.java b/src/main/java/net/neoforged/neoforge/items/wrapper/CombinedInvWrapper.java index 1ecd322e43..ddea638822 100644 --- a/src/main/java/net/neoforged/neoforge/items/wrapper/CombinedInvWrapper.java +++ b/src/main/java/net/neoforged/neoforge/items/wrapper/CombinedInvWrapper.java @@ -40,7 +40,7 @@ protected int getIndexForSlot(int slot) { protected IItemHandlerModifiable getHandlerFromIndex(int index) { if (index < 0 || index >= itemHandler.length) { - return (IItemHandlerModifiable) EmptyHandler.INSTANCE; + return (IItemHandlerModifiable) EmptyItemHandler.INSTANCE; } return itemHandler[index]; } diff --git a/src/main/java/net/neoforged/neoforge/items/wrapper/EmptyHandler.java b/src/main/java/net/neoforged/neoforge/items/wrapper/EmptyItemHandler.java similarity index 88% rename from src/main/java/net/neoforged/neoforge/items/wrapper/EmptyHandler.java rename to src/main/java/net/neoforged/neoforge/items/wrapper/EmptyItemHandler.java index f81efcd660..9d1aff9ce5 100644 --- a/src/main/java/net/neoforged/neoforge/items/wrapper/EmptyHandler.java +++ b/src/main/java/net/neoforged/neoforge/items/wrapper/EmptyItemHandler.java @@ -9,8 +9,8 @@ import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.IItemHandlerModifiable; -public class EmptyHandler implements IItemHandlerModifiable { - public static final IItemHandler INSTANCE = new EmptyHandler(); +public class EmptyItemHandler implements IItemHandlerModifiable { + public static final IItemHandler INSTANCE = new EmptyItemHandler(); @Override public int getSlots() { diff --git a/src/main/java/net/neoforged/neoforge/items/wrapper/EntityEquipmentInvWrapper.java b/src/main/java/net/neoforged/neoforge/items/wrapper/EntityEquipmentInvWrapper.java index 138bce6512..e4c10bf997 100644 --- a/src/main/java/net/neoforged/neoforge/items/wrapper/EntityEquipmentInvWrapper.java +++ b/src/main/java/net/neoforged/neoforge/items/wrapper/EntityEquipmentInvWrapper.java @@ -10,6 +10,7 @@ import java.util.List; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.IItemHandlerModifiable; @@ -124,7 +125,7 @@ public ItemStack extractItem(final int slot, final int amount, final boolean sim @Override public int getSlotLimit(final int slot) { final EquipmentSlot equipmentSlot = validateSlotIndex(slot); - return equipmentSlot.getType() == EquipmentSlot.Type.ARMOR ? 1 : 64; + return equipmentSlot.getType() == EquipmentSlot.Type.ARMOR ? 1 : Item.ABSOLUTE_MAX_STACK_SIZE; } protected int getStackLimit(final int slot, final ItemStack stack) { diff --git a/src/main/java/net/neoforged/neoforge/items/wrapper/ShulkerItemStackInvWrapper.java b/src/main/java/net/neoforged/neoforge/items/wrapper/ShulkerItemStackInvWrapper.java deleted file mode 100644 index 16b5b2a949..0000000000 --- a/src/main/java/net/neoforged/neoforge/items/wrapper/ShulkerItemStackInvWrapper.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.items.wrapper; - -import net.minecraft.core.NonNullList; -import net.minecraft.core.component.DataComponents; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.component.ItemContainerContents; -import net.neoforged.neoforge.items.IItemHandlerModifiable; -import net.neoforged.neoforge.items.ItemHandlerHelper; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public class ShulkerItemStackInvWrapper implements IItemHandlerModifiable { - private final ItemStack stack; - - public ShulkerItemStackInvWrapper(ItemStack stack) { - this.stack = stack; - } - - @Override - public int getSlots() { - return 27; - } - - @Override - public ItemStack getStackInSlot(int slot) { - validateSlotIndex(slot); - return getItemList().get(slot); - } - - @Override - public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { - if (stack.isEmpty()) - return ItemStack.EMPTY; - - if (!isItemValid(slot, stack)) - return stack; - - validateSlotIndex(slot); - - NonNullList itemStacks = getItemList(); - - ItemStack existing = itemStacks.get(slot); - - int limit = Math.min(getSlotLimit(slot), stack.getMaxStackSize()); - - if (!existing.isEmpty()) { - if (!ItemHandlerHelper.canItemStacksStack(stack, existing)) - return stack; - - limit -= existing.getCount(); - } - - if (limit <= 0) - return stack; - - boolean reachedLimit = stack.getCount() > limit; - - if (!simulate) { - if (existing.isEmpty()) { - itemStacks.set(slot, reachedLimit ? ItemHandlerHelper.copyStackWithSize(stack, limit) : stack); - } else { - existing.grow(reachedLimit ? limit : stack.getCount()); - } - setItemList(itemStacks); - } - - return reachedLimit ? ItemHandlerHelper.copyStackWithSize(stack, stack.getCount() - limit) : ItemStack.EMPTY; - } - - @Override - public ItemStack extractItem(int slot, int amount, boolean simulate) { - NonNullList itemStacks = getItemList(); - if (amount == 0) - return ItemStack.EMPTY; - - validateSlotIndex(slot); - - ItemStack existing = itemStacks.get(slot); - - if (existing.isEmpty()) - return ItemStack.EMPTY; - - int toExtract = Math.min(amount, existing.getMaxStackSize()); - - if (existing.getCount() <= toExtract) { - if (!simulate) { - itemStacks.set(slot, ItemStack.EMPTY); - setItemList(itemStacks); - return existing; - } else { - return existing.copy(); - } - } else { - if (!simulate) { - itemStacks.set(slot, ItemHandlerHelper.copyStackWithSize(existing, existing.getCount() - toExtract)); - setItemList(itemStacks); - } - - return ItemHandlerHelper.copyStackWithSize(existing, toExtract); - } - } - - private void validateSlotIndex(int slot) { - if (slot < 0 || slot >= getSlots()) - throw new RuntimeException("Slot " + slot + " not in valid range - [0," + getSlots() + ")"); - } - - @Override - public int getSlotLimit(int slot) { - return 64; - } - - @Override - public boolean isItemValid(int slot, ItemStack stack) { - return stack.getItem().canFitInsideContainerItems(); - } - - @Override - public void setStackInSlot(int slot, ItemStack stack) { - validateSlotIndex(slot); - if (!isItemValid(slot, stack)) throw new RuntimeException("Invalid stack " + stack + " for slot " + slot + ")"); - NonNullList itemStacks = getItemList(); - itemStacks.set(slot, stack); - setItemList(itemStacks); - } - - private NonNullList getItemList() { - ItemContainerContents contents = this.stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY); - NonNullList list = NonNullList.create(); - contents.copyInto(list); - return list; - } - - private void setItemList(NonNullList itemStacks) { - this.stack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(itemStacks)); - } -} diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/ItemInventoryTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/ItemInventoryTests.java new file mode 100644 index 0000000000..aca28bb254 --- /dev/null +++ b/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/ItemInventoryTests.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.debug.capabilities; + +import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponents; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.ItemContainerContents; +import net.neoforged.neoforge.capabilities.Capabilities.ItemHandler; +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; +import net.neoforged.neoforge.items.ComponentItemHandler; +import net.neoforged.neoforge.items.IItemHandler; +import net.neoforged.neoforge.registries.DeferredItem; +import net.neoforged.testframework.DynamicTest; +import net.neoforged.testframework.TestFramework; +import net.neoforged.testframework.annotation.ForEachTest; +import net.neoforged.testframework.annotation.OnInit; +import net.neoforged.testframework.annotation.TestHolder; +import net.neoforged.testframework.gametest.EmptyTemplate; +import net.neoforged.testframework.registration.DeferredItems; +import net.neoforged.testframework.registration.RegistrationHelper; + +@ForEachTest(groups = "capabilities.iteminventory") +public class ItemInventoryTests { + public static final int SLOTS = 128; + public static final int STICK_SLOT = 64; + + private static final RegistrationHelper HELPER = RegistrationHelper.create("item_inventory_tests"); + + private static final DeferredItems ITEMS = HELPER.items(); + private static final DeferredItem BACKPACK; + + static { + NonNullList defaultContents = NonNullList.withSize(SLOTS, ItemStack.EMPTY); + defaultContents.set(STICK_SLOT, Items.STICK.getDefaultInstance().copyWithCount(64)); + BACKPACK = ITEMS.register("test_backpack", () -> new Item(new Item.Properties().component(DataComponents.CONTAINER, ItemContainerContents.fromItems(defaultContents)))); + } + + @OnInit + static void init(final TestFramework framework) { + ITEMS.register(framework.modEventBus()); + framework.modEventBus().addListener(e -> { + e.registerItem(ItemHandler.ITEM, (stack, ctx) -> { + return new ComponentItemHandler(stack, DataComponents.CONTAINER, SLOTS); + }, BACKPACK); + }); + } + + @GameTest + @EmptyTemplate + @TestHolder(description = "Tests that ComponentItemHandler can read and write from a data component") + public static void testItemInventory(DynamicTest test, RegistrationHelper reg) { + test.onGameTest(helper -> { + ItemStack stack = BACKPACK.toStack(); + IItemHandler items = stack.getCapability(ItemHandler.ITEM); + + ItemStack storedStick = items.getStackInSlot(STICK_SLOT); + helper.assertValueEqual(storedStick.getItem(), Items.STICK, "Default contents should contain a stick at slot " + STICK_SLOT); + + ItemStack toInsert = Items.APPLE.getDefaultInstance().copyWithCount(32); + ItemContainerContents contents = stack.get(DataComponents.CONTAINER); + + ItemStack remainder = items.insertItem(STICK_SLOT, toInsert, false); + helper.assertTrue(ItemStack.isSameItemSameComponents(toInsert, remainder), "Inserting an item where it does not fit should return the original item."); + // Check identity equality to assert that the component object was not updated at all, even to an equivalent form. + helper.assertTrue(contents == stack.get(DataComponents.CONTAINER), "Inserting an item where it does not fit should not change the component."); + + remainder = items.insertItem(0, toInsert, false); + helper.assertTrue(remainder.isEmpty(), "Successfully inserting the entire item should return an empty stack."); + helper.assertTrue(ItemStack.isSameItemSameComponents(toInsert, items.getStackInSlot(0)), "Successfully inserting an item should be visible via getStackInSlot"); + + ItemContainerContents newContents = stack.get(DataComponents.CONTAINER); + helper.assertTrue(ItemStack.isSameItemSameComponents(toInsert, newContents.getStackInSlot(0)), "Successfully inserting an item should trigger a write-back to the component"); + + ItemStack extractedApple = items.extractItem(0, 64, false); + helper.assertTrue(ItemStack.isSameItemSameComponents(toInsert, extractedApple), "Extracting the entire inserted item should produce the same item."); + + ItemStack extractedStick = items.extractItem(STICK_SLOT, 64, false); + helper.assertTrue(extractedStick.getItem() == Items.STICK && extractedStick.getCount() == 64, "The extracted item from the stick slot should be a 64-count stick."); + + for (int i = 0; i < SLOTS; i++) { + helper.assertTrue(items.getStackInSlot(i).isEmpty(), "Stack at slot " + i + " must be empty."); + } + + helper.succeed(); + }); + } +}