1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-25 22:53:22 +00:00

Rewrite how we do inventory transfers

- Add a new ContainerTransfer class to handle moving items between
   containers. This is now used for turtle.drop/turtle.suck as well as
   inventory methods.

 - Any other usages of IItemHandler (which are mostly on turtle
   inventories) now use Container and a couple of helper methods.
This commit is contained in:
Jonathan Coates 2022-11-08 21:31:17 +00:00
parent 7d47b219c5
commit 55494b7671
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
24 changed files with 644 additions and 297 deletions

View File

@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/**
* Sets up the configurations for writing game tests.
*
@ -47,7 +45,5 @@ java.registerFeature("testFixtures") {
dependencies {
add(testFixtures.implementationConfigurationName, main.output)
testImplementation(testFixtures(project))
add(testMod.implementationConfigurationName, testFixtures(project))
}

View File

@ -10,7 +10,7 @@ parchmentMc = "1.19.2"
asm = "9.3"
autoService = "1.0.1"
checkerFramework = "3.12.0"
cobalt = { strictly = "[0.5.8,0.6.0)", prefer = "0.5.8" }
cobalt = "0.5.8"
fastutil = "8.5.6"
guava = "31.0.1-jre"
jetbrainsAnnotations = "23.0.0"
@ -18,7 +18,7 @@ jsr305 = "3.0.2"
kotlin = "1.7.10"
kotlin-coroutines = "1.6.0"
logback = "1.2.11"
netty = { strictly = "[4.1.77.Final,5.0)", prefer = "4.1.77.Final" }
netty = "4.1.77.Final"
nightConfig = "3.6.5"
slf4j = "1.7.36"

View File

@ -19,6 +19,7 @@
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.core.BlockPos;
@ -133,7 +134,7 @@ public void close() {
ServerContext.get(level.getServer()).registry().remove(instanceID);
}
private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage> createPacket) {
private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) {
var server = level.getServer();
for (var player : server.getPlayerList().getPlayers()) {

View File

@ -15,7 +15,6 @@
import dan200.computercraft.shared.network.client.PlayRecordClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.util.DefaultInventory;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
@ -209,7 +208,7 @@ public void setItem(int slot, @Nonnull ItemStack stack) {
}
synchronized (this) {
if (InventoryUtil.areItemsStackable(stack, diskStack)) {
if (ItemStack.isSameItemSameTags(stack, diskStack)) {
diskStack = stack;
return;
}

View File

@ -13,13 +13,13 @@
import dan200.computercraft.api.peripheral.GenericPeripheral;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.PeripheralType;
import dan200.computercraft.shared.platform.ForgeContainerTransfer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.Container;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.ItemHandlerHelper;
import net.minecraftforge.items.wrapper.InvWrapper;
import javax.annotation.Nonnull;
@ -286,21 +286,10 @@ private static IItemHandler extractHandler(@Nullable Object object) {
* @return The number of items moved.
*/
private static int moveItem(IItemHandler from, int fromSlot, IItemHandler to, int toSlot, final int limit) {
// See how much we can get out of this slot
var extracted = from.extractItem(fromSlot, limit, true);
if (extracted.isEmpty()) return 0;
var fromWrapper = new ForgeContainerTransfer(from).singleSlot(fromSlot);
var toWrapper = new ForgeContainerTransfer(to);
if (toSlot >= 0) toWrapper = toWrapper.singleSlot(toSlot);
// Limit the amount to extract
var extractCount = Math.min(extracted.getCount(), limit);
extracted.setCount(extractCount);
var remainder = toSlot < 0 ? ItemHandlerHelper.insertItem(to, extracted, false) : to.insertItem(toSlot, extracted, false);
var inserted = remainder.isEmpty() ? extractCount : extractCount - remainder.getCount();
if (inserted <= 0) return 0;
// Remove the item from the original inventory. Technically this could fail, but there's little we can do
// about that.
from.extractItem(fromSlot, inserted, false);
return inserted;
return Math.max(0, fromWrapper.moveTo(toWrapper, limit));
}
}

View File

@ -0,0 +1,48 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
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);
}
}

View File

@ -0,0 +1,91 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
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;
}
}

View File

@ -10,6 +10,7 @@
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.container.ContainerData;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
@ -19,7 +20,9 @@
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.WorldlyContainer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
@ -30,6 +33,7 @@
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.items.IItemHandlerModifiable;
import javax.annotation.Nullable;
import java.util.Collection;
@ -181,4 +185,35 @@ static PlatformHelper get() {
* @return Whether there is a wired element in the given direction.
*/
boolean hasWiredElementIn(Level level, BlockPos pos, Direction direction);
/**
* Wrap a vanilla Minecraft {@link Container} into a {@link ContainerTransfer}.
*
* @param container The container to wrap.
* @return The container transfer.
*/
ContainerTransfer.Slotted wrapContainer(Container container);
/**
* Get access to a {@link ContainerTransfer} for a given position. This should look up blocks, then fall back to
* {@link InventoryUtil#getEntityContainer(ServerLevel, BlockPos, Direction)}
*
* @param level The current level.
* @param pos The current position.
* @param side The side of the block we're viewing the inventory from. Equivalent to the direction argument for
* {@link WorldlyContainer}.
* @return The container, or {@code null} if none exists.
*/
@Nullable
ContainerTransfer getContainer(ServerLevel level, BlockPos pos, Direction side);
/**
* Wrap a vanilla Minecraft {@link Container} into Forge's {@link IItemHandlerModifiable}.
*
* @param container The container to wrap.
* @return The item handler.
* @deprecated This is only needed for backwards compatibility, and will be removed in 1.19.3.
*/
@Deprecated(forRemoval = true)
IItemHandlerModifiable wrapContainerToItemHandler(Container container);
}

View File

@ -15,6 +15,7 @@
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.container.ContainerData;
import dan200.computercraft.shared.util.CapabilityUtil;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
@ -25,7 +26,9 @@
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.WorldlyContainerHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
@ -38,9 +41,13 @@
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.common.extensions.IForgeMenuType;
import net.minecraftforge.common.util.NonNullConsumer;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.items.IItemHandlerModifiable;
import net.minecraftforge.items.wrapper.InvWrapper;
import net.minecraftforge.items.wrapper.SidedInvWrapper;
import net.minecraftforge.network.NetworkHooks;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistry;
@ -146,6 +153,38 @@ public boolean hasWiredElementIn(Level level, BlockPos pos, Direction direction)
return blockEntity != null && blockEntity.getCapability(Capabilities.CAPABILITY_WIRED_ELEMENT, direction.getOpposite()).isPresent();
}
@Override
public ContainerTransfer.Slotted wrapContainer(Container container) {
return new ForgeContainerTransfer(new InvWrapper(container));
}
@Nullable
@Override
public ContainerTransfer 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));
}
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));
}
}
var entity = InventoryUtil.getEntityContainer(level, pos, side);
return entity == null ? null : new ForgeContainerTransfer(new InvWrapper(entity));
}
@Override
@Deprecated(forRemoval = true)
public IItemHandlerModifiable wrapContainerToItemHandler(Container container) {
return new InvWrapper(container);
}
@Nullable
@Override
public CompoundTag getShareTag(ItemStack item) {

View File

@ -10,12 +10,9 @@
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.shared.PocketUpgrades;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import dan200.computercraft.shared.util.InventoryUtil;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.NonNullList;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.items.wrapper.PlayerMainInvWrapper;
/**
* Control the current pocket computer, adding or removing upgrades.
@ -70,15 +67,7 @@ public final Object[] equipBack() {
if (newUpgrade == null) return new Object[]{ false, "Cannot find a valid upgrade" };
// Remove the current upgrade
if (previousUpgrade != null) {
var stack = previousUpgrade.getCraftingItem();
if (!stack.isEmpty()) {
stack = InventoryUtil.storeItems(stack, new PlayerMainInvWrapper(inventory), inventory.selected);
if (!stack.isEmpty()) {
WorldUtil.dropItemStack(stack, player.getCommandSenderWorld(), player.position());
}
}
}
if (previousUpgrade != null) storeItem(player, previousUpgrade.getCraftingItem());
// Set the new upgrade
computer.setUpgrade(newUpgrade);
@ -104,17 +93,18 @@ public final Object[] unequipBack() {
computer.setUpgrade(null);
var stack = previousUpgrade.getCraftingItem();
if (!stack.isEmpty()) {
stack = InventoryUtil.storeItems(stack, new PlayerMainInvWrapper(inventory), inventory.selected);
if (stack.isEmpty()) {
WorldUtil.dropItemStack(stack, player.getCommandSenderWorld(), player.position());
}
}
storeItem(player, previousUpgrade.getCraftingItem());
return new Object[]{ true };
}
private static void storeItem(Player player, ItemStack stack) {
if (!stack.isEmpty() && !player.getInventory().add(stack)) {
var drop = player.drop(stack, false);
if (drop != null) drop.setNoPickUpDelay();
}
}
private static IPocketUpgrade findUpgrade(NonNullList<ItemStack> inv, int start, IPocketUpgrade previous) {
for (var i = 0; i < inv.size(); i++) {
var invStack = inv.get((i + start) % inv.size());

View File

@ -6,6 +6,8 @@
package dan200.computercraft.shared.turtle;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.shared.platform.ContainerTransfer;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.util.DropConsumer;
import dan200.computercraft.shared.util.InventoryUtil;
import dan200.computercraft.shared.util.WorldUtil;
@ -14,6 +16,27 @@
import java.util.function.Function;
public class TurtleUtil {
/**
* 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.
*
* @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());
}
/**
* Store an item in this turtle, or drop it if there is room remaining.
*
@ -28,14 +51,14 @@ public static void storeItemOrDrop(ITurtleAccess turtle, ItemStack stack) {
}
// Put the remainder back in the turtle
var remainder = InventoryUtil.storeItems(stack, turtle.getItemHandler(), turtle.getSelectedSlot());
var remainder = InventoryUtil.storeItemsFromOffset(turtle.getInventory(), stack, turtle.getSelectedSlot());
if (remainder.isEmpty()) return;
WorldUtil.dropItemStack(remainder, turtle.getLevel(), turtle.getPosition(), turtle.getDirection().getOpposite());
}
public static Function<ItemStack, ItemStack> dropConsumer(ITurtleAccess turtle) {
return stack -> turtle.isRemoved() ? stack : InventoryUtil.storeItems(stack, turtle.getItemHandler(), turtle.getSelectedSlot());
return stack -> turtle.isRemoved() ? stack : InventoryUtil.storeItemsFromOffset(turtle.getInventory(), stack, turtle.getSelectedSlot());
}
public static void stopConsuming(ITurtleAccess turtle) {

View File

@ -21,7 +21,10 @@
import dan200.computercraft.shared.turtle.apis.TurtleAPI;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import dan200.computercraft.shared.turtle.inventory.ContainerTurtle;
import dan200.computercraft.shared.util.*;
import dan200.computercraft.shared.util.DefaultInventory;
import dan200.computercraft.shared.util.DirectionUtil;
import dan200.computercraft.shared.util.RedstoneUtil;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.NonNullList;
@ -360,7 +363,7 @@ public ItemStack removeItem(int slot, int count) {
@Override
public void setItem(int i, @Nonnull ItemStack stack) {
if (i >= 0 && i < INVENTORY_SIZE && !InventoryUtil.areItemsEqual(stack, inventory.get(i))) {
if (i >= 0 && i < INVENTORY_SIZE && !ItemStack.matches(stack, inventory.get(i))) {
inventory.set(i, stack);
onInventoryDefinitelyChanged();
}

View File

@ -17,6 +17,7 @@
import dan200.computercraft.shared.TurtleUpgrades;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.blocks.TileTurtle;
import dan200.computercraft.shared.util.Holiday;
import dan200.computercraft.shared.util.HolidayUtil;
@ -38,7 +39,6 @@
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.items.IItemHandlerModifiable;
import net.minecraftforge.items.wrapper.InvWrapper;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -67,7 +67,6 @@ public class TurtleBrain implements ITurtleAccess {
private GameProfile owningPlayer;
private final Container inventory = (InventoryDelegate) () -> owner;
private final IItemHandlerModifiable inventoryWrapper = new InvWrapper(inventory);
private final Queue<TurtleCommandQueueEntry> commandQueue = new ArrayDeque<>();
private int commandsIssued = 0;
@ -387,8 +386,9 @@ public Container getInventory() {
@Nonnull
@Override
@Deprecated(forRemoval = true)
public IItemHandlerModifiable getItemHandler() {
return inventoryWrapper;
return PlatformHelper.get().wrapContainerToItemHandler(inventory);
}
@Override

View File

@ -8,7 +8,7 @@
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleCommand;
import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nonnull;
@ -24,7 +24,7 @@ public TurtleCompareToCommand(int slot) {
public TurtleCommandResult execute(@Nonnull ITurtleAccess turtle) {
var selectedStack = turtle.getInventory().getItem(turtle.getSelectedSlot());
var stack = turtle.getInventory().getItem(slot);
return InventoryUtil.areItemsStackable(selectedStack, stack)
return ItemStack.isSameItemSameTags(selectedStack, stack)
? TurtleCommandResult.success()
: TurtleCommandResult.failure();
}

View File

@ -9,8 +9,12 @@
import dan200.computercraft.api.turtle.ITurtleCommand;
import dan200.computercraft.api.turtle.TurtleAnimation;
import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.shared.util.InventoryUtil;
import dan200.computercraft.shared.platform.ContainerTransfer;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.LevelEvent;
import javax.annotation.Nonnull;
@ -35,12 +39,7 @@ public TurtleCommandResult execute(@Nonnull ITurtleAccess turtle) {
// Get world direction from direction
var direction = this.direction.toWorldDir(turtle);
// Get things to drop
var stack = turtle.getInventory().removeItem(turtle.getSelectedSlot(), quantity);
if (stack.isEmpty()) {
return TurtleCommandResult.failure("No items to drop");
}
turtle.getInventory().setChanged();
var source = TurtleUtil.getSelectedSlot(turtle);
// Get inventory for thing in front
var world = turtle.getLevel();
@ -48,29 +47,33 @@ public TurtleCommandResult execute(@Nonnull ITurtleAccess turtle) {
var newPosition = oldPosition.relative(direction);
var side = direction.getOpposite();
var inventory = InventoryUtil.getInventory(world, newPosition, side);
var inventory = PlatformHelper.get().getContainer((ServerLevel) world, newPosition, side);
int transferred;
if (inventory != null) {
// Drop the item into the inventory
var remainder = InventoryUtil.storeItems(stack, inventory);
if (!remainder.isEmpty()) {
// Put the remainder back in the turtle
InventoryUtil.storeItems(remainder, turtle.getItemHandler(), turtle.getSelectedSlot());
}
transferred = source.moveTo(inventory, quantity);
} else {
var stack = turtle.getInventory().removeItem(turtle.getSelectedSlot(), quantity);
if (stack.isEmpty()) {
transferred = ContainerTransfer.NO_ITEMS;
} else {
// Drop the item into the world
turtle.getInventory().setChanged();
transferred = stack.getCount();
// Return true if we stored anything
if (remainder != stack) {
WorldUtil.dropItemStack(stack, world, oldPosition, direction);
world.globalLevelEvent(LevelEvent.SOUND_DISPENSER_DISPENSE, newPosition, 0);
}
}
switch (transferred) {
case ContainerTransfer.NO_SPACE:
return TurtleCommandResult.failure("No space for items");
case ContainerTransfer.NO_ITEMS:
return TurtleCommandResult.failure("No items to drop");
default:
turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success();
} else {
return TurtleCommandResult.failure("No space for items");
}
} else {
// Drop the item into the world
WorldUtil.dropItemStack(stack, world, oldPosition, direction);
world.globalLevelEvent(1000, newPosition, 0);
turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success();
}
}
}

View File

@ -31,11 +31,10 @@
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.eventbus.api.Event.Result;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.wrapper.InvWrapper;
import javax.annotation.Nonnull;
@ -112,15 +111,14 @@ private static boolean deployOnEntity(@Nonnull ItemStack stack, final ITurtleAcc
final var world = turtle.getLevel();
var turtlePos = turtlePlayer.position();
var rayDir = turtlePlayer.getViewVector(1.0f);
var hit = WorldUtil.rayTraceEntities(world, turtlePos, rayDir, 1.5);
if (hit == null) return false;
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
if (!(hit instanceof EntityHitResult entityHit)) return false;
// Start claiming entity drops
var hitEntity = hit.getKey();
var hitPos = hit.getValue();
var hitEntity = entityHit.getEntity();
var hitPos = entityHit.getLocation();
IItemHandler itemHandler = new InvWrapper(turtlePlayer.getInventory());
DropConsumer.set(hitEntity, drop -> InventoryUtil.storeItems(drop, itemHandler, 1));
DropConsumer.set(hitEntity, drop -> InventoryUtil.storeItemsFromOffset(turtlePlayer.getInventory(), drop, 1));
var placed = doDeployOnEntity(stack, turtlePlayer, hitEntity, hitPos);

View File

@ -9,10 +9,15 @@
import dan200.computercraft.api.turtle.ITurtleCommand;
import dan200.computercraft.api.turtle.TurtleAnimation;
import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.shared.platform.ContainerTransfer;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleUtil;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntitySelector;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.LevelEvent;
import net.minecraft.world.phys.AABB;
import javax.annotation.Nonnull;
@ -44,26 +49,19 @@ public TurtleCommandResult execute(@Nonnull ITurtleAccess turtle) {
var blockPosition = turtlePosition.relative(direction);
var side = direction.getOpposite();
var inventory = InventoryUtil.getInventory(world, blockPosition, side);
var inventory = PlatformHelper.get().getContainer((ServerLevel) world, blockPosition, side);
if (inventory != null) {
// Take from inventory of thing in front
var stack = InventoryUtil.takeItems(quantity, inventory);
if (stack.isEmpty()) return TurtleCommandResult.failure("No items to take");
// Try to place into the turtle
var remainder = InventoryUtil.storeItems(stack, turtle.getItemHandler(), turtle.getSelectedSlot());
if (!remainder.isEmpty()) {
// Put the remainder back in the inventory
InventoryUtil.storeItems(remainder, inventory);
}
// Return true if we consumed anything
if (remainder != stack) {
turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success();
} else {
return TurtleCommandResult.failure("No space for items");
var transferred = inventory.moveTo(TurtleUtil.getOffsetInventory(turtle), quantity);
switch (transferred) {
case ContainerTransfer.NO_SPACE:
return TurtleCommandResult.failure("No space for items");
case ContainerTransfer.NO_ITEMS:
return TurtleCommandResult.failure("No items to drop");
default:
turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success();
}
} else {
// Suck up loose items off the ground
@ -88,9 +86,10 @@ public TurtleCommandResult execute(@Nonnull ITurtleAccess turtle) {
leaveStack = ItemStack.EMPTY;
}
var remainder = InventoryUtil.storeItems(storeStack, turtle.getItemHandler(), turtle.getSelectedSlot());
var oldCount = storeStack.getCount();
var remainder = InventoryUtil.storeItemsFromOffset(turtle.getInventory(), storeStack, turtle.getSelectedSlot());
if (remainder != storeStack) {
if (remainder.getCount() != oldCount) {
if (remainder.isEmpty() && leaveStack.isEmpty()) {
entity.discard();
} else if (remainder.isEmpty()) {
@ -103,7 +102,7 @@ public TurtleCommandResult execute(@Nonnull ITurtleAccess turtle) {
}
// Play fx
world.globalLevelEvent(1000, turtlePosition, 0); // BLOCK_DISPENSER_DISPENSE
world.globalLevelEvent(LevelEvent.SOUND_DISPENSER_DISPENSE, turtlePosition, 0);
turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success();
}

View File

@ -33,10 +33,10 @@ public TurtleCommandResult execute(@Nonnull ITurtleAccess turtle) {
}
// Store stack
var remainder = InventoryUtil.storeItems(stack, turtle.getItemHandler(), slot, 1, slot);
var remainder = InventoryUtil.storeItemsIntoSlot(turtle.getInventory(), stack, slot);
if (!remainder.isEmpty()) {
// Put the remainder back
InventoryUtil.storeItems(remainder, turtle.getItemHandler(), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot());
InventoryUtil.storeItemsIntoSlot(turtle.getInventory(), remainder, turtle.getSelectedSlot());
}
// Return true if we moved anything

View File

@ -28,6 +28,7 @@
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.ToolActions;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
@ -102,13 +103,13 @@ private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
// See if there is an entity present
var turtlePos = turtlePlayer.position();
var rayDir = turtlePlayer.getViewVector(1.0f);
var hit = WorldUtil.rayTraceEntities(world, turtlePos, rayDir, 1.5);
if (hit != null) {
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
if (hit instanceof EntityHitResult entityHit) {
// Load up the turtle's inventory
var stackCopy = item.copy();
turtlePlayer.loadInventory(stackCopy);
var hitEntity = hit.getKey();
var hitEntity = entityHit.getEntity();
// Fire several events to ensure we have permissions.
if (MinecraftForge.EVENT_BUS.post(new AttackEntityEvent(turtlePlayer, hitEntity)) || !hitEntity.isAttackable()) {

View File

@ -7,139 +7,91 @@
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.WorldlyContainer;
import net.minecraft.world.WorldlyContainerHolder;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.ItemHandlerHelper;
import net.minecraftforge.items.wrapper.InvWrapper;
import net.minecraftforge.items.wrapper.SidedInvWrapper;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public final class InventoryUtil {
private InventoryUtil() {
}
// Methods for comparing things:
public static boolean areItemsEqual(@Nonnull ItemStack a, @Nonnull ItemStack b) {
return a == b || ItemStack.matches(a, b);
}
public static boolean areItemsStackable(@Nonnull ItemStack a, @Nonnull ItemStack b) {
return a == b || ItemHandlerHelper.canItemStacksStack(a, b);
}
// Methods for finding inventories:
@Nullable
public static IItemHandler getInventory(@Nonnull Level world, @Nonnull BlockPos pos, @Nonnull Direction side) {
// Look for tile with inventory
var tileEntity = world.getBlockEntity(pos);
if (tileEntity != null) {
var itemHandler = tileEntity.getCapability(ForgeCapabilities.ITEM_HANDLER, side);
if (itemHandler.isPresent()) {
return itemHandler.orElseThrow(NullPointerException::new);
} else if (tileEntity instanceof WorldlyContainer) {
return new SidedInvWrapper((WorldlyContainer) tileEntity, side);
} else if (tileEntity instanceof Container) {
return new InvWrapper((Container) tileEntity);
}
}
var block = world.getBlockState(pos);
if (block.getBlock() instanceof WorldlyContainerHolder) {
var inventory = ((WorldlyContainerHolder) block.getBlock()).getContainer(block, world, pos);
return new SidedInvWrapper(inventory, side);
}
// Look for entity with inventory
public static @Nullable Container getEntityContainer(ServerLevel level, BlockPos pos, Direction side) {
var vecStart = new Vec3(
pos.getX() + 0.5 + 0.6 * side.getStepX(),
pos.getY() + 0.5 + 0.6 * side.getStepY(),
pos.getZ() + 0.5 + 0.6 * side.getStepZ()
);
var dir = side.getOpposite();
var vecDir = new Vec3(
dir.getStepX(), dir.getStepY(), dir.getStepZ()
);
var hit = WorldUtil.rayTraceEntities(world, vecStart, vecDir, 1.1);
if (hit != null) {
var entity = hit.getKey();
if (entity instanceof Container) {
return new InvWrapper((Container) entity);
}
}
return null;
var vecDir = new Vec3(dir.getStepX(), dir.getStepY(), dir.getStepZ());
var hit = WorldUtil.clip(level, vecStart, vecDir, 1.1, null);
return hit instanceof EntityHitResult entityHit && entityHit.getEntity() instanceof Container container ? container : null;
}
// Methods for placing into inventories:
@Nonnull
public static ItemStack storeItems(@Nonnull ItemStack itemstack, IItemHandler inventory, int begin) {
return storeItems(itemstack, inventory, 0, inventory.getSlots(), begin);
public static ItemStack storeItemsIntoSlot(Container container, ItemStack stack, int slot) {
return storeItems(container, stack, slot, 1);
}
@Nonnull
public static ItemStack storeItems(@Nonnull ItemStack itemstack, IItemHandler inventory) {
return storeItems(itemstack, inventory, 0, inventory.getSlots(), 0);
public static ItemStack storeItemsFromOffset(Container container, ItemStack stack, int offset) {
return storeItems(container, stack, offset, container.getContainerSize());
}
@Nonnull
public static ItemStack storeItems(@Nonnull ItemStack stack, IItemHandler inventory, int start, int range, int begin) {
if (stack.isEmpty()) return ItemStack.EMPTY;
// Inspect the slots in order and try to find empty or stackable slots
var remainder = stack.copy();
for (var i = 0; i < range; i++) {
var slot = start + (i + begin - start) % range;
if (remainder.isEmpty()) break;
remainder = inventory.insertItem(slot, remainder, false);
}
return areItemsEqual(stack, remainder) ? stack : remainder;
private static ItemStack storeItems(Container container, ItemStack stack, int offset, int slotCount) {
var originalCount = stack.getCount();
var remainder = storeItemsImpl(container, stack, offset, slotCount);
if (remainder.getCount() != originalCount) container.setChanged();
return remainder;
}
// Methods for taking out of inventories
private static ItemStack storeItemsImpl(Container container, ItemStack stack, int offset, int slotCount) {
var limit = container.getMaxStackSize();
var maxSize = Math.min(stack.getMaxStackSize(), container.getMaxStackSize());
if (maxSize <= 0) return stack;
@Nonnull
public static ItemStack takeItems(int count, IItemHandler inventory) {
return takeItems(count, inventory, 0, inventory.getSlots(), 0);
}
for (var i = 0; i < slotCount; i++) {
var slot = i + offset;
if (slot > limit) slot -= limit;
@Nonnull
public static ItemStack takeItems(int count, IItemHandler inventory, int start, int range, int begin) {
// Combine multiple stacks from inventory into one if necessary
var partialStack = ItemStack.EMPTY;
for (var i = 0; i < range; i++) {
var slot = start + (i + begin - start) % range;
var currentStack = container.getItem(slot);
if (currentStack.isEmpty()) {
// If the current slot is empty and we can place them item then there's two cases:
if (!container.canPlaceItem(slot, stack)) continue;
// If we've extracted all items, return
if (count <= 0) break;
// If this doesn't slot, abort.
var stack = inventory.getStackInSlot(slot);
if (!stack.isEmpty() && (partialStack.isEmpty() || areItemsStackable(stack, partialStack))) {
var extracted = inventory.extractItem(slot, count, false);
if (!extracted.isEmpty()) {
if (partialStack.isEmpty()) {
// If we've extracted for this first time, then limit the count to the maximum stack size.
partialStack = extracted;
count = Math.min(count, extracted.getMaxStackSize());
} else {
partialStack.grow(extracted.getCount());
}
count -= extracted.getCount();
if (stack.getCount() <= maxSize) {
// If there's room to put the item in directly, do so.
container.setItem(slot, stack);
return ItemStack.EMPTY;
} else {
// Otherwise, take maxSize items from the stack and continue on our loop.
container.setItem(slot, stack.split(maxSize));
}
}
} else {
// If the current slot is non-empty, we've got space, and there's compatible items then:
if (currentStack.getCount() >= Math.min(currentStack.getMaxStackSize(), maxSize)) continue;
if (!canMergeItems(currentStack, stack)) continue;
// Determine how much space we have, and thus how much we can move - then move it!
var toMove = Math.min(stack.getCount(), maxSize - currentStack.getCount());
currentStack.grow(toMove);
stack.shrink(toMove);
if (stack.isEmpty()) return ItemStack.EMPTY;
}
}
return partialStack;
return stack;
}
private static boolean canMergeItems(ItemStack stack1, ItemStack stack2) {
if (stack1.getItem() != stack2.getItem()) return false;
if (stack1.getDamageValue() != stack2.getDamageValue()) return false;
if (stack1.getCount() > stack1.getMaxStackSize()) return false;
return ItemStack.tagMatches(stack1, stack2);
}
}

View File

@ -5,51 +5,33 @@
*/
package dan200.computercraft.shared.util;
import com.google.common.base.Predicate;
import com.google.common.collect.MapMaker;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraftforge.common.ForgeMod;
import org.apache.commons.lang3.tuple.Pair;
import javax.annotation.Nonnull;
import java.util.Map;
import javax.annotation.Nullable;
import java.util.function.Predicate;
public final class WorldUtil {
@SuppressWarnings("Guava")
private static final Predicate<Entity> CAN_COLLIDE = x -> x != null && x.isAlive() && x.isPickable();
private static final Map<Level, Entity> entityCache = new MapMaker().weakKeys().weakValues().makeMap();
private static synchronized Entity getEntity(Level world) {
// TODO: It'd be nice if we could avoid this. Maybe always use the turtle player (if it's available).
var entity = entityCache.get(world);
if (entity != null) return entity;
entity = new ItemEntity(EntityType.ITEM, world) {
@Nonnull
@Override
public EntityDimensions getDimensions(@Nonnull Pose pose) {
return EntityDimensions.fixed(0, 0);
}
};
entity.noPhysics = true;
entity.refreshDimensions();
entityCache.put(world, entity);
return entity;
}
public static boolean isLiquidBlock(Level world, BlockPos pos) {
if (!world.isInWorldBounds(pos)) return false;
return world.getBlockState(pos).getMaterial().isLiquid();
@ -62,66 +44,72 @@ public static boolean isVecInside(VoxelShape shape, Vec3 vec) {
return vec.x >= bb.minX && vec.x <= bb.maxX && vec.y >= bb.minY && vec.y <= bb.maxY && vec.z >= bb.minZ && vec.z <= bb.maxZ;
}
public static Pair<Entity, Vec3> rayTraceEntities(Level world, Vec3 vecStart, Vec3 vecDir, double distance) {
var vecEnd = vecStart.add(vecDir.x * distance, vecDir.y * distance, vecDir.z * distance);
// Raycast for blocks
var collisionEntity = getEntity(world);
collisionEntity.setPos(vecStart.x, vecStart.y, vecStart.z);
var context = new ClipContext(vecStart, vecEnd, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, collisionEntity);
HitResult result = world.clip(context);
if (result != null && result.getType() == HitResult.Type.BLOCK) {
distance = vecStart.distanceTo(result.getLocation());
vecEnd = vecStart.add(vecDir.x * distance, vecDir.y * distance, vecDir.z * distance);
}
// Check for entities
var xStretch = Math.abs(vecDir.x) > 0.25f ? 0.0f : 1.0f;
var yStretch = Math.abs(vecDir.y) > 0.25f ? 0.0f : 1.0f;
var zStretch = Math.abs(vecDir.z) > 0.25f ? 0.0f : 1.0f;
var bigBox = new AABB(
Math.min(vecStart.x, vecEnd.x) - 0.375f * xStretch,
Math.min(vecStart.y, vecEnd.y) - 0.375f * yStretch,
Math.min(vecStart.z, vecEnd.z) - 0.375f * zStretch,
Math.max(vecStart.x, vecEnd.x) + 0.375f * xStretch,
Math.max(vecStart.y, vecEnd.y) + 0.375f * yStretch,
Math.max(vecStart.z, vecEnd.z) + 0.375f * zStretch
);
Entity closest = null;
var closestDist = 99.0;
var list = world.getEntitiesOfClass(Entity.class, bigBox, CAN_COLLIDE);
for (var entity : list) {
var littleBox = entity.getBoundingBox();
if (littleBox.contains(vecStart)) {
closest = entity;
closestDist = 0.0f;
continue;
}
var littleBoxResult = littleBox.clip(vecStart, vecEnd).orElse(null);
if (littleBoxResult != null) {
var dist = vecStart.distanceTo(littleBoxResult);
if (closest == null || dist <= closestDist) {
closest = entity;
closestDist = dist;
}
} else if (littleBox.intersects(bigBox)) {
if (closest == null) {
closest = entity;
closestDist = distance;
}
}
}
if (closest != null && closestDist <= distance) {
var closestPos = vecStart.add(vecDir.x * closestDist, vecDir.y * closestDist, vecDir.z * closestDist);
return Pair.of(closest, closestPos);
}
return null;
public static HitResult clip(Level world, Vec3 from, Vec3 direction, double distance, @Nullable Entity source) {
var to = from.add(direction.x * distance, direction.y * distance, direction.z * distance);
return clip(world, from, to, source);
}
public static Vec3 getRayStart(LivingEntity entity) {
return entity.getEyePosition(1);
public static HitResult clip(Level world, Vec3 from, Vec3 to, @Nullable Entity source) {
var context = source == null
? new ContextlessClipContext(world, from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE)
: new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, source);
var blockHit = world.clip(context);
var distance = blockHit.getType() == HitResult.Type.MISS
? from.distanceToSqr(to) : blockHit.getLocation().distanceToSqr(from);
var entityHit = getEntityHitResult(world, from, to, new AABB(from, to).inflate(1), distance, source);
return entityHit == null ? blockHit : entityHit;
}
/**
* Perform a ray trace to the nearest entity. Derived from the various methods in {@link ProjectileUtil}.
*
* @param level The current level.
* @param from The start point of the ray trace.
* @param to The end point of the ray trace.
* @param bounds The range the entities should be within.
* @param distanceSq The maximum distance an entity can be away from the start vector.
* @param source An optional entity to ignore, which typically will be the entity performing the ray trace.
* @return The found entity, or {@code null}.
*/
private static @Nullable EntityHitResult getEntityHitResult(
Level level, Vec3 from, Vec3 to, AABB bounds, double distanceSq, @Nullable Entity source
) {
// If the distance is empty, we'll never collide anyway!
if (distanceSq <= 0) return null;
var bestDistance = distanceSq;
Entity bestEntity = null;
Vec3 bestHit = null;
for (var entity : level.getEntities(source, bounds, WorldUtil.CAN_COLLIDE)) {
var aabb = entity.getBoundingBox().inflate(entity.getPickRadius());
// clip doesn't work when inside the entity. Just assume we've got a perfect match and break.
if (aabb.contains(from)) {
bestHit = from;
bestEntity = entity;
break;
}
var clip = aabb.clip(from, to);
if (clip.isEmpty()) continue;
var hit = clip.get();
var newDistance = from.distanceToSqr(hit);
if (newDistance < bestDistance) {
bestEntity = entity;
bestHit = hit;
bestDistance = newDistance;
}
}
return bestEntity == null ? null : new EntityHitResult(bestEntity, bestHit);
}
public static Vec3 getRayStart(Player entity) {
return entity.getEyePosition();
}
public static Vec3 getRayEnd(Player player) {
@ -168,4 +156,23 @@ public static void dropItemStack(@Nonnull ItemStack stack, Level world, Vec3 pos
item.setDefaultPickUpDelay();
world.addFreshEntity(item);
}
/**
* A custom {@link ClipContext} which allows an empty entity.
* <p>
* This isn't needed on Forge, but is useful on Fabric.
*/
private static class ContextlessClipContext extends ClipContext {
private final Block block;
ContextlessClipContext(Level level, Vec3 from, Vec3 to, Block block, Fluid fluid) {
super(from, to, block, fluid, new ItemEntity(EntityType.ITEM, level));
this.block = block;
}
@Override
public VoxelShape getBlockShape(BlockState state, BlockGetter levle, BlockPos pos) {
return block.get(state, levle, pos, CollisionContext.empty());
}
}
}

View File

@ -0,0 +1,122 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.platform;
import net.minecraft.world.Container;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import static dan200.computercraft.support.ItemStackMatcher.isStack;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Test Interface defining the behaviour of a {@link ContainerTransfer} implementation.
*/
public interface ContainerTransferContract {
ContainerTransfer.Slotted wrap(Container container);
@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);
assertThat(inv.getItem(0), isStack(new ItemStack(Items.DIRT, 64)));
assertNoOverlap(inv);
}
@Test
default void testMoveSameInventory() {
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);
assertEquals(64, move);
assertThat(inv.getItem(0), isStack(ItemStack.EMPTY));
assertThat(inv.getItem(1), isStack(new ItemStack(Items.DIRT, 64)));
assertNoOverlap(inv);
}
@Test
default void testMoveDifferentInventories() {
var destination = new SimpleContainer(4);
var source = new SimpleContainer(4);
source.setItem(0, new ItemStack(Items.DIRT, 64));
var move = wrap(source).moveTo(wrap(destination), 64);
assertEquals(64, move);
assertThat(source.getItem(0), isStack(ItemStack.EMPTY));
assertThat(destination.getItem(0), isStack(new ItemStack(Items.DIRT, 64)));
assertNoOverlap(source, destination);
}
@Test
default void testMoveDistributeDestination() {
var destination = new SimpleContainer(4);
destination.setItem(0, new ItemStack(Items.DIRT, 48));
destination.setItem(1, new ItemStack(Items.DIRT, 48));
destination.setItem(2, new ItemStack(Items.DIRT, 48));
destination.setItem(3, new ItemStack(Items.DIRT, 48));
var source = new SimpleContainer(4);
source.setItem(0, new ItemStack(Items.DIRT, 64));
var move = wrap(source).moveTo(wrap(destination), 64);
assertEquals(64, move);
assertThat(source.getItem(0), isStack(ItemStack.EMPTY));
for (var i = 0; i < 4; i++) {
assertThat("Stack in slot " + i, destination.getItem(i), isStack(new ItemStack(Items.DIRT, 64)));
}
assertNoOverlap(source, destination);
}
@Test
default void testMoveSkipFullSlot() {
var destination = new SimpleContainer(4);
destination.setItem(0, new ItemStack(Items.DIRT, 64));
var source = new SimpleContainer(4);
source.setItem(0, new ItemStack(Items.DIRT, 64));
var move = wrap(source).moveTo(wrap(destination), 64);
assertEquals(64, move);
assertThat(source.getItem(1), isStack(ItemStack.EMPTY));
assertThat(destination.getItem(0), isStack(new ItemStack(Items.DIRT, 64)));
assertThat(destination.getItem(1), isStack(new ItemStack(Items.DIRT, 64)));
assertNoOverlap(source, destination);
}
static void assertNoOverlap(Container... containers) {
Set<ItemStack> stacks = Collections.newSetFromMap(new IdentityHashMap<>());
for (var container : containers) {
for (var slot = 0; slot < container.getContainerSize(); slot++) {
var item = container.getItem(slot);
if (item == ItemStack.EMPTY) continue;
if (!stacks.add(item)) throw new AssertionError("Duplicate item in inventories");
}
}
}
}

View File

@ -0,0 +1,18 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.platform;
import dan200.computercraft.support.WithMinecraft;
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));
}
}

View File

@ -0,0 +1,33 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.support;
import net.minecraft.world.item.ItemStack;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
public class ItemStackMatcher extends TypeSafeMatcher<ItemStack> {
private final ItemStack stack;
public ItemStackMatcher(ItemStack stack) {
this.stack = stack;
}
@Override
protected boolean matchesSafely(ItemStack item) {
return ItemStack.isSameItemSameTags(item, stack) && item.getCount() == stack.getCount();
}
@Override
public void describeTo(Description description) {
description.appendValue(stack);
}
public static Matcher<ItemStack> isStack(ItemStack stack) {
return new ItemStackMatcher(stack);
}
}