mirror of
https://github.com/GTNewHorizons/Applied-Energistics-2-Unofficial.git
synced 2026-11-14 01:14:11 +00:00
Add multiple new Horizon-QA tests to cover AE2's logic (#1425)
This commit is contained in:
1 parent
689b53ec4a
commit
52d9e9c7c0
27 files changed
+3809
-728
No files matched your search
+1
-1
@@ -52,7 +52,7 @@ dependencies {
|
||||
exclude module: "waila"
|
||||
}
|
||||
|
||||
devOnlyNonPublishable("com.github.GTNewHorizons:Horizon-QA:0.7.0:dev")
|
||||
devOnlyNonPublishable("com.github.GTNewHorizons:Horizon-QA:0.9.0:dev")
|
||||
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Postea:1.2.5:dev")
|
||||
runtimeOnlyNonPublishable('com.github.GTNewHorizons:waila:1.19.30:dev') {transitive = false}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
package appeng.gametests;
|
||||
|
||||
import static appeng.util.item.AEFluidStackType.FLUID_STACK_TYPE;
|
||||
import static appeng.util.item.AEItemStackType.ITEM_STACK_TYPE;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.tileentity.TileEntityChest;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
import net.minecraftforge.fluids.Fluid;
|
||||
import net.minecraftforge.fluids.FluidStack;
|
||||
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestAssertException;
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.TestPos;
|
||||
|
||||
import appeng.api.AEApi;
|
||||
import appeng.api.config.Actionable;
|
||||
import appeng.api.networking.IGridNode;
|
||||
import appeng.api.networking.security.BaseActionSource;
|
||||
import appeng.api.networking.storage.IStorageGrid;
|
||||
import appeng.api.parts.IPart;
|
||||
import appeng.api.storage.ICellHandler;
|
||||
import appeng.api.storage.IMEInventoryHandler;
|
||||
import appeng.api.storage.IMEMonitor;
|
||||
import appeng.api.storage.data.IAEFluidStack;
|
||||
import appeng.api.storage.data.IAEItemStack;
|
||||
import appeng.me.GridAccessException;
|
||||
import appeng.me.helpers.AENetworkProxy;
|
||||
import appeng.tile.networking.TileCableBus;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.util.item.AEFluidStack;
|
||||
import appeng.util.item.AEItemStack;
|
||||
|
||||
public final class AEGameTestHelpers {
|
||||
|
||||
private static final BaseActionSource TEST_SOURCE = new BaseActionSource();
|
||||
|
||||
private AEGameTestHelpers() {}
|
||||
|
||||
public static <T extends TileEntity> T tile(GameTestHelper helper, Class<T> type, String label) {
|
||||
Coord pos = pos(helper, label);
|
||||
return helper.assertTileEntityPresent(type, pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
public static Coord pos(GameTestHelper helper, String label) {
|
||||
TestPos pos = helper.pos(label);
|
||||
return new Coord(pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
public static IPart part(GameTestHelper helper, String label, ForgeDirection side) {
|
||||
TileCableBus cableBus = tile(helper, TileCableBus.class, label);
|
||||
IPart part = cableBus.getPart(side);
|
||||
helper.assertNotNull(part, "Placed part should be readable from its host");
|
||||
return part;
|
||||
}
|
||||
|
||||
public static <T extends IPart> T part(GameTestHelper helper, String label, Class<T> type) {
|
||||
TileCableBus cableBus = tile(helper, TileCableBus.class, label);
|
||||
for (ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
|
||||
IPart candidate = cableBus.getPart(side);
|
||||
if (type.isInstance(candidate)) {
|
||||
return type.cast(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError("Template role '" + label + "' should contain part type " + type.getSimpleName());
|
||||
}
|
||||
|
||||
public static void setRedstoneInput(GameTestHelper helper, String label, int strength) {
|
||||
Coord pos = pos(helper, label);
|
||||
helper.setRedstoneInput(pos.x(), pos.y(), pos.z(), strength);
|
||||
}
|
||||
|
||||
public static void assertActive(GameTestHelper helper, AENetworkProxy proxy, String message) {
|
||||
IGridNode node = proxy.getNode();
|
||||
helper.assertNotNull(node, "Grid proxy should have a node");
|
||||
helper.assertTrue(node.isActive(), message);
|
||||
}
|
||||
|
||||
public static void assertInactive(GameTestHelper helper, AENetworkProxy proxy, String message) {
|
||||
IGridNode node = proxy.getNode();
|
||||
helper.assertFalse(node != null && node.isActive(), message);
|
||||
}
|
||||
|
||||
public static void assertActive(GameTestHelper helper, IPart part, String message) {
|
||||
IGridNode node = part.getGridNode();
|
||||
helper.assertNotNull(node, "Part should have a grid node");
|
||||
helper.assertTrue(node.isActive(), message);
|
||||
}
|
||||
|
||||
public static void assertInactive(GameTestHelper helper, IPart part, String message) {
|
||||
IGridNode node = part.getGridNode();
|
||||
helper.assertNotNull(node, "Part should have a grid node");
|
||||
helper.assertFalse(node.isActive(), message);
|
||||
}
|
||||
|
||||
public static IAEItemStack injectIntoGrid(TileController controller, Block block, long amount) {
|
||||
return itemMonitor(controller).injectItems(itemStack(block, amount), Actionable.MODULATE, TEST_SOURCE);
|
||||
}
|
||||
|
||||
public static IAEItemStack simulateInjectIntoGrid(TileController controller, Block block, long amount) {
|
||||
return itemMonitor(controller).injectItems(itemStack(block, amount), Actionable.SIMULATE, TEST_SOURCE);
|
||||
}
|
||||
|
||||
public static void assertItemRemainder(GameTestHelper helper, IAEItemStack remainder, Block block,
|
||||
long expectedAmount) {
|
||||
helper.assertNotNull(remainder, "Rejected " + describe(block) + " stack should be returned as a remainder");
|
||||
helper.assertTrue(
|
||||
remainder.isSameType(new ItemStack(block, 1)),
|
||||
"Rejected remainder item should be " + describe(block)
|
||||
+ "; actual="
|
||||
+ describe(remainder.getItemStack()));
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
remainder.getStackSize(),
|
||||
"Rejected remainder amount for " + describe(block) + " should match");
|
||||
}
|
||||
|
||||
public static void assertNetworkStoredAmount(GameTestHelper helper, TileController controller, Block block,
|
||||
long expectedAmount) {
|
||||
long actualAmount = networkStoredAmount(controller, block);
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
actualAmount,
|
||||
"Network storage for " + describe(block) + " should match; controller=" + describe(controller));
|
||||
}
|
||||
|
||||
public static long networkStoredAmount(TileController controller, Block block) {
|
||||
IAEItemStack extracted = itemMonitor(controller)
|
||||
.extractItems(itemStack(block, Long.MAX_VALUE), Actionable.SIMULATE, TEST_SOURCE);
|
||||
return extracted == null ? 0 : extracted.getStackSize();
|
||||
}
|
||||
|
||||
public static void assertNetworkStoredAmount(GameTestHelper helper, IGridNode node, Block block,
|
||||
long expectedAmount) {
|
||||
long actualAmount = networkStoredAmount(node, block);
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
actualAmount,
|
||||
"Network storage for " + describe(block) + " should match; node=" + describe(node));
|
||||
}
|
||||
|
||||
public static long networkStoredAmount(IGridNode node, Block block) {
|
||||
IAEItemStack extracted = itemMonitor(node)
|
||||
.extractItems(itemStack(block, Long.MAX_VALUE), Actionable.SIMULATE, TEST_SOURCE);
|
||||
return extracted == null ? 0 : extracted.getStackSize();
|
||||
}
|
||||
|
||||
public static void assertNetworkMonitorStoredAmount(GameTestHelper helper, TileController controller, Block block,
|
||||
long expectedAmount) {
|
||||
long actualAmount = networkMonitorStoredAmount(controller, block);
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
actualAmount,
|
||||
"Network monitor for " + describe(block) + " should match; controller=" + describe(controller));
|
||||
}
|
||||
|
||||
public static long networkMonitorStoredAmount(TileController controller, Block block) {
|
||||
IAEItemStack stored = itemMonitor(controller).getStorageList().findPrecise(itemStack(block, 1));
|
||||
return stored == null ? 0 : stored.getStackSize();
|
||||
}
|
||||
|
||||
public static IMEMonitor<IAEItemStack> itemMonitor(TileController controller) {
|
||||
try {
|
||||
return controller.getProxy().getStorage().getItemInventory();
|
||||
} catch (GridAccessException e) {
|
||||
throw new AssertionError("Network storage should be accessible", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static IMEMonitor<IAEItemStack> itemMonitor(IGridNode node) {
|
||||
if (node == null || node.getGrid() == null) {
|
||||
throw new AssertionError("Network storage should have an attached grid node");
|
||||
}
|
||||
|
||||
IStorageGrid storageGrid = node.getGrid().getCache(IStorageGrid.class);
|
||||
if (storageGrid == null) {
|
||||
throw new AssertionError("Network storage cache should be accessible");
|
||||
}
|
||||
return storageGrid.getItemInventory();
|
||||
}
|
||||
|
||||
public static void setChestSlot(TileEntityChest chest, int slot, Block block, int amount) {
|
||||
chest.setInventorySlotContents(slot, new ItemStack(block, amount));
|
||||
chest.markDirty();
|
||||
}
|
||||
|
||||
public static void clearChestSlot(TileEntityChest chest, int slot) {
|
||||
chest.setInventorySlotContents(slot, null);
|
||||
chest.markDirty();
|
||||
}
|
||||
|
||||
public static void fillChest(TileEntityChest chest, Block block) {
|
||||
for (int slot = 0; slot < chest.getSizeInventory(); slot++) {
|
||||
setChestSlot(chest, slot, block, 64);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertChestStoredAmount(GameTestHelper helper, TileEntityChest chest, Block block,
|
||||
long expectedAmount) {
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
chestStoredAmount(chest, block),
|
||||
"Chest storage for " + describe(block) + " should match; chest=" + describe(chest));
|
||||
}
|
||||
|
||||
public static long chestStoredAmount(TileEntityChest chest, Block block) {
|
||||
long amount = 0;
|
||||
ItemStack expectedStack = new ItemStack(block, 1);
|
||||
for (int slot = 0; slot < chest.getSizeInventory(); slot++) {
|
||||
ItemStack stack = chest.getStackInSlot(slot);
|
||||
if (stack != null && stack.isItemEqual(expectedStack)) {
|
||||
amount += stack.stackSize;
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
public static void insertItems(GameTestHelper helper, ItemStack cell, Block block, long amount) {
|
||||
IAEItemStack remainder = itemInventory(helper, cell)
|
||||
.injectItems(itemStack(block, amount), Actionable.MODULATE, TEST_SOURCE);
|
||||
helper.assertNull(remainder, "Items should fit completely into the cell");
|
||||
}
|
||||
|
||||
public static void insertFluids(GameTestHelper helper, ItemStack cell, Fluid fluid, long amount) {
|
||||
IAEFluidStack remainder = fluidInventory(helper, cell)
|
||||
.injectItems(fluidStack(fluid, amount), Actionable.MODULATE, TEST_SOURCE);
|
||||
helper.assertNull(remainder, "Fluids should fit completely into the cell");
|
||||
}
|
||||
|
||||
public static void assertStoredAmount(GameTestHelper helper, ItemStack cell, Block block, long expectedAmount) {
|
||||
helper.assertNotNull(cell, "Cell containing " + describe(block) + " should exist");
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
storedAmount(helper, cell, block),
|
||||
"Cell storage for " + describe(block) + " should match; cell=" + describe(cell));
|
||||
}
|
||||
|
||||
public static void assertStoredFluidAmount(GameTestHelper helper, ItemStack cell, Fluid fluid,
|
||||
long expectedAmount) {
|
||||
helper.assertNotNull(cell, "Cell should exist");
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
storedFluidAmount(helper, cell, fluid),
|
||||
"Cell storage for fluid " + fluid.getName() + " should match; cell=" + describe(cell));
|
||||
}
|
||||
|
||||
public static long storedAmount(GameTestHelper helper, ItemStack cell, Block block) {
|
||||
IAEItemStack extracted = itemInventory(helper, cell)
|
||||
.extractItems(itemStack(block, Long.MAX_VALUE), Actionable.SIMULATE, TEST_SOURCE);
|
||||
return extracted == null ? 0 : extracted.getStackSize();
|
||||
}
|
||||
|
||||
public static long storedFluidAmount(GameTestHelper helper, ItemStack cell, Fluid fluid) {
|
||||
IAEFluidStack extracted = fluidInventory(helper, cell)
|
||||
.extractItems(fluidStack(fluid, Long.MAX_VALUE), Actionable.SIMULATE, TEST_SOURCE);
|
||||
return extracted == null ? 0 : extracted.getStackSize();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static IMEInventoryHandler<IAEItemStack> itemInventory(GameTestHelper helper, ItemStack cell) {
|
||||
ICellHandler cellHandler = AEApi.instance().registries().cell().getHandler(cell);
|
||||
helper.assertNotNull(cellHandler, "Cell handler should exist");
|
||||
IMEInventoryHandler<IAEItemStack> cellInv = cellHandler.getCellInventory(cell, null, ITEM_STACK_TYPE);
|
||||
helper.assertNotNull(cellInv, "Item cell inventory should exist");
|
||||
return cellInv;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static IMEInventoryHandler<IAEFluidStack> fluidInventory(GameTestHelper helper, ItemStack cell) {
|
||||
ICellHandler cellHandler = AEApi.instance().registries().cell().getHandler(cell);
|
||||
helper.assertNotNull(cellHandler, "Cell handler should exist");
|
||||
IMEInventoryHandler<IAEFluidStack> cellInv = cellHandler.getCellInventory(cell, null, FLUID_STACK_TYPE);
|
||||
helper.assertNotNull(cellInv, "Fluid cell inventory should exist");
|
||||
return cellInv;
|
||||
}
|
||||
|
||||
public static IAEItemStack itemStack(Block block, long amount) {
|
||||
IAEItemStack stack = AEItemStack.create(new ItemStack(block, 1));
|
||||
stack.setStackSize(amount);
|
||||
return stack;
|
||||
}
|
||||
|
||||
public static IAEFluidStack fluidStack(Fluid fluid, long amount) {
|
||||
IAEFluidStack stack = AEFluidStack.create(new FluidStack(fluid, 1));
|
||||
stack.setStackSize(amount);
|
||||
return stack;
|
||||
}
|
||||
|
||||
public static ItemStack cell1k() {
|
||||
return AEApi.instance().definitions().items().cell1k().maybeStack(1).get();
|
||||
}
|
||||
|
||||
public static ItemStack cell4k() {
|
||||
return AEApi.instance().definitions().items().cell4k().maybeStack(1).get();
|
||||
}
|
||||
|
||||
public static ItemStack cell64k() {
|
||||
return AEApi.instance().definitions().items().cell64k().maybeStack(1).get();
|
||||
}
|
||||
|
||||
public static ContinuousInvariant continuousInvariant(GameTestHelper helper, String description,
|
||||
Runnable assertion) {
|
||||
ContinuousInvariant invariant = new ContinuousInvariant(description, assertion);
|
||||
helper.onEachTick(invariant::check);
|
||||
return invariant;
|
||||
}
|
||||
|
||||
private static String describe(Block block) {
|
||||
Object registryName = Block.blockRegistry.getNameForObject(block);
|
||||
return registryName == null ? block.getUnlocalizedName() : registryName.toString();
|
||||
}
|
||||
|
||||
private static String describe(ItemStack stack) {
|
||||
if (stack == null) {
|
||||
return "null";
|
||||
}
|
||||
return stack.getDisplayName() + " x" + stack.stackSize + " nbt=" + stack.getTagCompound();
|
||||
}
|
||||
|
||||
private static String describe(TileEntity tile) {
|
||||
return tile.getClass().getSimpleName() + "@(" + tile.xCoord + ',' + tile.yCoord + ',' + tile.zCoord + ')';
|
||||
}
|
||||
|
||||
private static String describe(IGridNode node) {
|
||||
if (node == null) {
|
||||
return "null";
|
||||
}
|
||||
return node.getMachine().getClass().getSimpleName() + "[active=" + node.isActive() + ']';
|
||||
}
|
||||
|
||||
public static final class ContinuousInvariant {
|
||||
|
||||
private final String description;
|
||||
private final Runnable assertion;
|
||||
private boolean enabled;
|
||||
|
||||
private ContinuousInvariant(String description, Runnable assertion) {
|
||||
this.description = description;
|
||||
this.assertion = assertion;
|
||||
}
|
||||
|
||||
public void enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public void disable() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
private void check() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.assertion.run();
|
||||
} catch (GameTestAssertException failure) {
|
||||
String message = "Continuous invariant '" + this.description + "' failed: " + failure.getMessage();
|
||||
GameTestAssertException enrichedFailure = failure.hasPosition()
|
||||
? new GameTestAssertException(message, failure.getPos())
|
||||
: new GameTestAssertException(message, failure.getX(), failure.getY(), failure.getZ());
|
||||
enrichedFailure.initCause(failure);
|
||||
throw enrichedFailure;
|
||||
} catch (AssertionError failure) {
|
||||
throw new AssertionError(
|
||||
"Continuous invariant '" + this.description + "' failed: " + failure.getMessage(),
|
||||
failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Coord {
|
||||
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
|
||||
public Coord(int x, int y, int z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public int x() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public int y() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
public int z() {
|
||||
return this.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,726 +0,0 @@
|
||||
package appeng.gametests;
|
||||
|
||||
import static appeng.util.item.AEFluidStackType.FLUID_STACK_TYPE;
|
||||
import static appeng.util.item.AEItemStackType.ITEM_STACK_TYPE;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.init.Items;
|
||||
import net.minecraft.inventory.IInventory;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
import net.minecraftforge.fluids.Fluid;
|
||||
import net.minecraftforge.fluids.FluidRegistry;
|
||||
import net.minecraftforge.fluids.FluidStack;
|
||||
|
||||
import com.glodblock.github.loader.ItemAndBlockHolder;
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.TestPos;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.AEApi;
|
||||
import appeng.api.config.Actionable;
|
||||
import appeng.api.config.FullnessMode;
|
||||
import appeng.api.config.OperationMode;
|
||||
import appeng.api.config.RedstoneMode;
|
||||
import appeng.api.config.Settings;
|
||||
import appeng.api.networking.security.BaseActionSource;
|
||||
import appeng.api.storage.ICellHandler;
|
||||
import appeng.api.storage.IMEInventoryHandler;
|
||||
import appeng.api.storage.data.IAEFluidStack;
|
||||
import appeng.api.storage.data.IAEItemStack;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
import appeng.tile.storage.TileIOPort;
|
||||
import appeng.util.item.AEFluidStack;
|
||||
import appeng.util.item.AEItemStack;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class IOPortTests {
|
||||
|
||||
private static final String IO_PORT_LABEL = "io_port";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final String REDSTONE_LABEL = "redstone";
|
||||
private static final BaseActionSource TEST_SOURCE = new BaseActionSource();
|
||||
|
||||
// Moves an empty cell from an input slot to an output slot.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void emptyCellMove(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack cell = cell1k();
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
ioport.setInventorySlotContents(0, cell.copy());
|
||||
helper.assertTrue(ItemStack.areItemStacksEqual(ioport.getStackInSlot(0), cell));
|
||||
helper.assertTrue(ioport.getStackInSlot(6) == null);
|
||||
}).thenExecute(() -> {
|
||||
helper.assertTrue(ioport.getStackInSlot(0) == null);
|
||||
ItemStack expectedCell = cell.copy();
|
||||
expectedCell.setTagCompound(new NBTTagCompound());
|
||||
helper.assertTrue(ItemStack.areItemStacksEqual(ioport.getStackInSlot(6), expectedCell));
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Exports items from a filled cell into ME network storage in EMPTY mode.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void emptyModeExportsCellContentsToNetwork(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 100);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenWaitUntil(10, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Exported cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Exported cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Imports items from ME network storage into an empty cell in FILL mode.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void fillModeImportsNetworkContentsIntoCell(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 100);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenWaitUntil(10, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Imported cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Imported cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 100);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Covers PR1197: FILL plus MoveWhenEmpty moves the cell when the remaining network amount exactly matches the
|
||||
// transfer budget.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void fillMoveWhenEmptyMovesCellWhenNetworkHasExactlyTransferBudget(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 256);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenExecute(() -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell should leave input when the network is exactly drained");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Cell should move to output when the network is exactly drained");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 256);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Covers PR1259: FILL plus MoveWhenEmpty does not move the cell when the network starts empty.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void fillMoveWhenEmptyDoesNotMoveWhenNetworkStartsEmpty(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell should stay in input when the network starts empty");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Cell should not move to output when no work was done");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Covers PR1259: FILL plus MoveWhenEmpty moves the cell when the target cell becomes full.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void fillMoveWhenEmptyMovesWhenTargetCellBecomesFull(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell64k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 9000);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenIdle(40).thenExecute(() -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Full target cell should leave input in move-when-empty mode");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Full target cell should move to output even when the network still has items");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 8128);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 872);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Keeps a cell in input under FullnessMode.EMPTY until the source cell is empty.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void emptyFullnessWaitsUntilSourceCellIsEmpty(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 300);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Partially transferred cell should remain in the input slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 44);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 256);
|
||||
}).thenWaitUntil(10, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Emptied cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Emptied cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 300);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Moves a partially transferred cell under FullnessMode.HALF.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void halfFullnessMovesPartiallyTransferredCell(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.EMPTY, FullnessMode.HALF);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack nearlyFullDriveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 300);
|
||||
insertItems(helper, nearlyFullDriveCell, Blocks.cobblestone, 8028);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, nearlyFullDriveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenWaitUntil(20, () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"HALF mode should remove the partially transferred cell from input");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"HALF mode should move the partially transferred cell to output");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 200);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 8128);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Moves a FILL-mode target cell only after it becomes full under FullnessMode.FULL.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void fullFullnessMovesOnlyAfterTargetCellIsFull(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.FULL);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell64k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 9000);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenWaitUntil(40, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Full target cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Full target cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 8128);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 872);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Moves cells from multiple input slots to output slots without loss or overwrite.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void multipleInputSlotsMoveToOutputSlots(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
for (int slot = 0; slot < 6; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
}).thenIdle(4).thenExecute(() -> {
|
||||
helper.assertEquals(1, countFilledSlots(ioport, 0, 6), "Input slots should contain 1 cell");
|
||||
helper.assertEquals(5, countFilledSlots(ioport, 6, 12), "Output slots should contain 5 cells");
|
||||
}).thenIdle(1).thenExecute(() -> {
|
||||
for (int slot = 0; slot < 6; slot++) {
|
||||
helper.assertNull(ioport.getStackInSlot(slot), "All input slots should become empty");
|
||||
}
|
||||
helper.assertEquals(6, countFilledSlots(ioport, 6, 12), "Output slots should contain 6 cells");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Keeps a cell queued while output is full, then moves it when an output slot opens.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void outputFullKeepsCellQueuedUntilSlotOpens(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack queuedCell = cell4k();
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
for (int slot = 6; slot < 12; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
ioport.setInventorySlotContents(0, queuedCell);
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Input cell should be retained while output is full");
|
||||
helper.assertEquals(6, countFilledSlots(ioport, 6, 12), "Output cell count should stay full");
|
||||
ioport.setInventorySlotContents(6, null);
|
||||
}).thenIdle(1).thenExecute(() -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Input cell should leave once an output slot opens");
|
||||
helper.assertTrue(
|
||||
ItemStack.areItemStacksEqual(ioport.getStackInSlot(6), queuedCell),
|
||||
"Queued cell should move into the opened output slot");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Queues an emptied source cell while output is full, then moves it with its contents preserved.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void outputFullQueuesTransferredCellUntilSlotOpens(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell4k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 100);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
for (int slot = 6; slot < 12; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Transferred cell should stay in input while output is full");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
ioport.setInventorySlotContents(6, null);
|
||||
}).thenIdle(1).thenExecute(() -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Transferred cell should leave input once output opens");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Transferred cell should move into the opened output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Keeps source cell contents unchanged when EMPTY mode cannot export into full network storage.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void emptyModeKeepsSourceCellWhenDestinationStorageIsFull(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack fullDriveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 100);
|
||||
insertItems(helper, fullDriveCell, Blocks.cobblestone, 8128);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, fullDriveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Source cell should remain in input when destination is full");
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Source cell should not move to output when no export was possible");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 8128);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Transfers 256 item units per tick without upgrades.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void noUpgradeTransfersTwoHundredFiftySixItemsPerTick(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 300);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell should remain in input after exhausting transfer budget");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 44);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 256);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Transfers 512 item units per tick with one Speed upgrade.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void speedUpgradeTransfersFiveHundredTwelveItemsPerTick(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
installUpgrade(ioport, AEApi.instance().definitions().materials().cardSpeed().maybeStack(1).get(), 0);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 600);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell with remaining contents should stay in input after speed transfer");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 88);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 512);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Transfers 256,000 mB per tick for fluid cells without upgrades.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void noUpgradeTransfersTwoHundredFiftySixBucketsOfFluidPerTick(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
Fluid water = FluidRegistry.WATER;
|
||||
helper.assertNotNull(water, "Water fluid should be registered");
|
||||
ItemStack sourceCell = fluidCell1k(helper);
|
||||
ItemStack driveCell = fluidCell1k(helper);
|
||||
insertFluids(helper, sourceCell, water, 300_000);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Fluid cell should remain in input after exhausting transfer budget");
|
||||
assertStoredFluidAmount(helper, ioport.getStackInSlot(0), water, 44_000);
|
||||
assertStoredFluidAmount(helper, drive.getStackInSlot(0), water, 256_000);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Runs in HIGH_SIGNAL mode only after redstone power is applied.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void redstoneHighSignalRequiresPower(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
installRedstoneUpgrade(ioport);
|
||||
configureRedstone(ioport, RedstoneMode.HIGH_SIGNAL);
|
||||
ioport.setInventorySlotContents(0, cell1k());
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"HIGH_SIGNAL mode should keep the input cell without power");
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenIdle(1).thenExecute(() -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"HIGH_SIGNAL mode should remove the input cell when powered");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"HIGH_SIGNAL mode should move the cell to output when powered");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Runs in LOW_SIGNAL mode only after redstone power is removed.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void redstoneLowSignalRunsOnlyWithoutPower(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
setRedstoneInput(helper, 15);
|
||||
installRedstoneUpgrade(ioport);
|
||||
configureRedstone(ioport, RedstoneMode.LOW_SIGNAL);
|
||||
ioport.setInventorySlotContents(0, cell1k());
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"LOW_SIGNAL mode should keep the input cell while powered");
|
||||
setRedstoneInput(helper, 0);
|
||||
}).thenIdle(1).thenExecute(() -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"LOW_SIGNAL mode should remove the input cell without power");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"LOW_SIGNAL mode should move the cell to output without power");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Processes exactly one cell for a single redstone pulse in SIGNAL_PULSE mode.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void redstonePulseModeRunsAfterPulse(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
installRedstoneUpgrade(ioport);
|
||||
configureRedstone(ioport, RedstoneMode.SIGNAL_PULSE);
|
||||
ioport.setInventorySlotContents(0, cell1k());
|
||||
ioport.setInventorySlotContents(1, cell1k());
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertEquals(
|
||||
2,
|
||||
countFilledSlots(ioport, 0, 6),
|
||||
"SIGNAL_PULSE mode should keep both cells before a pulse");
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertEquals(
|
||||
1,
|
||||
countFilledSlots(ioport, 0, 6),
|
||||
"SIGNAL_PULSE mode should leave one input cell after one pulse");
|
||||
helper.assertEquals(
|
||||
1,
|
||||
countFilledSlots(ioport, 6, 12),
|
||||
"SIGNAL_PULSE mode should move exactly one cell after one pulse");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Allows cell insertion only into input slots and extraction only from output slots.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void sidedInventoryAllowsCellInputAndOutputExtractionOnly(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
ItemStack cell = cell1k();
|
||||
ItemStack nonCell = new ItemStack(Items.apple);
|
||||
int side = ForgeDirection.UNKNOWN.ordinal();
|
||||
|
||||
helper.assertTrue(ioport.isItemValidForSlot(0, cell), "Storage cells should be valid items");
|
||||
helper.assertFalse(ioport.isItemValidForSlot(0, nonCell), "Non-cell items should be invalid");
|
||||
helper.assertTrue(ioport.canInsertItem(0, cell, side), "Input slots should accept storage cells");
|
||||
helper.assertFalse(ioport.canInsertItem(6, cell, side), "Output slots should not accept storage cells");
|
||||
helper.assertFalse(ioport.canInsertItem(0, nonCell, side), "Input slots should not accept non-cell items");
|
||||
helper.assertFalse(ioport.canExtractItem(0, cell, side), "Input slots should not allow extraction");
|
||||
helper.assertTrue(ioport.canExtractItem(6, cell, side), "Output slots should allow extraction");
|
||||
helper.assertEquals(
|
||||
12,
|
||||
ioport.getAccessibleSlotsBySide(ForgeDirection.UNKNOWN).length,
|
||||
"All 12 slots should be exposed");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
// Keeps source cell contents intact when no destination storage exists.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void noDestinationStorageKeepsSourceCellUnchanged(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 1);
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> ioport.setInventorySlotContents(0, sourceCell)).thenIdle(10)
|
||||
.thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Input cell should remain when no destination storage exists");
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Cell should not move to output when no destination storage exists");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 1);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Preserves queued cells without duplication or loss when settings change while output is full.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void settingChangeWhileQueuedDoesNotDuplicateOrLoseCell(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack queuedCell = cell4k();
|
||||
|
||||
helper.startSequence().thenWaitUntilAtEnd(() -> assertIOPortActive(helper, ioport)).thenIdle(1)
|
||||
.thenExecuteAtStart(() -> {
|
||||
for (int slot = 6; slot < 12; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
ioport.setInventorySlotContents(0, queuedCell);
|
||||
}).thenIdle(5).thenExecute(() -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Queued cell should remain in input before changing settings");
|
||||
configure(ioport, OperationMode.EMPTY, FullnessMode.HALF);
|
||||
ioport.setInventorySlotContents(6, null);
|
||||
}).thenWaitUntil(30, () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Queued cell should leave input after changing settings");
|
||||
helper.assertTrue(
|
||||
ItemStack.areItemStacksEqual(ioport.getStackInSlot(6), queuedCell),
|
||||
"Queued cell should move exactly once to output after changing settings");
|
||||
helper.assertEquals(6, countFilledSlots(ioport, 6, 12), "Output cell count should remain six");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
private static TileIOPort getIOPort(GameTestHelper helper) {
|
||||
TestPos pos = helper.pos(IO_PORT_LABEL);
|
||||
return helper.assertTileEntityPresent(TileIOPort.class, pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
private static TileDrive getDrive(GameTestHelper helper) {
|
||||
TestPos pos = helper.pos(DRIVE_LABEL);
|
||||
return helper.assertTileEntityPresent(TileDrive.class, pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
private static void setRedstoneInput(GameTestHelper helper, int strength) {
|
||||
TestPos pos = helper.pos(REDSTONE_LABEL);
|
||||
helper.setRedstoneInput(pos.x(), pos.y(), pos.z(), strength);
|
||||
}
|
||||
|
||||
private static void assertIOPortActive(GameTestHelper helper, TileIOPort ioport) {
|
||||
helper.assertTrue(ioport.getProxy().isActive(), "IO port network should become active");
|
||||
}
|
||||
|
||||
private static ItemStack cell1k() {
|
||||
return AEApi.instance().definitions().items().cell1k().maybeStack(1).get();
|
||||
}
|
||||
|
||||
private static ItemStack cell4k() {
|
||||
return AEApi.instance().definitions().items().cell4k().maybeStack(1).get();
|
||||
}
|
||||
|
||||
private static ItemStack cell64k() {
|
||||
return AEApi.instance().definitions().items().cell64k().maybeStack(1).get();
|
||||
}
|
||||
|
||||
private static ItemStack fluidCell1k(GameTestHelper helper) {
|
||||
helper.assertNotNull(ItemAndBlockHolder.CELL1K, "AE2FC 1k fluid cell should be available");
|
||||
return ItemAndBlockHolder.CELL1K.stack();
|
||||
}
|
||||
|
||||
private static void configure(TileIOPort ioport, OperationMode operationMode, FullnessMode fullnessMode) {
|
||||
ioport.getConfigManager().putSetting(Settings.OPERATION_MODE, operationMode);
|
||||
ioport.getConfigManager().putSetting(Settings.FULLNESS_MODE, fullnessMode);
|
||||
}
|
||||
|
||||
private static void configureRedstone(TileIOPort ioport, RedstoneMode redstoneMode) {
|
||||
ioport.getConfigManager().putSetting(Settings.REDSTONE_CONTROLLED, redstoneMode);
|
||||
}
|
||||
|
||||
private static void installRedstoneUpgrade(TileIOPort ioport) {
|
||||
installUpgrade(ioport, AEApi.instance().definitions().materials().cardRedstone().maybeStack(1).get(), 0);
|
||||
}
|
||||
|
||||
private static void installUpgrade(TileIOPort ioport, ItemStack upgrade, int slot) {
|
||||
IInventory upgrades = ioport.getInventoryByName("upgrades");
|
||||
upgrades.setInventorySlotContents(slot, upgrade);
|
||||
}
|
||||
|
||||
private static void insertItems(GameTestHelper helper, ItemStack cell, Block block, long amount) {
|
||||
IAEItemStack remainder = itemInventory(helper, cell)
|
||||
.injectItems(itemStack(block, amount), Actionable.MODULATE, TEST_SOURCE);
|
||||
helper.assertNull(remainder, "Items should fit completely into the cell");
|
||||
}
|
||||
|
||||
private static void insertFluids(GameTestHelper helper, ItemStack cell, Fluid fluid, long amount) {
|
||||
IAEFluidStack remainder = fluidInventory(helper, cell)
|
||||
.injectItems(fluidStack(fluid, amount), Actionable.MODULATE, TEST_SOURCE);
|
||||
helper.assertNull(remainder, "Fluids should fit completely into the cell");
|
||||
}
|
||||
|
||||
private static void assertStoredAmount(GameTestHelper helper, ItemStack cell, Block block, long expectedAmount) {
|
||||
helper.assertNotNull(cell, "Cell should exist");
|
||||
helper.assertEquals(expectedAmount, storedAmount(helper, cell, block), "Stored item amount should match");
|
||||
}
|
||||
|
||||
private static void assertStoredFluidAmount(GameTestHelper helper, ItemStack cell, Fluid fluid,
|
||||
long expectedAmount) {
|
||||
helper.assertNotNull(cell, "Cell should exist");
|
||||
helper.assertEquals(expectedAmount, storedFluidAmount(helper, cell, fluid), "Stored fluid amount should match");
|
||||
}
|
||||
|
||||
private static long storedAmount(GameTestHelper helper, ItemStack cell, Block block) {
|
||||
IAEItemStack request = itemStack(block, Integer.MAX_VALUE);
|
||||
IAEItemStack extracted = itemInventory(helper, cell).extractItems(request, Actionable.SIMULATE, TEST_SOURCE);
|
||||
return extracted == null ? 0 : extracted.getStackSize();
|
||||
}
|
||||
|
||||
private static long storedFluidAmount(GameTestHelper helper, ItemStack cell, Fluid fluid) {
|
||||
IAEFluidStack request = fluidStack(fluid, Long.MAX_VALUE);
|
||||
IAEFluidStack extracted = fluidInventory(helper, cell).extractItems(request, Actionable.SIMULATE, TEST_SOURCE);
|
||||
return extracted == null ? 0 : extracted.getStackSize();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IMEInventoryHandler<IAEItemStack> itemInventory(GameTestHelper helper, ItemStack cell) {
|
||||
ICellHandler cellHandler = AEApi.instance().registries().cell().getHandler(cell);
|
||||
helper.assertNotNull(cellHandler, "Cell handler should exist");
|
||||
IMEInventoryHandler<IAEItemStack> cellInv = cellHandler.getCellInventory(cell, null, ITEM_STACK_TYPE);
|
||||
helper.assertNotNull(cellInv, "Item cell inventory should exist");
|
||||
return cellInv;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IMEInventoryHandler<IAEFluidStack> fluidInventory(GameTestHelper helper, ItemStack cell) {
|
||||
ICellHandler cellHandler = AEApi.instance().registries().cell().getHandler(cell);
|
||||
helper.assertNotNull(cellHandler, "Cell handler should exist");
|
||||
IMEInventoryHandler<IAEFluidStack> cellInv = cellHandler.getCellInventory(cell, null, FLUID_STACK_TYPE);
|
||||
helper.assertNotNull(cellInv, "Fluid cell inventory should exist");
|
||||
return cellInv;
|
||||
}
|
||||
|
||||
private static IAEItemStack itemStack(Block block, long amount) {
|
||||
IAEItemStack stack = AEItemStack.create(new ItemStack(block, 1));
|
||||
stack.setStackSize(amount);
|
||||
return stack;
|
||||
}
|
||||
|
||||
private static IAEFluidStack fluidStack(Fluid fluid, long amount) {
|
||||
IAEFluidStack stack = AEFluidStack.create(new FluidStack(fluid, 1));
|
||||
stack.setStackSize(amount);
|
||||
return stack;
|
||||
}
|
||||
|
||||
private static int countFilledSlots(TileIOPort ioport, int startInclusive, int endExclusive) {
|
||||
int count = 0;
|
||||
for (int slot = startInclusive; slot < endExclusive; slot++) {
|
||||
if (ioport.getStackInSlot(slot) != null) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package appeng.gametests.automation.importexport;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertChestStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertNetworkStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.continuousInvariant;
|
||||
import static appeng.gametests.AEGameTestHelpers.fillChest;
|
||||
import static appeng.gametests.AEGameTestHelpers.injectIntoGrid;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemStack;
|
||||
import static appeng.gametests.AEGameTestHelpers.part;
|
||||
import static appeng.gametests.AEGameTestHelpers.setChestSlot;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.inventory.IInventory;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.tileentity.TileEntityChest;
|
||||
|
||||
import com.github.bsideup.jabel.Desugar;
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.AEApi;
|
||||
import appeng.api.config.RedstoneMode;
|
||||
import appeng.api.config.Settings;
|
||||
import appeng.api.storage.StorageName;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.gametests.AEGameTestHelpers;
|
||||
import appeng.gametests.AEGameTestHelpers.ContinuousInvariant;
|
||||
import appeng.parts.automation.PartExportBus;
|
||||
import appeng.parts.automation.PartImportBus;
|
||||
import appeng.parts.automation.PartSharedItemBus;
|
||||
import appeng.parts.automation.PartUpgradeable;
|
||||
import appeng.tile.inventory.IAEStackInventory;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class ImportExportBusTests {
|
||||
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final String SOURCE_CHEST_LABEL = "source_chest";
|
||||
private static final String DESTINATION_CHEST_LABEL = "destination_chest";
|
||||
private static final String IMPORT_BUS_LABEL = "import_bus";
|
||||
private static final String EXPORT_BUS_LABEL = "export_bus";
|
||||
private static final String REDSTONE_LABEL = "redstone";
|
||||
|
||||
// A filtered import bus should pull only matching stacks from the source chest into ME storage.
|
||||
@GameTest(template = "bus_io", timeoutTicks = 380)
|
||||
public static void importBusPullsFilteredStackIntoNetwork(GameTestHelper helper) {
|
||||
BusIO busIO = getBusIO(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
busIO.drive.setInventorySlotContents(0, driveCell);
|
||||
setChestSlot(busIO.sourceChest, 0, Blocks.cobblestone, 32);
|
||||
setChestSlot(busIO.sourceChest, 1, Blocks.dirt, 32);
|
||||
configureFilter(helper, busIO.importBus, Blocks.cobblestone);
|
||||
configureFilter(helper, busIO.exportBus, Blocks.dirt);
|
||||
ContinuousInvariant filterRejectsDirt = continuousInvariant(
|
||||
helper,
|
||||
"bus filters must retain source dirt and keep imported cobblestone out of the destination",
|
||||
() -> {
|
||||
assertNetworkStoredAmount(helper, busIO.controller, Blocks.dirt, 0);
|
||||
assertChestStoredAmount(helper, busIO.sourceChest, Blocks.dirt, 32);
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, 0);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil("wait for bus network activation", 60, () -> assertBusIOActive(helper, busIO))
|
||||
.thenExecute("begin filtered-import invariant", filterRejectsDirt::enable)
|
||||
.thenWaitUntil("wait for matching cobblestone import", 300, () -> {
|
||||
assertNetworkStoredAmount(helper, busIO.controller, Blocks.cobblestone, 32);
|
||||
assertChestStoredAmount(helper, busIO.sourceChest, Blocks.cobblestone, 0);
|
||||
}).thenExecute("stop filtered-import invariant", filterRejectsDirt::disable).thenSucceed();
|
||||
}
|
||||
|
||||
// A filtered export bus should move only the configured stack from ME storage into the destination chest.
|
||||
@GameTest(template = "bus_io", timeoutTicks = 320)
|
||||
public static void exportBusPushesFilteredStackToInventory(GameTestHelper helper) {
|
||||
BusIO busIO = getBusIO(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 1);
|
||||
insertItems(helper, driveCell, Blocks.dirt, 1);
|
||||
busIO.drive.setInventorySlotContents(0, driveCell);
|
||||
configureFilter(helper, busIO.importBus, Blocks.dirt);
|
||||
configureFilter(helper, busIO.exportBus, Blocks.cobblestone);
|
||||
ContinuousInvariant filterRetainsDirt = continuousInvariant(
|
||||
helper,
|
||||
"export bus filter must retain dirt in ME storage",
|
||||
() -> {
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.dirt, 0);
|
||||
assertStoredAmount(helper, busIO.drive.getStackInSlot(0), Blocks.dirt, 1);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil("wait for bus network activation", 60, () -> assertBusIOActive(helper, busIO))
|
||||
.thenExecute("begin filtered-export invariant", filterRetainsDirt::enable)
|
||||
.thenWaitUntil("wait for matching cobblestone export", 180, () -> {
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, 1);
|
||||
assertStoredAmount(helper, busIO.drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
}).thenExecute("stop filtered-export invariant", filterRetainsDirt::disable).thenSucceed();
|
||||
}
|
||||
|
||||
// A full destination inventory should make the export bus leave stored network items untouched.
|
||||
@GameTest(template = "bus_io", timeoutTicks = 240)
|
||||
public static void fullDestinationDoesNotVoidItems(GameTestHelper helper) {
|
||||
BusIO busIO = getBusIO(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 32);
|
||||
busIO.drive.setInventorySlotContents(0, driveCell);
|
||||
fillChest(busIO.destinationChest, Blocks.dirt);
|
||||
configureFilter(helper, busIO.importBus, Blocks.dirt);
|
||||
configureFilter(helper, busIO.exportBus, Blocks.cobblestone);
|
||||
ContinuousInvariant fullDestinationPreservesNetwork = continuousInvariant(
|
||||
helper,
|
||||
"full destination must not void or export network cobblestone",
|
||||
() -> {
|
||||
assertNetworkStoredAmount(helper, busIO.controller, Blocks.cobblestone, 32);
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, 0);
|
||||
assertChestStoredAmount(
|
||||
helper,
|
||||
busIO.destinationChest,
|
||||
Blocks.dirt,
|
||||
busIO.destinationChest.getSizeInventory() * 64L);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil("wait for bus network activation", 60, () -> assertBusIOActive(helper, busIO))
|
||||
.thenExecute("begin full-destination conservation invariant", fullDestinationPreservesNetwork::enable)
|
||||
.thenIdle(120)
|
||||
.thenExecute("finish full-destination observation window", fullDestinationPreservesNetwork::disable)
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// With a redstone card installed, HIGH, LOW, and PULSE modes should gate export-bus work.
|
||||
@GameTest(template = "bus_io", timeoutTicks = 800)
|
||||
public static void redstoneModesGateBusOperation(GameTestHelper helper) {
|
||||
BusIO busIO = getBusIO(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
busIO.drive.setInventorySlotContents(0, driveCell);
|
||||
configureFilter(helper, busIO.importBus, Blocks.dirt);
|
||||
configureFilter(helper, busIO.exportBus, Blocks.cobblestone);
|
||||
installRedstoneUpgrade(helper, busIO.exportBus);
|
||||
configureRedstone(busIO.exportBus, RedstoneMode.HIGH_SIGNAL);
|
||||
setRedstoneInput(helper, 0);
|
||||
long[] gatedAmounts = { 0, 0 };
|
||||
ContinuousInvariant gatedBusDoesNotMoveItems = continuousInvariant(
|
||||
helper,
|
||||
"redstone-gated export bus must not move cobblestone",
|
||||
() -> {
|
||||
assertStoredAmount(helper, busIO.drive.getStackInSlot(0), Blocks.cobblestone, gatedAmounts[0]);
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, gatedAmounts[1]);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil("wait for bus network activation", 60, () -> assertBusIOActive(helper, busIO))
|
||||
.thenExecute("inject while HIGH_SIGNAL is unpowered", () -> {
|
||||
injectCobblestone(helper, busIO, 1);
|
||||
gatedAmounts[0] = 1;
|
||||
gatedAmounts[1] = 0;
|
||||
gatedBusDoesNotMoveItems.enable();
|
||||
}).thenIdle(70).thenExecute("apply HIGH_SIGNAL power", () -> {
|
||||
gatedBusDoesNotMoveItems.disable();
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenWaitUntil("wait for HIGH_SIGNAL export", 90, () -> {
|
||||
assertStoredAmount(helper, busIO.drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, 1);
|
||||
}).thenExecute("configure powered LOW_SIGNAL and inject", () -> {
|
||||
configureRedstone(busIO.exportBus, RedstoneMode.LOW_SIGNAL);
|
||||
setRedstoneInput(helper, 15);
|
||||
injectCobblestone(helper, busIO, 1);
|
||||
gatedAmounts[0] = 1;
|
||||
gatedAmounts[1] = 1;
|
||||
gatedBusDoesNotMoveItems.enable();
|
||||
}).thenIdle(70).thenExecute("remove LOW_SIGNAL power", () -> {
|
||||
gatedBusDoesNotMoveItems.disable();
|
||||
setRedstoneInput(helper, 0);
|
||||
}).thenWaitUntil("wait for LOW_SIGNAL export", 90, () -> {
|
||||
assertStoredAmount(helper, busIO.drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, 2);
|
||||
}).thenExecute("configure pulse mode and inject before pulse", () -> {
|
||||
configureRedstone(busIO.exportBus, RedstoneMode.SIGNAL_PULSE);
|
||||
setRedstoneInput(helper, 0);
|
||||
injectCobblestone(helper, busIO, 1);
|
||||
gatedAmounts[0] = 1;
|
||||
gatedAmounts[1] = 2;
|
||||
gatedBusDoesNotMoveItems.enable();
|
||||
}).thenIdle(70).thenExecute("apply first signal pulse", () -> {
|
||||
gatedBusDoesNotMoveItems.disable();
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenWaitUntil("wait for first pulse export", 40, () -> {
|
||||
assertStoredAmount(helper, busIO.drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, 3);
|
||||
}).thenExecute("inject without a new pulse", () -> {
|
||||
injectCobblestone(helper, busIO, 1);
|
||||
gatedAmounts[0] = 1;
|
||||
gatedAmounts[1] = 3;
|
||||
gatedBusDoesNotMoveItems.enable();
|
||||
}).thenIdle(70)
|
||||
.thenExecute("lower pulse signal without triggering work", () -> setRedstoneInput(helper, 0))
|
||||
.thenIdle(5).thenExecute("apply second signal pulse", () -> {
|
||||
gatedBusDoesNotMoveItems.disable();
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenWaitUntil("wait for second pulse export", 40, () -> {
|
||||
assertStoredAmount(helper, busIO.drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertChestStoredAmount(helper, busIO.destinationChest, Blocks.cobblestone, 4);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
private static BusIO getBusIO(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
TileEntityChest sourceChest = getSourceChest(helper);
|
||||
TileEntityChest destinationChest = getDestinationChest(helper);
|
||||
PartImportBus importBus = getImportBus(helper);
|
||||
PartExportBus exportBus = getExportBus(helper);
|
||||
|
||||
return new BusIO(controller, drive, sourceChest, destinationChest, importBus, exportBus);
|
||||
}
|
||||
|
||||
private static TileController getController(GameTestHelper helper) {
|
||||
return tile(helper, TileController.class, CONTROLLER_LABEL);
|
||||
}
|
||||
|
||||
private static TileDrive getDrive(GameTestHelper helper) {
|
||||
return tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
}
|
||||
|
||||
private static TileEntityChest getSourceChest(GameTestHelper helper) {
|
||||
return tile(helper, TileEntityChest.class, SOURCE_CHEST_LABEL);
|
||||
}
|
||||
|
||||
private static TileEntityChest getDestinationChest(GameTestHelper helper) {
|
||||
return tile(helper, TileEntityChest.class, DESTINATION_CHEST_LABEL);
|
||||
}
|
||||
|
||||
private static PartImportBus getImportBus(GameTestHelper helper) {
|
||||
return part(helper, IMPORT_BUS_LABEL, PartImportBus.class);
|
||||
}
|
||||
|
||||
private static PartExportBus getExportBus(GameTestHelper helper) {
|
||||
return part(helper, EXPORT_BUS_LABEL, PartExportBus.class);
|
||||
}
|
||||
|
||||
private static void assertBusIOActive(GameTestHelper helper, BusIO busIO) {
|
||||
assertStorageNetworkActive(helper, busIO);
|
||||
assertActive(helper, busIO.importBus, "Import bus should receive a channel");
|
||||
assertActive(helper, busIO.exportBus, "Export bus should receive a channel");
|
||||
}
|
||||
|
||||
private static void assertStorageNetworkActive(GameTestHelper helper, BusIO busIO) {
|
||||
assertActive(helper, busIO.controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, busIO.drive.getProxy(), "Drive grid proxy should become active");
|
||||
}
|
||||
|
||||
private static void configureFilter(GameTestHelper helper, PartSharedItemBus<?> bus, Block block) {
|
||||
IAEStackInventory config = bus.getAEInventoryByName(StorageName.CONFIG);
|
||||
helper.assertNotNull(config, "Bus config inventory should exist");
|
||||
config.putAEStackInSlot(0, itemStack(block, 1));
|
||||
}
|
||||
|
||||
private static void configureRedstone(PartSharedItemBus<?> bus, RedstoneMode redstoneMode) {
|
||||
bus.getConfigManager().putSetting(Settings.REDSTONE_CONTROLLED, redstoneMode);
|
||||
}
|
||||
|
||||
private static void installRedstoneUpgrade(GameTestHelper helper, PartUpgradeable bus) {
|
||||
installUpgrade(helper, bus, AEApi.instance().definitions().materials().cardRedstone().maybeStack(1).get(), 0);
|
||||
}
|
||||
|
||||
private static void installUpgrade(GameTestHelper helper, PartUpgradeable bus, ItemStack upgrade, int slot) {
|
||||
IInventory upgrades = bus.getInventoryByName("upgrades");
|
||||
helper.assertNotNull(upgrades, "Bus upgrade inventory should exist");
|
||||
upgrades.setInventorySlotContents(slot, upgrade);
|
||||
}
|
||||
|
||||
private static void setRedstoneInput(GameTestHelper helper, int strength) {
|
||||
AEGameTestHelpers.setRedstoneInput(helper, REDSTONE_LABEL, strength);
|
||||
}
|
||||
|
||||
private static void injectCobblestone(GameTestHelper helper, BusIO busIO, long amount) {
|
||||
helper.assertNull(
|
||||
injectIntoGrid(busIO.controller, Blocks.cobblestone, amount),
|
||||
"Injected cobblestone should fit into the drive cell");
|
||||
}
|
||||
|
||||
@Desugar
|
||||
private record BusIO(TileController controller, TileDrive drive, TileEntityChest sourceChest,
|
||||
TileEntityChest destinationChest, PartImportBus importBus, PartExportBus exportBus) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package appeng.gametests.automation.storagebus;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertChestStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertItemRemainder;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertNetworkMonitorStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertNetworkStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.clearChestSlot;
|
||||
import static appeng.gametests.AEGameTestHelpers.injectIntoGrid;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemStack;
|
||||
import static appeng.gametests.AEGameTestHelpers.part;
|
||||
import static appeng.gametests.AEGameTestHelpers.setChestSlot;
|
||||
import static appeng.gametests.AEGameTestHelpers.simulateInjectIntoGrid;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.tileentity.TileEntityChest;
|
||||
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.config.AccessRestriction;
|
||||
import appeng.api.config.Settings;
|
||||
import appeng.api.storage.StorageName;
|
||||
import appeng.api.storage.data.IAEItemStack;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.parts.misc.PartStorageBus;
|
||||
import appeng.tile.inventory.IAEStackInventory;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class StorageBusTests {
|
||||
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String STORAGE_BUS_LABEL = "storage_bus";
|
||||
private static final String EXTERNAL_CHEST_LABEL = "external_chest";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final int STORAGE_BUS_REFRESH_TIMEOUT_TICKS = 80;
|
||||
|
||||
// Exposes items in the adjacent vanilla chest through the ME item storage monitor.
|
||||
@GameTest(template = "storage_bus", timeoutTicks = 100)
|
||||
public static void storageBusExposesExternalChestContents(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileEntityChest chest = getExternalChest(helper);
|
||||
PartStorageBus storageBus = getStorageBus(helper);
|
||||
setChestSlot(chest, 0, Blocks.cobblestone, 64);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for storage bus to expose 64 external cobblestone", 60, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, storageBus, "Storage bus should receive a channel");
|
||||
assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 64);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Reflects external chest mutations after the storage bus monitor refreshes.
|
||||
@GameTest(template = "storage_bus", timeoutTicks = 140)
|
||||
public static void storageBusReflectsExternalMutation(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileEntityChest chest = getExternalChest(helper);
|
||||
PartStorageBus storageBus = getStorageBus(helper);
|
||||
setChestSlot(chest, 0, Blocks.cobblestone, 16);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for storage bus to expose the initial 16 cobblestone", 60, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, storageBus, "Storage bus should receive a channel");
|
||||
assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 16);
|
||||
}).thenExecute(
|
||||
"replace external stack with 40 cobblestone",
|
||||
() -> setChestSlot(chest, 0, Blocks.cobblestone, 40))
|
||||
.thenWaitUntil(
|
||||
"wait for storage monitor to refresh to 40 external cobblestone",
|
||||
80,
|
||||
() -> assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 40))
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// READ mode exposes the external chest but refuses network insertions into it.
|
||||
@GameTest(template = "storage_bus", timeoutTicks = 220)
|
||||
public static void accessModeReadPreventsInsertion(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileEntityChest chest = getExternalChest(helper);
|
||||
PartStorageBus storageBus = getStorageBus(helper);
|
||||
setChestSlot(chest, 0, Blocks.cobblestone, 1);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for READ-mode test storage bus to become visible", 60, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, storageBus, "Storage bus should receive a channel");
|
||||
assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 1);
|
||||
}).thenExecute("empty the external chest before enabling READ mode", () -> clearChestSlot(chest, 0))
|
||||
.thenWaitUntil(
|
||||
"wait for cleared external chest to disappear from the network monitor",
|
||||
60,
|
||||
() -> assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 0))
|
||||
.thenExecute(
|
||||
"configure storage bus for READ-only access",
|
||||
() -> storageBus.getConfigManager().putSetting(Settings.ACCESS, AccessRestriction.READ))
|
||||
.thenWaitUntil(
|
||||
"wait for READ mode to reject simulated insertion",
|
||||
STORAGE_BUS_REFRESH_TIMEOUT_TICKS,
|
||||
() -> {
|
||||
IAEItemStack remainder = simulateInjectIntoGrid(controller, Blocks.cobblestone, 64);
|
||||
assertItemRemainder(helper, remainder, Blocks.cobblestone, 64);
|
||||
})
|
||||
.thenExecute("attempt real insertion through READ-only storage bus", () -> {
|
||||
IAEItemStack remainder = injectIntoGrid(controller, Blocks.cobblestone, 64);
|
||||
|
||||
assertItemRemainder(helper, remainder, Blocks.cobblestone, 64);
|
||||
assertChestStoredAmount(helper, chest, Blocks.cobblestone, 0);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// New items should route to the higher-priority external chest before the lower-priority drive cell.
|
||||
@GameTest(template = "storage_bus", timeoutTicks = 220)
|
||||
public static void storageBusPriorityBeatsDriveCell(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileEntityChest chest = getExternalChest(helper);
|
||||
PartStorageBus storageBus = getStorageBus(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
setChestSlot(chest, 0, Blocks.cobblestone, 1);
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 1);
|
||||
storageBus.setPriority(100);
|
||||
drive.setPriority(0);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for priority test storage network to activate", 60, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, storageBus, "Storage bus should receive a channel");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should become active");
|
||||
assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 1);
|
||||
}).thenExecute("empty the high-priority external chest", () -> clearChestSlot(chest, 0))
|
||||
.thenWaitUntil(
|
||||
"wait for the empty external chest to disappear from the storage monitor",
|
||||
60,
|
||||
() -> assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 0))
|
||||
.thenExecute("insert lower-priority drive cell", () -> drive.setInventorySlotContents(0, driveCell))
|
||||
.thenWaitUntil(
|
||||
"wait for lower-priority drive contents to become visible",
|
||||
60,
|
||||
() -> { assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 1); })
|
||||
.thenExecute("inject 64 cobblestone and validate high-priority routing", () -> {
|
||||
IAEItemStack remainder = injectIntoGrid(controller, Blocks.cobblestone, 64);
|
||||
|
||||
helper.assertNull(remainder, "Injected items should fit into available network storage");
|
||||
assertChestStoredAmount(helper, chest, Blocks.cobblestone, 64);
|
||||
assertStoredAmount(helper, driveCell, Blocks.cobblestone, 1);
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 65);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// A storage bus whitelist should accept matching items and reject non-matching insertions.
|
||||
@GameTest(template = "storage_bus", timeoutTicks = 220)
|
||||
public static void filteredStorageBusRejectsNonMatchingItems(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileEntityChest chest = getExternalChest(helper);
|
||||
PartStorageBus storageBus = getStorageBus(helper);
|
||||
setChestSlot(chest, 0, Blocks.cobblestone, 1);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for filtered storage bus network to activate", 60, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, storageBus, "Storage bus should receive a channel");
|
||||
assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 1);
|
||||
}).thenExecute("empty the external chest before configuring its filter", () -> clearChestSlot(chest, 0))
|
||||
.thenWaitUntil(
|
||||
"wait for the empty external chest to disappear from the storage monitor",
|
||||
60,
|
||||
() -> assertNetworkMonitorStoredAmount(helper, controller, Blocks.cobblestone, 0))
|
||||
.thenExecute(
|
||||
"configure cobblestone-only storage bus filter",
|
||||
() -> configureStorageBusFilter(helper, storageBus, Blocks.cobblestone))
|
||||
.thenWaitUntil(
|
||||
"wait for filter to accept cobblestone and reject dirt in simulation",
|
||||
STORAGE_BUS_REFRESH_TIMEOUT_TICKS,
|
||||
() -> {
|
||||
helper.assertNull(
|
||||
simulateInjectIntoGrid(controller, Blocks.cobblestone, 1),
|
||||
"Storage bus should accept matching items");
|
||||
IAEItemStack remainder = simulateInjectIntoGrid(controller, Blocks.dirt, 16);
|
||||
assertItemRemainder(helper, remainder, Blocks.dirt, 16);
|
||||
})
|
||||
.thenExecute("inject matching cobblestone and non-matching dirt", () -> {
|
||||
IAEItemStack matchingRemainder = injectIntoGrid(controller, Blocks.cobblestone, 16);
|
||||
IAEItemStack nonMatchingRemainder = injectIntoGrid(controller, Blocks.dirt, 16);
|
||||
|
||||
helper.assertNull(matchingRemainder, "Matching stack should enter the filtered storage bus");
|
||||
assertItemRemainder(helper, nonMatchingRemainder, Blocks.dirt, 16);
|
||||
assertChestStoredAmount(helper, chest, Blocks.cobblestone, 16);
|
||||
assertChestStoredAmount(helper, chest, Blocks.dirt, 0);
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 16);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
private static TileController getController(GameTestHelper helper) {
|
||||
return tile(helper, TileController.class, CONTROLLER_LABEL);
|
||||
}
|
||||
|
||||
private static TileEntityChest getExternalChest(GameTestHelper helper) {
|
||||
return tile(helper, TileEntityChest.class, EXTERNAL_CHEST_LABEL);
|
||||
}
|
||||
|
||||
private static PartStorageBus getStorageBus(GameTestHelper helper) {
|
||||
return part(helper, STORAGE_BUS_LABEL, PartStorageBus.class);
|
||||
}
|
||||
|
||||
private static TileDrive getDrive(GameTestHelper helper) {
|
||||
return tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
}
|
||||
|
||||
private static void configureStorageBusFilter(GameTestHelper helper, PartStorageBus storageBus, Block block) {
|
||||
IAEStackInventory config = storageBus.getAEInventoryByName(StorageName.CONFIG);
|
||||
helper.assertNotNull(config, "Storage bus config inventory should exist");
|
||||
config.putAEStackInSlot(0, itemStack(block, 1));
|
||||
}
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package appeng.gametests.compatibility.appliedenergistics2_ae2fc;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredFluidAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertFluids;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraftforge.fluids.Fluid;
|
||||
import net.minecraftforge.fluids.FluidRegistry;
|
||||
|
||||
import com.glodblock.github.loader.ItemAndBlockHolder;
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.core.AppEng;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
import appeng.tile.storage.TileIOPort;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class IOPortFluidCompatibilityTests {
|
||||
|
||||
private static final String IO_PORT_LABEL = "io_port";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
|
||||
// AE2FC fluid cells use the real IO port transfer path and the same base transfer budget as item cells.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void noUpgradeTransfersTwoHundredFiftySixBucketsOfFluidPerTick(GameTestHelper helper) {
|
||||
TileIOPort ioport = tile(helper, TileIOPort.class, IO_PORT_LABEL);
|
||||
TileDrive drive = tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
Fluid water = FluidRegistry.WATER;
|
||||
helper.assertNotNull(water, "Water fluid should be registered");
|
||||
ItemStack sourceCell = fluidCell1k(helper);
|
||||
ItemStack driveCell = fluidCell1k(helper);
|
||||
insertFluids(helper, sourceCell, water, 300_000);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd(
|
||||
"wait for AE2FC IO port network activation",
|
||||
() -> assertActive(helper, ioport.getProxy(), "IO port network should become active"))
|
||||
.thenIdle(1).thenExecuteAtStart("insert AE2FC source and destination fluid cells", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute("assert the first tick transfers exactly 256,000 mB", () -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Fluid cell should remain in input after exhausting transfer budget");
|
||||
assertStoredFluidAmount(helper, ioport.getStackInSlot(0), water, 44_000);
|
||||
assertStoredFluidAmount(helper, drive.getStackInSlot(0), water, 256_000);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
private static ItemStack fluidCell1k(GameTestHelper helper) {
|
||||
helper.assertNotNull(ItemAndBlockHolder.CELL1K, "AE2FC 1k fluid cell should be available");
|
||||
return ItemAndBlockHolder.CELL1K.stack();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
package appeng.gametests.crafting;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertChestStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertNetworkStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.continuousInvariant;
|
||||
import static appeng.gametests.AEGameTestHelpers.injectIntoGrid;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemStack;
|
||||
import static appeng.gametests.AEGameTestHelpers.networkStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.pos;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.entity.item.EntityItem;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.inventory.IInventory;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.crafting.CraftingManager;
|
||||
import net.minecraft.item.crafting.IRecipe;
|
||||
import net.minecraft.item.crafting.ShapedRecipes;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.tileentity.TileEntityChest;
|
||||
|
||||
import com.github.bsideup.jabel.Desugar;
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.AEApi;
|
||||
import appeng.api.config.LockCraftingMode;
|
||||
import appeng.api.config.Settings;
|
||||
import appeng.api.networking.IGrid;
|
||||
import appeng.api.networking.crafting.ICraftingGrid;
|
||||
import appeng.api.networking.crafting.ICraftingJob;
|
||||
import appeng.api.networking.crafting.ICraftingLink;
|
||||
import appeng.api.networking.security.BaseActionSource;
|
||||
import appeng.api.storage.data.IAEItemStack;
|
||||
import appeng.api.storage.data.IAEStack;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.gametests.AEGameTestHelpers.ContinuousInvariant;
|
||||
import appeng.gametests.AEGameTestHelpers.Coord;
|
||||
import appeng.me.GridAccessException;
|
||||
import appeng.tile.crafting.TileCraftingStorageTile;
|
||||
import appeng.tile.crafting.TileCraftingTile;
|
||||
import appeng.tile.crafting.TileMolecularAssembler;
|
||||
import appeng.tile.misc.TileInterface;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
import appeng.util.Platform;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class CraftingExecutionTests {
|
||||
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final String CPU_STORAGE_LABEL = "cpu_storage";
|
||||
private static final String CPU_UNIT_LABEL = "cpu_unit";
|
||||
private static final String INTERFACE_LABEL = "interface";
|
||||
private static final String ASSEMBLER_LABEL = "assembler";
|
||||
|
||||
private static final int JOB_CALCULATION_TIMEOUT_MS = 5_000;
|
||||
private static final Block TEST_RECIPE_CORNER = Blocks.bedrock;
|
||||
private static final Block TEST_RECIPE_EDGE = Blocks.obsidian;
|
||||
private static final Block TEST_RECIPE_CENTER = Blocks.diamond_block;
|
||||
private static final Block TEST_RECIPE_OUTPUT = Blocks.sponge;
|
||||
|
||||
// A scoped real crafting recipe should execute through the CPU, interface, molecular assembler, and ME storage.
|
||||
@GameTest(template = "crafting_cpu", timeoutTicks = 520)
|
||||
public static void molecularAssemblerCraftsScopedShapedRecipe(GameTestHelper helper) {
|
||||
registerScopedCraftingRecipe(helper);
|
||||
CraftingNetwork network = getCraftingNetwork(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, TEST_RECIPE_CORNER, 4);
|
||||
insertItems(helper, driveCell, TEST_RECIPE_EDGE, 4);
|
||||
insertItems(helper, driveCell, TEST_RECIPE_CENTER, 1);
|
||||
network.drive.setInventorySlotContents(0, driveCell);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for crafting CPU, interface, assembler, drive, and controller to activate",
|
||||
100,
|
||||
() -> assertCraftingNetworkActive(helper, network))
|
||||
.thenExecute(
|
||||
"install scoped shaped-recipe encoded pattern",
|
||||
() -> installPattern(network.blockInterface, encodedScopedCraftingPattern()))
|
||||
.thenWaitUntil(
|
||||
"wait for scoped shaped-recipe pattern advertisement",
|
||||
80,
|
||||
() -> helper.assertFalse(
|
||||
craftingOptionsFor(network.controller, TEST_RECIPE_OUTPUT).isEmpty(),
|
||||
"Scoped shaped-recipe output should be advertised"))
|
||||
.thenExecute(
|
||||
"submit one scoped shaped-recipe craft",
|
||||
() -> submitCraft(helper, network.controller, TEST_RECIPE_OUTPUT, 1))
|
||||
.thenWaitUntil(
|
||||
"wait for real assembler craft to consume the nine supplied blocks and store one sponge",
|
||||
260,
|
||||
() -> {
|
||||
assertNetworkStoredAmount(helper, network.controller, TEST_RECIPE_OUTPUT, 1);
|
||||
assertNetworkStoredAmount(helper, network.controller, TEST_RECIPE_CORNER, 0);
|
||||
assertNetworkStoredAmount(helper, network.controller, TEST_RECIPE_EDGE, 0);
|
||||
assertNetworkStoredAmount(helper, network.controller, TEST_RECIPE_CENTER, 0);
|
||||
assertStoredAmount(helper, driveCell, TEST_RECIPE_OUTPUT, 1);
|
||||
assertStoredAmount(helper, driveCell, TEST_RECIPE_CORNER, 0);
|
||||
assertStoredAmount(helper, driveCell, TEST_RECIPE_EDGE, 0);
|
||||
assertStoredAmount(helper, driveCell, TEST_RECIPE_CENTER, 0);
|
||||
assertNotRequesting(helper, network.controller, TEST_RECIPE_OUTPUT);
|
||||
})
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// A processing pattern should push inputs out, wait for the declared output, then complete once it returns.
|
||||
@GameTest(template = "crafting_cpu", timeoutTicks = 620)
|
||||
public static void processingPatternPushesInputsAndAcceptsReturnedOutput(GameTestHelper helper) {
|
||||
CraftingNetwork network = getCraftingNetwork(helper);
|
||||
AtomicReference<TileEntityChest> processingTarget = new AtomicReference<>();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 1);
|
||||
network.drive.setInventorySlotContents(0, driveCell);
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for processing-pattern crafting network to activate",
|
||||
100,
|
||||
() -> assertCraftingNetworkActive(helper, network))
|
||||
.thenExecute(
|
||||
"replace assembler role with processing-output chest",
|
||||
() -> processingTarget.set(placeProcessingTarget(helper)))
|
||||
.thenExecute("install locked cobblestone-to-stone processing pattern", () -> {
|
||||
installPattern(
|
||||
network.blockInterface,
|
||||
encodedProcessingPattern(Blocks.cobblestone, 1, Blocks.stone, 1));
|
||||
network.blockInterface.getConfigManager()
|
||||
.putSetting(Settings.LOCK_CRAFTING_MODE, LockCraftingMode.LOCK_UNTIL_RESULT);
|
||||
})
|
||||
.thenWaitUntil(
|
||||
"wait for processing pattern output advertisement",
|
||||
80,
|
||||
() -> helper.assertFalse(
|
||||
craftingOptionsFor(network.controller, Blocks.stone).isEmpty(),
|
||||
"Processing pattern output should be advertised"))
|
||||
.thenExecute(
|
||||
"submit one stone processing craft",
|
||||
() -> submitCraft(helper, network.controller, Blocks.stone, 1))
|
||||
.thenWaitUntil("wait for interface to push cobblestone and lock pending returned stone", 220, () -> {
|
||||
assertStoredAmount(helper, driveCell, Blocks.cobblestone, 0);
|
||||
assertChestStoredAmount(helper, processingTarget.get(), Blocks.cobblestone, 1);
|
||||
assertNetworkStoredAmount(helper, network.controller, Blocks.stone, 0);
|
||||
helper.assertTrue(
|
||||
network.blockInterface.getInterfaceDuality().getCraftingLockedReason()
|
||||
== LockCraftingMode.LOCK_UNTIL_RESULT,
|
||||
"Interface should wait for the processing result");
|
||||
}).thenExecute("return declared stone output to the ME network", () -> {
|
||||
clearInventory(processingTarget.get());
|
||||
IAEItemStack remainder = injectIntoGrid(network.controller, Blocks.stone, 1);
|
||||
helper.assertNull(remainder, "Returned processing output should fit into the network");
|
||||
}).thenWaitUntil("wait for returned stone to finish the job and unlock the interface", 160, () -> {
|
||||
assertNetworkStoredAmount(helper, network.controller, Blocks.stone, 1);
|
||||
assertNetworkStoredAmount(helper, network.controller, Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, driveCell, Blocks.stone, 1);
|
||||
helper.assertTrue(
|
||||
network.blockInterface.getInterfaceDuality().getCraftingLockedReason()
|
||||
== LockCraftingMode.NONE,
|
||||
"Returned output should unlock the interface");
|
||||
assertNotRequesting(helper, network.controller, Blocks.stone);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Cancelling a blocked processing job should return CPU-held ingredients without producing output.
|
||||
@GameTest(template = "crafting_cpu", timeoutTicks = 380)
|
||||
public static void cancelledJobReturnsIngredients(GameTestHelper helper) {
|
||||
CraftingNetwork network = getCraftingNetwork(helper);
|
||||
AtomicReference<ICraftingLink> link = new AtomicReference<>();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 1);
|
||||
network.drive.setInventorySlotContents(0, driveCell);
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for cancellation-test crafting network to activate",
|
||||
100,
|
||||
() -> assertCraftingNetworkActive(helper, network))
|
||||
.thenExecute(
|
||||
"install blocked cobblestone-to-stone processing pattern",
|
||||
() -> installPattern(
|
||||
network.blockInterface,
|
||||
encodedProcessingPattern(Blocks.cobblestone, 1, Blocks.stone, 1)))
|
||||
.thenWaitUntil(
|
||||
"wait for blocked processing pattern advertisement",
|
||||
80,
|
||||
() -> helper.assertFalse(
|
||||
craftingOptionsFor(network.controller, Blocks.stone).isEmpty(),
|
||||
"Blocked processing pattern should be advertised"))
|
||||
.thenExecute("submit blocked stone craft and verify active request", () -> {
|
||||
link.set(submitCraft(helper, network.controller, Blocks.stone, 1));
|
||||
IAEStack<?> requestedOutput = itemStack(Blocks.stone, 1);
|
||||
helper.assertTrue(
|
||||
craftingGrid(network.controller).isRequesting(requestedOutput),
|
||||
"Submitted craft should be tracked as an active request");
|
||||
}).thenExecute("cancel blocked crafting link", () -> {
|
||||
link.get().cancel();
|
||||
helper.assertTrue(link.get().isCanceled(), "Crafting link should be canceled");
|
||||
}).thenWaitUntil("wait for canceled job to return cobblestone without producing stone", 120, () -> {
|
||||
assertStoredAmount(helper, driveCell, Blocks.cobblestone, 1);
|
||||
assertNetworkStoredAmount(helper, network.controller, Blocks.cobblestone, 1);
|
||||
assertNetworkStoredAmount(helper, network.controller, Blocks.stone, 0);
|
||||
assertNotRequesting(helper, network.controller, Blocks.stone);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Breaking a CPU tile during a job should stop the craft without duplicating ingredients or outputs.
|
||||
@GameTest(template = "crafting_cpu", timeoutTicks = 380)
|
||||
public static void cpuBreakCancelsWithoutDuplication(GameTestHelper helper) {
|
||||
CraftingNetwork network = getCraftingNetwork(helper);
|
||||
AtomicReference<ICraftingLink> link = new AtomicReference<>();
|
||||
Set<EntityItem> cpuBreakDrops = new HashSet<>();
|
||||
helper.afterTest(() -> {
|
||||
cpuBreakDrops.addAll(craftingCpuDrops(helper));
|
||||
cpuBreakDrops.forEach(EntityItem::setDead);
|
||||
});
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 1);
|
||||
network.drive.setInventorySlotContents(0, driveCell);
|
||||
ContinuousInvariant cpuBreakDoesNotDuplicateOrProduceOutput = continuousInvariant(
|
||||
helper,
|
||||
"CPU break must not duplicate ingredients or produce processing output",
|
||||
() -> {
|
||||
cpuBreakDrops.addAll(craftingCpuDrops(helper));
|
||||
long accountedCobblestone = networkStoredAmount(network.controller, Blocks.cobblestone)
|
||||
+ droppedItemAmount(cpuBreakDrops, Blocks.cobblestone);
|
||||
long accountedStone = networkStoredAmount(network.controller, Blocks.stone)
|
||||
+ droppedItemAmount(cpuBreakDrops, Blocks.stone);
|
||||
helper.assertTrue(
|
||||
accountedCobblestone <= 1,
|
||||
"At most one ingredient may exist while CPU break cancellation settles; observed="
|
||||
+ accountedCobblestone);
|
||||
helper.assertEquals(0L, accountedStone, "CPU break must never produce the requested output");
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for CPU-break crafting network to activate",
|
||||
100,
|
||||
() -> assertCraftingNetworkActive(helper, network))
|
||||
.thenExecute(
|
||||
"install blocked cobblestone-to-stone processing pattern",
|
||||
() -> installPattern(
|
||||
network.blockInterface,
|
||||
encodedProcessingPattern(Blocks.cobblestone, 1, Blocks.stone, 1)))
|
||||
.thenWaitUntil(
|
||||
"wait for CPU-break processing pattern advertisement",
|
||||
80,
|
||||
() -> helper.assertFalse(
|
||||
craftingOptionsFor(network.controller, Blocks.stone).isEmpty(),
|
||||
"Blocked processing pattern should be advertised"))
|
||||
.thenExecute(
|
||||
"submit blocked stone craft",
|
||||
() -> link.set(submitCraft(helper, network.controller, Blocks.stone, 1)))
|
||||
.thenWaitUntil(
|
||||
"wait for CPU to take the cobblestone ingredient",
|
||||
100,
|
||||
() -> assertStoredAmount(helper, driveCell, Blocks.cobblestone, 0))
|
||||
.thenExecute("break CPU unit during active processing job", () -> {
|
||||
destroyBlock(helper, CPU_UNIT_LABEL);
|
||||
cpuBreakDrops.addAll(craftingCpuDrops(helper));
|
||||
cpuBreakDoesNotDuplicateOrProduceOutput.enable();
|
||||
}).thenWaitUntil("wait for CPU-break cancellation and ingredient recovery", 40, () -> {
|
||||
cpuBreakDrops.addAll(craftingCpuDrops(helper));
|
||||
long accountedCobblestone = networkStoredAmount(network.controller, Blocks.cobblestone)
|
||||
+ droppedItemAmount(cpuBreakDrops, Blocks.cobblestone);
|
||||
long accountedStone = networkStoredAmount(network.controller, Blocks.stone)
|
||||
+ droppedItemAmount(cpuBreakDrops, Blocks.stone);
|
||||
|
||||
helper.assertTrue(link.get().isCanceled(), "Crafting link should be canceled when the CPU breaks");
|
||||
helper.assertEquals(
|
||||
1L,
|
||||
accountedCobblestone,
|
||||
"Ingredient should exist exactly once after CPU break");
|
||||
helper.assertEquals(0L, accountedStone, "CPU break should not produce the requested output");
|
||||
assertNotRequesting(helper, network.controller, Blocks.stone);
|
||||
})
|
||||
.thenExecute("stop CPU-break conservation invariant", cpuBreakDoesNotDuplicateOrProduceOutput::disable)
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
private static CraftingNetwork getCraftingNetwork(GameTestHelper helper) {
|
||||
TileController controller = tile(helper, TileController.class, CONTROLLER_LABEL);
|
||||
TileDrive drive = tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
TileCraftingStorageTile cpuStorage = tile(helper, TileCraftingStorageTile.class, CPU_STORAGE_LABEL);
|
||||
TileCraftingTile cpuUnit = tile(helper, TileCraftingTile.class, CPU_UNIT_LABEL);
|
||||
TileInterface blockInterface = tile(helper, TileInterface.class, INTERFACE_LABEL);
|
||||
TileMolecularAssembler assembler = tile(helper, TileMolecularAssembler.class, ASSEMBLER_LABEL);
|
||||
|
||||
return new CraftingNetwork(controller, drive, cpuStorage, cpuUnit, blockInterface, assembler);
|
||||
}
|
||||
|
||||
private static void assertCraftingNetworkActive(GameTestHelper helper, CraftingNetwork network) {
|
||||
assertActive(helper, network.controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, network.drive.getProxy(), "Drive grid proxy should become active");
|
||||
assertActive(helper, network.cpuStorage.getProxy(), "CPU storage should receive a channel");
|
||||
assertActive(helper, network.cpuUnit.getProxy(), "CPU unit should receive a channel");
|
||||
assertActive(helper, network.blockInterface.getProxy(), "Interface should receive a channel");
|
||||
assertActive(helper, network.assembler.getProxy(), "Assembler should receive a channel");
|
||||
helper.assertTrue(network.cpuStorage.isFormed(), "CPU storage should form a crafting CPU");
|
||||
helper.assertTrue(network.cpuUnit.isFormed(), "CPU unit should form a crafting CPU");
|
||||
}
|
||||
|
||||
private static TileEntityChest placeProcessingTarget(GameTestHelper helper) {
|
||||
Coord assemblerPos = pos(helper, ASSEMBLER_LABEL);
|
||||
helper.setBlock(assemblerPos.x(), assemblerPos.y(), assemblerPos.z(), Blocks.chest);
|
||||
return helper
|
||||
.assertTileEntityPresent(TileEntityChest.class, assemblerPos.x(), assemblerPos.y(), assemblerPos.z());
|
||||
}
|
||||
|
||||
private static void installPattern(TileInterface blockInterface, ItemStack encodedPattern) {
|
||||
blockInterface.getInterfaceDuality().getPatterns().setInventorySlotContents(0, encodedPattern);
|
||||
}
|
||||
|
||||
private static ICraftingLink submitCraft(GameTestHelper helper, TileController controller, Block output,
|
||||
long amount) {
|
||||
BaseActionSource source = new BaseActionSource();
|
||||
|
||||
try {
|
||||
IGrid grid = controller.getProxy().getGrid();
|
||||
ICraftingGrid crafting = controller.getProxy().getCrafting();
|
||||
IAEStack<?> requestedOutput = itemStack(output, amount);
|
||||
Future<ICraftingJob> future = crafting
|
||||
.beginCraftingJob(controller.getWorldObj(), grid, source, requestedOutput, null);
|
||||
ICraftingJob job = future.get(JOB_CALCULATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
|
||||
helper.assertFalse(job.isSimulation(), "Crafting job should be executable");
|
||||
ICraftingLink link = crafting.submitJob(job, null, null, false, source);
|
||||
helper.assertNotNull(link, "Crafting job should submit to an available CPU");
|
||||
return link;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError("Crafting job calculation should not be interrupted", e);
|
||||
} catch (ExecutionException | TimeoutException | GridAccessException e) {
|
||||
throw new AssertionError("Crafting job should calculate and submit", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNotRequesting(GameTestHelper helper, TileController controller, Block output) {
|
||||
IAEStack<?> requestedOutput = itemStack(output, 1);
|
||||
helper.assertFalse(
|
||||
craftingGrid(controller).isRequesting(requestedOutput),
|
||||
"Crafting grid should not still request the output");
|
||||
}
|
||||
|
||||
private static ICraftingGrid craftingGrid(TileController controller) {
|
||||
try {
|
||||
return controller.getProxy().getCrafting();
|
||||
} catch (GridAccessException e) {
|
||||
throw new AssertionError("Network crafting cache should be accessible", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Collection<?> craftingOptionsFor(TileController controller, Block output) {
|
||||
IAEStack<?> requestedOutput = itemStack(output, 1);
|
||||
return craftingGrid(controller).getCraftingFor(requestedOutput, null, -1, controller.getWorldObj());
|
||||
}
|
||||
|
||||
private static ItemStack encodedScopedCraftingPattern() {
|
||||
ItemStack encodedPattern = encodedPattern();
|
||||
NBTTagCompound patternTags = new NBTTagCompound();
|
||||
NBTTagList inputs = new NBTTagList();
|
||||
NBTTagList outputs = new NBTTagList();
|
||||
|
||||
patternTags.setBoolean("crafting", true);
|
||||
patternTags.setBoolean("substitute", false);
|
||||
patternTags.setBoolean("beSubstitute", false);
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_CORNER, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_EDGE, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_CORNER, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_EDGE, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_CENTER, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_EDGE, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_CORNER, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_EDGE, 1));
|
||||
inputs.appendTag(itemTag(TEST_RECIPE_CORNER, 1));
|
||||
outputs.appendTag(itemTag(TEST_RECIPE_OUTPUT, 1));
|
||||
patternTags.setTag("in", inputs);
|
||||
patternTags.setTag("out", outputs);
|
||||
encodedPattern.setTagCompound(patternTags);
|
||||
|
||||
return encodedPattern;
|
||||
}
|
||||
|
||||
private static void registerScopedCraftingRecipe(GameTestHelper helper) {
|
||||
ItemStack[] inputs = { new ItemStack(TEST_RECIPE_CORNER), new ItemStack(TEST_RECIPE_EDGE),
|
||||
new ItemStack(TEST_RECIPE_CORNER), new ItemStack(TEST_RECIPE_EDGE), new ItemStack(TEST_RECIPE_CENTER),
|
||||
new ItemStack(TEST_RECIPE_EDGE), new ItemStack(TEST_RECIPE_CORNER), new ItemStack(TEST_RECIPE_EDGE),
|
||||
new ItemStack(TEST_RECIPE_CORNER) };
|
||||
IRecipe recipe = new ShapedRecipes(3, 3, inputs, new ItemStack(TEST_RECIPE_OUTPUT));
|
||||
ScopedCraftingRecipe scopedRecipe = new ScopedCraftingRecipe(
|
||||
CraftingManager.getInstance().getRecipeList(),
|
||||
recipe);
|
||||
|
||||
helper.afterTest(scopedRecipe::remove);
|
||||
scopedRecipe.register();
|
||||
}
|
||||
|
||||
private static ItemStack encodedProcessingPattern(Block input, int inputAmount, Block output, int outputAmount) {
|
||||
ItemStack encodedPattern = encodedPattern();
|
||||
NBTTagCompound patternTags = new NBTTagCompound();
|
||||
NBTTagList inputs = new NBTTagList();
|
||||
NBTTagList outputs = new NBTTagList();
|
||||
|
||||
patternTags.setBoolean("crafting", false);
|
||||
patternTags.setBoolean("substitute", false);
|
||||
patternTags.setBoolean("beSubstitute", false);
|
||||
inputs.appendTag(itemTag(input, inputAmount));
|
||||
outputs.appendTag(itemTag(output, outputAmount));
|
||||
patternTags.setTag("in", inputs);
|
||||
patternTags.setTag("out", outputs);
|
||||
encodedPattern.setTagCompound(patternTags);
|
||||
|
||||
return encodedPattern;
|
||||
}
|
||||
|
||||
private static ItemStack encodedPattern() {
|
||||
return AEApi.instance().definitions().items().encodedPattern().maybeStack(1).get();
|
||||
}
|
||||
|
||||
private static NBTTagCompound itemTag(Block block, int amount) {
|
||||
NBTTagCompound tag = new NBTTagCompound();
|
||||
Platform.writeItemStackToNBT(new ItemStack(block, amount), tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static void clearInventory(IInventory inventory) {
|
||||
for (int slot = 0; slot < inventory.getSizeInventory(); slot++) {
|
||||
inventory.setInventorySlotContents(slot, null);
|
||||
}
|
||||
inventory.markDirty();
|
||||
}
|
||||
|
||||
private static void destroyBlock(GameTestHelper helper, String label) {
|
||||
Coord pos = pos(helper, label);
|
||||
helper.destroyBlock(pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
private static List<EntityItem> craftingCpuDrops(GameTestHelper helper) {
|
||||
Coord storage = pos(helper, CPU_STORAGE_LABEL);
|
||||
Coord unit = pos(helper, CPU_UNIT_LABEL);
|
||||
|
||||
// CraftingCPUCluster may spill inventory at the broken tile or any free block adjacent to another CPU tile.
|
||||
return helper.getEntities(
|
||||
EntityItem.class,
|
||||
Math.min(storage.x(), unit.x()) - 1,
|
||||
Math.min(storage.y(), unit.y()) - 1,
|
||||
Math.min(storage.z(), unit.z()) - 1,
|
||||
Math.max(storage.x(), unit.x()) + 1,
|
||||
Math.max(storage.y(), unit.y()) + 1,
|
||||
Math.max(storage.z(), unit.z()) + 1);
|
||||
}
|
||||
|
||||
private static long droppedItemAmount(Collection<EntityItem> drops, Block block) {
|
||||
ItemStack expected = new ItemStack(block, 1);
|
||||
long amount = 0;
|
||||
|
||||
for (EntityItem drop : drops) {
|
||||
ItemStack stack = drop.getEntityItem();
|
||||
if (!drop.isDead && stack != null && stack.isItemEqual(expected)) {
|
||||
amount += stack.stackSize;
|
||||
}
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Desugar
|
||||
private record CraftingNetwork(TileController controller, TileDrive drive, TileCraftingStorageTile cpuStorage,
|
||||
TileCraftingTile cpuUnit, TileInterface blockInterface, TileMolecularAssembler assembler) {
|
||||
|
||||
}
|
||||
|
||||
private static final class ScopedCraftingRecipe {
|
||||
|
||||
private final List<IRecipe> recipes;
|
||||
private final IRecipe recipe;
|
||||
private boolean registered;
|
||||
|
||||
private ScopedCraftingRecipe(List<IRecipe> recipes, IRecipe recipe) {
|
||||
this.recipes = recipes;
|
||||
this.recipe = recipe;
|
||||
}
|
||||
|
||||
private void register() {
|
||||
this.recipes.add(0, this.recipe);
|
||||
this.registered = true;
|
||||
}
|
||||
|
||||
private void remove() {
|
||||
if (!this.registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int index = 0; index < this.recipes.size(); index++) {
|
||||
if (this.recipes.get(index) == this.recipe) {
|
||||
this.recipes.remove(index);
|
||||
this.registered = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError("Scoped crafting recipe should still be registered during test teardown");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package appeng.gametests.interfaces;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertChestStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.continuousInvariant;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemStack;
|
||||
import static appeng.gametests.AEGameTestHelpers.part;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.tileentity.TileEntityChest;
|
||||
|
||||
import com.github.bsideup.jabel.Desugar;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.AEApi;
|
||||
import appeng.api.config.Settings;
|
||||
import appeng.api.config.YesNo;
|
||||
import appeng.api.networking.crafting.ICraftingGrid;
|
||||
import appeng.api.networking.crafting.ICraftingPatternDetails;
|
||||
import appeng.container.ContainerNull;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.gametests.AEGameTestHelpers;
|
||||
import appeng.gametests.AEGameTestHelpers.ContinuousInvariant;
|
||||
import appeng.helpers.DualityInterface;
|
||||
import appeng.helpers.IInterfaceHost;
|
||||
import appeng.me.GridAccessException;
|
||||
import appeng.parts.misc.PartInterface;
|
||||
import appeng.tile.crafting.TileMolecularAssembler;
|
||||
import appeng.tile.misc.TileInterface;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
import appeng.util.Platform;
|
||||
import appeng.util.inv.MEInventoryCrafting;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class InterfaceTests {
|
||||
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final String BLOCK_INTERFACE_LABEL = "block_interface";
|
||||
private static final String PART_INTERFACE_HOST_LABEL = "part_interface_host";
|
||||
private static final String ADJACENT_CHEST_LABEL = "adjacent_chest";
|
||||
private static final String ASSEMBLER_LABEL = "assembler";
|
||||
|
||||
private static final int STOCK_AMOUNT = 32;
|
||||
|
||||
// Configured interface stock should be pulled from ME storage into the interface inventory.
|
||||
@GameTest(template = "interface_network", timeoutTicks = 160)
|
||||
public static void interfaceStocksConfiguredItem(GameTestHelper helper) {
|
||||
InterfaceNetwork network = getInterfaceNetwork(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 64);
|
||||
network.drive.setInventorySlotContents(0, driveCell);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for interface stocking network to activate",
|
||||
80,
|
||||
() -> assertInterfaceNetworkActive(helper, network))
|
||||
.thenExecute(
|
||||
"configure block interface to stock 32 cobblestone",
|
||||
() -> configureStock(network.blockInterface, Blocks.cobblestone, STOCK_AMOUNT))
|
||||
.thenWaitUntil("wait for interface to stock 32 cobblestone from the drive", 80, () -> {
|
||||
assertInterfaceStoredAmount(helper, network.blockInterface, Blocks.cobblestone, STOCK_AMOUNT);
|
||||
assertStoredAmount(helper, driveCell, Blocks.cobblestone, 64 - STOCK_AMOUNT);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Blocking mode should reject a pattern push while the target inventory contains non-ignored items.
|
||||
@GameTest(template = "interface_network", timeoutTicks = 160)
|
||||
public static void blockingModeWaitsForEmptyInventory(GameTestHelper helper) {
|
||||
InterfaceNetwork network = getInterfaceNetwork(helper);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for blocking-interface network to activate",
|
||||
80,
|
||||
() -> assertInterfaceNetworkActive(helper, network))
|
||||
.thenExecute("enable blocking mode, install pattern, and occupy target with dirt", () -> {
|
||||
network.partInterface.getConfigManager().putSetting(Settings.BLOCK, YesNo.YES);
|
||||
network.partInterface.getInterfaceDuality().getPatterns().setInventorySlotContents(
|
||||
0,
|
||||
encodedProcessingPattern(Blocks.cobblestone, 1, Blocks.stone, 1));
|
||||
AEGameTestHelpers.setChestSlot(network.adjacentChest, 0, Blocks.dirt, 1);
|
||||
})
|
||||
.thenWaitUntil(
|
||||
"wait for blocking pattern advertisement",
|
||||
40,
|
||||
() -> helper.assertFalse(
|
||||
craftingOptionsFor(network.controller, Blocks.stone).isEmpty(),
|
||||
"Blocking interface should advertise the processing pattern"))
|
||||
.thenExecute("attempt pattern push into occupied target", () -> {
|
||||
ICraftingPatternDetails details = firstPattern(helper, network.controller, Blocks.stone);
|
||||
boolean pushed = network.partInterface.pushPattern(details, craftingTable(Blocks.cobblestone, 1));
|
||||
|
||||
helper.assertFalse(pushed, "Blocking interface should not push into a non-empty target");
|
||||
assertChestStoredAmount(helper, network.adjacentChest, Blocks.dirt, 1);
|
||||
assertChestStoredAmount(helper, network.adjacentChest, Blocks.cobblestone, 0);
|
||||
}).thenExecute("clear blocking target inventory", () -> clearChest(network.adjacentChest))
|
||||
.thenExecute("attempt pattern push into empty target", () -> {
|
||||
ICraftingPatternDetails details = firstPattern(helper, network.controller, Blocks.stone);
|
||||
boolean pushed = network.partInterface.pushPattern(details, craftingTable(Blocks.cobblestone, 1));
|
||||
|
||||
helper.assertTrue(pushed, "Blocking interface should push once the target is empty");
|
||||
assertChestStoredAmount(helper, network.adjacentChest, Blocks.cobblestone, 1);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Encoded patterns in interface pattern slots should be advertised by the network crafting cache.
|
||||
@GameTest(template = "interface_network", timeoutTicks = 160)
|
||||
public static void interfacePatternSlotsAdvertiseCraftableOutput(GameTestHelper helper) {
|
||||
InterfaceNetwork network = getInterfaceNetwork(helper);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for pattern-advertisement network to activate",
|
||||
80,
|
||||
() -> assertInterfaceNetworkActive(helper, network))
|
||||
.thenExecute(
|
||||
"install cobblestone-to-stone processing pattern",
|
||||
() -> network.blockInterface.getInterfaceDuality().getPatterns().setInventorySlotContents(
|
||||
0,
|
||||
encodedProcessingPattern(Blocks.cobblestone, 1, Blocks.stone, 1)))
|
||||
.thenWaitUntil(
|
||||
"wait for interface pattern output to become craftable",
|
||||
80,
|
||||
() -> helper.assertFalse(
|
||||
craftingOptionsFor(network.controller, Blocks.stone).isEmpty(),
|
||||
"Interface pattern output should be craftable"))
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// Block and part interfaces should both maintain configured stock in their internal inventories.
|
||||
@GameTest(template = "interface_network", timeoutTicks = 180)
|
||||
public static void partAndBlockInterfacesExposeSameStockBehavior(GameTestHelper helper) {
|
||||
InterfaceNetwork network = getInterfaceNetwork(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 64);
|
||||
insertItems(helper, driveCell, Blocks.dirt, 64);
|
||||
network.drive.setInventorySlotContents(0, driveCell);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for block-and-part interface network to activate",
|
||||
80,
|
||||
() -> assertInterfaceNetworkActive(helper, network))
|
||||
.thenExecute("configure block and part interface stock targets", () -> {
|
||||
configureStock(network.blockInterface, Blocks.cobblestone, 16);
|
||||
configureStock(network.partInterface, Blocks.dirt, 16);
|
||||
}).thenWaitUntil("wait for both interfaces to reach their 16-item stock targets", 100, () -> {
|
||||
assertInterfaceStoredAmount(helper, network.blockInterface, Blocks.cobblestone, 16);
|
||||
assertInterfaceStoredAmount(helper, network.partInterface, Blocks.dirt, 16);
|
||||
assertStoredAmount(helper, driveCell, Blocks.cobblestone, 48);
|
||||
assertStoredAmount(helper, driveCell, Blocks.dirt, 48);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// If the interface storage already satisfies the configured stock amount, ME storage should not be drained.
|
||||
@GameTest(template = "interface_network", timeoutTicks = 120)
|
||||
public static void interfaceDoesNotOverstock(GameTestHelper helper) {
|
||||
InterfaceNetwork network = getInterfaceNetwork(helper);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 64);
|
||||
network.drive.setInventorySlotContents(0, driveCell);
|
||||
ContinuousInvariant configuredStockDoesNotDrainNetwork = continuousInvariant(
|
||||
helper,
|
||||
"already satisfied interface stock must not drain ME storage",
|
||||
() -> {
|
||||
assertInterfaceStoredAmount(helper, network.blockInterface, Blocks.cobblestone, STOCK_AMOUNT);
|
||||
assertStoredAmount(helper, driveCell, Blocks.cobblestone, 64);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for no-overstock interface network to activate",
|
||||
80,
|
||||
() -> assertInterfaceNetworkActive(helper, network))
|
||||
.thenExecute("pre-stock interface and enable no-overstock invariant", () -> {
|
||||
configureStock(network.blockInterface, Blocks.cobblestone, STOCK_AMOUNT);
|
||||
network.blockInterface.getInterfaceDuality().getStorage()
|
||||
.setInventorySlotContents(0, new ItemStack(Blocks.cobblestone, STOCK_AMOUNT));
|
||||
configuredStockDoesNotDrainNetwork.enable();
|
||||
}).thenIdle(30)
|
||||
.thenExecute("finish no-overstock observation window", configuredStockDoesNotDrainNetwork::disable)
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
private static InterfaceNetwork getInterfaceNetwork(GameTestHelper helper) {
|
||||
TileController controller = tile(helper, TileController.class, CONTROLLER_LABEL);
|
||||
TileDrive drive = tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
TileInterface blockInterface = tile(helper, TileInterface.class, BLOCK_INTERFACE_LABEL);
|
||||
PartInterface partInterface = part(helper, PART_INTERFACE_HOST_LABEL, PartInterface.class);
|
||||
TileEntityChest adjacentChest = tile(helper, TileEntityChest.class, ADJACENT_CHEST_LABEL);
|
||||
tile(helper, TileMolecularAssembler.class, ASSEMBLER_LABEL);
|
||||
|
||||
return new InterfaceNetwork(controller, drive, blockInterface, partInterface, adjacentChest);
|
||||
}
|
||||
|
||||
private static void assertInterfaceNetworkActive(GameTestHelper helper, InterfaceNetwork network) {
|
||||
assertActive(helper, network.controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, network.drive.getProxy(), "Drive grid proxy should become active");
|
||||
assertActive(helper, network.blockInterface.getProxy(), "Block interface should receive a channel");
|
||||
assertActive(helper, network.partInterface, "Part interface should receive a channel");
|
||||
}
|
||||
|
||||
private static void configureStock(IInterfaceHost interfaceHost, Block block, int amount) {
|
||||
interfaceHost.getInterfaceDuality().getConfig().setInventorySlotContents(0, new ItemStack(block, amount));
|
||||
}
|
||||
|
||||
private static void assertInterfaceStoredAmount(GameTestHelper helper, IInterfaceHost interfaceHost, Block block,
|
||||
long expectedAmount) {
|
||||
Object blockId = Block.blockRegistry.getNameForObject(block);
|
||||
helper.assertEquals(
|
||||
expectedAmount,
|
||||
interfaceStoredAmount(interfaceHost, block),
|
||||
"Interface storage for " + (blockId == null ? block.getUnlocalizedName() : blockId)
|
||||
+ " should match; host="
|
||||
+ interfaceHost.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
private static long interfaceStoredAmount(IInterfaceHost interfaceHost, Block block) {
|
||||
long amount = 0;
|
||||
ItemStack expected = new ItemStack(block, 1);
|
||||
DualityInterface duality = interfaceHost.getInterfaceDuality();
|
||||
|
||||
for (ItemStack stack : duality.getStorage()) {
|
||||
if (stack != null && stack.isItemEqual(expected)) {
|
||||
amount += stack.stackSize;
|
||||
}
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
private static ImmutableCollection<ICraftingPatternDetails> craftingOptionsFor(TileController controller,
|
||||
Block output) {
|
||||
try {
|
||||
ICraftingGrid crafting = controller.getProxy().getCrafting();
|
||||
return crafting.getCraftingFor(itemStack(output, 1), null, -1, controller.getWorldObj());
|
||||
} catch (GridAccessException e) {
|
||||
throw new AssertionError("Network crafting cache should be accessible", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ICraftingPatternDetails firstPattern(GameTestHelper helper, TileController controller,
|
||||
Block output) {
|
||||
ImmutableCollection<ICraftingPatternDetails> patterns = craftingOptionsFor(controller, output);
|
||||
helper.assertFalse(patterns.isEmpty(), "Network crafting cache should advertise the encoded pattern");
|
||||
return patterns.iterator().next();
|
||||
}
|
||||
|
||||
private static MEInventoryCrafting craftingTable(Block block, int amount) {
|
||||
MEInventoryCrafting table = new MEInventoryCrafting(new ContainerNull(), 1, 1);
|
||||
table.setInventorySlotContents(0, itemStack(block, amount));
|
||||
return table;
|
||||
}
|
||||
|
||||
private static ItemStack encodedProcessingPattern(Block input, int inputAmount, Block output, int outputAmount) {
|
||||
ItemStack encodedPattern = AEApi.instance().definitions().items().encodedPattern().maybeStack(1).get();
|
||||
NBTTagCompound patternTags = new NBTTagCompound();
|
||||
NBTTagList inputs = new NBTTagList();
|
||||
NBTTagList outputs = new NBTTagList();
|
||||
|
||||
patternTags.setBoolean("crafting", false);
|
||||
patternTags.setBoolean("substitute", false);
|
||||
patternTags.setBoolean("beSubstitute", false);
|
||||
inputs.appendTag(itemTag(input, inputAmount));
|
||||
outputs.appendTag(itemTag(output, outputAmount));
|
||||
patternTags.setTag("in", inputs);
|
||||
patternTags.setTag("out", outputs);
|
||||
encodedPattern.setTagCompound(patternTags);
|
||||
|
||||
return encodedPattern;
|
||||
}
|
||||
|
||||
private static NBTTagCompound itemTag(Block block, int amount) {
|
||||
NBTTagCompound tag = new NBTTagCompound();
|
||||
Platform.writeItemStackToNBT(new ItemStack(block, amount), tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static void clearChest(TileEntityChest chest) {
|
||||
for (int slot = 0; slot < chest.getSizeInventory(); slot++) {
|
||||
chest.setInventorySlotContents(slot, null);
|
||||
}
|
||||
chest.markDirty();
|
||||
}
|
||||
|
||||
@Desugar
|
||||
private record InterfaceNetwork(TileController controller, TileDrive drive, TileInterface blockInterface,
|
||||
PartInterface partInterface, TileEntityChest adjacentChest) {}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package appeng.gametests.network;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertInactive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertNetworkStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.continuousInvariant;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.pos;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.AEApi;
|
||||
import appeng.api.networking.IGridNode;
|
||||
import appeng.api.parts.IPart;
|
||||
import appeng.api.parts.IPartHost;
|
||||
import appeng.api.util.AEColor;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.gametests.AEGameTestHelpers;
|
||||
import appeng.gametests.AEGameTestHelpers.ContinuousInvariant;
|
||||
import appeng.gametests.AEGameTestHelpers.Coord;
|
||||
import appeng.tile.networking.TileCableBus;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class NetworkCoreTests {
|
||||
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final String DEVICE_A_LABEL = "device_a";
|
||||
private static final String DEVICE_B_LABEL = "device_b";
|
||||
private static final String BREAKABLE_CABLE_LABEL = "breakable_cable";
|
||||
private static final String TOGGLE_BUS_LABEL = "toggle_bus";
|
||||
private static final String REDSTONE_LABEL = "redstone";
|
||||
private static final String[] FULL_CABLE_LINE = { "cable_1", "cable_2", "cable_3", "cable_4", "cable_5", "cable_6",
|
||||
"cable_7", "cable_8", "cable_9", "cable_10" };
|
||||
private static final String[] CHANNEL_LIMIT_CABLE_LINE = { "cable_1", "cable_2", "cable_3", "cable_4", "cable_5",
|
||||
"cable_6", "cable_7", "cable_8", "cable_9" };
|
||||
private static final String[] UPSTREAM_CABLE_LINE = { "cable_1", "cable_2", "cable_3" };
|
||||
private static final String[] DOWNSTREAM_CABLE_LINE = { "cable_5", "cable_6", "cable_7", "cable_8", "cable_9",
|
||||
"cable_10" };
|
||||
private static final String[] CHANNEL_DEVICE_LABELS = { "channel_device_1", "channel_device_2", "channel_device_3",
|
||||
"channel_device_4", "channel_device_5", "channel_device_6", "channel_device_7", "channel_device_8",
|
||||
"channel_device_9" };
|
||||
|
||||
// Boots a controller-backed grid and gives channels to labelled devices.
|
||||
@GameTest(template = "network_core", timeoutTicks = 80)
|
||||
public static void networkBootsAndActivatesDevices(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
installCableLine(helper, FULL_CABLE_LINE);
|
||||
IPart deviceA = placePart(helper, DEVICE_A_LABEL, ForgeDirection.UP, terminal());
|
||||
IPart deviceB = placePart(helper, DEVICE_B_LABEL, ForgeDirection.UP, storageMonitor());
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil("wait for controller, drive, and both labelled devices to activate", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should become active");
|
||||
assertActive(helper, deviceA, "Device A should receive a channel");
|
||||
assertActive(helper, deviceB, "Device B should receive a channel");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Splits the drive off the controller, then reconnects it without losing stored cell contents.
|
||||
@GameTest(template = "network_core", timeoutTicks = 120)
|
||||
public static void splitAndMergePreservesStorageVisibility(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
installCableLine(helper, FULL_CABLE_LINE);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 100);
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for connected drive contents to become network-visible", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should become active");
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 100);
|
||||
}).thenExecute("remove the breakable cable", () -> removeBlock(helper, BREAKABLE_CABLE_LABEL))
|
||||
.thenWaitUntil("wait for split controller side to lose drive visibility", 30, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller side should stay active after split");
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 0);
|
||||
}).thenExecute("restore the breakable cable", () -> placeCable(helper, BREAKABLE_CABLE_LABEL))
|
||||
.thenWaitUntil("wait for merged network to restore drive visibility without data loss", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should reactivate after merge");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should reactivate after merge");
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 100);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// A glass cable can carry eight channels, so the ninth device must be left without a channel.
|
||||
@GameTest(template = "network_core", timeoutTicks = 100)
|
||||
public static void channelLimitDeactivatesOverflowDevice(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
installCableLine(helper, CHANNEL_LIMIT_CABLE_LINE);
|
||||
List<IPart> devices = new ArrayList<>();
|
||||
for (String deviceLabel : CHANNEL_DEVICE_LABELS) {
|
||||
devices.add(placePart(helper, deviceLabel, ForgeDirection.UP, terminal()));
|
||||
}
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil("wait for glass cable to allocate eight channels and reject the ninth", 50, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
int activeDevices = countActiveOnGrid(helper, controller.getProxy().getNode(), devices);
|
||||
helper.assertEquals(
|
||||
8,
|
||||
activeDevices,
|
||||
"Only eight devices should receive channels on a glass cable");
|
||||
helper.assertEquals(
|
||||
1,
|
||||
devices.size() - activeDevices,
|
||||
"One device should overflow without a channel");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// A toggle bus gates the downstream cable only while redstone is applied.
|
||||
@GameTest(template = "network_core", timeoutTicks = 140)
|
||||
public static void toggleBusGatesNetworkOnRedstone(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
installCableLine(helper, UPSTREAM_CABLE_LINE);
|
||||
placeCable(helper, TOGGLE_BUS_LABEL);
|
||||
placePart(helper, TOGGLE_BUS_LABEL, directionBetween(helper, TOGGLE_BUS_LABEL, DEVICE_B_LABEL), toggleBus());
|
||||
installCableLine(helper, DOWNSTREAM_CABLE_LINE);
|
||||
IPart upstreamDevice = placePart(helper, DEVICE_A_LABEL, ForgeDirection.UP, terminal());
|
||||
IPart downstreamDevice = placePart(helper, DEVICE_B_LABEL, ForgeDirection.UP, terminal());
|
||||
ContinuousInvariant unpoweredToggleBusGatesDownstream = continuousInvariant(
|
||||
helper,
|
||||
"unpowered toggle bus must keep only the upstream network active",
|
||||
() -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller side should remain active");
|
||||
assertActive(helper, upstreamDevice, "Upstream device should remain active");
|
||||
assertInactive(helper, drive.getProxy(), "Drive should remain gated");
|
||||
assertInactive(helper, downstreamDevice, "Downstream device should remain gated");
|
||||
});
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for initial unpowered toggle-bus state", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller side should boot without redstone");
|
||||
assertActive(helper, upstreamDevice, "Upstream device should stay active without redstone");
|
||||
assertInactive(helper, drive.getProxy(), "Drive should be gated while toggle bus is unpowered");
|
||||
assertInactive(helper, downstreamDevice, "Downstream device should be gated while toggle bus is unpowered");
|
||||
}).thenExecute("begin unpowered gating invariant", unpoweredToggleBusGatesDownstream::enable).thenIdle(5)
|
||||
.thenExecute("power toggle bus", () -> {
|
||||
unpoweredToggleBusGatesDownstream.disable();
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenWaitUntil("wait for powered downstream activation", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller side should stay active with redstone");
|
||||
assertActive(helper, upstreamDevice, "Upstream device should stay active with redstone");
|
||||
assertActive(helper, drive.getProxy(), "Drive should become active when the toggle bus is powered");
|
||||
assertActive(
|
||||
helper,
|
||||
downstreamDevice,
|
||||
"Downstream device should become active when the toggle bus is powered");
|
||||
}).thenExecute("remove toggle-bus power", () -> setRedstoneInput(helper, 0))
|
||||
.thenWaitUntil("wait for downstream network to become gated again", 40, () -> {
|
||||
assertActive(
|
||||
helper,
|
||||
controller.getProxy(),
|
||||
"Controller side should stay active after redstone is removed");
|
||||
assertActive(
|
||||
helper,
|
||||
upstreamDevice,
|
||||
"Upstream device should stay active after redstone is removed");
|
||||
assertInactive(helper, drive.getProxy(), "Drive should be gated again after redstone is removed");
|
||||
assertInactive(
|
||||
helper,
|
||||
downstreamDevice,
|
||||
"Downstream device should be gated again after redstone is removed");
|
||||
}).thenExecute("begin restored gating invariant", unpoweredToggleBusGatesDownstream::enable).thenIdle(5)
|
||||
.thenExecute("finish restored gating observation", unpoweredToggleBusGatesDownstream::disable)
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
private static TileController getController(GameTestHelper helper) {
|
||||
return tile(helper, TileController.class, CONTROLLER_LABEL);
|
||||
}
|
||||
|
||||
private static TileDrive getDrive(GameTestHelper helper) {
|
||||
return tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
}
|
||||
|
||||
private static void installCableLine(GameTestHelper helper, String... cableRoles) {
|
||||
for (String cableRole : cableRoles) {
|
||||
placeCable(helper, cableRole);
|
||||
}
|
||||
}
|
||||
|
||||
private static TileCableBus placeCable(GameTestHelper helper, String label) {
|
||||
return placeCable(helper, pos(helper, label));
|
||||
}
|
||||
|
||||
private static TileCableBus placeCable(GameTestHelper helper, Coord pos) {
|
||||
Block cableBusBlock = cableBusBlock();
|
||||
helper.setBlock(pos.x(), pos.y(), pos.z(), cableBusBlock);
|
||||
helper.assertBlockPresent(cableBusBlock, pos.x(), pos.y(), pos.z());
|
||||
TileCableBus cableBus = helper.assertTileEntityPresent(TileCableBus.class, pos.x(), pos.y(), pos.z());
|
||||
addPart(helper, cableBus, cableStack(), ForgeDirection.UNKNOWN);
|
||||
return cableBus;
|
||||
}
|
||||
|
||||
private static IPart placePart(GameTestHelper helper, String label, ForgeDirection side, ItemStack stack) {
|
||||
return placePart(helper, pos(helper, label), side, stack);
|
||||
}
|
||||
|
||||
private static IPart placePart(GameTestHelper helper, Coord pos, ForgeDirection side, ItemStack stack) {
|
||||
TileEntity tile = helper.assertTileEntityPresent(pos.x(), pos.y(), pos.z());
|
||||
helper.assertTrue(tile instanceof IPartHost, "Labelled cable position should contain an AE part host");
|
||||
IPartHost host = (IPartHost) tile;
|
||||
addPart(helper, host, stack, side);
|
||||
IPart part = host.getPart(side);
|
||||
helper.assertNotNull(part, "Placed part should be readable from its host");
|
||||
return part;
|
||||
}
|
||||
|
||||
private static void addPart(GameTestHelper helper, IPartHost host, ItemStack stack, ForgeDirection side) {
|
||||
ForgeDirection placedSide = host.addPart(stack.copy(), side, null);
|
||||
helper.assertNotNull(placedSide, "AE part should be accepted by the cable bus");
|
||||
}
|
||||
|
||||
private static void removeBlock(GameTestHelper helper, String label) {
|
||||
Coord pos = pos(helper, label);
|
||||
helper.destroyBlock(pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
private static void setRedstoneInput(GameTestHelper helper, int strength) {
|
||||
AEGameTestHelpers.setRedstoneInput(helper, REDSTONE_LABEL, strength);
|
||||
}
|
||||
|
||||
private static ForgeDirection directionBetween(GameTestHelper helper, String fromRole, String toRole) {
|
||||
Coord from = pos(helper, fromRole);
|
||||
Coord to = pos(helper, toRole);
|
||||
int dx = Integer.signum(to.x() - from.x());
|
||||
int dy = Integer.signum(to.y() - from.y());
|
||||
int dz = Integer.signum(to.z() - from.z());
|
||||
for (ForgeDirection direction : ForgeDirection.VALID_DIRECTIONS) {
|
||||
if (direction.offsetX == dx && direction.offsetY == dy && direction.offsetZ == dz) {
|
||||
return direction;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Roles '" + fromRole + "' and '" + toRole + "' must define a direction");
|
||||
}
|
||||
|
||||
private static int countActiveOnGrid(GameTestHelper helper, IGridNode expectedGridNode, List<IPart> parts) {
|
||||
int count = 0;
|
||||
for (IPart part : parts) {
|
||||
IGridNode node = part.getGridNode();
|
||||
helper.assertNotNull(node, "Every channel-limited device should have a grid node");
|
||||
helper.assertSame(
|
||||
expectedGridNode.getGrid(),
|
||||
node.getGrid(),
|
||||
"Every channel-limited device should join the controller grid");
|
||||
if (node.isActive()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static ItemStack cableStack() {
|
||||
return AEApi.instance().definitions().parts().cableGlass().stack(AEColor.Transparent, 1);
|
||||
}
|
||||
|
||||
private static ItemStack terminal() {
|
||||
return AEApi.instance().definitions().parts().terminal().maybeStack(1).get();
|
||||
}
|
||||
|
||||
private static ItemStack storageMonitor() {
|
||||
return AEApi.instance().definitions().parts().storageMonitor().maybeStack(1).get();
|
||||
}
|
||||
|
||||
private static ItemStack toggleBus() {
|
||||
return AEApi.instance().definitions().parts().toggleBus().maybeStack(1).get();
|
||||
}
|
||||
|
||||
private static Block cableBusBlock() {
|
||||
return AEApi.instance().definitions().blocks().multiPart().maybeBlock().get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package appeng.gametests.network.p2p;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertNetworkStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.continuousInvariant;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.part;
|
||||
import static appeng.gametests.AEGameTestHelpers.setChestSlot;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.inventory.IInventory;
|
||||
import net.minecraft.item.Item;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.tileentity.TileEntityChest;
|
||||
import net.minecraft.tileentity.TileEntityHopper;
|
||||
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.TestPos;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.networking.IGridConnection;
|
||||
import appeng.api.networking.IGridNode;
|
||||
import appeng.api.parts.IPart;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.gametests.AEGameTestHelpers.ContinuousInvariant;
|
||||
import appeng.me.GridAccessException;
|
||||
import appeng.parts.p2p.PartP2PItems;
|
||||
import appeng.parts.p2p.PartP2PRedstone;
|
||||
import appeng.parts.p2p.PartP2PTunnel;
|
||||
import appeng.parts.p2p.PartP2PTunnelME;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class P2PTests {
|
||||
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String INPUT_TUNNEL_HOST_LABEL = "input_tunnel_host";
|
||||
private static final String OUTPUT_TUNNEL_HOST_LABEL = "output_tunnel_host";
|
||||
private static final String SOURCE_CHEST_LABEL = "source_chest";
|
||||
private static final String SOURCE_INSERTER_LABEL = "source_inserter";
|
||||
private static final String DESTINATION_CHEST_LABEL = "destination_chest";
|
||||
private static final String REDSTONE_SOURCE_LABEL = "redstone_source";
|
||||
private static final String REDSTONE_PROBE_LABEL = "redstone_probe";
|
||||
private static final String REMOTE_STORAGE_LABEL = "remote_storage";
|
||||
|
||||
private static final long ITEM_FREQUENCY = 101L;
|
||||
private static final long REDSTONE_FREQUENCY = 202L;
|
||||
private static final long ME_FREQUENCY = 303L;
|
||||
|
||||
// P1: a real hopper supplies the input tunnel, and conservation is checked on every observed tick.
|
||||
@GameTest(template = "p2p_tunnels", timeoutTicks = 160)
|
||||
public static void itemP2PMovesItemsWithoutDuplication(GameTestHelper helper) {
|
||||
P2PFixture fixture = getFixture(helper);
|
||||
PartP2PItems input = inputTunnel(helper, PartP2PItems.class);
|
||||
PartP2PItems output = outputTunnel(helper, PartP2PItems.class);
|
||||
setChestSlot(fixture.sourceChest, 0, Blocks.cobblestone, 1);
|
||||
ContinuousInvariant itemConservation = itemConservationInvariant(helper, fixture, 1);
|
||||
itemConservation.enable();
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for the item P2P pair to become active and linked", 60, () -> {
|
||||
assertCarrierActive(helper, fixture.controller);
|
||||
assertLinkedPair(helper, input, output, ITEM_FREQUENCY);
|
||||
}).thenWaitUntil("wait for the supplied item to reach only the destination inventory", 80, () -> {
|
||||
assertInventoryAmount(helper, fixture.sourceChest, Blocks.cobblestone, 0, SOURCE_CHEST_LABEL);
|
||||
assertInventoryAmount(helper, fixture.sourceInserter, Blocks.cobblestone, 0, SOURCE_INSERTER_LABEL);
|
||||
assertInventoryAmount(helper, fixture.destinationChest, Blocks.cobblestone, 1, DESTINATION_CHEST_LABEL);
|
||||
}).thenExecute("finish item-conservation observation", itemConservation::disable).thenSucceed();
|
||||
}
|
||||
|
||||
// P1: the outer ME connection must expose storage that is physically present only behind the output tunnel.
|
||||
@GameTest(template = "p2p_tunnels", timeoutTicks = 180)
|
||||
public static void meP2PCarriesRemoteStorageChannel(GameTestHelper helper) {
|
||||
P2PFixture fixture = getFixture(helper);
|
||||
PartP2PTunnelME input = inputTunnel(helper, PartP2PTunnelME.class);
|
||||
PartP2PTunnelME output = outputTunnel(helper, PartP2PTunnelME.class);
|
||||
TileDrive remoteDrive = tile(helper, TileDrive.class, REMOTE_STORAGE_LABEL);
|
||||
ItemStack remoteCell = cell1k();
|
||||
insertItems(helper, remoteCell, Blocks.cobblestone, 64);
|
||||
remoteDrive.setInventorySlotContents(0, remoteCell);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil("wait for the ME P2P pair to create its outer grid connection", 100, () -> {
|
||||
assertCarrierActive(helper, fixture.controller);
|
||||
assertLinkedPair(helper, input, output, ME_FREQUENCY);
|
||||
IGridNode inputOuter = input.getExternalFacingNode();
|
||||
IGridNode outputOuter = output.getExternalFacingNode();
|
||||
helper.assertNotNull(inputOuter, "Input ME tunnel should expose an outer grid node");
|
||||
helper.assertNotNull(outputOuter, "Output ME tunnel should expose an outer grid node");
|
||||
helper.assertTrue(inputOuter.isActive(), "Input-side main network should be powered and active");
|
||||
helper.assertTrue(
|
||||
outputOuter.isActive(),
|
||||
"Remote storage side should be powered through the ME tunnel");
|
||||
helper.assertTrue(
|
||||
hasConnection(inputOuter, outputOuter),
|
||||
"ME P2P outer nodes should have the tunnel-created connection; "
|
||||
+ describePair(input, output));
|
||||
assertNetworkStoredAmount(helper, inputOuter, Blocks.cobblestone, 64);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// P1: all three links are authored in the exported cable-bus NBT; the test performs no binding setup.
|
||||
@GameTest(template = "p2p_tunnels", timeoutTicks = 100)
|
||||
public static void frequencyPersistsThroughTemplateNbt(GameTestHelper helper) {
|
||||
P2PFixture fixture = getFixture(helper);
|
||||
PartP2PItems itemInput = inputTunnel(helper, PartP2PItems.class);
|
||||
PartP2PItems itemOutput = outputTunnel(helper, PartP2PItems.class);
|
||||
PartP2PRedstone redstoneInput = inputTunnel(helper, PartP2PRedstone.class);
|
||||
PartP2PRedstone redstoneOutput = outputTunnel(helper, PartP2PRedstone.class);
|
||||
PartP2PTunnelME meInput = inputTunnel(helper, PartP2PTunnelME.class);
|
||||
PartP2PTunnelME meOutput = outputTunnel(helper, PartP2PTunnelME.class);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for all NBT-authored P2P frequencies to reconnect", 80, () -> {
|
||||
assertCarrierActive(helper, fixture.controller);
|
||||
assertLinkedPair(helper, itemInput, itemOutput, ITEM_FREQUENCY);
|
||||
assertLinkedPair(helper, redstoneInput, redstoneOutput, REDSTONE_FREQUENCY);
|
||||
assertLinkedPair(helper, meInput, meOutput, ME_FREQUENCY);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// P2: the output must track both edges of the input signal, including returning to zero.
|
||||
@GameTest(template = "p2p_tunnels", timeoutTicks = 160)
|
||||
public static void redstoneP2PMirrorsSignal(GameTestHelper helper) {
|
||||
P2PFixture fixture = getFixture(helper);
|
||||
PartP2PRedstone input = inputTunnel(helper, PartP2PRedstone.class);
|
||||
PartP2PRedstone output = outputTunnel(helper, PartP2PRedstone.class);
|
||||
ContinuousInvariant unpoweredOutputStaysLow = continuousInvariant(
|
||||
helper,
|
||||
"an unpowered redstone P2P input must not produce output power",
|
||||
() -> {
|
||||
assertCarrierActive(helper, fixture.controller);
|
||||
assertLinkedPair(helper, input, output, REDSTONE_FREQUENCY);
|
||||
assertRedstonePower(helper, 0);
|
||||
});
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for the linked redstone P2P pair to settle low", 60, () -> {
|
||||
assertCarrierActive(helper, fixture.controller);
|
||||
assertLinkedPair(helper, input, output, REDSTONE_FREQUENCY);
|
||||
assertRedstonePower(helper, 0);
|
||||
}).thenExecute("begin unpowered-output invariant", unpoweredOutputStaysLow::enable).thenIdle(5)
|
||||
.thenExecute("power the redstone P2P input", () -> {
|
||||
unpoweredOutputStaysLow.disable();
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenWaitUntil("wait for the redstone P2P output to become powered", 40, () -> {
|
||||
assertLinkedPair(helper, input, output, REDSTONE_FREQUENCY);
|
||||
assertRedstonePower(helper, 15);
|
||||
}).thenExecute("remove power from the redstone P2P input", () -> setRedstoneInput(helper, 0))
|
||||
.thenWaitUntil("wait for the redstone P2P output to return to zero", 40, () -> {
|
||||
assertCarrierActive(helper, fixture.controller);
|
||||
assertLinkedPair(helper, input, output, REDSTONE_FREQUENCY);
|
||||
assertRedstonePower(helper, 0);
|
||||
}).thenExecute("begin restored-low invariant", unpoweredOutputStaysLow::enable).thenIdle(5)
|
||||
.thenExecute("finish restored-low observation", unpoweredOutputStaysLow::disable).thenSucceed();
|
||||
}
|
||||
|
||||
// P2: after both exported tunnels are explicitly unbound, no destination mutation is allowed for the window.
|
||||
@GameTest(template = "p2p_tunnels", timeoutTicks = 120)
|
||||
public static void unboundTunnelDoesNotTransfer(GameTestHelper helper) {
|
||||
P2PFixture fixture = getFixture(helper);
|
||||
PartP2PItems input = (PartP2PItems) inputTunnel(helper, PartP2PItems.class).unbind(null);
|
||||
PartP2PItems output = (PartP2PItems) outputTunnel(helper, PartP2PItems.class).unbind(null);
|
||||
setChestSlot(fixture.sourceChest, 0, Blocks.cobblestone, 1);
|
||||
ContinuousInvariant noUnboundTransfer = continuousInvariant(
|
||||
helper,
|
||||
"unbound item tunnels must neither transfer nor duplicate the supplied item",
|
||||
() -> {
|
||||
assertUnboundPairOnCarrier(helper, fixture.controller, input, output);
|
||||
assertInventoryAmount(
|
||||
helper,
|
||||
fixture.destinationChest,
|
||||
Blocks.cobblestone,
|
||||
0,
|
||||
DESTINATION_CHEST_LABEL);
|
||||
assertInventoryTotal(helper, fixture, Blocks.cobblestone, 1);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(
|
||||
"wait for both item tunnels to be active and unbound",
|
||||
60,
|
||||
() -> assertUnboundPairOnCarrier(helper, fixture.controller, input, output))
|
||||
.thenExecute("begin unbound-transfer invariant", noUnboundTransfer::enable).thenIdle(40)
|
||||
.thenExecute("finish unbound-transfer observation", noUnboundTransfer::disable)
|
||||
.thenExecute("assert destination remained untouched for the full observation window", () -> {
|
||||
assertInventoryAmount(
|
||||
helper,
|
||||
fixture.destinationChest,
|
||||
Blocks.cobblestone,
|
||||
0,
|
||||
DESTINATION_CHEST_LABEL);
|
||||
assertInventoryTotal(helper, fixture, Blocks.cobblestone, 1);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
private static P2PFixture getFixture(GameTestHelper helper) {
|
||||
return new P2PFixture(
|
||||
tile(helper, TileController.class, CONTROLLER_LABEL),
|
||||
tile(helper, TileEntityChest.class, SOURCE_CHEST_LABEL),
|
||||
tile(helper, TileEntityHopper.class, SOURCE_INSERTER_LABEL),
|
||||
tile(helper, TileEntityChest.class, DESTINATION_CHEST_LABEL));
|
||||
}
|
||||
|
||||
private static <T extends PartP2PTunnel> T inputTunnel(GameTestHelper helper, Class<T> type) {
|
||||
return part(helper, INPUT_TUNNEL_HOST_LABEL, type);
|
||||
}
|
||||
|
||||
private static <T extends PartP2PTunnel> T outputTunnel(GameTestHelper helper, Class<T> type) {
|
||||
return part(helper, OUTPUT_TUNNEL_HOST_LABEL, type);
|
||||
}
|
||||
|
||||
private static void assertCarrierActive(GameTestHelper helper, TileController controller) {
|
||||
assertActive(helper, controller.getProxy(), "P2P carrier controller should be active");
|
||||
}
|
||||
|
||||
private static void assertUnboundPairOnCarrier(GameTestHelper helper, TileController controller, PartP2PItems input,
|
||||
PartP2PItems output) {
|
||||
assertCarrierActive(helper, controller);
|
||||
assertUnbound(helper, input, "input");
|
||||
assertUnbound(helper, output, "output");
|
||||
assertOnCarrierGrid(helper, controller, input, output);
|
||||
}
|
||||
|
||||
private static void assertOnCarrierGrid(GameTestHelper helper, TileController controller, PartP2PTunnel<?> input,
|
||||
PartP2PTunnel<?> output) {
|
||||
IGridNode carrierNode = controller.getProxy().getNode();
|
||||
helper.assertSame(
|
||||
carrierNode.getGrid(),
|
||||
input.getGridNode().getGrid(),
|
||||
"Unbound P2P input should remain on the controller's carrier grid");
|
||||
helper.assertSame(
|
||||
carrierNode.getGrid(),
|
||||
output.getGridNode().getGrid(),
|
||||
"Unbound P2P output should remain on the controller's carrier grid");
|
||||
}
|
||||
|
||||
private static <T extends PartP2PTunnel> void assertLinkedPair(GameTestHelper helper, T input, T output,
|
||||
long expectedFrequency) {
|
||||
assertActive(helper, input, "P2P input should be active; " + describePair(input, output));
|
||||
assertActive(helper, output, "P2P output should be active; " + describePair(input, output));
|
||||
helper.assertFalse(
|
||||
input.isOutput(),
|
||||
"NBT-authored input should remain an input; " + describePair(input, output));
|
||||
helper.assertTrue(
|
||||
output.isOutput(),
|
||||
"NBT-authored output should remain an output; " + describePair(input, output));
|
||||
helper.assertEquals(
|
||||
expectedFrequency,
|
||||
input.getFrequency(),
|
||||
"Input frequency should match exported template NBT; " + describePair(input, output));
|
||||
helper.assertEquals(
|
||||
expectedFrequency,
|
||||
output.getFrequency(),
|
||||
"Output frequency should match exported template NBT; " + describePair(input, output));
|
||||
helper.assertSame(
|
||||
input,
|
||||
output.getInput(),
|
||||
"Output should resolve its exported input; " + describePair(input, output));
|
||||
|
||||
try {
|
||||
boolean foundOutput = false;
|
||||
for (Object candidate : input.getOutputs()) {
|
||||
if (candidate == output) {
|
||||
foundOutput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(
|
||||
foundOutput,
|
||||
"Input should enumerate its exported output; " + describePair(input, output));
|
||||
} catch (GridAccessException e) {
|
||||
throw new AssertionError("P2P cache should be accessible; " + describePair(input, output), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertUnbound(GameTestHelper helper, PartP2PItems tunnel, String role) {
|
||||
assertActive(helper, tunnel, "Unbound item tunnel should remain active on the carrier network; role=" + role);
|
||||
helper.assertEquals(0L, tunnel.getFrequency(), "Unbound item tunnel frequency should be zero; role=" + role);
|
||||
helper.assertFalse(tunnel.isOutput(), "Unbound tunnel should be reset to input mode; role=" + role);
|
||||
helper.assertNull(tunnel.getInput(), "Unbound tunnel should not resolve an input; role=" + role);
|
||||
}
|
||||
|
||||
private static ContinuousInvariant itemConservationInvariant(GameTestHelper helper, P2PFixture fixture,
|
||||
long expectedTotal) {
|
||||
return continuousInvariant(
|
||||
helper,
|
||||
"item P2P transport must conserve the supplied stack on every tick",
|
||||
() -> assertInventoryTotal(helper, fixture, Blocks.cobblestone, expectedTotal));
|
||||
}
|
||||
|
||||
private static void assertInventoryTotal(GameTestHelper helper, P2PFixture fixture, Block block,
|
||||
long expectedTotal) {
|
||||
long source = inventoryAmount(fixture.sourceChest, block);
|
||||
long inserter = inventoryAmount(fixture.sourceInserter, block);
|
||||
long destination = inventoryAmount(fixture.destinationChest, block);
|
||||
helper.assertEquals(
|
||||
expectedTotal,
|
||||
source + inserter + destination,
|
||||
"Item total across source_chest, source_inserter, and destination_chest should be conserved"
|
||||
+ "; source="
|
||||
+ source
|
||||
+ ", inserter="
|
||||
+ inserter
|
||||
+ ", destination="
|
||||
+ destination);
|
||||
helper.assertTrue(
|
||||
destination <= expectedTotal,
|
||||
"Destination must never contain duplicated items; destination=" + destination
|
||||
+ ", supplied="
|
||||
+ expectedTotal);
|
||||
}
|
||||
|
||||
private static void assertInventoryAmount(GameTestHelper helper, IInventory inventory, Block block, long expected,
|
||||
String role) {
|
||||
long actual = inventoryAmount(inventory, block);
|
||||
helper.assertEquals(
|
||||
expected,
|
||||
actual,
|
||||
"Inventory role '" + role + "' should contain the expected " + blockName(block) + " amount");
|
||||
}
|
||||
|
||||
private static long inventoryAmount(IInventory inventory, Block block) {
|
||||
long amount = 0;
|
||||
for (int slot = 0; slot < inventory.getSizeInventory(); slot++) {
|
||||
ItemStack stack = inventory.getStackInSlot(slot);
|
||||
if (stack != null && stack.getItem() == Item.getItemFromBlock(block)) {
|
||||
amount += stack.stackSize;
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
private static boolean hasConnection(IGridNode from, IGridNode to) {
|
||||
for (IGridConnection connection : from.getConnections()) {
|
||||
if (connection.getOtherSide(from) == to) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void setRedstoneInput(GameTestHelper helper, int strength) {
|
||||
TestPos source = helper.pos(REDSTONE_SOURCE_LABEL);
|
||||
helper.setRedstoneInput(source.x(), source.y(), source.z(), strength);
|
||||
}
|
||||
|
||||
private static void assertRedstonePower(GameTestHelper helper, int expectedPower) {
|
||||
TestPos probe = helper.pos(REDSTONE_PROBE_LABEL);
|
||||
int actualPower = helper.getWorld().getStrongestIndirectPower(
|
||||
helper.absolute(probe.x(), probe.y(), probe.z()).x(),
|
||||
helper.absolute(probe.x(), probe.y(), probe.z()).y(),
|
||||
helper.absolute(probe.x(), probe.y(), probe.z()).z());
|
||||
helper.assertEquals(
|
||||
expectedPower,
|
||||
actualPower,
|
||||
"Redstone probe should mirror the P2P input; source=" + REDSTONE_SOURCE_LABEL
|
||||
+ ", probe="
|
||||
+ REDSTONE_PROBE_LABEL);
|
||||
}
|
||||
|
||||
private static String describePair(PartP2PTunnel input, PartP2PTunnel output) {
|
||||
return "input[type=" + input.getClass().getSimpleName()
|
||||
+ ", frequency="
|
||||
+ input.getFrequency()
|
||||
+ ", output="
|
||||
+ input.isOutput()
|
||||
+ ", active="
|
||||
+ isActive(input)
|
||||
+ "], output[type="
|
||||
+ output.getClass().getSimpleName()
|
||||
+ ", frequency="
|
||||
+ output.getFrequency()
|
||||
+ ", output="
|
||||
+ output.isOutput()
|
||||
+ ", active="
|
||||
+ isActive(output)
|
||||
+ ']';
|
||||
}
|
||||
|
||||
private static boolean isActive(IPart part) {
|
||||
return part.getGridNode() != null && part.getGridNode().isActive();
|
||||
}
|
||||
|
||||
private static String blockName(Block block) {
|
||||
Object name = Block.blockRegistry.getNameForObject(block);
|
||||
return name == null ? block.getLocalizedName() : name.toString();
|
||||
}
|
||||
|
||||
private static final class P2PFixture {
|
||||
|
||||
private final TileController controller;
|
||||
private final TileEntityChest sourceChest;
|
||||
private final TileEntityHopper sourceInserter;
|
||||
private final TileEntityChest destinationChest;
|
||||
|
||||
private P2PFixture(TileController controller, TileEntityChest sourceChest, TileEntityHopper sourceInserter,
|
||||
TileEntityChest destinationChest) {
|
||||
this.controller = controller;
|
||||
this.sourceChest = sourceChest;
|
||||
this.sourceInserter = sourceInserter;
|
||||
this.destinationChest = destinationChest;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package appeng.gametests.storage.drive;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertItemRemainder;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertNetworkStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.injectIntoGrid;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemInventory;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemStack;
|
||||
import static appeng.gametests.AEGameTestHelpers.simulateInjectIntoGrid;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.item.ItemStack;
|
||||
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.storage.ICellInventory;
|
||||
import appeng.api.storage.ICellInventoryHandler;
|
||||
import appeng.api.storage.IMEInventoryHandler;
|
||||
import appeng.api.storage.data.IAEItemStack;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.tile.inventory.IAEStackInventory;
|
||||
import appeng.tile.networking.TileController;
|
||||
import appeng.tile.storage.TileChest;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class DriveAndCellTests {
|
||||
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final String ME_CHEST_LABEL = "me_chest";
|
||||
private static final long CELL_1K_ONE_TYPE_CAPACITY = 8128;
|
||||
|
||||
// A prefilled cell in a drive should be visible through the storage grid.
|
||||
@GameTest(template = "drive_cells", timeoutTicks = 80)
|
||||
public static void driveExposesInsertedCellContents(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack cell = cell1k();
|
||||
insertItems(helper, cell, Blocks.cobblestone, 100);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for drive network activation", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should become active");
|
||||
}).thenExecute("insert prefilled cell into drive", () -> drive.setInventorySlotContents(0, cell))
|
||||
.thenWaitUntil(
|
||||
"wait for prefilled cell contents to become network-visible",
|
||||
20,
|
||||
() -> assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 100))
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// A partitioned cell should accept only stacks matching its configured partition list.
|
||||
@GameTest(template = "drive_cells", timeoutTicks = 100)
|
||||
public static void partitionedCellRejectsUnconfiguredItem(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack cell = cell1k();
|
||||
partitionCell(helper, cell, Blocks.cobblestone);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for drive network activation", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should become active");
|
||||
}).thenExecute("insert partitioned cell into drive", () -> drive.setInventorySlotContents(0, cell))
|
||||
.thenWaitUntil("wait for partition rules to become network-visible", 20, () -> {
|
||||
helper.assertNull(
|
||||
simulateInjectIntoGrid(controller, Blocks.cobblestone, 64),
|
||||
"Configured cobblestone should be accepted by the partitioned cell");
|
||||
assertItemRemainder(helper, simulateInjectIntoGrid(controller, Blocks.dirt, 64), Blocks.dirt, 64);
|
||||
}).thenExecute("inject configured and rejected stacks", () -> {
|
||||
IAEItemStack acceptedRemainder = injectIntoGrid(controller, Blocks.cobblestone, 64);
|
||||
IAEItemStack rejectedRemainder = injectIntoGrid(controller, Blocks.dirt, 64);
|
||||
|
||||
helper.assertNull(acceptedRemainder, "Configured stack should enter the partitioned cell");
|
||||
assertItemRemainder(helper, rejectedRemainder, Blocks.dirt, 64);
|
||||
assertStoredAmount(helper, cell, Blocks.cobblestone, 64);
|
||||
assertStoredAmount(helper, cell, Blocks.dirt, 0);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// New items should route to the higher-priority ME chest before the lower-priority drive.
|
||||
@GameTest(template = "drive_cells", timeoutTicks = 100)
|
||||
public static void higherPriorityCellReceivesNewItemsFirst(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
TileChest meChest = getMEChest(helper);
|
||||
ItemStack highPriorityCell = cell1k();
|
||||
ItemStack lowPriorityCell = cell1k();
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for prioritized storage network activation", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should become active");
|
||||
assertActive(helper, meChest.getProxy(), "ME chest grid proxy should become active");
|
||||
}).thenExecute("configure priorities and insert storage cells", () -> {
|
||||
meChest.setPriority(100);
|
||||
drive.setPriority(0);
|
||||
meChest.setInventorySlotContents(1, highPriorityCell);
|
||||
drive.setInventorySlotContents(0, lowPriorityCell);
|
||||
}).thenWaitUntil(
|
||||
"wait for prioritized storage to accept the test stack",
|
||||
20,
|
||||
() -> helper.assertNull(
|
||||
simulateInjectIntoGrid(controller, Blocks.cobblestone, 128),
|
||||
"Prioritized storage should accept 128 cobblestone"))
|
||||
.thenExecute("inject cobblestone and validate high-priority routing", () -> {
|
||||
IAEItemStack remainder = injectIntoGrid(controller, Blocks.cobblestone, 128);
|
||||
|
||||
helper.assertNull(remainder, "Injected items should fit into available network storage");
|
||||
assertStoredAmount(helper, highPriorityCell, Blocks.cobblestone, 128);
|
||||
assertStoredAmount(helper, lowPriorityCell, Blocks.cobblestone, 0);
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, 128);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Once high-priority storage is full, overflow should route to the lower-priority drive cell.
|
||||
@GameTest(template = "drive_cells", timeoutTicks = 100)
|
||||
public static void fullHighPriorityFallsBackToLowerPriority(GameTestHelper helper) {
|
||||
TileController controller = getController(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
TileChest meChest = getMEChest(helper);
|
||||
ItemStack highPriorityCell = cell1k();
|
||||
ItemStack lowPriorityCell = cell1k();
|
||||
insertItems(helper, highPriorityCell, Blocks.cobblestone, CELL_1K_ONE_TYPE_CAPACITY);
|
||||
|
||||
helper.startSequence().thenWaitUntil("wait for prioritized storage network activation", 40, () -> {
|
||||
assertActive(helper, controller.getProxy(), "Controller grid proxy should become active");
|
||||
assertActive(helper, drive.getProxy(), "Drive grid proxy should become active");
|
||||
assertActive(helper, meChest.getProxy(), "ME chest grid proxy should become active");
|
||||
}).thenExecute("configure priorities and insert full and empty cells", () -> {
|
||||
meChest.setPriority(100);
|
||||
drive.setPriority(0);
|
||||
meChest.setInventorySlotContents(1, highPriorityCell);
|
||||
drive.setInventorySlotContents(0, lowPriorityCell);
|
||||
}).thenWaitUntil("wait for the full high-priority cell to become visible", 20, () -> {
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, CELL_1K_ONE_TYPE_CAPACITY);
|
||||
helper.assertNull(
|
||||
simulateInjectIntoGrid(controller, Blocks.cobblestone, 64),
|
||||
"Lower-priority drive cell should be ready to accept overflow");
|
||||
}).thenExecute("inject overflow and validate fallback routing", () -> {
|
||||
IAEItemStack remainder = injectIntoGrid(controller, Blocks.cobblestone, 64);
|
||||
|
||||
helper.assertNull(remainder, "Overflow should fit into lower-priority storage");
|
||||
assertStoredAmount(helper, highPriorityCell, Blocks.cobblestone, CELL_1K_ONE_TYPE_CAPACITY);
|
||||
assertStoredAmount(helper, lowPriorityCell, Blocks.cobblestone, 64);
|
||||
assertNetworkStoredAmount(helper, controller, Blocks.cobblestone, CELL_1K_ONE_TYPE_CAPACITY + 64);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
private static TileController getController(GameTestHelper helper) {
|
||||
return tile(helper, TileController.class, CONTROLLER_LABEL);
|
||||
}
|
||||
|
||||
private static TileDrive getDrive(GameTestHelper helper) {
|
||||
return tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
}
|
||||
|
||||
private static TileChest getMEChest(GameTestHelper helper) {
|
||||
return tile(helper, TileChest.class, ME_CHEST_LABEL);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void partitionCell(GameTestHelper helper, ItemStack cell, Block block) {
|
||||
IMEInventoryHandler<IAEItemStack> handler = itemInventory(helper, cell);
|
||||
helper.assertTrue(handler instanceof ICellInventoryHandler, "Item cell should expose a configurable inventory");
|
||||
ICellInventoryHandler<IAEItemStack> cellHandler = (ICellInventoryHandler<IAEItemStack>) handler;
|
||||
ICellInventory<IAEItemStack> cellInventory = cellHandler.getCellInv();
|
||||
helper.assertNotNull(cellInventory, "Item cell inventory should expose cell details");
|
||||
|
||||
IAEStackInventory config = cellInventory.getConfigAEInventory();
|
||||
helper.assertNotNull(config, "Item cell config inventory should exist");
|
||||
config.putAEStackInSlot(0, itemStack(block, 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
package appeng.gametests.storage.ioport;
|
||||
|
||||
import static appeng.gametests.AEGameTestHelpers.assertActive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertInactive;
|
||||
import static appeng.gametests.AEGameTestHelpers.assertStoredAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell1k;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell4k;
|
||||
import static appeng.gametests.AEGameTestHelpers.cell64k;
|
||||
import static appeng.gametests.AEGameTestHelpers.continuousInvariant;
|
||||
import static appeng.gametests.AEGameTestHelpers.insertItems;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemInventory;
|
||||
import static appeng.gametests.AEGameTestHelpers.itemStack;
|
||||
import static appeng.gametests.AEGameTestHelpers.pos;
|
||||
import static appeng.gametests.AEGameTestHelpers.storedAmount;
|
||||
import static appeng.gametests.AEGameTestHelpers.tile;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.init.Items;
|
||||
import net.minecraft.inventory.IInventory;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.tileentity.TileEntityHopper;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizons.horizonqa.api.GameTestHelper;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTest;
|
||||
import com.gtnewhorizons.horizonqa.api.annotation.GameTestHolder;
|
||||
|
||||
import appeng.api.AEApi;
|
||||
import appeng.api.config.FullnessMode;
|
||||
import appeng.api.config.OperationMode;
|
||||
import appeng.api.config.RedstoneMode;
|
||||
import appeng.api.config.Settings;
|
||||
import appeng.api.config.Upgrades;
|
||||
import appeng.api.storage.ICellInventory;
|
||||
import appeng.api.storage.ICellInventoryHandler;
|
||||
import appeng.api.storage.IMEInventoryHandler;
|
||||
import appeng.api.storage.data.IAEItemStack;
|
||||
import appeng.core.AppEng;
|
||||
import appeng.gametests.AEGameTestHelpers;
|
||||
import appeng.gametests.AEGameTestHelpers.ContinuousInvariant;
|
||||
import appeng.gametests.AEGameTestHelpers.Coord;
|
||||
import appeng.tile.inventory.IAEStackInventory;
|
||||
import appeng.tile.storage.TileDrive;
|
||||
import appeng.tile.storage.TileIOPort;
|
||||
|
||||
@GameTestHolder(AppEng.MOD_ID)
|
||||
public class IOPortTests {
|
||||
|
||||
private static final String IO_PORT_LABEL = "io_port";
|
||||
private static final String CONTROLLER_LABEL = "controller";
|
||||
private static final String DRIVE_LABEL = "drive";
|
||||
private static final String REDSTONE_LABEL = "redstone";
|
||||
private static final String AUTOMATION_LABEL = "automation";
|
||||
|
||||
// Moves an empty cell from an input slot to an output slot.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void emptyCellMove(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack cell = cell1k();
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert empty cell into IO port", () -> {
|
||||
ioport.setInventorySlotContents(0, cell.copy());
|
||||
helper.assertTrue(
|
||||
ItemStack.areItemStacksEqual(ioport.getStackInSlot(0), cell),
|
||||
"Inserted cell should be present in the input slot before the IO port ticks");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Output slot should be empty before the IO port ticks");
|
||||
}).thenExecute("assert empty cell reached output in the same tick", () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Input slot should be empty after one processing tick");
|
||||
ItemStack expectedCell = cell.copy();
|
||||
expectedCell.setTagCompound(new NBTTagCompound());
|
||||
helper.assertTrue(
|
||||
ItemStack.areItemStacksEqual(ioport.getStackInSlot(6), expectedCell),
|
||||
"Empty cell should reach the output slot after one processing tick");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Allows cell insertion only into input slots and extraction only from output slots.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void sidedInventoryAllowsCellInputAndOutputExtractionOnly(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
ItemStack cell = cell1k();
|
||||
ItemStack nonCell = new ItemStack(Items.apple);
|
||||
int side = ForgeDirection.UNKNOWN.ordinal();
|
||||
|
||||
helper.assertTrue(ioport.isItemValidForSlot(0, cell), "Storage cells should be valid items");
|
||||
helper.assertFalse(ioport.isItemValidForSlot(0, nonCell), "Non-cell items should be invalid");
|
||||
helper.assertTrue(ioport.canInsertItem(0, cell, side), "Input slots should accept storage cells");
|
||||
helper.assertFalse(ioport.canInsertItem(6, cell, side), "Output slots should not accept storage cells");
|
||||
helper.assertFalse(ioport.canInsertItem(0, nonCell, side), "Input slots should not accept non-cell items");
|
||||
helper.assertFalse(ioport.canExtractItem(0, cell, side), "Input slots should not allow extraction");
|
||||
helper.assertTrue(ioport.canExtractItem(6, cell, side), "Output slots should allow extraction");
|
||||
helper.assertEquals(
|
||||
12,
|
||||
ioport.getAccessibleSlotsBySide(ForgeDirection.UNKNOWN).length,
|
||||
"All 12 slots should be exposed");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
// Exports items from a filled cell into ME network storage in EMPTY mode.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void emptyModeExportsCellContentsToNetwork(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 100);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenWaitUntil("wait for source cell to empty into the drive and move to output", 10, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Exported cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Exported cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Imports items from ME network storage into an empty cell in FILL mode.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void fillModeImportsNetworkContentsIntoCell(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 100);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenWaitUntil("wait for target cell to receive all network items and move to output", 10, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Imported cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Imported cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 100);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Imports only stacks allowed by the target cell partition in FILL mode.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void partitionedTargetCellOnlyImportsMatchingStacks(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
partitionCell(helper, targetCell, Blocks.cobblestone);
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 100);
|
||||
insertItems(helper, driveCell, Blocks.dirt, 100);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenWaitUntil("wait for partitioned import to stop at non-matching stacks", 10, () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Partitioned cell should leave input once no more matching stacks can be imported");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Partitioned cell should move to output after importing matching stacks");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 100);
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.dirt, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.dirt, 100);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Covers PR1197: FILL plus MoveWhenEmpty moves the cell when the remaining network amount exactly matches the
|
||||
// transfer budget.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void fillMoveWhenEmptyMovesCellWhenNetworkHasExactlyTransferBudget(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 256);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert target and source cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenExecute("assert exact-budget import completes in the same tick", () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell should leave input when the network is exactly drained");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Cell should move to output when the network is exactly drained");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 256);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Covers PR1259: FILL plus MoveWhenEmpty does not move the cell when the network starts empty.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void fillMoveWhenEmptyDoesNotMoveWhenNetworkStartsEmpty(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
ContinuousInvariant emptyNetworkKeepsCell = continuousInvariant(
|
||||
helper,
|
||||
"an empty network must not move an unfilled target cell",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Cell should stay in input");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Cell should not move to output");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
emptyNetworkKeepsCell.enable();
|
||||
}).thenIdle(5).thenExecute("finish empty-network observation window", emptyNetworkKeepsCell::disable)
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// Covers PR1259: FILL plus MoveWhenEmpty moves the cell when the target cell becomes full.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void fillMoveWhenEmptyMovesWhenTargetCellBecomesFull(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.EMPTY);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell64k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 9000);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenWaitUntil("wait for target cell to become full and move to output", 45, () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Full target cell should leave input in move-when-empty mode");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Full target cell should move to output even when the network still has items");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 8128);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 872);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Keeps a cell in input under FullnessMode.EMPTY until the source cell is empty.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void emptyFullnessWaitsUntilSourceCellIsEmpty(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 300);
|
||||
ContinuousInvariant nonEmptySourceStaysInInput = continuousInvariant(
|
||||
helper,
|
||||
"source cell must remain in input while it still contains items",
|
||||
() -> {
|
||||
if (storedAmount(helper, sourceCell, Blocks.cobblestone) > 0) {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Non-empty source cell should remain in input");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Non-empty source cell should not reach output");
|
||||
}
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
nonEmptySourceStaysInInput.enable();
|
||||
}).thenWaitUntil("wait until the source cell is empty", 10, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Emptied cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Emptied cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 300);
|
||||
}).thenExecute("stop source-cell invariant", nonEmptySourceStaysInInput::disable).thenSucceed();
|
||||
}
|
||||
|
||||
// Moves a partially transferred cell under FullnessMode.HALF.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void halfFullnessMovesPartiallyTransferredCell(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.EMPTY, FullnessMode.HALF);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack nearlyFullDriveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 300);
|
||||
insertItems(helper, nearlyFullDriveCell, Blocks.cobblestone, 8028);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, nearlyFullDriveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenWaitUntil("wait for HALF mode to move the partially transferred source cell", 20, () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"HALF mode should remove the partially transferred cell from input");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"HALF mode should move the partially transferred cell to output");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 200);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 8128);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Moves a FILL-mode target cell only after it becomes full under FullnessMode.FULL.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void fullFullnessMovesOnlyAfterTargetCellIsFull(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
configure(ioport, OperationMode.FILL, FullnessMode.FULL);
|
||||
ItemStack targetCell = cell1k();
|
||||
ItemStack driveCell = cell64k();
|
||||
insertItems(helper, driveCell, Blocks.cobblestone, 9000);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, targetCell);
|
||||
}).thenWaitUntil("wait for FULL mode to fill the target cell before moving it", 40, () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Full target cell should leave the input slot");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Full target cell should move to the output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 8128);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 872);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Moves cells from multiple input slots to output slots without loss or overwrite.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void multipleInputSlotsMoveToOutputSlots(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ContinuousInvariant cellConservation = continuousInvariant(
|
||||
helper,
|
||||
"six supplied cells must remain accounted for while moving between slots",
|
||||
() -> helper.assertEquals(
|
||||
6,
|
||||
countFilledSlots(ioport, 0, 12),
|
||||
"Total IO port cell count should remain six"));
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
for (int slot = 0; slot < 6; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
cellConservation.enable();
|
||||
}).thenIdle(4).thenExecute("assert five cells moved after five processing ticks", () -> {
|
||||
helper.assertEquals(1, countFilledSlots(ioport, 0, 6), "Input slots should contain 1 cell");
|
||||
helper.assertEquals(5, countFilledSlots(ioport, 6, 12), "Output slots should contain 5 cells");
|
||||
}).thenIdle(1).thenExecute("assert all six cells moved after six processing ticks", () -> {
|
||||
for (int slot = 0; slot < 6; slot++) {
|
||||
helper.assertNull(ioport.getStackInSlot(slot), "All input slots should become empty");
|
||||
}
|
||||
helper.assertEquals(6, countFilledSlots(ioport, 6, 12), "Output slots should contain 6 cells");
|
||||
}).thenExecute("stop cell-conservation invariant", cellConservation::disable).thenSucceed();
|
||||
}
|
||||
|
||||
// Keeps a cell queued while output is full, then moves it when an output slot opens.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void outputFullKeepsCellQueuedUntilSlotOpens(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack queuedCell = cell4k();
|
||||
ContinuousInvariant fullOutputRetainsCell = continuousInvariant(
|
||||
helper,
|
||||
"a full output inventory must retain the queued cell in input",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Queued cell should remain in input");
|
||||
helper.assertEquals(6, countFilledSlots(ioport, 6, 12), "Output should remain full");
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
for (int slot = 6; slot < 12; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
ioport.setInventorySlotContents(0, queuedCell);
|
||||
fullOutputRetainsCell.enable();
|
||||
}).thenIdle(5).thenExecute("open an output slot", () -> {
|
||||
fullOutputRetainsCell.disable();
|
||||
ioport.setInventorySlotContents(6, null);
|
||||
}).thenIdle(1).thenExecute("assert queued cell entered the opened output slot", () -> {
|
||||
helper.assertNull(ioport.getStackInSlot(0), "Input cell should leave once an output slot opens");
|
||||
helper.assertTrue(
|
||||
ItemStack.areItemStacksEqual(ioport.getStackInSlot(6), queuedCell),
|
||||
"Queued cell should move into the opened output slot");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Queues an emptied source cell while output is full, then moves it with its contents preserved.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void outputFullQueuesTransferredCellUntilSlotOpens(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell4k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 100);
|
||||
ContinuousInvariant transferredCellRemainsQueued = continuousInvariant(
|
||||
helper,
|
||||
"a transferred cell must remain queued while output is full",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Transferred cell should remain in input");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
for (int slot = 6; slot < 12; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
transferredCellRemainsQueued.enable();
|
||||
}).thenIdle(5).thenExecute("open an output slot", () -> {
|
||||
transferredCellRemainsQueued.disable();
|
||||
ioport.setInventorySlotContents(6, null);
|
||||
}).thenIdle(1).thenExecute("assert transferred cell entered the opened output slot", () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Transferred cell should leave input once output opens");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Transferred cell should move into the opened output slot");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Preserves a queued transferred cell through network loss and resumes without duplication or loss.
|
||||
@GameTest(template = "ioport", timeoutTicks = 120)
|
||||
public static void queuedCellSurvivesPowerLoss(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell4k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 100);
|
||||
ContinuousInvariant inactivePortRetainsQueuedCell = continuousInvariant(
|
||||
helper,
|
||||
"inactive IO port must retain the queued cell without moving or duplicating contents",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Queued cell should remain in input while inactive");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Opened output should stay empty while inactive");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
for (int slot = 6; slot < 12; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenWaitUntil("wait for transferred cell to queue behind full output", 10, () -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Transferred cell should be queued in input while output is full");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
}).thenExecute("remove controller power and channel", () -> removePowerAndChannel(helper))
|
||||
.thenWaitUntil(
|
||||
"wait for IO port to lose its channel after controller removal",
|
||||
40,
|
||||
() -> assertInactive(
|
||||
helper,
|
||||
ioport.getProxy(),
|
||||
"IO port should lose its channel when the controller is removed"))
|
||||
.thenExecute("open output slot while IO port is inactive", () -> {
|
||||
ioport.setInventorySlotContents(6, null);
|
||||
inactivePortRetainsQueuedCell.enable();
|
||||
}).thenIdle(5).thenExecute("restore controller power and channel", () -> {
|
||||
inactivePortRetainsQueuedCell.disable();
|
||||
restorePowerAndChannel(helper);
|
||||
}).thenWaitUntil("wait for queued cell processing to resume after controller restoration", 40, () -> {
|
||||
assertIOPortActive(helper, ioport);
|
||||
helper.assertEquals(0, countFilledSlots(ioport, 0, 6), "No input cells should remain after resume");
|
||||
helper.assertEquals(6, countFilledSlots(ioport, 6, 12), "Output cells should be exactly full");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"Queued cell should move into the reopened output slot after power returns");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(6), Blocks.cobblestone, 0);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Keeps source cell contents unchanged when EMPTY mode cannot export into full network storage.
|
||||
@GameTest(template = "ioport", timeoutTicks = 40)
|
||||
public static void emptyModeKeepsSourceCellWhenDestinationStorageIsFull(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack fullDriveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 100);
|
||||
insertItems(helper, fullDriveCell, Blocks.cobblestone, 8128);
|
||||
ContinuousInvariant fullDestinationPreservesSource = continuousInvariant(
|
||||
helper,
|
||||
"full destination storage must preserve the source cell and its contents",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Source cell should remain in input");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Source cell should not move to output");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 100);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 8128);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, fullDriveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
fullDestinationPreservesSource.enable();
|
||||
}).thenIdle(5)
|
||||
.thenExecute("finish full-destination observation window", fullDestinationPreservesSource::disable)
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// Transfers 256 item units per tick without upgrades.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void noUpgradeTransfersTwoHundredFiftySixItemsPerTick(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 300);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute("assert the first tick transfers exactly 256 items", () -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell should remain in input after exhausting transfer budget");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 44);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 256);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Transfers 512 item units per tick with one Speed upgrade.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void speedUpgradeTransfersFiveHundredTwelveItemsPerTick(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
installUpgrade(ioport, AEApi.instance().definitions().materials().cardSpeed().maybeStack(1).get(), 0);
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 600);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute("assert the first tick transfers exactly 512 items", () -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell with remaining contents should stay in input after speed transfer");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 88);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 512);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Transfers 2048 item units per tick with all three Speed upgrade slots filled.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void maxSpeedUpgradesApplyExpectedBudget(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
TileDrive drive = getDrive(helper);
|
||||
installSpeedUpgrades(ioport);
|
||||
helper.assertEquals(3, ioport.getInstalledUpgrades(Upgrades.SPEED), "All Speed upgrades should be installed");
|
||||
ItemStack sourceCell = cell1k();
|
||||
ItemStack driveCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 3000);
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
drive.setInventorySlotContents(0, driveCell);
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
}).thenExecute("assert the first tick transfers exactly 2048 items", () -> {
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Cell with remaining contents should stay in input after max-speed transfer");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 952);
|
||||
assertStoredAmount(helper, drive.getStackInSlot(0), Blocks.cobblestone, 2048);
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Runs in HIGH_SIGNAL mode only after redstone power is applied.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void redstoneHighSignalRequiresPower(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ContinuousInvariant unpoweredHighSignalDoesNotRun = continuousInvariant(
|
||||
helper,
|
||||
"HIGH_SIGNAL mode must not run without redstone power",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Unpowered cell should remain in input");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Unpowered cell should not reach output");
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
installRedstoneUpgrade(ioport);
|
||||
configureRedstone(ioport, RedstoneMode.HIGH_SIGNAL);
|
||||
ioport.setInventorySlotContents(0, cell1k());
|
||||
unpoweredHighSignalDoesNotRun.enable();
|
||||
}).thenIdle(5).thenExecute("apply redstone power", () -> {
|
||||
unpoweredHighSignalDoesNotRun.disable();
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenIdle(1).thenExecute("assert HIGH_SIGNAL operation on the next tick", () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"HIGH_SIGNAL mode should remove the input cell when powered");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"HIGH_SIGNAL mode should move the cell to output when powered");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Runs in LOW_SIGNAL mode only after redstone power is removed.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void redstoneLowSignalRunsOnlyWithoutPower(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ContinuousInvariant poweredLowSignalDoesNotRun = continuousInvariant(
|
||||
helper,
|
||||
"LOW_SIGNAL mode must not run while redstone is powered",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Powered cell should remain in input");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Powered cell should not reach output");
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
setRedstoneInput(helper, 15);
|
||||
installRedstoneUpgrade(ioport);
|
||||
configureRedstone(ioport, RedstoneMode.LOW_SIGNAL);
|
||||
ioport.setInventorySlotContents(0, cell1k());
|
||||
poweredLowSignalDoesNotRun.enable();
|
||||
}).thenIdle(5).thenExecute("remove redstone power", () -> {
|
||||
poweredLowSignalDoesNotRun.disable();
|
||||
setRedstoneInput(helper, 0);
|
||||
}).thenIdle(1).thenExecute("assert LOW_SIGNAL operation on the next tick", () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"LOW_SIGNAL mode should remove the input cell without power");
|
||||
helper.assertNotNull(
|
||||
ioport.getStackInSlot(6),
|
||||
"LOW_SIGNAL mode should move the cell to output without power");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Processes exactly one cell for a single redstone pulse in SIGNAL_PULSE mode.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void redstonePulseModeRunsAfterPulse(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ContinuousInvariant noPulseDoesNotRun = continuousInvariant(
|
||||
helper,
|
||||
"SIGNAL_PULSE mode must not run before a pulse",
|
||||
() -> {
|
||||
helper.assertEquals(2, countFilledSlots(ioport, 0, 6), "Both cells should remain in input");
|
||||
helper.assertEquals(0, countFilledSlots(ioport, 6, 12), "No cell should reach output");
|
||||
});
|
||||
ContinuousInvariant onePulseMovesOnlyOneCell = continuousInvariant(
|
||||
helper,
|
||||
"one signal pulse must not process a second cell",
|
||||
() -> {
|
||||
helper.assertEquals(1, countFilledSlots(ioport, 0, 6), "One cell should remain in input");
|
||||
helper.assertEquals(1, countFilledSlots(ioport, 6, 12), "Exactly one cell should be in output");
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
installRedstoneUpgrade(ioport);
|
||||
configureRedstone(ioport, RedstoneMode.SIGNAL_PULSE);
|
||||
ioport.setInventorySlotContents(0, cell1k());
|
||||
ioport.setInventorySlotContents(1, cell1k());
|
||||
noPulseDoesNotRun.enable();
|
||||
}).thenIdle(5).thenExecute("apply one redstone pulse", () -> {
|
||||
noPulseDoesNotRun.disable();
|
||||
setRedstoneInput(helper, 15);
|
||||
}).thenIdle(1).thenExecute("assert one pulse processes one cell on the next tick", () -> {
|
||||
helper.assertEquals(
|
||||
1,
|
||||
countFilledSlots(ioport, 0, 6),
|
||||
"SIGNAL_PULSE mode should leave one input cell after one pulse");
|
||||
helper.assertEquals(
|
||||
1,
|
||||
countFilledSlots(ioport, 6, 12),
|
||||
"SIGNAL_PULSE mode should move exactly one cell after one pulse");
|
||||
}).thenExecute("begin post-pulse invariant window", onePulseMovesOnlyOneCell::enable).thenIdle(5)
|
||||
.thenExecute("finish post-pulse invariant window", onePulseMovesOnlyOneCell::disable).thenSucceed();
|
||||
}
|
||||
|
||||
// A real hopper should reject non-cells, then insert a storage cell for processing.
|
||||
@GameTest(template = "ioport", timeoutTicks = 20)
|
||||
public static void sidedAutomationRejectsNonCellsAndInsertsStorageCells(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
ItemStack cell = cell1k();
|
||||
ItemStack nonCell = new ItemStack(Items.apple);
|
||||
Coord automationPos = pos(helper, AUTOMATION_LABEL);
|
||||
ForgeDirection towardIOPort = directionBetween(helper, AUTOMATION_LABEL, IO_PORT_LABEL);
|
||||
helper.setBlock(automationPos.x(), automationPos.y(), automationPos.z(), Blocks.hopper, towardIOPort.ordinal());
|
||||
TileEntityHopper hopper = tile(helper, TileEntityHopper.class, AUTOMATION_LABEL);
|
||||
ContinuousInvariant rejectedItemStaysInHopper = continuousInvariant(
|
||||
helper,
|
||||
"hopper must not insert an apple into the IO port",
|
||||
() -> {
|
||||
helper.assertTrue(
|
||||
ItemStack.areItemStacksEqual(nonCell, hopper.getStackInSlot(0)),
|
||||
"Automation input should still contain the rejected apple");
|
||||
helper.assertNull(ioport.getStackInSlot(0), "IO port input should reject the apple");
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("supply non-cell through automation", () -> {
|
||||
hopper.setInventorySlotContents(0, nonCell.copy());
|
||||
rejectedItemStaysInHopper.enable();
|
||||
}).thenIdle(5).thenExecute("replace rejected item with a storage cell", () -> {
|
||||
rejectedItemStaysInHopper.disable();
|
||||
hopper.setInventorySlotContents(0, cell.copy());
|
||||
}).thenWaitUntil("wait for hopper-fed cell to be processed", 10, () -> {
|
||||
helper.assertNull(hopper.getStackInSlot(0), "Hopper should insert the storage cell");
|
||||
helper.assertNotNull(ioport.getStackInSlot(6), "Processed hopper-fed cell should reach output");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
// Keeps source cell contents intact when no destination storage exists.
|
||||
@GameTest(template = "ioport", timeoutTicks = 30)
|
||||
public static void noDestinationStorageKeepsSourceCellUnchanged(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack sourceCell = cell1k();
|
||||
insertItems(helper, sourceCell, Blocks.cobblestone, 1);
|
||||
ContinuousInvariant noDestinationPreservesSource = continuousInvariant(
|
||||
helper,
|
||||
"missing destination storage must preserve the source cell",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Input cell should remain without a destination");
|
||||
helper.assertNull(ioport.getStackInSlot(6), "Cell should not move without a destination");
|
||||
assertStoredAmount(helper, ioport.getStackInSlot(0), Blocks.cobblestone, 1);
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert source cell without destination storage", () -> {
|
||||
ioport.setInventorySlotContents(0, sourceCell);
|
||||
noDestinationPreservesSource.enable();
|
||||
}).thenIdle(10)
|
||||
.thenExecute("finish missing-destination observation window", noDestinationPreservesSource::disable)
|
||||
.thenSucceed();
|
||||
}
|
||||
|
||||
// Preserves queued cells without duplication or loss when settings change while output is full.
|
||||
@GameTest(template = "ioport", timeoutTicks = 60)
|
||||
public static void settingChangeWhileQueuedDoesNotDuplicateOrLoseCell(GameTestHelper helper) {
|
||||
TileIOPort ioport = getIOPort(helper);
|
||||
getDrive(helper);
|
||||
ItemStack queuedCell = cell4k();
|
||||
ContinuousInvariant queuedCellIsConserved = continuousInvariant(
|
||||
helper,
|
||||
"queued cell must remain exactly once while output is full",
|
||||
() -> {
|
||||
helper.assertNotNull(ioport.getStackInSlot(0), "Queued cell should remain in input");
|
||||
helper.assertEquals(
|
||||
7,
|
||||
countFilledSlots(ioport, 0, 12),
|
||||
"Seven total cells should be accounted for");
|
||||
});
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntilAtEnd("wait for IO port network activation", () -> assertIOPortActive(helper, ioport))
|
||||
.thenIdle(1).thenExecuteAtStart("insert test cells into the IO port network", () -> {
|
||||
for (int slot = 6; slot < 12; slot++) {
|
||||
ioport.setInventorySlotContents(slot, cell1k());
|
||||
}
|
||||
ioport.setInventorySlotContents(0, queuedCell);
|
||||
queuedCellIsConserved.enable();
|
||||
}).thenIdle(5).thenExecute("change settings and open output slot", () -> {
|
||||
queuedCellIsConserved.disable();
|
||||
configure(ioport, OperationMode.EMPTY, FullnessMode.HALF);
|
||||
ioport.setInventorySlotContents(6, null);
|
||||
}).thenWaitUntil("wait for the conserved queued cell to move once after settings change", 30, () -> {
|
||||
helper.assertNull(
|
||||
ioport.getStackInSlot(0),
|
||||
"Queued cell should leave input after changing settings");
|
||||
helper.assertTrue(
|
||||
ItemStack.areItemStacksEqual(ioport.getStackInSlot(6), queuedCell),
|
||||
"Queued cell should move exactly once to output after changing settings");
|
||||
helper.assertEquals(6, countFilledSlots(ioport, 6, 12), "Output cell count should remain six");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
private static TileIOPort getIOPort(GameTestHelper helper) {
|
||||
return tile(helper, TileIOPort.class, IO_PORT_LABEL);
|
||||
}
|
||||
|
||||
private static TileDrive getDrive(GameTestHelper helper) {
|
||||
return tile(helper, TileDrive.class, DRIVE_LABEL);
|
||||
}
|
||||
|
||||
private static void setRedstoneInput(GameTestHelper helper, int strength) {
|
||||
AEGameTestHelpers.setRedstoneInput(helper, REDSTONE_LABEL, strength);
|
||||
}
|
||||
|
||||
private static void assertIOPortActive(GameTestHelper helper, TileIOPort ioport) {
|
||||
assertActive(helper, ioport.getProxy(), "IO port network should become active");
|
||||
}
|
||||
|
||||
private static void configure(TileIOPort ioport, OperationMode operationMode, FullnessMode fullnessMode) {
|
||||
ioport.getConfigManager().putSetting(Settings.OPERATION_MODE, operationMode);
|
||||
ioport.getConfigManager().putSetting(Settings.FULLNESS_MODE, fullnessMode);
|
||||
}
|
||||
|
||||
private static void configureRedstone(TileIOPort ioport, RedstoneMode redstoneMode) {
|
||||
ioport.getConfigManager().putSetting(Settings.REDSTONE_CONTROLLED, redstoneMode);
|
||||
}
|
||||
|
||||
private static void installRedstoneUpgrade(TileIOPort ioport) {
|
||||
installUpgrade(ioport, AEApi.instance().definitions().materials().cardRedstone().maybeStack(1).get(), 0);
|
||||
}
|
||||
|
||||
private static void installSpeedUpgrades(TileIOPort ioport) {
|
||||
ItemStack speedUpgrade = AEApi.instance().definitions().materials().cardSpeed().maybeStack(1).get();
|
||||
for (int slot = 0; slot < 3; slot++) {
|
||||
installUpgrade(ioport, speedUpgrade.copy(), slot);
|
||||
}
|
||||
}
|
||||
|
||||
private static void installUpgrade(TileIOPort ioport, ItemStack upgrade, int slot) {
|
||||
IInventory upgrades = ioport.getInventoryByName("upgrades");
|
||||
upgrades.setInventorySlotContents(slot, upgrade);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void partitionCell(GameTestHelper helper, ItemStack cell, Block block) {
|
||||
IMEInventoryHandler<IAEItemStack> handler = itemInventory(helper, cell);
|
||||
helper.assertTrue(handler instanceof ICellInventoryHandler, "Item cell should expose a configurable inventory");
|
||||
ICellInventoryHandler<IAEItemStack> cellHandler = (ICellInventoryHandler<IAEItemStack>) handler;
|
||||
ICellInventory<IAEItemStack> cellInventory = cellHandler.getCellInv();
|
||||
helper.assertNotNull(cellInventory, "Item cell inventory should expose cell details");
|
||||
|
||||
IAEStackInventory config = cellInventory.getConfigAEInventory();
|
||||
config.putAEStackInSlot(0, itemStack(block, 1));
|
||||
}
|
||||
|
||||
private static void removePowerAndChannel(GameTestHelper helper) {
|
||||
Coord pos = controllerPos(helper);
|
||||
helper.destroyBlock(pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
private static void restorePowerAndChannel(GameTestHelper helper) {
|
||||
Coord pos = controllerPos(helper);
|
||||
Block controller = AEApi.instance().definitions().blocks().creativeEnergyController().maybeBlock().get();
|
||||
helper.setBlock(pos.x(), pos.y(), pos.z(), controller);
|
||||
helper.assertBlockPresent(controller, pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
|
||||
private static Coord controllerPos(GameTestHelper helper) {
|
||||
return pos(helper, CONTROLLER_LABEL);
|
||||
}
|
||||
|
||||
private static ForgeDirection directionBetween(GameTestHelper helper, String fromRole, String toRole) {
|
||||
Coord from = pos(helper, fromRole);
|
||||
Coord to = pos(helper, toRole);
|
||||
int dx = Integer.signum(to.x() - from.x());
|
||||
int dy = Integer.signum(to.y() - from.y());
|
||||
int dz = Integer.signum(to.z() - from.z());
|
||||
for (ForgeDirection direction : ForgeDirection.VALID_DIRECTIONS) {
|
||||
if (direction.offsetX == dx && direction.offsetY == dy && direction.offsetZ == dz) {
|
||||
return direction;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Roles '" + fromRole + "' and '" + toRole + "' must define a direction");
|
||||
}
|
||||
|
||||
private static int countFilledSlots(TileIOPort ioport, int startInclusive, int endExclusive) {
|
||||
int count = 0;
|
||||
for (int slot = startInclusive; slot < endExclusive; slot++) {
|
||||
if (ioport.getStackInSlot(slot) != null) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"size": [5, 1, 3],
|
||||
"palette": {
|
||||
"A": {"name": "appliedenergistics2:tile.BlockCableBus", "meta": 0, "label": "AE2 Cable and/or Bus"},
|
||||
"B": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
|
||||
"C": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"},
|
||||
"D": {"name": "minecraft:chest", "meta": 3, "label": "Chest"}
|
||||
},
|
||||
"layers": [
|
||||
[
|
||||
".....",
|
||||
"DABAD",
|
||||
"..C.."
|
||||
]
|
||||
],
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"controller": [2, 0, 1],
|
||||
"destination_chest": [4, 0, 1],
|
||||
"drive": [2, 0, 2],
|
||||
"export_bus": [3, 0, 1],
|
||||
"import_bus": [1, 0, 1],
|
||||
"redstone": [3, 0, 0],
|
||||
"source_chest": [0, 0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"size": [6, 1, 1],
|
||||
"palette": {
|
||||
"A": {"name": "appliedenergistics2:tile.BlockCraftingStorage", "meta": 12, "label": "§61k§r Crafting Storage"},
|
||||
"B": {"name": "appliedenergistics2:tile.BlockCraftingUnit", "meta": 12, "label": "Crafting Unit"},
|
||||
"C": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
|
||||
"D": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"},
|
||||
"E": {"name": "appliedenergistics2:tile.BlockInterface", "meta": 0, "label": "ME Interface"},
|
||||
"F": {"name": "appliedenergistics2:tile.BlockMolecularAssembler", "meta": 0, "label": "Molecular Assembler"}
|
||||
},
|
||||
"layers": [
|
||||
[
|
||||
"CDABEF"
|
||||
]
|
||||
],
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"assembler": [5, 0, 0],
|
||||
"controller": [0, 0, 0],
|
||||
"cpu_storage": [2, 0, 0],
|
||||
"cpu_unit": [3, 0, 0],
|
||||
"drive": [1, 0, 0],
|
||||
"interface": [4, 0, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"size": [3, 1, 1],
|
||||
"palette": {
|
||||
"A": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self Powered Controller"},
|
||||
"B": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"},
|
||||
"C": {"name": "appliedenergistics2:tile.BlockChest", "meta": 0, "label": "ME Chest"}
|
||||
},
|
||||
"layers": [
|
||||
[
|
||||
"BAC"
|
||||
]
|
||||
],
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"controller": [1, 0, 0],
|
||||
"drive": [0, 0, 0],
|
||||
"me_chest": [2, 0, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"size": [5, 1, 3],
|
||||
"palette": {
|
||||
"A": {"name": "appliedenergistics2:tile.BlockCableBus", "meta": 0, "label": "AE2 Cable and/or Bus"},
|
||||
"B": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
|
||||
"C": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"},
|
||||
"D": {"name": "appliedenergistics2:tile.BlockInterface", "meta": 0, "label": "ME Interface"},
|
||||
"E": {"name": "appliedenergistics2:tile.BlockMolecularAssembler", "meta": 0, "label": "Molecular Assembler"},
|
||||
"F": {"name": "minecraft:chest", "meta": 3, "label": "Chest"}
|
||||
},
|
||||
"layers": [
|
||||
[
|
||||
"BCAAA",
|
||||
"..DFA",
|
||||
"..E.."
|
||||
]
|
||||
],
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"adjacent_chest": [3, 0, 1],
|
||||
"assembler": [2, 0, 2],
|
||||
"block_interface": [2, 0, 1],
|
||||
"controller": [0, 0, 0],
|
||||
"drive": [1, 0, 0],
|
||||
"part_interface_host": [4, 0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -15,8 +15,10 @@
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"io_port": [0, 0, 0],
|
||||
"controller": [1, 0, 0],
|
||||
"drive": [1, 0, 1],
|
||||
"redstone": [0, 0, 1]
|
||||
"redstone": [0, 0, 1],
|
||||
"automation": [0, 0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"size": [12, 1, 3],
|
||||
"palette": {
|
||||
"A": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
|
||||
"B": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"}
|
||||
},
|
||||
"layers": [
|
||||
[
|
||||
"............",
|
||||
"A..........B",
|
||||
"............"
|
||||
]
|
||||
],
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"controller": [0, 0, 1],
|
||||
"drive": [11, 0, 1],
|
||||
"device_a": [2, 0, 1],
|
||||
"device_b": [7, 0, 1],
|
||||
"breakable_cable": [5, 0, 1],
|
||||
"toggle_bus": [4, 0, 1],
|
||||
"redstone": [4, 0, 0],
|
||||
"cable_1": [1, 0, 1],
|
||||
"cable_2": [2, 0, 1],
|
||||
"cable_3": [3, 0, 1],
|
||||
"cable_4": [4, 0, 1],
|
||||
"cable_5": [5, 0, 1],
|
||||
"cable_6": [6, 0, 1],
|
||||
"cable_7": [7, 0, 1],
|
||||
"cable_8": [8, 0, 1],
|
||||
"cable_9": [9, 0, 1],
|
||||
"cable_10": [10, 0, 1],
|
||||
"channel_device_1": [1, 0, 1],
|
||||
"channel_device_2": [2, 0, 1],
|
||||
"channel_device_3": [3, 0, 1],
|
||||
"channel_device_4": [4, 0, 1],
|
||||
"channel_device_5": [5, 0, 1],
|
||||
"channel_device_6": [6, 0, 1],
|
||||
"channel_device_7": [7, 0, 1],
|
||||
"channel_device_8": [8, 0, 1],
|
||||
"channel_device_9": [9, 0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"size": [7, 2, 3],
|
||||
"palette": {
|
||||
"A": {"name": "appliedenergistics2:tile.BlockCableBus", "meta": 0, "label": "AE2 Cable and/or Bus"},
|
||||
"B": {"name": "appliedenergistics2:tile.BlockCreativeEnergyCell", "meta": 0, "label": "Creative Energy Cell"},
|
||||
"C": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
|
||||
"D": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"},
|
||||
"E": {"name": "minecraft:chest", "meta": 3, "label": "Chest"},
|
||||
"F": {"name": "minecraft:hopper", "meta": 5, "label": "Hopper"}
|
||||
},
|
||||
"layers": [
|
||||
[
|
||||
".......",
|
||||
"FAACAAE",
|
||||
"......."
|
||||
],
|
||||
[
|
||||
".......",
|
||||
"EB...D.",
|
||||
"......."
|
||||
]
|
||||
],
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"controller": [3, 0, 1],
|
||||
"destination_chest": [6, 0, 1],
|
||||
"input_tunnel_host": [1, 0, 1],
|
||||
"main_network_power": [1, 1, 1],
|
||||
"output_tunnel_host": [5, 0, 1],
|
||||
"redstone_probe": [5, 0, 0],
|
||||
"redstone_source": [1, 0, 0],
|
||||
"remote_storage": [5, 1, 1],
|
||||
"source_chest": [0, 1, 1],
|
||||
"source_inserter": [0, 0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"size": [3, 1, 2],
|
||||
"palette": {
|
||||
"A": {"name": "appliedenergistics2:tile.BlockCableBus", "meta": 0, "label": "AE2 Cable and/or Bus"},
|
||||
"B": {"name": "appliedenergistics2:tile.BlockCreativeEnergyController", "meta": 1, "label": "ME Self-Powered Controller"},
|
||||
"C": {"name": "appliedenergistics2:tile.BlockDrive", "meta": 0, "label": "ME Drive"},
|
||||
"D": {"name": "minecraft:chest", "meta": 2, "label": "Chest"}
|
||||
},
|
||||
"layers": [
|
||||
[
|
||||
"BAD",
|
||||
"C.."
|
||||
]
|
||||
],
|
||||
"annotations": {
|
||||
"labels": {
|
||||
"controller": [0, 0, 0],
|
||||
"drive": [0, 0, 1],
|
||||
"external_chest": [2, 0, 0],
|
||||
"storage_bus": [1, 0, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in new issue
Block a user