Add cross-platform abstraction for inventories

- Replace our existing ContainerTransfer class with a more general
   ItemContainer. This supports all the functionality we need for our
   inventory peripheral.

 - Change AbstractInventoryMethod to use ContainerTransfer. This reduces
   the duplicated code between the two InventoryMethod implementations
   (well, somewhat) — they only now have to handle extracting
   ContainerTransfers.
This commit is contained in:
Jonathan Coates
2026-10-02 21:56:37 +01:00
parent cc00854821
commit 9b3f3f2619
22 changed files with 720 additions and 647 deletions
@@ -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<T> 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<T> 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<T> implements GenericPeripheral {
* @cc.see item_details
*/
@LuaFunction(mainThread = true)
public abstract Map<Integer, Map<String, ?>> list(T inventory);
public final Map<Integer, Map<String, ?>> list(T inventory) {
var container = getContainer(inventory);
Map<Integer, Map<String, ?>> 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<T> 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<T> 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.
* <p>
* This allows you to push an item in an inventory to another inventory <em>on the same wired network</em>. 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<T> implements GenericPeripheral {
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract int pushItems(
public final int pushItems(
T from, IComputerAccess computer, String toName, int fromSlot, Optional<Integer> limit, Optional<Integer> 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.
* <p>
* This allows you to transfer items between inventories <em>on the same wired network</em>. 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<T> implements GenericPeripheral {
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract int pullItems(
public final int pullItems(
T to, IComputerAccess computer, String fromName, int fromSlot, Optional<Integer> limit, Optional<Integer> 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<Integer> toSlot, Optional<Integer> 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));
}
}
@@ -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
* <em>NEVER</em> 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);
}
}
@@ -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<ItemStack> 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
* <em>NEVER</em> 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
* <em>NEVER</em> return 0.
*/
int moveTo(int fromSlot, ItemContainer destination, int maxAmount);
}
@@ -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.
@@ -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());
}
/**
@@ -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();
@@ -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();
@@ -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");
}
@@ -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));
@@ -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.
* <p>
@@ -42,15 +30,14 @@ import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween;
public final class InventoryMethods extends AbstractInventoryMethods<InventoryMethods.StorageWrapper> {
/**
* Wrapper over a {@link SlottedStorage}.
*
*/
public static final class StorageWrapper {
private final Storage<ItemVariant> storage;
private final @Nullable SlottedStorage<ItemVariant> slottedStorage;
private final FabricItemContainer container;
public StorageWrapper(Storage<ItemVariant> storage) {
this.storage = storage;
this.slottedStorage = storage instanceof SlottedStorage<ItemVariant> slotted ? slotted : null;
this.container = new FabricItemContainer(storage);
}
@Override
@@ -81,105 +68,26 @@ public final class InventoryMethods extends AbstractInventoryMethods<InventoryMe
public int hashCode() {
return storage instanceof CombinedSlottedStorage<?, ?> 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<ItemVariant> 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<Integer, Map<String, ?>> list(StorageWrapper inventory) {
Map<Integer, Map<String, ?>> 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<String, ?> 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<Integer> limit, Optional<Integer> 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<Integer> limit, Optional<Integer> 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<Integer> toSlot, Optional<Integer> limit) throws LuaException {
assertBetween(fromSlot, 1, from.size(), "From slot out of range (%s)");
var fromStorage = from.getSlot(fromSlot - 1);
Storage<ItemVariant> 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<InventoryMe
return direction != null ? ItemStorage.SIDED.find(level, pos, state, blockEntity, direction) : null;
}
@Nullable
private static StorageWrapper extractHandler(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 = extractContainer(blockEntity.getLevel(), blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity, direction);
if (found != null) return found;
}
return null;
}
private static ItemStack toStack(StorageView<ItemVariant> 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);
}
}
@@ -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<ItemVariant> {
@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<StorageView<ItemVariant>> iterator() {
return new Iterator<>() {
int i = 0;
@Override
public boolean hasNext() {
return i < getSlotCount();
}
@Override
public StorageView<ItemVariant> next() {
var slot = i++;
if (slot >= getSlotCount()) throw new NoSuchElementException();
return getSlot(slot);
}
};
}
}
@@ -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<ItemVariant> storage;
private FabricContainerTransfer(Storage<ItemVariant> storage) {
this.storage = storage;
}
public static ContainerTransfer of(Storage<ItemVariant> storage) {
return storage instanceof SlottedStorage<ItemVariant> inv ? new SlottedImpl(inv) : new FabricContainerTransfer(storage);
}
public static ContainerTransfer.Slotted of(SlottedStorage<ItemVariant> 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
* <em>NEVER</em> return 0.
* @see StorageUtil#move(Storage, Storage, Predicate, long, TransactionContext)
* @see #moveTo(ContainerTransfer, int)
*/
public static int move(StorageView<ItemVariant> from, Storage<ItemVariant> 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<ItemVariant> storage;
SlottedImpl(SlottedStorage<ItemVariant> 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<ItemVariant> storage, int offset) implements Storage<ItemVariant> {
@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<StorageView<ItemVariant>> 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<ItemVariant> 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;
}
}
}
@@ -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<ItemVariant> storage;
private final @Nullable SlottedStorage<ItemVariant> slottedStorage;
public FabricItemContainer(SlottedStorage<ItemVariant> storage) {
this.storage = this.slottedStorage = storage;
}
public FabricItemContainer(Storage<ItemVariant> storage) {
this.storage = storage;
this.slottedStorage = storage instanceof SlottedStorage<ItemVariant> slotted ? slotted : null;
}
@Override
public int size() {
return slottedStorage != null ? slottedStorage.getSlotCount() : Iterables.size(storage);
}
@Override
public Iterator<ItemStack> contents() {
return Iterators.transform(storage.iterator(), FabricItemContainer::toStack);
}
private StorageView<ItemVariant> 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<ItemVariant> 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<ItemVariant> from, Storage<ItemVariant> 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<ItemVariant> storage;
private final int offset;
private OffsetStorage(SlottedStorage<ItemVariant> 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<ItemVariant> 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<ItemVariant> storage, int offset) {
return new FabricItemContainer(new OffsetStorage(storage, offset));
}
}
@@ -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
@@ -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));
}
}
@@ -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);
}
}
@@ -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<IItemHandler> {
@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<Integer, Map<String, ?>> list(IItemHandler inventory) {
Map<Integer, Map<String, ?>> 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<String, ?> 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<Integer> limit, Optional<Integer> 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<Integer> limit, Optional<Integer> 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<IItemHandle
if (object instanceof Container container) return new InvWrapper(container);
return null;
}
/**
* Move an item from one handler to another.
*
* @param from The handler to move from.
* @param fromSlot The slot to move from.
* @param to The handler to move to.
* @param toSlot The slot to move to. Use any number < 0 to represent any slot.
* @param limit The max number to move. {@link Integer#MAX_VALUE} for no limit.
* @return The number of items moved.
*/
private static int moveItem(IItemHandler from, int fromSlot, IItemHandler to, int toSlot, final int limit) {
var fromWrapper = new ForgeContainerTransfer(from).singleSlot(fromSlot);
var toWrapper = new ForgeContainerTransfer(to);
if (toSlot >= 0) toWrapper = toWrapper.singleSlot(toSlot);
return Math.max(0, fromWrapper.moveTo(toWrapper, limit));
}
}
@@ -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;
}
}
@@ -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<ItemStack> contents() {
class ContentsIterator implements Iterator<ItemStack> {
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));
}
}
@@ -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
@@ -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));
}
}
@@ -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);
}
}