Add multiple new Horizon-QA tests to cover AE2's logic (#1425)

This commit is contained in:
Worive authored and GitHub committed 2026-07-19 08:28:49 +02:00
1 parent 689b53ec4a
commit 52d9e9c7c0
27 files changed
+3809 -728

No files matched your search

+1 -1
View File
@@ -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));
}
}
@@ -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]
}
}
}
@@ -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]
}
}
}
@@ -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]
}
}
}
@@ -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]
}
}
}
@@ -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]
}
}
}
@@ -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]
}
}
}
@@ -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]
}
}
}