mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 13:42:59 +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:
		| @@ -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,27 +47,31 @@ 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()); | ||||
|             } | ||||
| 
 | ||||
|             // Return true if we stored anything | ||||
|             if (remainder != stack) { | ||||
|                 turtle.playAnimation(TurtleAnimation.WAIT); | ||||
|                 return TurtleCommandResult.success(); | ||||
|             transferred = source.moveTo(inventory, quantity); | ||||
|         } else { | ||||
|                 return TurtleCommandResult.failure("No space for items"); | ||||
|             } | ||||
|             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(); | ||||
| 
 | ||||
|                 WorldUtil.dropItemStack(stack, world, oldPosition, direction); | ||||
|             world.globalLevelEvent(1000, newPosition, 0); | ||||
|                 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(); | ||||
|         } | ||||
|   | ||||
| @@ -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) { | ||||
|             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 { | ||||
|                 return TurtleCommandResult.failure("No space for items"); | ||||
|             } | ||||
|         } 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()); | ||||
|                 if (stack.getCount() <= maxSize) { | ||||
|                     // If there's room to put the item in directly, do so. | ||||
|                     container.setItem(slot, stack); | ||||
|                     return ItemStack.EMPTY; | ||||
|                 } else { | ||||
|                         partialStack.grow(extracted.getCount()); | ||||
|                     // 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; | ||||
| 
 | ||||
|                     count -= extracted.getCount(); | ||||
|                 // 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 stack; | ||||
|     } | ||||
| 
 | ||||
|         return partialStack; | ||||
|     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); | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|         // 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 | ||||
|         ); | ||||
|     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); | ||||
| 
 | ||||
|         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 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; | ||||
|     } | ||||
| 
 | ||||
|             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; | ||||
|     /** | ||||
|      * 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; | ||||
|             } | ||||
| 
 | ||||
|     public static Vec3 getRayStart(LivingEntity entity) { | ||||
|         return entity.getEyePosition(1); | ||||
|             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); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates