CreateMod/src/main/java/com/simibubi/create/gametest/infrastructure/CreateGameTestHelper.java
2023-05-11 15:00:32 +02:00

453 lines
15 KiB
Java

package com.simibubi.create.gametest.infrastructure;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import com.simibubi.create.foundation.mixin.accessor.GameTestHelperAccessor;
import it.unimi.dsi.fastutil.objects.Object2LongArrayMap;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LeverBlock;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
import net.minecraftforge.fluids.capability.IFluidHandler;
import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
import net.minecraftforge.items.CapabilityItemHandler;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.ItemHandlerHelper;
import org.jetbrains.annotations.Contract;
import com.simibubi.create.AllTileEntities;
import com.simibubi.create.content.logistics.block.belts.tunnel.BrassTunnelTileEntity.SelectionMode;
import com.simibubi.create.content.logistics.block.redstone.NixieTubeTileEntity;
import com.simibubi.create.foundation.item.ItemHelper;
import com.simibubi.create.foundation.tileEntity.IMultiTileContainer;
import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
import com.simibubi.create.foundation.tileEntity.behaviour.BehaviourType;
import com.simibubi.create.foundation.tileEntity.behaviour.scrollvalue.ScrollOptionBehaviour;
import com.simibubi.create.foundation.tileEntity.behaviour.scrollvalue.ScrollValueBehaviour;
import com.simibubi.create.foundation.utility.RegisteredObjects;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.gametest.framework.GameTestInfo;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
/**
* A helper class expanding the functionality of {@link GameTestHelper}.
* This class may replace the default helper parameter if a test is registered through {@link CreateTestFunction}.
*/
public class CreateGameTestHelper extends GameTestHelper {
public static final int TICKS_PER_SECOND = 20;
public static final int TEN_SECONDS = 10 * TICKS_PER_SECOND;
public static final int FIFTEEN_SECONDS = 15 * TICKS_PER_SECOND;
public static final int TWENTY_SECONDS = 20 * TICKS_PER_SECOND;
private CreateGameTestHelper(GameTestInfo testInfo) {
super(testInfo);
}
public static CreateGameTestHelper of(GameTestHelper original) {
GameTestHelperAccessor access = (GameTestHelperAccessor) original;
CreateGameTestHelper helper = new CreateGameTestHelper(access.getTestInfo());
//noinspection DataFlowIssue // accessor applied at runtime
GameTestHelperAccessor newAccess = (GameTestHelperAccessor) helper;
newAccess.setFinalCheckAdded(access.getFinalCheckAdded());
return helper;
}
// blocks
/**
* Flip the direction of any block with the {@link BlockStateProperties#FACING} property.
*/
public void flipBlock(BlockPos pos) {
BlockState original = getBlockState(pos);
if (!original.hasProperty(BlockStateProperties.FACING))
fail("FACING property not in block: " + Registry.BLOCK.getId(original.getBlock()));
Direction facing = original.getValue(BlockStateProperties.FACING);
BlockState reversed = original.setValue(BlockStateProperties.FACING, facing.getOpposite());
setBlock(pos, reversed);
}
public void assertNixiePower(BlockPos pos, int strength) {
NixieTubeTileEntity nixie = getBlockEntity(AllTileEntities.NIXIE_TUBE.get(), pos);
int actualStrength = nixie.getRedstoneStrength();
if (actualStrength != strength)
fail("Expected nixie tube at %s to have power of %s, got %s".formatted(pos, strength, actualStrength));
}
/**
* Turn off a lever.
*/
public void powerLever(BlockPos pos) {
assertBlockPresent(Blocks.LEVER, pos);
if (!getBlockState(pos).getValue(LeverBlock.POWERED)) {
pullLever(pos);
}
}
/**
* Turn on a lever.
*/
public void unpowerLever(BlockPos pos) {
assertBlockPresent(Blocks.LEVER, pos);
if (getBlockState(pos).getValue(LeverBlock.POWERED)) {
pullLever(pos);
}
}
/**
* Set the {@link SelectionMode} of a belt tunnel at the given position.
* @param pos
* @param mode
*/
public void setTunnelMode(BlockPos pos, SelectionMode mode) {
ScrollValueBehaviour behavior = getBehavior(pos, ScrollOptionBehaviour.TYPE);
behavior.setValue(mode.ordinal());
}
// block entities
/**
* Get the block entity of the expected type. If the type does not match, this fails the test.
*/
public <T extends BlockEntity> T getBlockEntity(BlockEntityType<T> type, BlockPos pos) {
BlockEntity be = getBlockEntity(pos);
BlockEntityType<?> actualType = be == null ? null : be.getType();
if (actualType != type) {
String actualId = actualType == null ? "null" : RegisteredObjects.getKeyOrThrow(actualType).toString();
String error = "Expected block entity at pos [%s] with type [%s], got [%s]".formatted(
pos, RegisteredObjects.getKeyOrThrow(type), actualId
);
fail(error);
}
return (T) be;
}
/**
* Given any segment of an {@link IMultiTileContainer}, get the controller for it.
*/
public <T extends BlockEntity & IMultiTileContainer> T getControllerBlockEntity(BlockEntityType<T> type, BlockPos anySegment) {
T be = getBlockEntity(type, anySegment).getControllerTE();
if (be == null)
fail("Could not get block entity controller with type [%s] from pos [%s]".formatted(RegisteredObjects.getKeyOrThrow(type), anySegment));
return be;
}
/**
* Get the expected {@link TileEntityBehaviour} from the given position, failing if not present.
*/
public <T extends TileEntityBehaviour> T getBehavior(BlockPos pos, BehaviourType<T> type) {
T behavior = TileEntityBehaviour.get(getLevel(), absolutePos(pos), type);
if (behavior == null)
fail("Behavior at " + pos + " missing, expected " + type.getName());
return behavior;
}
// entities
/**
* Spawn an item entity at the given position with no velocity.
*/
public ItemEntity spawnItem(BlockPos pos, ItemStack stack) {
Vec3 spawn = Vec3.atCenterOf(absolutePos(pos));
ServerLevel level = getLevel();
ItemEntity item = new ItemEntity(level, spawn.x, spawn.y, spawn.z, stack, 0, 0, 0);
level.addFreshEntity(item);
return item;
}
/**
* Spawn item entities given an item and amount. The amount will be split into multiple entities if
* larger than the item's max stack size.
*/
public void spawnItems(BlockPos pos, Item item, int amount) {
while (amount > 0) {
int toSpawn = Math.min(amount, item.getMaxStackSize());
amount -= toSpawn;
ItemStack stack = new ItemStack(item, toSpawn);
spawnItem(pos, stack);
}
}
/**
* Get the first entity found at the given position.
*/
public <T extends Entity> T getFirstEntity(EntityType<T> type, BlockPos pos) {
List<T> list = getEntitiesBetween(type, pos.north().east().above(), pos.south().west().below());
if (list.isEmpty())
fail("No entities at pos: " + pos);
return list.get(0);
}
/**
* Get a list of all entities between two positions, inclusive.
*/
public <T extends Entity> List<T> getEntitiesBetween(EntityType<T> type, BlockPos pos1, BlockPos pos2) {
BoundingBox box = BoundingBox.fromCorners(absolutePos(pos1), absolutePos(pos2));
List<? extends T> entities = getLevel().getEntities(type, e -> box.isInside(e.blockPosition()));
return (List<T>) entities;
}
// transfer - fluids
public IFluidHandler fluidStorageAt(BlockPos pos) {
BlockEntity be = getBlockEntity(pos);
if (be == null)
fail("BlockEntity not present");
Optional<IFluidHandler> handler = be.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY).resolve();
if (handler.isEmpty())
fail("handler not present");
return handler.get();
}
/**
* Get the content of the tank at the pos.
* content is determined by what the tank allows to be extracted.
*/
public FluidStack getTankContents(BlockPos tank) {
IFluidHandler handler = fluidStorageAt(tank);
return handler.drain(Integer.MAX_VALUE, FluidAction.SIMULATE);
}
/**
* Get the total capacity of a tank at the given position.
*/
public long getTankCapacity(BlockPos pos) {
IFluidHandler handler = fluidStorageAt(pos);
long total = 0;
for (int i = 0; i < handler.getTanks(); i++) {
total += handler.getTankCapacity(i);
}
return total;
}
/**
* Get the total fluid amount across all fluid tanks at the given positions.
*/
public long getFluidInTanks(BlockPos... tanks) {
long total = 0;
for (BlockPos tank : tanks) {
total += getTankContents(tank).getAmount();
}
return total;
}
/**
* Assert that the given fluid stack is present in the given tank. The tank might also hold more than the fluid.
*/
public void assertFluidPresent(FluidStack fluid, BlockPos pos) {
FluidStack contained = getTankContents(pos);
if (!fluid.isFluidEqual(contained))
fail("Different fluids");
if (fluid.getAmount() != contained.getAmount())
fail("Different amounts");
}
/**
* Assert that the given tank holds no fluid.
*/
public void assertTankEmpty(BlockPos pos) {
assertFluidPresent(FluidStack.EMPTY, pos);
}
public void assertTanksEmpty(BlockPos... tanks) {
for (BlockPos tank : tanks) {
assertTankEmpty(tank);
}
}
// transfer - items
public IItemHandler itemStorageAt(BlockPos pos) {
BlockEntity be = getBlockEntity(pos);
if (be == null)
fail("BlockEntity not present");
Optional<IItemHandler> handler = be.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).resolve();
if (handler.isEmpty())
fail("handler not present");
return handler.get();
}
/**
* Get a map of contained items to their amounts. This is not safe for NBT!
*/
public Object2LongMap<Item> getItemContent(BlockPos pos) {
IItemHandler handler = itemStorageAt(pos);
Object2LongMap<Item> map = new Object2LongArrayMap<>();
for (int i = 0; i < handler.getSlots(); i++) {
ItemStack stack = handler.getStackInSlot(i);
if (stack.isEmpty())
continue;
Item item = stack.getItem();
long amount = map.getLong(item);
amount += stack.getCount();
map.put(item, amount);
}
return map;
}
/**
* Get the combined total of all ItemStacks inside the inventory.
*/
public long getTotalItems(BlockPos pos) {
IItemHandler storage = itemStorageAt(pos);
long total = 0;
for (int i = 0; i < storage.getSlots(); i++) {
total += storage.getStackInSlot(i).getCount();
}
return total;
}
/**
* Of the provided items, assert that at least one is present in the given inventory.
*/
public void assertAnyContained(BlockPos pos, Item... items) {
IItemHandler handler = itemStorageAt(pos);
boolean noneFound = true;
for (int i = 0; i < handler.getSlots(); i++) {
for (Item item : items) {
if (handler.getStackInSlot(i).is(item)) {
noneFound = false;
break;
}
}
}
if (noneFound)
fail("No matching items " + Arrays.toString(items) + " found in handler at pos: " + pos);
}
/**
* Assert that the inventory contains all the provided content.
*/
public void assertContentPresent(Object2LongMap<Item> content, BlockPos pos) {
IItemHandler handler = itemStorageAt(pos);
Object2LongMap<Item> map = new Object2LongArrayMap<>(content);
for (int i = 0; i < handler.getSlots(); i++) {
ItemStack stack = handler.getStackInSlot(i);
if (stack.isEmpty())
continue;
Item item = stack.getItem();
long amount = map.getLong(item);
amount -= stack.getCount();
if (amount == 0)
map.removeLong(item);
else map.put(item, amount);
}
if (!map.isEmpty())
fail("Storage missing content: " + map);
}
/**
* Assert that all the given inventories hold no items.
*/
public void assertContainersEmpty(List<BlockPos> positions) {
for (BlockPos pos : positions) {
assertContainerEmpty(pos);
}
}
/**
* Assert that the given inventory holds no items.
*/
@Override
public void assertContainerEmpty(@NotNull BlockPos pos) {
IItemHandler storage = itemStorageAt(pos);
for (int i = 0; i < storage.getSlots(); i++) {
if (!storage.getStackInSlot(i).isEmpty())
fail("Storage not empty");
}
}
/** @see CreateGameTestHelper#assertContainerContains(BlockPos, ItemStack) */
public void assertContainerContains(BlockPos pos, ItemLike item) {
assertContainerContains(pos, item.asItem());
}
/** @see CreateGameTestHelper#assertContainerContains(BlockPos, ItemStack) */
@Override
public void assertContainerContains(@NotNull BlockPos pos, @NotNull Item item) {
assertContainerContains(pos, new ItemStack(item));
}
/**
* Assert that the inventory holds at least the given ItemStack. It may also hold more than the stack.
*/
public void assertContainerContains(BlockPos pos, ItemStack item) {
IItemHandler storage = itemStorageAt(pos);
ItemStack extracted = ItemHelper.extract(storage, stack -> ItemHandlerHelper.canItemStacksStack(stack, item), item.getCount(), true);
if (extracted.isEmpty())
fail("item not present: " + item);
}
// time
/**
* Fail unless the desired number seconds have passed since test start.
*/
public void assertSecondsPassed(int seconds) {
if (getTick() < (long) seconds * TICKS_PER_SECOND)
fail("Waiting for %s seconds to pass".formatted(seconds));
}
/**
* Get the total number of seconds that have passed since test start.
*/
public long secondsPassed() {
return getTick() % 20;
}
/**
* Run an action later, once enough time has passed.
*/
public void whenSecondsPassed(int seconds, Runnable run) {
runAfterDelay((long) seconds * TICKS_PER_SECOND, run);
}
// numbers
/**
* Assert that a number is <1 away from its expected value
*/
public void assertCloseEnoughTo(double value, double expected) {
assertInRange(value, expected - 1, expected + 1);
}
public void assertInRange(double value, double min, double max) {
if (value < min)
fail("Value %s below expected min of %s".formatted(value, min));
if (value > max)
fail("Value %s greater than expected max of %s".formatted(value, max));
}
// misc
@Contract("_->fail") // make IDEA happier
@Override
public void fail(@NotNull String exceptionMessage) {
super.fail(exceptionMessage);
}
}