diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java b/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java index b0d1a3cf..a2496203 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java @@ -4,6 +4,7 @@ import io.netty.buffer.Unpooled; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTestAssertException; @@ -24,6 +25,8 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.BundleContents; +import net.minecraft.world.item.component.ItemContainerContents; import net.minecraft.world.level.GameType; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; @@ -49,6 +52,7 @@ import org.cyclops.colossalchests2.blockentity.BlockEntityChestCore; import org.cyclops.colossalchests2.blockentity.BlockEntityChestWall; import org.cyclops.colossalchests2.blockentity.DisplayOption; +import org.cyclops.colossalchests2.capability.ItemHandlerLogic; import org.cyclops.colossalchests2.capability.WallAccess; import org.cyclops.colossalchests2.config.ChestTables; import org.cyclops.colossalchests2.config.ChestTablesLoader; @@ -74,6 +78,7 @@ import org.cyclops.colossalchests2.storage.CompressionFamily; import org.cyclops.colossalchests2.storage.DeepSlot; import org.cyclops.colossalchests2.storage.DisplayStats; +import org.cyclops.colossalchests2.storage.NestedChests; import org.cyclops.colossalchests2.upgrade.ChestUpgrade; import org.cyclops.colossalchests2.upgrade.ChestUpgradeInventory; import org.cyclops.colossalchests2.upgrade.ChestUpgradeRules; @@ -1966,4 +1971,45 @@ public void testDisplayWallOnlyMinedWithAPickaxe(GameTestHelper helper) { .thenSucceed(); } + // Nested chests + + @GameTest(template = TEMPLATE_EMPTY) + public void testFilledCoresCanNotBeStored(GameTestHelper helper) { + BlockPos corePosA = buildChest(helper, MIN_A, 3, ChestMaterial.WOOD); + BlockPos corePosB = buildChest(helper, MIN_B, 3, ChestMaterial.WOOD); + helper.startSequence() + .thenWaitUntil(() -> { + assertFormed(helper, corePosA, MIN_A, 3); + assertFormed(helper, corePosB, MIN_B, 3); + }) + .thenExecute(() -> { + helper.assertValueEqual(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(RegistryEntries.COMPONENT_CHEST_CONTENTS.value()), + NestedChests.CONTENTS_COMPONENT, "contents component id"); + getCore(helper, corePosA).getStorage().insert(STONE, 100, false); + ItemStack filledCore = breakCoreAndPickUp(helper, corePosA); + helper.assertTrue(filledCore.has(RegistryEntries.COMPONENT_CHEST_CONTENTS.value()), "Expected a core with contents"); + ChestStorage storage = getCore(helper, corePosB).getStorage(); + // Not through any insert path, and no slot can be locked to it. + helper.assertValueEqual(storage.insert(filledCore, 1, false), 0L, "filled core inserted"); + helper.assertValueEqual(storage.insertAutomated(filledCore, 1, false), 0L, "filled core inserted by automation"); + helper.assertTrue(ItemStack.matches(new ItemHandlerLogic(storage).insertItem(0, filledCore.copy(), false), filledCore), + "Expected an item handler to refuse a filled core"); + helper.assertFalse(storage.lockTo(0, filledCore), "Expected no lock to a filled core"); + // Nor inside a shulker box or bundle. + ItemStack shulker = new ItemStack(Items.SHULKER_BOX); + shulker.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(List.of(filledCore))); + helper.assertValueEqual(storage.insert(shulker, 1, false), 0L, "shulker box with a filled core inserted"); + ItemStack bundle = new ItemStack(Items.BUNDLE); + bundle.set(DataComponents.BUNDLE_CONTENTS, new BundleContents(List.of(shulker))); + helper.assertValueEqual(storage.insert(bundle, 1, false), 0L, "bundle with a filled core inserted"); + // Empty cores and other containers are fine. + ItemStack emptyCore = new ItemStack(filledCore.getItem()); + helper.assertValueEqual(storage.insert(emptyCore, 1, false), 1L, "empty core inserted"); + ItemStack stoneShulker = new ItemStack(Items.SHULKER_BOX); + stoneShulker.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(List.of(STONE.copyWithCount(64)))); + helper.assertValueEqual(storage.insert(stoneShulker, 1, false), 1L, "shulker box with stone inserted"); + }) + .thenSucceed(); + } + } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/storage/ChestStorage.java b/loader-common/src/main/java/org/cyclops/colossalchests2/storage/ChestStorage.java index c0215067..02067e8e 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/storage/ChestStorage.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/storage/ChestStorage.java @@ -179,7 +179,7 @@ public boolean isExtractOnly(int slot) { * @return If the type may go into the slot, ignoring how full it is. */ public boolean canAccept(int slot, ItemStack type) { - if (type.isEmpty() || getCapacity(type) <= 0) { + if (type.isEmpty() || getCapacity(type) <= 0 || !NestedChests.canStore(type)) { return false; } DeepSlot deepSlot = slots[slot]; @@ -334,7 +334,7 @@ public boolean setLocked(int slot, boolean locked) { */ public boolean lockTo(int slot, ItemStack type) { DeepSlot deepSlot = slots[slot]; - if (type.isEmpty()) { + if (type.isEmpty() || !NestedChests.canStore(type)) { return false; } type = getStoredType(type); diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/storage/NestedChests.java b/loader-common/src/main/java/org/cyclops/colossalchests2/storage/NestedChests.java new file mode 100644 index 00000000..496bf7f7 --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/storage/NestedChests.java @@ -0,0 +1,73 @@ +package org.cyclops.colossalchests2.storage; + +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.BundleContents; +import net.minecraft.world.item.component.ItemContainerContents; +import org.cyclops.colossalchests2.Reference; + +import java.util.Map; +import java.util.Optional; + +/** + * Keeps chest cores that carry items out of chests, so cores can not be nested in each other without limit and + * overflow the size of saved data. Also looks inside vanilla containers, such as shulker boxes and bundles. + * @author rubensworks + */ +public final class NestedChests { + + /** + * The item component of a core item that carries the chest's contents. + */ + public static final ResourceLocation CONTENTS_COMPONENT = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "chest_contents"); + // Deeper nesting is refused outright, so a crafted stack can not make this check itself expensive. + private static final int MAX_DEPTH = 8; + + private NestedChests() { + } + + /** + * @param stack An item stack. + * @return If a chest may store the stack: it is not a core with contents, and holds none. + */ + public static boolean canStore(ItemStack stack) { + return !holdsChestContents(stack, 0); + } + + private static boolean holdsChestContents(ItemStack stack, int depth) { + DataComponentPatch patch = stack.getComponentsPatch(); + if (patch.isEmpty()) { + return false; + } + if (depth >= MAX_DEPTH) { + return true; + } + // Compared by id, so this works without the mod's registries, such as in unit tests. + for (Map.Entry, Optional> entry : patch.entrySet()) { + if (entry.getValue().isPresent() && CONTENTS_COMPONENT.equals(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(entry.getKey()))) { + return true; + } + } + ItemContainerContents container = stack.get(DataComponents.CONTAINER); + if (container != null) { + for (ItemStack inner : container.nonEmptyItems()) { + if (holdsChestContents(inner, depth + 1)) { + return true; + } + } + } + BundleContents bundle = stack.get(DataComponents.BUNDLE_CONTENTS); + if (bundle != null) { + for (ItemStack inner : bundle.items()) { + if (holdsChestContents(inner, depth + 1)) { + return true; + } + } + } + return false; + } +} diff --git a/loader-common/src/test/java/org/cyclops/colossalchests2/storage/TestNestedChests.java b/loader-common/src/test/java/org/cyclops/colossalchests2/storage/TestNestedChests.java new file mode 100644 index 00000000..12677ff3 --- /dev/null +++ b/loader-common/src/test/java/org/cyclops/colossalchests2/storage/TestNestedChests.java @@ -0,0 +1,49 @@ +package org.cyclops.colossalchests2.storage; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.BundleContents; +import net.minecraft.world.item.component.ItemContainerContents; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Cores with contents are refused through game tests, which have the mod's item components. + * @author rubensworks + */ +public class TestNestedChests extends BootstrapTest { + + private static ItemStack shulkerWith(ItemStack... contents) { + ItemStack shulker = new ItemStack(Items.SHULKER_BOX); + shulker.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(List.of(contents))); + return shulker; + } + + @Test + public void testPlainItems() { + assertTrue(NestedChests.canStore(new ItemStack(Items.STONE))); + assertTrue(NestedChests.canStore(new ItemStack(Items.SHULKER_BOX))); + } + + @Test + public void testContainers() { + assertTrue(NestedChests.canStore(shulkerWith(new ItemStack(Items.STONE, 64)))); + ItemStack bundle = new ItemStack(Items.BUNDLE); + bundle.set(DataComponents.BUNDLE_CONTENTS, new BundleContents(List.of(shulkerWith(new ItemStack(Items.STONE))))); + assertTrue(NestedChests.canStore(bundle)); + } + + @Test + public void testTooDeep() { + ItemStack stack = new ItemStack(Items.STONE); + for (int i = 0; i < 9; i++) { + stack = shulkerWith(stack); + } + assertFalse(NestedChests.canStore(stack)); + } +}