diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java index fc01f2f5c..0d39df9d3 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/AbstractInventoryMethods.java @@ -5,16 +5,22 @@ package dan200.computercraft.shared.peripheral.generic.methods; import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.detail.VanillaDetailRegistries; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.GenericPeripheral; import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.PeripheralType; +import dan200.computercraft.shared.platform.ItemContainer; import org.jspecify.annotations.Nullable; +import java.util.HashMap; import java.util.Map; import java.util.Optional; +import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween; + /** * Methods for interacting with inventories. * @@ -33,6 +39,22 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { return ComputerCraftAPI.MOD_ID + ":inventory"; } + /** + * Get an {@link ItemContainer} for the generic inventory type. + * + * @param inventory The inventory to get our {@link ItemContainer} from. + * @return The resulting {@link ItemContainer}. + */ + protected abstract ItemContainer getContainer(T inventory); + + /** + * Try to extract a {@link ItemContainer} from a peripheral. + * + * @param peripheral The peripheral to get our {@link ItemContainer} from. + * @return The resulting {@link ItemContainer}, or {@code null} if this peripheral is not a container. + */ + protected abstract @Nullable ItemContainer getContainer(IPeripheral peripheral); + /** * Get the size of this inventory. * @@ -40,7 +62,9 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { * @return The number of slots in this inventory. */ @LuaFunction(mainThread = true) - public abstract int size(T inventory); + public final int size(T inventory) { + return getContainer(inventory).size(); + } /** * List all items in this inventory. This returns a table, with an entry for each slot. @@ -67,7 +91,17 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { * @cc.see item_details */ @LuaFunction(mainThread = true) - public abstract Map> list(T inventory); + public final Map> list(T inventory) { + var container = getContainer(inventory); + Map> result = new HashMap<>(); + var i = 0; + for (var slots = container.contents(); slots.hasNext(); i++) { + var stack = slots.next(); + if (!stack.isEmpty()) result.put(i + 1, VanillaDetailRegistries.ITEM_STACK.getBasicDetails(stack)); + } + + return result; + } /** * Get [detailed information][`item_details`] about an item. @@ -94,7 +128,13 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { */ @Nullable @LuaFunction(mainThread = true) - public abstract Map getItemDetail(T inventory, int slot) throws LuaException; + public final Map getItemDetail(T inventory, int slot) throws LuaException { + var container = getContainer(inventory); + assertBetween(slot, 1, container.size(), "Slot out of range (%s)"); + + var stack = container.getItem(slot - 1); + return stack.isEmpty() ? null : VanillaDetailRegistries.ITEM_STACK.getDetails(stack); + } /** * Get the maximum number of items which can be stored in this slot. @@ -118,13 +158,17 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { * @cc.since 1.96.0 */ @LuaFunction(mainThread = true) - public abstract long getItemLimit(T inventory, int slot) throws LuaException; + public final long getItemLimit(T inventory, int slot) throws LuaException { + var container = getContainer(inventory); + assertBetween(slot, 1, container.size(), "Slot out of range (%s)"); + return container.getCapacity(slot); + } /** * Push items from one inventory to another connected one. *

* This allows you to push an item in an inventory to another inventory on the same wired network. Both - * inventories must attached to wired modems which are connected via a cable. + * inventories must be attached to wired modems which are connected via a cable. * * @param from Inventory to move items from. * @param computer The current computer. @@ -146,15 +190,24 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { * } */ @LuaFunction(mainThread = true) - public abstract int pushItems( + public final int pushItems( T from, IComputerAccess computer, String toName, int fromSlot, Optional limit, Optional toSlot - ) throws LuaException; + ) throws LuaException { + // Find location to transfer to + var location = computer.getAvailablePeripheral(toName); + if (location == null) throw new LuaException("Target '" + toName + "' does not exist"); + + var to = getContainer(location); + if (to == null) throw new LuaException("Target '" + toName + "' is not an inventory"); + + return move(getContainer(from), fromSlot, to, toSlot, limit); + } /** * Pull items from a connected inventory into this one. *

* This allows you to transfer items between inventories on the same wired network. Both this and the source - * inventory must attached to wired modems which are connected via a cable. + * inventory must be attached to wired modems which are connected via a cable. * * @param to Inventory to move items to. * @param computer The current computer. @@ -176,7 +229,31 @@ public abstract class AbstractInventoryMethods implements GenericPeripheral { * } */ @LuaFunction(mainThread = true) - public abstract int pullItems( + public final int pullItems( T to, IComputerAccess computer, String fromName, int fromSlot, Optional limit, Optional toSlot - ) throws LuaException; + ) throws LuaException { + // Find location to transfer to + var location = computer.getAvailablePeripheral(fromName); + if (location == null) throw new LuaException("Source '" + fromName + "' does not exist"); + + var from = getContainer(location); + if (from == null) throw new LuaException("Source '" + fromName + "' is not an inventory"); + + return move(from, fromSlot, getContainer(to), toSlot, limit); + } + + public static int move(ItemContainer from, int fromSlot, ItemContainer to, Optional toSlot, Optional limit) throws LuaException { + assertBetween(fromSlot, 1, from.size(), "From slot out of range (%s)"); + + if (toSlot.isPresent()) { + assertBetween(toSlot.get(), 1, to.size(), "To slot out of range (%s)"); + to = to.singleSlot(toSlot.get() - 1); + if (to == null) throw new LuaException("Cannot specify toSlot for this inventory"); + } + + int actualLimit = limit.orElse(Integer.MAX_VALUE); + if (actualLimit <= 0) return 0; + + return Math.max(0, from.moveTo(fromSlot - 1, to, actualLimit)); + } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/ContainerTransfer.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/ContainerTransfer.java deleted file mode 100644 index bc6e969e8..000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/platform/ContainerTransfer.java +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.platform; - -import net.minecraft.world.Container; - -/** - * A quasi-{@link Container}, which just supports transferring items. - */ -public interface ContainerTransfer { - int NO_ITEMS = -1; - int NO_SPACE = -2; - - /** - * Push an item from this container to another. - * - * @param destination The container to push to. - * @param maxAmount The maximum number of items to move. - * @return The number of items which were transferred, or one of {@link #NO_ITEMS} or {@link #NO_SPACE}. This will - * NEVER return 0. - */ - int moveTo(ContainerTransfer destination, int maxAmount); - - /** - * A {@link ContainerTransfer} which also has slots. - */ - interface Slotted extends ContainerTransfer { - /** - * Create a new {@link ContainerTransfer} which rotates the inventory, so that inserts start at {@code offset} - * instead. - * - * @param offset The slot offset - * @return The new container transfer. - */ - ContainerTransfer rotate(int offset); - - /** - * Create a new {@link ContainerTransfer} which can view a single slot of this container. - * - * @param slot The slot we can view. - * @return The new container transfer. - */ - ContainerTransfer singleSlot(int slot); - } -} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/ItemContainer.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/ItemContainer.java new file mode 100644 index 000000000..c52c7e139 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/ItemContainer.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +import java.util.Iterator; + +/** + * An abstraction over containers of items, such as {@link Container} and mod-loader specific inventory APIs. + */ +public interface ItemContainer { + int NO_ITEMS = -1; + int NO_SPACE = -2; + + int size(); + + Iterator contents(); + + ItemStack getItem(int slot); + + int getCapacity(int slot); + + @Nullable ItemContainer singleSlot(int slot); + + /** + * Push an item from this container to another. + * + * @param destination The container to push to. + * @param maxAmount The maximum number of items to move. + * @return The number of items which were transferred, or one of {@link #NO_ITEMS} or {@link #NO_SPACE}. This will + * NEVER return 0. + */ + int moveTo(ItemContainer destination, int maxAmount); + + /** + * Push an item from this container to another. + * + * @param fromSlot The slot to move items from. + * @param destination The container to push to. + * @param maxAmount The maximum number of items to move. + * @return The number of items which were transferred, or one of {@link #NO_ITEMS} or {@link #NO_SPACE}. This will + * NEVER return 0. + */ + int moveTo(int fromSlot, ItemContainer destination, int maxAmount); +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java index 48b6959c1..8bd9102da 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/platform/PlatformHelper.java @@ -224,15 +224,25 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper boolean hasWiredElementIn(Level level, BlockPos pos, Direction direction); /** - * Wrap a vanilla Minecraft {@link Container} into a {@link ContainerTransfer}. + * Wrap a vanilla Minecraft {@link Container} into a {@link ItemContainer}. * * @param container The container to wrap. * @return The container transfer. */ - ContainerTransfer.Slotted wrapContainer(Container container); + ItemContainer wrapContainer(Container container); /** - * Get access to a {@link ContainerTransfer} for a given position. This should look up blocks, then fall back to + * Wrap a vanilla Minecraft {@link Container} into a {@link ItemContainer}, "rotating" the container so that every + * slot {@code i} maps to {@code (i + offset) % size} of the original container. + * + * @param container The container to wrap. + * @param offset The offset into the container. + * @return The container transfer. + */ + ItemContainer wrapContainerRotated(Container container, int offset); + + /** + * Get access to a {@link ItemContainer} for a given position. This should look up blocks, then fall back to * {@link InventoryUtil#getEntityContainer(ServerLevel, BlockPos, Direction)} * * @param level The current level. @@ -242,7 +252,7 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper * @return The container, or {@code null} if none exists. */ @Nullable - ContainerTransfer getContainer(ServerLevel level, BlockPos pos, Direction side); + ItemContainer getContainer(ServerLevel level, BlockPos pos, Direction side); /** * Get the {@link RecipeIngredients} for this loader. diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/TurtleUtil.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/TurtleUtil.java index cfacf1e07..044c90476 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/TurtleUtil.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/TurtleUtil.java @@ -5,7 +5,7 @@ package dan200.computercraft.shared.turtle; import dan200.computercraft.api.turtle.ITurtleAccess; -import dan200.computercraft.shared.platform.ContainerTransfer; +import dan200.computercraft.shared.platform.ItemContainer; import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.util.DropConsumer; import dan200.computercraft.shared.util.InventoryUtil; @@ -15,6 +15,16 @@ import net.minecraft.world.item.ItemStack; import java.util.function.Function; public class TurtleUtil { + /** + * Get a view of the turtle's inventory. This should be used when transferring items from the turtle. + * + * @param turtle The turtle to transfer items into. + * @return The container transfer + */ + public static ItemContainer getInventory(ITurtleAccess turtle) { + return PlatformHelper.get().wrapContainer(turtle.getInventory()); + } + /** * Get a view of the turtle's inventory starting at the currently selected slot. This should be used when * transferring items in to the turtle. @@ -22,18 +32,8 @@ public class TurtleUtil { * @param turtle The turtle to transfer items into. * @return The container transfer */ - public static ContainerTransfer getOffsetInventory(ITurtleAccess turtle) { - return PlatformHelper.get().wrapContainer(turtle.getInventory()).rotate(turtle.getSelectedSlot()); - } - - /** - * Get a view of the turtle's currently selected slot. This should be used when transferring items from the turtle. - * - * @param turtle The turtle to transfer items from. - * @return The container transfer. - */ - public static ContainerTransfer getSelectedSlot(ITurtleAccess turtle) { - return PlatformHelper.get().wrapContainer(turtle.getInventory()).singleSlot(turtle.getSelectedSlot()); + public static ItemContainer getOffsetInventory(ITurtleAccess turtle) { + return PlatformHelper.get().wrapContainerRotated(turtle.getInventory(), turtle.getSelectedSlot()); } /** diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java index a1848ecf2..2bcb24491 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java @@ -8,7 +8,7 @@ import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.TurtleAnimation; import dan200.computercraft.api.turtle.TurtleCommand; import dan200.computercraft.api.turtle.TurtleCommandResult; -import dan200.computercraft.shared.platform.ContainerTransfer; +import dan200.computercraft.shared.platform.ItemContainer; import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.turtle.TurtleUtil; import dan200.computercraft.shared.util.WorldUtil; @@ -35,8 +35,6 @@ public class TurtleDropCommand implements TurtleCommand { // Get world direction from direction var direction = this.direction.toWorldDir(turtle); - var source = TurtleUtil.getSelectedSlot(turtle); - // Get inventory for thing in front var world = turtle.getLevel(); var oldPosition = turtle.getPosition(); @@ -47,11 +45,11 @@ public class TurtleDropCommand implements TurtleCommand { int transferred; if (inventory != null) { - transferred = source.moveTo(inventory, quantity); + transferred = TurtleUtil.getInventory(turtle).moveTo(turtle.getSelectedSlot(), inventory, quantity); } else { var stack = turtle.getInventory().removeItem(turtle.getSelectedSlot(), quantity); if (stack.isEmpty()) { - transferred = ContainerTransfer.NO_ITEMS; + transferred = ItemContainer.NO_ITEMS; } else { // Drop the item into the world turtle.getInventory().setChanged(); @@ -63,8 +61,8 @@ public class TurtleDropCommand implements TurtleCommand { } return switch (transferred) { - case ContainerTransfer.NO_SPACE -> TurtleCommandResult.failure("No space for items"); - case ContainerTransfer.NO_ITEMS -> TurtleCommandResult.failure("No items to drop"); + case ItemContainer.NO_SPACE -> TurtleCommandResult.failure("No space for items"); + case ItemContainer.NO_ITEMS -> TurtleCommandResult.failure("No items to drop"); default -> { turtle.playAnimation(TurtleAnimation.WAIT); yield TurtleCommandResult.success(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java index 4cd1e4351..682325b56 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java @@ -8,7 +8,7 @@ import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.TurtleAnimation; import dan200.computercraft.api.turtle.TurtleCommand; import dan200.computercraft.api.turtle.TurtleCommandResult; -import dan200.computercraft.shared.platform.ContainerTransfer; +import dan200.computercraft.shared.platform.ItemContainer; import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.turtle.TurtleUtil; import dan200.computercraft.shared.util.InventoryUtil; @@ -51,8 +51,8 @@ public class TurtleSuckCommand implements TurtleCommand { // Take from inventory of thing in front var transferred = inventory.moveTo(TurtleUtil.getOffsetInventory(turtle), quantity); return switch (transferred) { - case ContainerTransfer.NO_SPACE -> TurtleCommandResult.failure("No space for items"); - case ContainerTransfer.NO_ITEMS -> TurtleCommandResult.failure("No items to take"); + case ItemContainer.NO_SPACE -> TurtleCommandResult.failure("No space for items"); + case ItemContainer.NO_ITEMS -> TurtleCommandResult.failure("No items to take"); default -> { turtle.playAnimation(TurtleAnimation.WAIT); yield TurtleCommandResult.success(); diff --git a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java index a2746842d..2b11886dc 100644 --- a/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java +++ b/projects/common/src/test/java/dan200/computercraft/TestPlatformHelper.java @@ -235,13 +235,18 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat } @Override - public ContainerTransfer.Slotted wrapContainer(Container container) { + public ItemContainer wrapContainer(Container container) { + throw new UnsupportedOperationException("Cannot wrap container"); + } + + @Override + public ItemContainer wrapContainerRotated(Container container, int offset) { throw new UnsupportedOperationException("Cannot wrap container"); } @Nullable @Override - public ContainerTransfer getContainer(ServerLevel level, BlockPos pos, Direction side) { + public ItemContainer getContainer(ServerLevel level, BlockPos pos, Direction side) { throw new UnsupportedOperationException("Cannot interact with the world inside tests"); } diff --git a/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/platform/ContainerTransferContract.java b/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/platform/ItemContainerContract.java similarity index 90% rename from projects/common/src/testFixtures/java/dan200/computercraft/test/shared/platform/ContainerTransferContract.java rename to projects/common/src/testFixtures/java/dan200/computercraft/test/shared/platform/ItemContainerContract.java index 1e878031a..b04573f20 100644 --- a/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/platform/ContainerTransferContract.java +++ b/projects/common/src/testFixtures/java/dan200/computercraft/test/shared/platform/ItemContainerContract.java @@ -4,7 +4,7 @@ package dan200.computercraft.test.shared.platform; -import dan200.computercraft.shared.platform.ContainerTransfer; +import dan200.computercraft.shared.platform.ItemContainer; import net.minecraft.world.Container; import net.minecraft.world.SimpleContainer; import net.minecraft.world.item.ItemStack; @@ -22,18 +22,22 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Test Interface defining the behaviour of a {@link ContainerTransfer} implementation. + * Test Interface defining the behaviour of a {@link ItemContainer} implementation. */ -public interface ContainerTransferContract { - ContainerTransfer.Slotted wrap(Container container); +public interface ItemContainerContract { + ItemContainer wrap(Container container); + + ItemContainer wrapSlot(Container container, int slot); + + ItemContainer wrapRotated(Container container, int offset); @Test default void testMoveSameInventorySlot() { var inv = new SimpleContainer(4); inv.setItem(0, new ItemStack(Items.DIRT, 64)); - var move = wrap(inv).singleSlot(0).moveTo(wrap(inv).singleSlot(0), 64); - assertEquals(ContainerTransfer.NO_SPACE, move); + var move = wrap(inv).moveTo(0, wrapSlot(inv, 0), 64); + assertEquals(ItemContainer.NO_SPACE, move); assertThat(inv.getItem(0), isStack(Items.DIRT, 64)); @@ -45,7 +49,7 @@ public interface ContainerTransferContract { var inv = new SimpleContainer(4); inv.setItem(0, new ItemStack(Items.DIRT, 64)); - var move = wrap(inv).singleSlot(0).moveTo(wrap(inv).singleSlot(1), 64); + var move = wrap(inv).moveTo(0, wrapSlot(inv, 1), 64); assertEquals(64, move); assertThat(inv.getItem(0), isStack(ItemStack.EMPTY)); @@ -106,7 +110,7 @@ public interface ContainerTransferContract { source.setItem(0, new ItemStack(Items.DIRT, 64)); source.setItem(1, new ItemStack(Items.DIRT, 64)); - var move = wrap(source).moveTo(wrap(destination).rotate(offset), 17); + var move = wrap(source).moveTo(wrapRotated(destination, offset), 17); assertEquals(17, move); assertThat("Source stack in slot 0", source.getItem(0), isStack(Items.DIRT, 47)); @@ -151,7 +155,7 @@ public interface ContainerTransferContract { source.setItem(0, new ItemStack(Items.DIRT, 64)); var move = wrap(source).moveTo(wrap(destination), 64); - assertEquals(ContainerTransfer.NO_SPACE, move); + assertEquals(ItemContainer.NO_SPACE, move); assertThat(source.getItem(0), isStack(Items.DIRT, 64)); assertThat(destination.getItem(0), isStack(ItemStack.EMPTY)); @@ -168,7 +172,7 @@ public interface ContainerTransferContract { destination.setItem(0, new ItemStack(Items.DIRT)); for (var slot = 4; slot < 9; slot++) destination.setItem(slot, new ItemStack(Items.DIRT)); - var move = wrap(source).moveTo(wrap(destination).rotate(4), 64); + var move = wrap(source).moveTo(wrapRotated(destination, 4), 64); assertEquals(32, move); assertThat("Source is empty", source.getItem(0), isStack(ItemStack.EMPTY)); diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java index f683c6268..ebe9ffea8 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -4,34 +4,22 @@ package dan200.computercraft.shared.peripheral.generic.methods; -import com.google.common.collect.Iterables; -import dan200.computercraft.api.detail.VanillaDetailRegistries; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.shared.platform.FabricContainerTransfer; +import dan200.computercraft.shared.platform.FabricItemContainer; +import dan200.computercraft.shared.platform.ItemContainer; 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.storage.StorageView; import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedSlottedStorage; import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; -import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import org.jspecify.annotations.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween; - /** * Inventory methods for Fabric's {@link SlottedStorage} and {@link ItemVariant}s. *

@@ -42,15 +30,14 @@ import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween; public final class InventoryMethods extends AbstractInventoryMethods { /** * Wrapper over a {@link SlottedStorage}. - * */ public static final class StorageWrapper { private final Storage storage; - private final @Nullable SlottedStorage slottedStorage; + private final FabricItemContainer container; public StorageWrapper(Storage storage) { this.storage = storage; - this.slottedStorage = storage instanceof SlottedStorage slotted ? slotted : null; + this.container = new FabricItemContainer(storage); } @Override @@ -81,105 +68,26 @@ public final class InventoryMethods extends AbstractInventoryMethods cs ? cs.parts.hashCode() : storage.hashCode(); } + } - private int size() { - return slottedStorage != null ? slottedStorage.getSlotCount() : Iterables.size(storage); + @Override + protected ItemContainer getContainer(StorageWrapper inventory) { + return inventory.container; + } + + @Override + protected @Nullable ItemContainer getContainer(IPeripheral peripheral) { + var object = peripheral.getTarget(); + var direction = peripheral instanceof dan200.computercraft.shared.peripheral.generic.GenericPeripheral sided ? sided.side() : null; + + if (object instanceof BlockEntity blockEntity) { + if (blockEntity.isRemoved()) return null; + + var found = extractContainerImpl(blockEntity.getLevel(), blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity, direction); + if (found != null) return new FabricItemContainer(found); } - private StorageView getSlot(int slot) { - if (slottedStorage != null) return slottedStorage.getSlot(slot); - - var singleSlot = Iterables.get(storage, slot); - if (singleSlot == null) throw new IndexOutOfBoundsException(); - return singleSlot; - } - } - - @Override - @LuaFunction(mainThread = true) - public int size(StorageWrapper inventory) { - return inventory.size(); - } - - @Override - @LuaFunction(mainThread = true) - public Map> list(StorageWrapper inventory) { - Map> result = new HashMap<>(); - var i = 0; - for (var slots = inventory.storage.iterator(); slots.hasNext(); i++) { - var stack = toStack(slots.next()); - if (!stack.isEmpty()) result.put(i + 1, VanillaDetailRegistries.ITEM_STACK.getBasicDetails(stack)); - } - - return result; - } - - @Override - @Nullable - @LuaFunction(mainThread = true) - public Map getItemDetail(StorageWrapper inventory, int slot) throws LuaException { - assertBetween(slot, 1, inventory.size(), "Slot out of range (%s)"); - - var stack = toStack(inventory.getSlot(slot - 1)); - return stack.isEmpty() ? null : VanillaDetailRegistries.ITEM_STACK.getDetails(stack); - } - - @Override - @LuaFunction(mainThread = true) - public long getItemLimit(StorageWrapper inventory, int slot) throws LuaException { - assertBetween(slot, 1, inventory.size(), "Slot out of range (%s)"); - return inventory.getSlot(slot - 1).getCapacity(); - } - - @Override - @LuaFunction(mainThread = true) - public int pushItems( - StorageWrapper from, IComputerAccess computer, - String toName, int fromSlot, Optional limit, Optional toSlot - ) throws LuaException { - // Find location to transfer to - var location = computer.getAvailablePeripheral(toName); - if (location == null) throw new LuaException("Target '" + toName + "' does not exist"); - - var to = extractHandler(location); - if (to == null) throw new LuaException("Target '" + toName + "' is not an inventory"); - - return moveImpl(from, fromSlot, to, toSlot, limit); - } - - @Override - @LuaFunction(mainThread = true) - public int pullItems( - StorageWrapper to, IComputerAccess computer, - String fromName, int fromSlot, Optional limit, Optional toSlot - ) throws LuaException { - // Find location to transfer to - var location = computer.getAvailablePeripheral(fromName); - if (location == null) throw new LuaException("Source '" + fromName + "' does not exist"); - - var from = extractHandler(location); - if (from == null) throw new LuaException("Source '" + fromName + "' is not an inventory"); - - return moveImpl(from, fromSlot, to, toSlot, limit); - } - - private static int moveImpl(StorageWrapper from, int fromSlot, StorageWrapper to, Optional toSlot, Optional limit) throws LuaException { - assertBetween(fromSlot, 1, from.size(), "From slot out of range (%s)"); - var fromStorage = from.getSlot(fromSlot - 1); - - Storage toStorage; - if (toSlot.isPresent()) { - if (to.slottedStorage == null) throw new LuaException("Cannot specify toSlot for this inventory"); - assertBetween(toSlot.get(), 1, to.size(), "To slot out of range (%s)"); - toStorage = to.slottedStorage.getSlot(toSlot.get() - 1); - } else { - toStorage = to.storage; - } - - int actualLimit = limit.orElse(Integer.MAX_VALUE); - if (actualLimit <= 0) return 0; - - return Math.max(0, FabricContainerTransfer.move(fromStorage, toStorage, actualLimit)); + return null; } public static @Nullable StorageWrapper extractContainer(Level level, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, @Nullable Direction direction) { @@ -194,29 +102,4 @@ public final class InventoryMethods extends AbstractInventoryMethods variant) { - if (variant.isResourceBlank() || variant.getAmount() <= 0) return ItemStack.EMPTY; - return toStack(variant.getResource(), variant.getAmount()); - } - - private static ItemStack toStack(ItemVariant variant, long amount) { - // I don't care about supporting this much power creep :D - return variant.toStack(amount >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) amount); - } } diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/BasicSlottedStorage.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/BasicSlottedStorage.java new file mode 100644 index 000000000..dad730888 --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/BasicSlottedStorage.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +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.StorageView; +import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +@SuppressWarnings("UnstableApiUsage") +abstract class BasicSlottedStorage implements SlottedStorage { + @Override + public final long insert(ItemVariant resource, long maxAmount, TransactionContext transaction) { + long amount = 0; + + for (int i = 0, size = getSlotCount(); i < size; i++) { + amount += getSlot(i).insert(resource, maxAmount - amount, transaction); + if (amount == maxAmount) break; + } + + return amount; + } + + @Override + public final long extract(ItemVariant resource, long maxAmount, TransactionContext transaction) { + long amount = 0; + + for (int i = 0, size = getSlotCount(); i < size; i++) { + amount += getSlot(i).extract(resource, maxAmount - amount, transaction); + if (amount == maxAmount) break; + } + + return amount; + } + + @Override + public final Iterator> iterator() { + return new Iterator<>() { + int i = 0; + + @Override + public boolean hasNext() { + return i < getSlotCount(); + } + + @Override + public StorageView next() { + var slot = i++; + if (slot >= getSlotCount()) throw new NoSuchElementException(); + return getSlot(slot); + } + }; + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricContainerTransfer.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricContainerTransfer.java deleted file mode 100644 index 51d6bbb04..000000000 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricContainerTransfer.java +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.platform; - -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.storage.StorageUtil; -import net.fabricmc.fabric.api.transfer.v1.storage.StorageView; -import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; -import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Predicate; - -@SuppressWarnings("UnstableApiUsage") -public class FabricContainerTransfer implements ContainerTransfer { - private final Storage storage; - - private FabricContainerTransfer(Storage storage) { - this.storage = storage; - } - - public static ContainerTransfer of(Storage storage) { - return storage instanceof SlottedStorage inv ? new SlottedImpl(inv) : new FabricContainerTransfer(storage); - } - - public static ContainerTransfer.Slotted of(SlottedStorage storage) { - return new SlottedImpl(storage); - } - - /** - * Move an item from a slot into another storage. - * - * @param from The slot to move from. - * @param to The storage to move to. - * @param maxAmount The max amount to move. - * @return The number of items which were transferred, or one of {@link #NO_ITEMS} or {@link #NO_SPACE}. This will - * NEVER return 0. - * @see StorageUtil#move(Storage, Storage, Predicate, long, TransactionContext) - * @see #moveTo(ContainerTransfer, int) - */ - public static int move(StorageView from, Storage to, long maxAmount) { - var resource = from.getResource(); - if (resource.isBlank()) return NO_ITEMS; - - try (var transaction = Transaction.openOuter()) { - // Check how much can be extracted and inserted. - var maxExtracted = StorageUtil.simulateExtract(from, resource, maxAmount, transaction); - if (maxExtracted == 0) return NO_ITEMS; - - var accepted = to.insert(resource, maxExtracted, transaction); - if (accepted == 0) return NO_SPACE; - - // Extract or rollback. - if (from.extract(resource, accepted, transaction) == accepted) { - transaction.commit(); - return accepted > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) accepted; - } - - return NO_SPACE; - } - } - - @Override - public int moveTo(ContainerTransfer destination, int maxAmount) { - var hasItem = false; - var destStorage = ((FabricContainerTransfer) destination).storage; - for (var slot : storage.nonEmptyViews()) { - var result = move(slot, destStorage, maxAmount); - if (result >= 0) return result; - - if (result == NO_SPACE) hasItem = true; - } - - return hasItem ? NO_SPACE : NO_ITEMS; - } - - private static final class SlottedImpl extends FabricContainerTransfer implements ContainerTransfer.Slotted { - private final SlottedStorage storage; - - SlottedImpl(SlottedStorage storage) { - super(storage); - this.storage = storage; - } - - @Override - public ContainerTransfer rotate(int offset) { - return offset == 0 ? this : of(new OffsetStorage(storage, offset)); - } - - @Override - public ContainerTransfer singleSlot(int slot) { - return of(storage.getSlot(slot)); - } - } - - private record OffsetStorage(SlottedStorage storage, int offset) implements Storage { - @Override - public boolean supportsInsertion() { - for (var slot : storage.getSlots()) { - if (slot.supportsInsertion()) return true; - } - return false; - } - - @Override - public boolean supportsExtraction() { - for (var slot : storage.getSlots()) { - if (slot.supportsExtraction()) return true; - } - return false; - } - - @Override - public long insert(ItemVariant resource, long maxAmount, TransactionContext transaction) { - var slots = storage.getSlots(); - var size = slots.size(); - - long transferred = 0; - for (var i = 0; i < size; i++) { - transferred += slots.get(wrap(i, size)).insert(resource, maxAmount - transferred, transaction); - if (transferred >= maxAmount) break; - } - - return transferred; - } - - @Override - public long extract(ItemVariant resource, long maxAmount, TransactionContext transaction) { - var slots = storage.getSlots(); - var size = slots.size(); - - long transferred = 0; - for (var i = 0; i < size; i++) { - transferred += slots.get(wrap(i, size)).extract(resource, maxAmount - transferred, transaction); - if (transferred >= maxAmount) break; - } - - return transferred; - } - - @Override - public Iterator> iterator() { - var slots = storage.getSlots(); - var size = slots.size(); - return new Iterator<>() { - int i = 0; - - @Override - public boolean hasNext() { - return i < size; - } - - @Override - public StorageView next() { - var slot = i++; - if (slot >= size) throw new NoSuchElementException(); - return slots.get(wrap(slot, size)); - } - }; - } - - int wrap(int slot, int size) { - var actualSlot = slot + offset; - return actualSlot >= size ? actualSlot - size : actualSlot; - } - } -} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricItemContainer.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricItemContainer.java new file mode 100644 index 000000000..05e8e70ae --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricItemContainer.java @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; +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.storage.StorageUtil; +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.Transaction; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +import java.util.Iterator; +import java.util.Objects; + +@SuppressWarnings("UnstableApiUsage") +public final class FabricItemContainer implements ItemContainer { + private final Storage storage; + private final @Nullable SlottedStorage slottedStorage; + + public FabricItemContainer(SlottedStorage storage) { + this.storage = this.slottedStorage = storage; + } + + public FabricItemContainer(Storage storage) { + this.storage = storage; + this.slottedStorage = storage instanceof SlottedStorage slotted ? slotted : null; + } + + @Override + public int size() { + return slottedStorage != null ? slottedStorage.getSlotCount() : Iterables.size(storage); + } + + @Override + public Iterator contents() { + return Iterators.transform(storage.iterator(), FabricItemContainer::toStack); + } + + private StorageView getSlot(int slot) { + if (slottedStorage != null) return slottedStorage.getSlot(slot); + + var singleSlot = Iterables.get(storage, slot); + if (singleSlot == null) throw new IndexOutOfBoundsException(); + return singleSlot; + } + + @Override + public ItemStack getItem(int slot) { + return toStack(getSlot(slot)); + } + + private static ItemStack toStack(StorageView variant) { + if (variant.isResourceBlank() || variant.getAmount() <= 0) return ItemStack.EMPTY; + return variant.getResource().toStack(saturatingToInt(variant.getAmount())); + } + + @Override + public int getCapacity(int slot) { + return saturatingToInt(getSlot(slot).getCapacity()); + } + + @Override + public @Nullable ItemContainer singleSlot(int slot) { + return slottedStorage != null ? new FabricItemContainer(slottedStorage.getSlot(slot)) : null; + } + + @Override + public int moveTo(ItemContainer destination, int maxAmount) { + var hasItems = false; + var destStorage = ((FabricItemContainer) destination).storage; + for (var slot : storage.nonEmptyViews()) { + var result = move(slot, destStorage, maxAmount); + if (result >= 0) return result; + + if (result == NO_SPACE) hasItems = true; + } + + return hasItems ? NO_SPACE : NO_ITEMS; + } + + @Override + public int moveTo(int fromSlot, ItemContainer destination, int maxAmount) { + return move(getSlot(fromSlot), ((FabricItemContainer) destination).storage, maxAmount); + } + + private static int move(StorageView from, Storage to, long maxAmount) { + var resource = from.getResource(); + if (resource.isBlank()) return NO_ITEMS; + + try (var transaction = Transaction.openOuter()) { + // Check how much can be extracted and inserted. + var maxExtracted = StorageUtil.simulateExtract(from, resource, maxAmount, transaction); + if (maxExtracted == 0) return NO_ITEMS; + + var accepted = to.insert(resource, maxExtracted, transaction); + if (accepted == 0) return NO_SPACE; + + // Extract or rollback. + if (from.extract(resource, accepted, transaction) == accepted) { + transaction.commit(); + return saturatingToInt(accepted); + } + + return NO_SPACE; + } + } + + private static int saturatingToInt(long value) { + return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value; + } + + private static final class OffsetStorage extends BasicSlottedStorage { + private final SlottedStorage storage; + private final int offset; + + private OffsetStorage(SlottedStorage storage, int offset) { + this.storage = storage; + this.offset = offset; + } + + @Override + public boolean supportsInsertion() { + return storage.supportsInsertion(); + } + + @Override + public boolean supportsExtraction() { + return storage.supportsExtraction(); + } + + @Override + public long getVersion() { + return storage.getVersion(); + } + + @Override + public int getSlotCount() { + return storage.getSlotCount(); + } + + @Override + public SingleSlotStorage getSlot(int slot) { + var size = getSlotCount(); + Objects.checkIndex(slot, size); + + var actualSlot = slot + offset; + return storage.getSlot(actualSlot >= size ? actualSlot - size : actualSlot); + } + } + + static ItemContainer rotated(SlottedStorage storage, int offset) { + return new FabricItemContainer(new OffsetStorage(storage, offset)); + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 44348e609..879d6b231 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -204,18 +204,23 @@ public class PlatformHelperImpl implements PlatformHelper { } @Override - public ContainerTransfer.Slotted wrapContainer(Container container) { - return FabricContainerTransfer.of(InventoryStorage.of(container, null)); + public ItemContainer wrapContainer(Container container) { + return new FabricItemContainer(InventoryStorage.of(container, null)); + } + + @Override + public ItemContainer wrapContainerRotated(Container container, int offset) { + return FabricItemContainer.rotated(InventoryStorage.of(container, null), offset); } @Override @SuppressWarnings("UnstableApiUsage") - public @Nullable ContainerTransfer getContainer(ServerLevel level, BlockPos pos, Direction side) { + public @Nullable ItemContainer getContainer(ServerLevel level, BlockPos pos, Direction side) { var storage = ItemStorage.SIDED.find(level, pos, side); - if (storage != null) return FabricContainerTransfer.of(storage); + if (storage != null) return new FabricItemContainer(storage); var entity = InventoryUtil.getEntityContainer(level, pos, side); - return entity == null ? null : FabricContainerTransfer.of(InventoryStorage.of(entity, side)); + return entity == null ? null : new FabricItemContainer(InventoryStorage.of(entity, side)); } @Override diff --git a/projects/fabric/src/test/java/dan200/computercraft/shared/platform/FabricContainerTransferTest.java b/projects/fabric/src/test/java/dan200/computercraft/shared/platform/FabricContainerTransferTest.java deleted file mode 100644 index 93269e2e1..000000000 --- a/projects/fabric/src/test/java/dan200/computercraft/shared/platform/FabricContainerTransferTest.java +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.platform; - -import dan200.computercraft.test.shared.WithMinecraft; -import dan200.computercraft.test.shared.platform.ContainerTransferContract; -import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; -import net.minecraft.world.Container; - -@WithMinecraft -public class FabricContainerTransferTest implements ContainerTransferContract { - @Override - @SuppressWarnings("UnstableApiUsage") - public ContainerTransfer.Slotted wrap(Container container) { - return FabricContainerTransfer.of(InventoryStorage.of(container, null)); - } -} diff --git a/projects/fabric/src/test/java/dan200/computercraft/shared/platform/FabricItemContainerTest.java b/projects/fabric/src/test/java/dan200/computercraft/shared/platform/FabricItemContainerTest.java new file mode 100644 index 000000000..421048148 --- /dev/null +++ b/projects/fabric/src/test/java/dan200/computercraft/shared/platform/FabricItemContainerTest.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +import dan200.computercraft.test.shared.WithMinecraft; +import dan200.computercraft.test.shared.platform.ItemContainerContract; +import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; +import net.minecraft.world.Container; + +@WithMinecraft +@SuppressWarnings("UnstableApiUsage") +public class FabricItemContainerTest implements ItemContainerContract { + @Override + public ItemContainer wrap(Container container) { + return new FabricItemContainer(InventoryStorage.of(container, null)); + } + + @Override + public ItemContainer wrapSlot(Container container, int slot) { + return new FabricItemContainer(InventoryStorage.of(container, null).getSlot(slot)); + } + + @Override + public ItemContainer wrapRotated(Container container, int offset) { + return FabricItemContainer.rotated(InventoryStorage.of(container, null), offset); + } +} diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java index adcb59459..2003f9a62 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -4,12 +4,9 @@ package dan200.computercraft.shared.peripheral.generic.methods; -import dan200.computercraft.api.detail.VanillaDetailRegistries; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.shared.platform.ForgeContainerTransfer; +import dan200.computercraft.shared.platform.ForgeItemContainer; +import dan200.computercraft.shared.platform.ItemContainer; import dan200.computercraft.shared.util.CapabilityUtil; import net.minecraft.world.Container; import net.minecraft.world.level.block.entity.BlockEntity; @@ -19,94 +16,19 @@ import net.minecraftforge.items.IItemHandler; import net.minecraftforge.items.wrapper.InvWrapper; import org.jspecify.annotations.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween; - /** * Inventory methods for Forge's {@link IItemHandler}. */ public final class InventoryMethods extends AbstractInventoryMethods { @Override - @LuaFunction(mainThread = true) - public int size(IItemHandler inventory) { - return inventory.getSlots(); + protected ItemContainer getContainer(IItemHandler inventory) { + return new ForgeItemContainer(inventory); } @Override - @LuaFunction(mainThread = true) - public Map> list(IItemHandler inventory) { - Map> result = new HashMap<>(); - var size = inventory.getSlots(); - for (var i = 0; i < size; i++) { - var stack = inventory.getStackInSlot(i); - if (!stack.isEmpty()) result.put(i + 1, VanillaDetailRegistries.ITEM_STACK.getBasicDetails(stack)); - } - - return result; - } - - @Override - @Nullable - @LuaFunction(mainThread = true) - public Map getItemDetail(IItemHandler inventory, int slot) throws LuaException { - assertBetween(slot, 1, inventory.getSlots(), "Slot out of range (%s)"); - - var stack = inventory.getStackInSlot(slot - 1); - return stack.isEmpty() ? null : VanillaDetailRegistries.ITEM_STACK.getDetails(stack); - } - - @Override - @LuaFunction(mainThread = true) - public long getItemLimit(IItemHandler inventory, int slot) throws LuaException { - assertBetween(slot, 1, inventory.getSlots(), "Slot out of range (%s)"); - return inventory.getSlotLimit(slot - 1); - } - - @Override - @LuaFunction(mainThread = true) - public int pushItems( - IItemHandler from, IComputerAccess computer, - String toName, int fromSlot, Optional limit, Optional toSlot - ) throws LuaException { - // Find location to transfer to - var location = computer.getAvailablePeripheral(toName); - if (location == null) throw new LuaException("Target '" + toName + "' does not exist"); - - var to = extractHandler(location); - if (to == null) throw new LuaException("Target '" + toName + "' is not an inventory"); - - // Validate slots - int actualLimit = limit.orElse(Integer.MAX_VALUE); - assertBetween(fromSlot, 1, from.getSlots(), "From slot out of range (%s)"); - if (toSlot.isPresent()) assertBetween(toSlot.get(), 1, to.getSlots(), "To slot out of range (%s)"); - - if (actualLimit <= 0) return 0; - return moveItem(from, fromSlot - 1, to, toSlot.orElse(0) - 1, actualLimit); - } - - @Override - @LuaFunction(mainThread = true) - public int pullItems( - IItemHandler to, IComputerAccess computer, - String fromName, int fromSlot, Optional limit, Optional toSlot - ) throws LuaException { - // Find location to transfer to - var location = computer.getAvailablePeripheral(fromName); - if (location == null) throw new LuaException("Source '" + fromName + "' does not exist"); - - var from = extractHandler(location); - if (from == null) throw new LuaException("Source '" + fromName + "' is not an inventory"); - - // Validate slots - int actualLimit = limit.orElse(Integer.MAX_VALUE); - assertBetween(fromSlot, 1, from.getSlots(), "From slot out of range (%s)"); - if (toSlot.isPresent()) assertBetween(toSlot.get(), 1, to.getSlots(), "To slot out of range (%s)"); - - if (actualLimit <= 0) return 0; - return moveItem(from, fromSlot - 1, to, toSlot.orElse(0) - 1, actualLimit); + protected @Nullable ItemContainer getContainer(IPeripheral peripheral) { + var handler = extractHandler(peripheral); + return handler == null ? null : new ForgeItemContainer(handler); } @Nullable @@ -125,22 +47,4 @@ public final class InventoryMethods extends AbstractInventoryMethods= 0) toWrapper = toWrapper.singleSlot(toSlot); - - return Math.max(0, fromWrapper.moveTo(toWrapper, limit)); - } } diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeContainerTransfer.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeContainerTransfer.java deleted file mode 100644 index d802cfc2e..000000000 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeContainerTransfer.java +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.platform; - -import net.minecraft.world.item.ItemStack; -import net.minecraftforge.items.IItemHandler; - -public class ForgeContainerTransfer implements ContainerTransfer.Slotted { - private final IItemHandler handler; - private final int offset; - private final int limit; - private final int slots; - - public ForgeContainerTransfer(IItemHandler handler) { - this(handler, 0, handler.getSlots(), handler.getSlots()); - } - - public ForgeContainerTransfer(IItemHandler handler, int offset, int limit, int slots) { - this.handler = handler; - this.offset = offset; - this.limit = limit; - this.slots = slots; - } - - private int mapSlot(int slot) { - if (slot < 0 || slot >= limit) throw new IllegalArgumentException("slot is out of bounds"); - - slot += offset; - if (slot >= slots) slot -= limit; - return slot; - } - - @Override - public ForgeContainerTransfer rotate(int offset) { - return offset == 0 ? this : new ForgeContainerTransfer(handler, mapSlot(offset), limit, slots); - } - - @Override - public ForgeContainerTransfer singleSlot(int slot) { - return slot == 0 && limit == 1 ? this : new ForgeContainerTransfer(handler, mapSlot(slot), 1, slots); - } - - @Override - public int moveTo(ContainerTransfer destination, int maxAmount) { - return moveItem(this, (ForgeContainerTransfer) destination, maxAmount); - } - - public static int moveItem(ForgeContainerTransfer src, ForgeContainerTransfer dest, int maxAmount) { - var targetSlot = 0; - - var movedStack = ItemStack.EMPTY; - var moved = 0; - - outer: - for (var srcSlot = 0; srcSlot < src.limit; srcSlot++) { - var actualSrcSlot = src.mapSlot(srcSlot); - var stack = src.handler.extractItem(actualSrcSlot, maxAmount, true); - if (stack.isEmpty()) continue; - - // Pick the first item in the inventory to be the one we transfer, skipping those that match. - if (movedStack.isEmpty()) { - movedStack = stack.copy(); - if (stack.getMaxStackSize() < maxAmount) maxAmount = stack.getMaxStackSize(); - } else if (!ItemStack.isSameItemSameTags(stack, movedStack)) { - continue; - } - - for (; targetSlot < dest.limit; targetSlot++) { - var oldCount = stack.getCount(); - stack = dest.handler.insertItem(dest.mapSlot(targetSlot), stack, false); - - var transferred = oldCount - stack.getCount(); - var extracted = src.handler.extractItem(actualSrcSlot, transferred, false); - - moved += transferred; - - // We failed to extract as much as we should have. This should never happen, but goodness knows. - if (extracted.getCount() < transferred) break outer; - - if (moved >= maxAmount) return moved; - if (stack.isEmpty()) break; - } - } - - if (moved == 0) return movedStack.isEmpty() ? NO_ITEMS : NO_SPACE; - return moved; - } -} diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeItemContainer.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeItemContainer.java new file mode 100644 index 000000000..ea8af7bfd --- /dev/null +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/ForgeItemContainer.java @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.IItemHandler; + +import java.util.Iterator; +import java.util.Objects; + +public final class ForgeItemContainer implements ItemContainer { + private final IItemHandler handler; + + public ForgeItemContainer(IItemHandler handler) { + this.handler = handler; + } + + @Override + public int size() { + return handler.getSlots(); + } + + @Override + public Iterator contents() { + class ContentsIterator implements Iterator { + private int slot; + private final int size; + private final IItemHandler handler; + + ContentsIterator(IItemHandler handler) { + this.handler = handler; + this.size = handler.getSlots(); + } + + @Override + public boolean hasNext() { + return slot < size; + } + + @Override + public ItemStack next() { + return handler.getStackInSlot(this.slot++); + } + } + + return new ContentsIterator(handler); + } + + @Override + public ItemStack getItem(int slot) { + return handler.getStackInSlot(slot); + } + + @Override + public int getCapacity(int slot) { + return handler.getSlotLimit(slot); + } + + @Override + public ItemContainer singleSlot(int slot) { + Objects.checkIndex(slot, size()); + return handler instanceof SingleSlotWrapper ? this : new ForgeItemContainer(new SingleSlotWrapper(handler, slot)); + } + + @Override + public int moveTo(ItemContainer destination, int maxAmount) { + var hasItems = false; + var destHandler = ((ForgeItemContainer) destination).handler; + for (int fromSlot = 0, fromSize = handler.getSlots(); fromSlot < fromSize; fromSlot++) { + var result = move(handler, fromSlot, destHandler, maxAmount); + if (result >= 0) return result; + + if (result == NO_SPACE) hasItems = true; + } + + return hasItems ? NO_SPACE : NO_ITEMS; + } + + @Override + public int moveTo(int fromSlot, ItemContainer destination, int maxAmount) { + return move(handler, fromSlot, ((ForgeItemContainer) destination).handler, maxAmount); + } + + public static int move(IItemHandler from, int fromSlot, IItemHandler to, int maxAmount) { + var stack = from.extractItem(fromSlot, saturatingToInt(maxAmount), true); + if (stack.isEmpty()) return NO_ITEMS; + + // Pick the first item in the inventory to be the one we transfer, skipping those that match. + if (stack.getMaxStackSize() < maxAmount) maxAmount = stack.getMaxStackSize(); + + long moved = 0; + for (int toSlot = 0, toSize = to.getSlots(); toSlot < toSize; toSlot++) { + var oldCount = stack.getCount(); + stack = to.insertItem(toSlot, stack, false); + + var transferred = oldCount - stack.getCount(); + if (transferred == 0) continue; + + var extracted = from.extractItem(fromSlot, transferred, false); + + moved += transferred; + + // We failed to extract as much as we should have. This should never happen, but goodness knows. + if (extracted.getCount() < transferred) break; + + // Abort if we've extracted all our items, abort. + if (moved >= maxAmount) break; + if (stack.isEmpty()) break; + } + + return moved == 0 ? NO_SPACE : saturatingToInt(moved); + } + + private static int saturatingToInt(long value) { + return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value; + } + + private abstract static class MappedSlotWrapper implements IItemHandler { + final IItemHandler handler; + + private MappedSlotWrapper(IItemHandler handler) { + this.handler = handler; + } + + abstract int mapSlot(int slot); + + @Override + public final ItemStack getStackInSlot(int slot) { + return handler.getStackInSlot(mapSlot(slot)); + } + + @Override + public final int getSlotLimit(int slot) { + return handler.getSlotLimit(mapSlot(slot)); + } + + @Override + public final ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { + return handler.insertItem(mapSlot(slot), stack, simulate); + } + + @Override + public final ItemStack extractItem(int slot, int limit, boolean simulate) { + return handler.extractItem(mapSlot(slot), limit, simulate); + } + + @Override + public final boolean isItemValid(int slot, ItemStack stack) { + return handler.isItemValid(mapSlot(slot), stack); + } + } + + private static final class SingleSlotWrapper extends MappedSlotWrapper { + private final int actualSlot; + + private SingleSlotWrapper(IItemHandler handler, int slot) { + super(handler); + this.actualSlot = slot; + } + + @Override + public int getSlots() { + return 1; + } + + @Override + int mapSlot(int slot) { + Objects.checkIndex(slot, 1); + return actualSlot; + } + } + + private static final class RotatedWrapper extends MappedSlotWrapper { + private final int offset; + + RotatedWrapper(IItemHandler handler, int offset) { + super(handler); + this.offset = offset; + } + + @Override + public int getSlots() { + return handler.getSlots(); + } + + @Override + protected int mapSlot(int slot) { + var size = getSlots(); + Objects.checkIndex(slot, size); + + var actualSlot = slot + offset; + return actualSlot >= size ? actualSlot - size : actualSlot; + } + } + + static ItemContainer rotated(IItemHandler handler, int offset) { + return new ForgeItemContainer(new RotatedWrapper(handler, offset)); + } +} diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java index 8475b0c6b..0014883d9 100644 --- a/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java +++ b/projects/forge/src/main/java/dan200/computercraft/shared/platform/PlatformHelperImpl.java @@ -194,29 +194,34 @@ public class PlatformHelperImpl implements PlatformHelper { } @Override - public ContainerTransfer.Slotted wrapContainer(Container container) { - return new ForgeContainerTransfer(new InvWrapper(container)); + public ItemContainer wrapContainer(Container container) { + return new ForgeItemContainer(new InvWrapper(container)); + } + + @Override + public ItemContainer wrapContainerRotated(Container container, int offset) { + return ForgeItemContainer.rotated(new InvWrapper(container), offset); } @Nullable @Override - public ContainerTransfer getContainer(ServerLevel level, BlockPos pos, Direction side) { + public ItemContainer getContainer(ServerLevel level, BlockPos pos, Direction side) { var block = level.getBlockState(pos); if (block.getBlock() instanceof WorldlyContainerHolder holder) { var container = holder.getContainer(block, level, pos); - return new ForgeContainerTransfer(new SidedInvWrapper(container, side)); + return new ForgeItemContainer(new SidedInvWrapper(container, side)); } var blockEntity = level.getBlockEntity(pos); if (blockEntity != null) { var inventory = blockEntity.getCapability(ForgeCapabilities.ITEM_HANDLER, side); if (inventory.isPresent()) { - return new ForgeContainerTransfer(inventory.orElseThrow(NullPointerException::new)); + return new ForgeItemContainer(inventory.orElseThrow(NullPointerException::new)); } } var entity = InventoryUtil.getEntityContainer(level, pos, side); - return entity == null ? null : new ForgeContainerTransfer(new InvWrapper(entity)); + return entity == null ? null : new ForgeItemContainer(new InvWrapper(entity)); } @Nullable diff --git a/projects/forge/src/test/java/dan200/computercraft/shared/platform/ForgeContainerTransferTest.java b/projects/forge/src/test/java/dan200/computercraft/shared/platform/ForgeContainerTransferTest.java deleted file mode 100644 index c2465f00e..000000000 --- a/projects/forge/src/test/java/dan200/computercraft/shared/platform/ForgeContainerTransferTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.platform; - -import dan200.computercraft.test.shared.WithMinecraft; -import dan200.computercraft.test.shared.platform.ContainerTransferContract; -import net.minecraft.world.Container; -import net.minecraftforge.items.wrapper.InvWrapper; - -@WithMinecraft -public class ForgeContainerTransferTest implements ContainerTransferContract { - @Override - public ContainerTransfer.Slotted wrap(Container container) { - return new ForgeContainerTransfer(new InvWrapper(container)); - } -} diff --git a/projects/forge/src/test/java/dan200/computercraft/shared/platform/ForgeItemContainerTest.java b/projects/forge/src/test/java/dan200/computercraft/shared/platform/ForgeItemContainerTest.java new file mode 100644 index 000000000..8919282db --- /dev/null +++ b/projects/forge/src/test/java/dan200/computercraft/shared/platform/ForgeItemContainerTest.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.platform; + +import dan200.computercraft.test.shared.WithMinecraft; +import dan200.computercraft.test.shared.platform.ItemContainerContract; +import net.minecraft.world.Container; +import net.minecraftforge.items.wrapper.InvWrapper; + +@WithMinecraft +public class ForgeItemContainerTest implements ItemContainerContract { + @Override + public ItemContainer wrap(Container container) { + return new ForgeItemContainer(new InvWrapper(container)); + } + + @Override + public ItemContainer wrapSlot(Container container, int slot) { + return wrap(container).singleSlot(slot); + } + + @Override + public ItemContainer wrapRotated(Container container, int offset) { + return ForgeItemContainer.rotated(new InvWrapper(container), offset); + } +}