mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-04-22 10:43:13 +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:
parent
7d47b219c5
commit
55494b7671
buildSrc/src/main/kotlin
gradle
src
main/java/dan200/computercraft/shared
computer/core
peripheral
platform
pocket/apis
turtle
TurtleUtil.java
blocks
core
TurtleBrain.javaTurtleCompareToCommand.javaTurtleDropCommand.javaTurtlePlaceCommand.javaTurtleSuckCommand.javaTurtleTransferToCommand.java
upgrades
util
test/java/dan200/computercraft
shared/platform
support
@ -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))
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
|
@ -19,6 +19,7 @@ import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
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 class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
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()) {
|
||||
|
@ -15,7 +15,6 @@ import dan200.computercraft.shared.common.TileGeneric;
|
||||
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 final class TileDiskDrive extends TileGeneric implements DefaultInventory
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (InventoryUtil.areItemsStackable(stack, diskStack)) {
|
||||
if (ItemStack.isSameItemSameTags(stack, diskStack)) {
|
||||
diskStack = stack;
|
||||
return;
|
||||
}
|
||||
|
@ -13,13 +13,13 @@ import dan200.computercraft.api.lua.LuaFunction;
|
||||
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 @@ public class InventoryMethods implements GenericPeripheral {
|
||||
* @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));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
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.resources.ResourceLocation;
|
||||
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.entity.BlockEntityType;
|
||||
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 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
|
||||
* @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);
|
||||
}
|
||||
|
@ -15,6 +15,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.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.resources.ResourceLocation;
|
||||
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.block.state.BlockState;
|
||||
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 class PlatformHelperImpl implements PlatformHelper {
|
||||
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) {
|
||||
|
@ -10,12 +10,9 @@ import dan200.computercraft.api.lua.LuaFunction;
|
||||
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 class PocketAPI implements ILuaAPI {
|
||||
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 class PocketAPI implements ILuaAPI {
|
||||
|
||||
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());
|
||||
|
@ -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 net.minecraft.world.item.ItemStack;
|
||||
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 class TurtleUtil {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -21,7 +21,10 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
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 class TileTurtle extends TileComputerBase implements ITurtleTile, Default
|
||||
|
||||
@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();
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import dan200.computercraft.core.util.Colour;
|
||||
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.level.material.PushReaction;
|
||||
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 class TurtleBrain implements ITurtleAccess {
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
@Deprecated(forRemoval = true)
|
||||
public IItemHandlerModifiable getItemHandler() {
|
||||
return inventoryWrapper;
|
||||
return PlatformHelper.get().wrapContainerToItemHandler(inventory);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -8,7 +8,7 @@ package dan200.computercraft.shared.turtle.core;
|
||||
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 class TurtleCompareToCommand implements ITurtleCommand {
|
||||
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();
|
||||
}
|
||||
|
@ -9,8 +9,12 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
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 class TurtleDropCommand implements ITurtleCommand {
|
||||
// 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 class TurtleDropCommand implements ITurtleCommand {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,11 +31,10 @@ import net.minecraft.world.level.block.Block;
|
||||
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 @@ public class TurtlePlaceCommand implements ITurtleCommand {
|
||||
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);
|
||||
|
||||
|
@ -9,10 +9,15 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
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 class TurtleSuckCommand implements ITurtleCommand {
|
||||
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 class TurtleSuckCommand implements ITurtleCommand {
|
||||
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 class TurtleSuckCommand implements ITurtleCommand {
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
@ -33,10 +33,10 @@ public class TurtleTransferToCommand implements ITurtleCommand {
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -28,6 +28,7 @@ import net.minecraft.world.level.Level;
|
||||
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 @@ public class TurtleTool extends AbstractTurtleUpgrade {
|
||||
// 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()) {
|
||||
|
@ -7,139 +7,91 @@ package dan200.computercraft.shared.util;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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 final class WorldUtil {
|
||||
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 final class WorldUtil {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user