Add GameTests by TropheusJ (#4496)

This commit is contained in:
TropheusJ 2023-05-11 09:00:32 -04:00 committed by GitHub
parent f8ec8e5ded
commit beb61708a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1881 additions and 122 deletions

23
.github/workflows/gametest.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: gametest
on: [ pull_request, push, workflow_dispatch ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@v3
- name: setup Java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
cache: gradle
- name: make gradle wrapper executable
run: chmod +x ./gradlew
- name: run gametests
run: ./gradlew prepareRunGameTestServer runGameTestServer --no-daemon

View file

@ -90,6 +90,18 @@ minecraft {
}
}
}
gameTestServer {
workingDirectory project.file('run/gametest')
arg '-mixin.config=create.mixins.json'
property 'forge.logging.console.level', 'info'
mods {
create {
source sourceSets.main
}
}
setForceExit false
}
}
}

View file

@ -559,7 +559,7 @@ bf2b0310500213ff853c748c236eb5d01f61658e assets/create/blockstates/yellow_toolbo
7f39521b211441f5c3e06d60c5978cebe16cacfb assets/create/blockstates/zinc_block.json
b7181bcd8182b2f17088e5aa881f374c9c65470c assets/create/blockstates/zinc_ore.json
f85edc574ee6de0de7693ffb031266643db6724a assets/create/lang/en_ud.json
eb624aafc91b284143c3a0cc7d9bbb8de66e8950 assets/create/lang/en_us.json
5ca6b7f3f7f515134269ff45496bb2be53d7e67c assets/create/lang/en_us.json
487a511a01b2a4531fb672f917922312db78f958 assets/create/models/block/acacia_window.json
b48060cba1a382f373a05bf0039054053eccf076 assets/create/models/block/acacia_window_pane_noside.json
3066db1bf03cffa1a9c7fbacf47ae586632f4eb3 assets/create/models/block/acacia_window_pane_noside_alt.json

View file

@ -1117,6 +1117,8 @@
"create.schematicAndQuill.convert": "Save and Upload Immediately",
"create.schematicAndQuill.fallbackName": "My Schematic",
"create.schematicAndQuill.saved": "Saved as %1$s",
"create.schematicAndQuill.failed": "Failed to save schematic, check logs for details",
"create.schematicAndQuill.instant_failed": "Schematic instant-upload failed, check logs for details",
"create.schematic.invalid": "[!] Invalid Item - Use the Schematic Table instead",
"create.schematic.error": "Schematic failed to Load - Check Game Logs",

View file

@ -125,4 +125,8 @@ public class HosePulleyFluidHandler implements IFluidHandler {
return internalTank.isFluidValid(tank, stack);
}
public SmartFluidTank getInternalTank() {
return internalTank;
}
}

View file

@ -43,7 +43,8 @@ public class SequencedAssemblyRecipe implements Recipe<RecipeWrapper> {
protected List<SequencedRecipe<?>> sequence;
protected int loops;
protected ProcessingOutput transitionalItem;
protected List<ProcessingOutput> resultPool;
public final List<ProcessingOutput> resultPool;
public SequencedAssemblyRecipe(ResourceLocation recipeId, SequencedAssemblyRecipeSerializer serializer) {
this.id = recipeId;
@ -213,7 +214,7 @@ public class SequencedAssemblyRecipe implements Recipe<RecipeWrapper> {
public boolean isSpecial() {
return true;
}
@Override
public RecipeType<?> getType() {
return AllRecipeTypes.SEQUENCED_ASSEMBLY.getType();

View file

@ -7,6 +7,7 @@ import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.common.capabilities.Capability;
@ -33,4 +34,8 @@ public class DepotTileEntity extends SmartTileEntity {
return depotBehaviour.getItemCapability(cap, side);
return super.getCapability(cap, side);
}
public ItemStack getHeldItem() {
return depotBehaviour.getHeldItemStack();
}
}

View file

@ -123,6 +123,10 @@ public class NixieTubeTileEntity extends SmartTileEntity {
customText = Optional.empty();
}
public int getRedstoneStrength() {
return redstoneStrength;
}
//
@Override

View file

@ -89,11 +89,11 @@ public class FlapDisplaySection {
int max = Math.max(4, (int) (cyclingOptions.length * 1.75f));
if (spinningTicks > max)
return 0;
spinningTicks++;
if (spinningTicks <= max && spinningTicks < 2)
return spinningTicks == 1 ? 0 : spinning.length;
int spinningFlaps = 0;
for (int i = 0; i < spinning.length; i++) {
int increasingChance = Mth.clamp(8 - spinningTicks, 1, 10);
@ -107,11 +107,11 @@ public class FlapDisplaySection {
spinning[i + 1] &= continueSpin;
if (spinningTicks > max)
spinning[i] = false;
if (spinning[i])
spinningFlaps++;
}
return spinningFlaps;
}
@ -169,10 +169,14 @@ public class FlapDisplaySection {
return !singleFlap;
}
public Component getText() {
return component;
}
public static String[] getFlapCycle(String key) {
return LOADED_FLAP_CYCLES.computeIfAbsent(key, k -> Lang.translateDirect("flap_display.cycles." + key)
.getString()
.split(";"));
}
}
}

View file

@ -0,0 +1,75 @@
package com.simibubi.create.content.schematics;
import com.simibubi.create.Create;
import com.simibubi.create.content.schematics.item.SchematicAndQuillItem;
import com.simibubi.create.foundation.utility.FilesHelper;
import com.simibubi.create.foundation.utility.Lang;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLPaths;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class SchematicExport {
public static final Path SCHEMATICS = FMLPaths.GAMEDIR.get().resolve("schematics");
/**
* Save a schematic to a file from a world.
* @param dir the directory the schematic will be created in
* @param fileName the ideal name of the schematic, may not be the name of the created file
* @param overwrite whether overwriting an existing schematic is allowed
* @param level the level where the schematic structure is placed
* @param first the first corner of the schematic area
* @param second the second corner of the schematic area
* @return a SchematicExportResult, or null if an error occurred.
*/
@Nullable
public static SchematicExportResult saveSchematic(Path dir, String fileName, boolean overwrite, Level level, BlockPos first, BlockPos second) {
BoundingBox bb = BoundingBox.fromCorners(first, second);
BlockPos origin = new BlockPos(bb.minX(), bb.minY(), bb.minZ());
BlockPos bounds = new BlockPos(bb.getXSpan(), bb.getYSpan(), bb.getZSpan());
StructureTemplate structure = new StructureTemplate();
structure.fillFromWorld(level, origin, bounds, true, Blocks.AIR);
CompoundTag data = structure.save(new CompoundTag());
SchematicAndQuillItem.replaceStructureVoidWithAir(data);
SchematicAndQuillItem.clampGlueBoxes(level, new AABB(origin, origin.offset(bounds)), data);
if (fileName.isEmpty())
fileName = Lang.translateDirect("schematicAndQuill.fallbackName").getString();
if (!overwrite)
fileName = FilesHelper.findFirstValidFilename(fileName, dir, "nbt");
if (!fileName.endsWith(".nbt"))
fileName += ".nbt";
Path file = dir.resolve(fileName).toAbsolutePath();
try {
Files.createDirectories(dir);
boolean overwritten = Files.deleteIfExists(file);
try (OutputStream out = Files.newOutputStream(file, StandardOpenOption.CREATE)) {
NbtIo.writeCompressed(data, out);
}
return new SchematicExportResult(file, dir, fileName, overwritten, origin, bounds);
} catch (IOException e) {
Create.LOGGER.error("An error occurred while saving schematic [" + fileName + "]", e);
return null;
}
}
public record SchematicExportResult(Path file, Path dir, String fileName, boolean overwritten, BlockPos origin, BlockPos bounds) {
}
}

View file

@ -8,6 +8,7 @@ import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@ -16,8 +17,8 @@ import java.util.stream.Stream;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.AllItems;
import com.simibubi.create.Create;
import com.simibubi.create.content.schematics.SchematicExport.SchematicExportResult;
import com.simibubi.create.content.schematics.block.SchematicTableTileEntity;
import com.simibubi.create.content.schematics.item.SchematicAndQuillItem;
import com.simibubi.create.content.schematics.item.SchematicItem;
import com.simibubi.create.foundation.config.AllConfigs;
import com.simibubi.create.foundation.config.CSchematics;
@ -25,18 +26,14 @@ import com.simibubi.create.foundation.utility.Components;
import com.simibubi.create.foundation.utility.FilesHelper;
import com.simibubi.create.foundation.utility.Lang;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.phys.AABB;
public class ServerSchematicLoader {
@ -164,7 +161,7 @@ public class ServerSchematicLoader {
protected boolean validateSchematicSizeOnServer(ServerPlayer player, long size) {
Integer maxFileSize = getConfig().maxTotalSchematicSize.get();
if (size > maxFileSize * 1000) {
player.sendMessage(Lang.translateDirect("schematics.uploadTooLarge")
.append(Components.literal(" (" + size / 1000 + " KB).")), Util.NIL_UUID);
player.sendMessage(Lang.translateDirect("schematics.maxAllowedSize")
@ -284,10 +281,9 @@ public class ServerSchematicLoader {
public void handleInstantSchematic(ServerPlayer player, String schematic, Level world, BlockPos pos,
BlockPos bounds) {
String playerPath = getSchematicPath() + "/" + player.getGameProfile()
.getName();
String playerSchematicId = player.getGameProfile()
.getName() + "/" + schematic;
String playerName = player.getGameProfile().getName();
String playerPath = getSchematicPath() + "/" + playerName;
String playerSchematicId = playerName + "/" + schematic;
FilesHelper.createFolderIfMissing(playerPath);
// Unsupported Format
@ -310,43 +306,43 @@ public class ServerSchematicLoader {
if (!AllItems.SCHEMATIC_AND_QUILL.isIn(player.getMainHandItem()))
return;
// if there's too many schematics, delete oldest
Path playerSchematics = Paths.get(playerPath);
if (!tryDeleteOldestSchematic(playerSchematics))
return;
SchematicExportResult result = SchematicExport.saveSchematic(
playerSchematics, schematic, true,
world, pos, pos.offset(bounds).offset(-1, -1, -1)
);
if (result != null)
player.setItemInHand(InteractionHand.MAIN_HAND, SchematicItem.create(schematic, playerName));
else Lang.translate("schematicAndQuill.instant_failed")
.style(ChatFormatting.RED)
.sendStatus(player);
}
private boolean tryDeleteOldestSchematic(Path dir) {
try (Stream<Path> stream = Files.list(dir)) {
List<Path> files = stream.toList();
if (files.size() < getConfig().maxSchematics.get())
return true;
Optional<Path> oldest = files.stream().min(Comparator.comparingLong(this::getLastModifiedTime));
Files.delete(oldest.orElseThrow());
return true;
} catch (IOException | IllegalStateException e) {
Create.LOGGER.error("Error deleting oldest schematic", e);
return false;
}
}
private long getLastModifiedTime(Path file) {
try {
// Delete schematic with same name
Files.deleteIfExists(path);
// Too many Schematics
long count;
try (Stream<Path> list = Files.list(Paths.get(playerPath))) {
count = list.count();
}
if (count >= getConfig().maxSchematics.get()) {
Stream<Path> list2 = Files.list(Paths.get(playerPath));
Optional<Path> lastFilePath = list2.filter(f -> !Files.isDirectory(f))
.min(Comparator.comparingLong(f -> f.toFile()
.lastModified()));
list2.close();
if (lastFilePath.isPresent())
Files.deleteIfExists(lastFilePath.get());
}
StructureTemplate t = new StructureTemplate();
t.fillFromWorld(world, pos, bounds, true, Blocks.AIR);
try (OutputStream outputStream = Files.newOutputStream(path)) {
CompoundTag nbttagcompound = t.save(new CompoundTag());
SchematicAndQuillItem.replaceStructureVoidWithAir(nbttagcompound);
SchematicAndQuillItem.clampGlueBoxes(world, new AABB(pos, pos.offset(bounds)), nbttagcompound);
NbtIo.writeCompressed(nbttagcompound, outputStream);
player.setItemInHand(InteractionHand.MAIN_HAND, SchematicItem.create(schematic, player.getGameProfile()
.getName()));
} catch (IOException e) {
e.printStackTrace();
}
return Files.getLastModifiedTime(file).toMillis();
} catch (IOException e) {
Create.LOGGER.error("Exception Thrown in direct Schematic Upload: " + playerSchematicId);
e.printStackTrace();
Create.LOGGER.error("Error getting modification time of file " + file.getFileName(), e);
throw new IllegalStateException(e);
}
}

View file

@ -1,13 +1,14 @@
package com.simibubi.create.content.schematics.client;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.apache.commons.io.IOUtils;
import com.simibubi.create.content.schematics.SchematicExport;
import com.simibubi.create.content.schematics.SchematicExport.SchematicExportResult;
import net.minecraft.ChatFormatting;
import com.simibubi.create.AllItems;
import com.simibubi.create.AllKeys;
@ -15,12 +16,10 @@ import com.simibubi.create.AllSpecialTextures;
import com.simibubi.create.Create;
import com.simibubi.create.CreateClient;
import com.simibubi.create.content.schematics.ClientSchematicLoader;
import com.simibubi.create.content.schematics.item.SchematicAndQuillItem;
import com.simibubi.create.content.schematics.packet.InstantSchematicPacket;
import com.simibubi.create.foundation.gui.ScreenOpener;
import com.simibubi.create.foundation.networking.AllPackets;
import com.simibubi.create.foundation.utility.AnimationTickHolder;
import com.simibubi.create.foundation.utility.FilesHelper;
import com.simibubi.create.foundation.utility.Lang;
import com.simibubi.create.foundation.utility.RaycastHelper;
import com.simibubi.create.foundation.utility.RaycastHelper.PredicateTraceResult;
@ -33,16 +32,10 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Direction.AxisDirection;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.util.Mth;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult.Type;
@ -52,8 +45,8 @@ public class SchematicAndQuillHandler {
private Object outlineSlot = new Object();
private BlockPos firstPos;
private BlockPos secondPos;
public BlockPos firstPos;
public BlockPos secondPos;
private BlockPos selectedPos;
private Direction selectedFace;
private int range = 10;
@ -212,58 +205,30 @@ public class SchematicAndQuillHandler {
}
public void saveSchematic(String string, boolean convertImmediately) {
StructureTemplate t = new StructureTemplate();
BoundingBox bb = BoundingBox.fromCorners(firstPos, secondPos);
BlockPos origin = new BlockPos(bb.minX(), bb.minY(), bb.minZ());
BlockPos bounds = new BlockPos(bb.getXSpan(), bb.getYSpan(), bb.getZSpan());
Level level = Minecraft.getInstance().level;
t.fillFromWorld(level, origin, bounds, true, Blocks.AIR);
if (string.isEmpty())
string = Lang.translateDirect("schematicAndQuill.fallbackName")
.getString();
String folderPath = "schematics";
FilesHelper.createFolderIfMissing(folderPath);
String filename = FilesHelper.findFirstValidFilename(string, folderPath, "nbt");
String filepath = folderPath + "/" + filename;
Path path = Paths.get(filepath);
OutputStream outputStream = null;
try {
outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE);
CompoundTag nbttagcompound = t.save(new CompoundTag());
SchematicAndQuillItem.replaceStructureVoidWithAir(nbttagcompound);
SchematicAndQuillItem.clampGlueBoxes(level, new AABB(origin, origin.offset(bounds)), nbttagcompound);
NbtIo.writeCompressed(nbttagcompound, outputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null)
IOUtils.closeQuietly(outputStream);
SchematicExportResult result = SchematicExport.saveSchematic(
SchematicExport.SCHEMATICS, string, false,
Minecraft.getInstance().level, firstPos, secondPos
);
LocalPlayer player = Minecraft.getInstance().player;
if (result == null) {
Lang.translate("schematicAndQuill.failed")
.style(ChatFormatting.RED)
.sendStatus(player);
return;
}
Path file = result.file();
Lang.translate("schematicAndQuill.saved", file)
.sendStatus(player);
firstPos = null;
secondPos = null;
LocalPlayer player = Minecraft.getInstance().player;
Lang.translate("schematicAndQuill.saved", filepath)
.sendStatus(player);
if (!convertImmediately)
return;
if (!Files.exists(path)) {
Create.LOGGER.error("Missing Schematic file: " + path.toString());
return;
}
try {
if (!ClientSchematicLoader.validateSizeLimitation(Files.size(path)))
if (!ClientSchematicLoader.validateSizeLimitation(Files.size(file)))
return;
AllPackets.channel.sendToServer(new InstantSchematicPacket(filename, origin, bounds));
AllPackets.channel.sendToServer(new InstantSchematicPacket(result.fileName(), result.origin(), result.bounds()));
} catch (IOException e) {
Create.LOGGER.error("Error finding Schematic file: " + path.toString());
e.printStackTrace();
return;
Create.LOGGER.error("Error instantly uploading Schematic file: " + file, e);
}
}
@ -271,4 +236,4 @@ public class SchematicAndQuillHandler {
return CreateClient.OUTLINER;
}
}
}

View file

@ -109,5 +109,4 @@ public class SchematicPromptScreen extends AbstractSimiScreen {
CreateClient.SCHEMATIC_AND_QUILL_HANDLER.saveSchematic(nameField.getValue(), convertImmediately);
onClose();
}
}

View file

@ -11,6 +11,8 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.loading.FMLLoader;
public class AllCommands {
@ -20,7 +22,7 @@ public class AllCommands {
LiteralCommandNode<CommandSourceStack> util = buildUtilityCommands();
LiteralCommandNode<CommandSourceStack> createRoot = dispatcher.register(Commands.literal("create")
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("create")
.requires(cs -> cs.hasPermission(0))
// general purpose
.then(new ToggleDebugCommand().register())
@ -38,8 +40,12 @@ public class AllCommands {
.then(GlueCommand.register())
// utility
.then(util)
);
.then(util);
if (!FMLLoader.isProduction() && FMLLoader.getDist() == Dist.CLIENT)
root.then(CreateTestCommand.register());
LiteralCommandNode<CommandSourceStack> createRoot = dispatcher.register(root);
createRoot.addChild(buildRedirect("u", util));

View file

@ -0,0 +1,111 @@
package com.simibubi.create.foundation.command;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.simibubi.create.CreateClient;
import com.simibubi.create.content.schematics.SchematicExport;
import com.simibubi.create.content.schematics.SchematicExport.SchematicExportResult;
import com.simibubi.create.content.schematics.client.SchematicAndQuillHandler;
import com.simibubi.create.foundation.utility.Components;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.level.ServerLevel;
import net.minecraftforge.fml.loading.FMLPaths;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
/**
* This command allows for quick exporting of GameTests.
* It is only registered in a client development environment. It is not safe in production or multiplayer.
*/
public class CreateTestCommand {
private static final Path gametests = FMLPaths.GAMEDIR.get()
.getParent()
.resolve("src/main/resources/data/create/structures/gametest")
.toAbsolutePath();
public static ArgumentBuilder<CommandSourceStack, ?> register() {
return literal("test")
.then(literal("export")
.then(argument("path", StringArgumentType.greedyString())
.suggests(CreateTestCommand::getSuggestions)
.executes(ctx -> handleExport(
ctx.getSource(),
ctx.getSource().getLevel(),
StringArgumentType.getString(ctx, "path")
))
)
);
}
private static int handleExport(CommandSourceStack source, ServerLevel level, String path) {
SchematicAndQuillHandler handler = CreateClient.SCHEMATIC_AND_QUILL_HANDLER;
if (handler.firstPos == null || handler.secondPos == null) {
source.sendFailure(Components.literal("You must select an area with the Schematic and Quill first."));
return 0;
}
SchematicExportResult result = SchematicExport.saveSchematic(
gametests, path, true,
level, handler.firstPos, handler.secondPos
);
if (result == null)
source.sendFailure(Components.literal("Failed to export, check logs").withStyle(ChatFormatting.RED));
else {
sendSuccess(source, "Successfully exported test!", ChatFormatting.GREEN);
sendSuccess(source, "Overwritten: " + result.overwritten(), ChatFormatting.AQUA);
sendSuccess(source, "File: " + result.file(), ChatFormatting.GRAY);
}
return 0;
}
private static void sendSuccess(CommandSourceStack source, String text, ChatFormatting color) {
source.sendSuccess(Components.literal(text).withStyle(color), true);
}
// find existing tests and folders for autofill
private static CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSourceStack> context,
SuggestionsBuilder builder) throws CommandSyntaxException {
String path = builder.getRemaining();
if (!path.contains("/") || path.contains(".."))
return findInDir(gametests, builder);
int lastSlash = path.lastIndexOf("/");
Path subDir = gametests.resolve(path.substring(0, lastSlash));
if (Files.exists(subDir))
findInDir(subDir, builder);
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> findInDir(Path dir, SuggestionsBuilder builder) {
try (Stream<Path> paths = Files.list(dir)) {
paths.filter(p -> Files.isDirectory(p) || p.toString().endsWith(".nbt"))
.forEach(path -> {
String file = path.toString()
.replaceAll("\\\\", "/")
.substring(gametests.toString().length() + 1);
if (Files.isDirectory(path))
file += "/";
builder.suggest(file);
});
} catch (IOException e) {
throw new RuntimeException(e);
}
return builder.buildFuture();
}
}

View file

@ -0,0 +1,53 @@
package com.simibubi.create.foundation.mixin;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestRegistry;
import net.minecraft.gametest.framework.GameTestRunner;
import net.minecraft.gametest.framework.GameTestServer;
import net.minecraft.server.Main;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import java.util.Collection;
@Mixin(Main.class)
public class MainMixin {
/**
* Forge completely bypasses vanilla's
* {@link GameTestServer#create(Thread, LevelStorageAccess, PackRepository, Collection, BlockPos)},
* which causes tests to generate at bedrock level in a regular world. This causes interference
* (ex. darkness, liquids, gravel) that makes tests fail and act inconsistently. Replacing the server Forge
* makes with one made by vanilla's factory causes tests to run on a superflat, as they should.
* <p>
* The system property 'create.useOriginalGametestServer' may be set to true to avoid this behavior.
* This may be desirable for other mods which pull Create into their development environments.
*/
@ModifyVariable(
method = "lambda$main$5",
at = @At(
value = "STORE",
ordinal = 0
),
require = 0 // don't crash if this fails
)
private static MinecraftServer create$correctlyInitializeGametestServer(MinecraftServer original) {
if (original instanceof GameTestServer && !Boolean.getBoolean("create.useOriginalGametestServer")) {
return GameTestServer.create(
original.getRunningThread(),
original.storageSource,
original.getPackRepository(),
GameTestRunner.groupTestsIntoBatches(GameTestRegistry.getAllTestFunctions()),
BlockPos.ZERO
);
}
return original;
}
}

View file

@ -0,0 +1,46 @@
package com.simibubi.create.foundation.mixin;
import com.simibubi.create.gametest.infrastructure.CreateTestFunction;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestRegistry;
import net.minecraft.gametest.framework.MultipleTestTracker;
import net.minecraft.gametest.framework.TestCommand;
import net.minecraft.gametest.framework.TestFunction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.StructureBlockEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import javax.annotation.Nullable;
@Mixin(TestCommand.class)
public class TestCommandMixin {
@Redirect(
method = "runTest(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/gametest/framework/MultipleTestTracker;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/gametest/framework/GameTestRegistry;getTestFunction(Ljava/lang/String;)Lnet/minecraft/gametest/framework/TestFunction;"
),
require = 0 // don't crash if this fails. non-critical
)
private static TestFunction create$getCorrectTestFunction(String testName,
ServerLevel level, BlockPos pos, @Nullable MultipleTestTracker tracker) {
StructureBlockEntity be = (StructureBlockEntity) level.getBlockEntity(pos);
CompoundTag data = be.getTileData();
if (!data.contains("CreateTestFunction", Tag.TAG_STRING))
return GameTestRegistry.getTestFunction(testName);
String name = data.getString("CreateTestFunction");
CreateTestFunction function = CreateTestFunction.NAMES_TO_FUNCTIONS.get(name);
if (function == null)
throw new IllegalStateException("Structure block has CreateTestFunction attached, but test [" + name + "] doesn't exist");
return function;
}
}

View file

@ -0,0 +1,17 @@
package com.simibubi.create.foundation.mixin.accessor;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.gametest.framework.GameTestInfo;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(GameTestHelper.class)
public interface GameTestHelperAccessor {
@Accessor
GameTestInfo getTestInfo();
@Accessor
boolean getFinalCheckAdded();
@Accessor
void setFinalCheckAdded(boolean value);
}

View file

@ -5,6 +5,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
@ -27,15 +28,15 @@ public class FilesHelper {
}
}
public static String findFirstValidFilename(String name, String folderPath, String extension) {
public static String findFirstValidFilename(String name, Path folderPath, String extension) {
int index = 0;
String filename;
String filepath;
Path filepath;
do {
filename = slug(name) + ((index == 0) ? "" : "_" + index) + "." + extension;
index++;
filepath = folderPath + "/" + filename;
} while (Files.exists(Paths.get(filepath)));
filepath = folderPath.resolve(filename);
} while (Files.exists(filepath));
return filename;
}

View file

@ -0,0 +1,39 @@
package com.simibubi.create.gametest;
import java.util.Collection;
import com.simibubi.create.gametest.infrastructure.CreateTestFunction;
import com.simibubi.create.gametest.tests.TestContraptions;
import com.simibubi.create.gametest.tests.TestFluids;
import com.simibubi.create.gametest.tests.TestItems;
import com.simibubi.create.gametest.tests.TestMisc;
import com.simibubi.create.gametest.tests.TestProcessing;
import net.minecraft.gametest.framework.GameTestGenerator;
import net.minecraft.gametest.framework.TestFunction;
import net.minecraftforge.event.RegisterGameTestsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod.EventBusSubscriber;
import net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus;
@EventBusSubscriber(bus = Bus.MOD)
public class CreateGameTests {
private static final Class<?>[] testHolders = {
TestContraptions.class,
TestFluids.class,
TestItems.class,
TestMisc.class,
TestProcessing.class
};
@SubscribeEvent
public static void registerTests(RegisterGameTestsEvent event) {
event.register(CreateGameTests.class);
}
@GameTestGenerator
public static Collection<TestFunction> generateTests() {
return CreateTestFunction.getTestsFrom(testHolders);
}
}

View file

@ -0,0 +1,15 @@
# Adding to GameTests
#### Adding Tests
All tests must be static, take a `CreateGameTestHelper`, return void, and be annotated with `@GameTest`.
Non-annotated methods will be ignored. The annotation must also specify a structure template.
Classes holding registered tests must be annotated with `GameTestGroup`.
#### Adding Groups/Classes
Added test classes must be added to the list in `CreateGameTests`. They must be annotated with
`@GameTestGroup` and given a structure path.
#### Exporting Structures
Structures can be quickly exported using the `/create test export` command (or `/c test export`).
Select an area with the Schematic and Quill, and run it to quickly export a test structure
directly to the correct directory.

View file

@ -0,0 +1,452 @@
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);
}
}

View file

@ -0,0 +1,122 @@
package com.simibubi.create.gametest.infrastructure;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestGenerator;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.gametest.framework.StructureUtils;
import net.minecraft.gametest.framework.TestFunction;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.entity.StructureBlockEntity;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Stream;
/**
* An extension to game tests implementing functionality for {@link CreateGameTestHelper} and {@link GameTestGroup}.
* To use, create a {@link GameTestGenerator} that provides tests using {@link #getTestsFrom(Class[])}.
*/
public class CreateTestFunction extends TestFunction {
// for structure blocks and /test runthis
public static final Map<String, CreateTestFunction> NAMES_TO_FUNCTIONS = new HashMap<>();
public final String fullName;
public final String simpleName;
protected CreateTestFunction(String fullName, String simpleName, String pBatchName, String pTestName,
String pStructureName, Rotation pRotation, int pMaxTicks, long pSetupTicks,
boolean pRequired, int pRequiredSuccesses, int pMaxAttempts, Consumer<GameTestHelper> pFunction) {
super(pBatchName, pTestName, pStructureName, pRotation, pMaxTicks, pSetupTicks, pRequired, pRequiredSuccesses, pMaxAttempts, pFunction);
this.fullName = fullName;
this.simpleName = simpleName;
NAMES_TO_FUNCTIONS.put(fullName, this);
}
@Override
public String getTestName() {
return simpleName;
}
/**
* Get all Create test functions from the given classes. This enables functionality
* of {@link CreateGameTestHelper} and {@link GameTestGroup}.
*/
public static Collection<TestFunction> getTestsFrom(Class<?>... classes) {
return Stream.of(classes)
.map(Class::getDeclaredMethods)
.flatMap(Stream::of)
.map(CreateTestFunction::of)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(TestFunction::getTestName))
.toList();
}
@Nullable
public static TestFunction of(Method method) {
GameTest gt = method.getAnnotation(GameTest.class);
if (gt == null) // skip non-test methods
return null;
Class<?> owner = method.getDeclaringClass();
GameTestGroup group = owner.getAnnotation(GameTestGroup.class);
String simpleName = owner.getSimpleName() + '.' + method.getName();
validateTestMethod(method, gt, owner, group, simpleName);
String structure = "%s:gametest/%s/%s".formatted(group.namespace(), group.path(), gt.template());
Rotation rotation = StructureUtils.getRotationForRotationSteps(gt.rotationSteps());
String fullName = owner.getName() + "." + method.getName();
return new CreateTestFunction(
// use structure for test name since that's what MC fills structure blocks with for some reason
fullName, simpleName, gt.batch(), structure, structure, rotation, gt.timeoutTicks(), gt.setupTicks(),
gt.required(), gt.requiredSuccesses(), gt.attempts(), asConsumer(method)
);
}
private static void validateTestMethod(Method method, GameTest gt, Class<?> owner, GameTestGroup group, String simpleName) {
if (gt.template().isEmpty())
throw new IllegalArgumentException(simpleName + " must provide a template structure");
if (!Modifier.isStatic(method.getModifiers()))
throw new IllegalArgumentException(simpleName + " must be static");
if (method.getReturnType() != void.class)
throw new IllegalArgumentException(simpleName + " must return void");
if (method.getParameterCount() != 1 || method.getParameterTypes()[0] != CreateGameTestHelper.class)
throw new IllegalArgumentException(simpleName + " must take 1 parameter of type CreateGameTestHelper");
if (group == null)
throw new IllegalArgumentException(owner.getName() + " must be annotated with @GameTestGroup");
}
private static Consumer<GameTestHelper> asConsumer(Method method) {
return (helper) -> {
try {
method.invoke(null, helper);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
};
}
@Override
public void run(@NotNull GameTestHelper helper) {
// give structure block test info
StructureBlockEntity be = (StructureBlockEntity) helper.getBlockEntity(BlockPos.ZERO);
be.getTileData().putString("CreateTestFunction", fullName);
super.run(CreateGameTestHelper.of(helper));
}
}

View file

@ -0,0 +1,25 @@
package com.simibubi.create.gametest.infrastructure;
import com.simibubi.create.Create;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Allows for test method declarations to be concise by moving subdirectories and namespaces to the class level.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface GameTestGroup {
/**
* The subdirectory to search for test structures in.
*/
String path();
/**
* The namespace to search for test structures in.
*/
String namespace() default Create.ID;
}

View file

@ -0,0 +1,98 @@
package com.simibubi.create.gametest.tests;
import java.util.List;
import com.simibubi.create.gametest.infrastructure.CreateGameTestHelper;
import com.simibubi.create.gametest.infrastructure.GameTestGroup;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.projectile.Arrow;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.fluids.FluidStack;
@GameTestGroup(path = "contraptions")
public class TestContraptions {
@GameTest(template = "arrow_dispenser", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void arrowDispenser(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(2, 3, 1);
helper.pullLever(lever);
BlockPos pos1 = new BlockPos(0, 5, 0);
BlockPos pos2 = new BlockPos(4, 5, 4);
helper.succeedWhen(() -> {
helper.assertSecondsPassed(7);
List<Arrow> arrows = helper.getEntitiesBetween(EntityType.ARROW, pos1, pos2);
if (arrows.size() != 4)
helper.fail("Expected 4 arrows");
helper.powerLever(lever); // disassemble contraption
BlockPos dispenser = new BlockPos(2, 5, 2);
// there should be 1 left over
helper.assertContainerContains(dispenser, Items.ARROW);
});
}
@GameTest(template = "crop_farming", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void cropFarming(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(4, 3, 1);
helper.pullLever(lever);
BlockPos output = new BlockPos(1, 3, 12);
helper.succeedWhen(() -> helper.assertAnyContained(output, Items.WHEAT, Items.POTATO, Items.CARROT));
}
@GameTest(template = "mounted_item_extract", timeoutTicks = CreateGameTestHelper.TWENTY_SECONDS)
public static void mountedItemExtract(CreateGameTestHelper helper) {
BlockPos barrel = new BlockPos(1, 3, 2);
Object2LongMap<Item> content = helper.getItemContent(barrel);
BlockPos lever = new BlockPos(1, 5, 1);
helper.pullLever(lever);
BlockPos outputPos = new BlockPos(4, 2, 1);
helper.succeedWhen(() -> {
helper.assertContentPresent(content, outputPos); // verify all extracted
helper.powerLever(lever);
helper.assertContainerEmpty(barrel); // verify nothing left
});
}
@GameTest(template = "mounted_fluid_drain", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void mountedFluidDrain(CreateGameTestHelper helper) {
BlockPos tank = new BlockPos(1, 3, 2);
FluidStack fluid = helper.getTankContents(tank);
if (fluid.isEmpty())
helper.fail("Tank empty");
BlockPos lever = new BlockPos(1, 5, 1);
helper.pullLever(lever);
BlockPos output = new BlockPos(4, 2, 1);
helper.succeedWhen(() -> {
helper.assertFluidPresent(fluid, output); // verify all extracted
helper.powerLever(lever); // disassemble contraption
helper.assertTankEmpty(tank); // verify nothing left
});
}
@GameTest(template = "ploughing")
public static void ploughing(CreateGameTestHelper helper) {
BlockPos dirt = new BlockPos(4, 2, 1);
BlockPos lever = new BlockPos(3, 3, 2);
helper.pullLever(lever);
helper.succeedWhen(() -> helper.assertBlockPresent(Blocks.FARMLAND, dirt));
}
@GameTest(template = "redstone_contacts")
public static void redstoneContacts(CreateGameTestHelper helper) {
BlockPos end = new BlockPos(5, 10, 1);
BlockPos lever = new BlockPos(1, 3, 2);
helper.pullLever(lever);
helper.succeedWhen(() -> helper.assertBlockPresent(Blocks.DIAMOND_BLOCK, end));
}
// FIXME: trains do not enjoy being loaded in structures
// https://gist.github.com/TropheusJ/f2d0a7df48360d2e078d0987c115c6ef
// @GameTest(template = "train_observer")
// public static void trainObserver(CreateGameTestHelper helper) {
// helper.fail("NYI");
// }
}

View file

@ -0,0 +1,152 @@
package com.simibubi.create.gametest.tests;
import com.simibubi.create.AllTileEntities;
import com.simibubi.create.content.contraptions.fluids.actors.HosePulleyFluidHandler;
import com.simibubi.create.content.contraptions.relays.gauge.SpeedGaugeTileEntity;
import com.simibubi.create.content.contraptions.relays.gauge.StressGaugeTileEntity;
import com.simibubi.create.gametest.infrastructure.CreateGameTestHelper;
import com.simibubi.create.gametest.infrastructure.GameTestGroup;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.util.Mth;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RedStoneWireBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.RedstoneSide;
import net.minecraft.world.level.material.Fluids;
import net.minecraftforge.fluids.FluidAttributes;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.capability.IFluidHandler;
import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
@GameTestGroup(path = "fluids")
public class TestFluids {
@GameTest(template = "hose_pulley_transfer", timeoutTicks = CreateGameTestHelper.TWENTY_SECONDS)
public static void hosePulleyTransfer(CreateGameTestHelper helper) {
// there was supposed to be redstone here built in, but it kept popping off, so put it there manually
BlockPos brokenRedstone = new BlockPos(4, 8, 3);
BlockState redstone = Blocks.REDSTONE_WIRE.defaultBlockState()
.setValue(RedStoneWireBlock.NORTH, RedstoneSide.NONE)
.setValue(RedStoneWireBlock.SOUTH, RedstoneSide.NONE)
.setValue(RedStoneWireBlock.EAST, RedstoneSide.UP)
.setValue(RedStoneWireBlock.WEST, RedstoneSide.SIDE)
.setValue(RedStoneWireBlock.POWER, 14);
helper.setBlock(brokenRedstone, redstone);
// pump
BlockPos lever = new BlockPos(6, 9, 3);
helper.pullLever(lever);
helper.succeedWhen(() -> {
helper.assertSecondsPassed(15);
// check filled
BlockPos filledLowerCorner = new BlockPos(8, 3, 2);
BlockPos filledUpperCorner = new BlockPos(10, 5, 4);
BlockPos.betweenClosed(filledLowerCorner, filledUpperCorner)
.forEach(pos -> helper.assertBlockPresent(Blocks.WATER, pos));
// check emptied
BlockPos emptiedLowerCorner = new BlockPos(2, 3, 2);
BlockPos emptiedUpperCorner = new BlockPos(4, 5, 4);
BlockPos.betweenClosed(emptiedLowerCorner, emptiedUpperCorner)
.forEach(pos -> helper.assertBlockPresent(Blocks.AIR, pos));
// check nothing left in pulley
BlockPos pulleyPos = new BlockPos(8, 7, 4);
IFluidHandler storage = helper.fluidStorageAt(pulleyPos);
if (storage instanceof HosePulleyFluidHandler hose) {
IFluidHandler internalTank = hose.getInternalTank();
if (!internalTank.drain(1, FluidAction.SIMULATE).isEmpty())
helper.fail("Pulley not empty");
} else {
helper.fail("Not a pulley");
}
});
}
@GameTest(template = "in_world_pumping_out")
public static void inWorldPumpingOutput(CreateGameTestHelper helper) {
BlockPos pumpPos = new BlockPos(3, 2, 2);
BlockPos waterPos = pumpPos.west();
BlockPos basinPos = pumpPos.east();
helper.flipBlock(pumpPos);
helper.succeedWhen(() -> {
helper.assertBlockPresent(Blocks.WATER, waterPos);
helper.assertTankEmpty(basinPos);
});
}
@GameTest(template = "in_world_pumping_in")
public static void inWorldPumpingPickup(CreateGameTestHelper helper) {
BlockPos pumpPos = new BlockPos(3, 2, 2);
BlockPos basinPos = pumpPos.east();
BlockPos waterPos = pumpPos.west();
FluidStack expectedResult = new FluidStack(Fluids.WATER, FluidAttributes.BUCKET_VOLUME);
helper.flipBlock(pumpPos);
helper.succeedWhen(() -> {
helper.assertBlockPresent(Blocks.AIR, waterPos);
helper.assertFluidPresent(expectedResult, basinPos);
});
}
@GameTest(template = "steam_engine")
public static void steamEngine(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(4, 3, 3);
helper.pullLever(lever);
BlockPos stressometer = new BlockPos(5, 2, 5);
BlockPos speedometer = new BlockPos(4, 2, 5);
helper.succeedWhen(() -> {
StressGaugeTileEntity stress = helper.getBlockEntity(AllTileEntities.STRESSOMETER.get(), stressometer);
SpeedGaugeTileEntity speed = helper.getBlockEntity(AllTileEntities.SPEEDOMETER.get(), speedometer);
float capacity = stress.getNetworkCapacity();
helper.assertCloseEnoughTo(capacity, 2048);
float rotationSpeed = Mth.abs(speed.getSpeed());
helper.assertCloseEnoughTo(rotationSpeed, 16);
});
}
@GameTest(template = "3_pipe_combine", timeoutTicks = CreateGameTestHelper.TWENTY_SECONDS)
public static void threePipeCombine(CreateGameTestHelper helper) {
BlockPos tank1Pos = new BlockPos(5, 2, 1);
BlockPos tank2Pos = tank1Pos.south();
BlockPos tank3Pos = tank2Pos.south();
long initialContents = helper.getFluidInTanks(tank1Pos, tank2Pos, tank3Pos);
BlockPos pumpPos = new BlockPos(2, 2, 2);
helper.flipBlock(pumpPos);
helper.succeedWhen(() -> {
helper.assertSecondsPassed(13);
// make sure fully drained
helper.assertTanksEmpty(tank1Pos, tank2Pos, tank3Pos);
// and fully moved
BlockPos outputTankPos = new BlockPos(1, 2, 2);
long moved = helper.getFluidInTanks(outputTankPos);
if (moved != initialContents)
helper.fail("Wrong amount of fluid amount. expected [%s], got [%s]".formatted(initialContents, moved));
// verify nothing was duped or deleted
});
}
@GameTest(template = "3_pipe_split", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void threePipeSplit(CreateGameTestHelper helper) {
BlockPos pumpPos = new BlockPos(2, 2, 2);
BlockPos tank1Pos = new BlockPos(5, 2, 1);
BlockPos tank2Pos = tank1Pos.south();
BlockPos tank3Pos = tank2Pos.south();
BlockPos outputTankPos = new BlockPos(1, 2, 2);
long totalContents = helper.getFluidInTanks(tank1Pos, tank2Pos, tank3Pos, outputTankPos);
helper.flipBlock(pumpPos);
helper.succeedWhen(() -> {
helper.assertSecondsPassed(7);
FluidStack contents = helper.getTankContents(outputTankPos);
if (!contents.isEmpty()) {
helper.fail("Tank not empty: " + contents.getAmount());
}
long newTotalContents = helper.getFluidInTanks(tank1Pos, tank2Pos, tank3Pos);
if (newTotalContents != totalContents) {
helper.fail("Wrong total fluid amount. expected [%s], got [%s]".formatted(totalContents, newTotalContents));
}
});
}
}

View file

@ -0,0 +1,331 @@
package com.simibubi.create.gametest.tests;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Stream;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.AllItems;
import com.simibubi.create.AllTileEntities;
import com.simibubi.create.content.logistics.block.belts.tunnel.BrassTunnelTileEntity.SelectionMode;
import com.simibubi.create.content.logistics.block.depot.DepotTileEntity;
import com.simibubi.create.content.logistics.block.redstone.NixieTubeTileEntity;
import com.simibubi.create.content.logistics.trains.management.display.FlapDisplayLayout;
import com.simibubi.create.content.logistics.trains.management.display.FlapDisplaySection;
import com.simibubi.create.content.logistics.trains.management.display.FlapDisplayTileEntity;
import com.simibubi.create.gametest.infrastructure.CreateGameTestHelper;
import com.simibubi.create.gametest.infrastructure.GameTestGroup;
import com.simibubi.create.foundation.utility.Components;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.item.EnchantedBookItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.enchantment.EnchantmentInstance;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RedstoneLampBlock;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.ItemHandlerHelper;
@GameTestGroup(path = "items")
public class TestItems {
@GameTest(template = "andesite_tunnel_split")
public static void andesiteTunnelSplit(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(2, 6, 2);
helper.pullLever(lever);
Map<BlockPos, ItemStack> outputs = Map.of(
new BlockPos(2, 2, 1), new ItemStack(AllItems.BRASS_INGOT.get(), 1),
new BlockPos(3, 2, 1), new ItemStack(AllItems.BRASS_INGOT.get(), 1),
new BlockPos(4, 2, 2), new ItemStack(AllItems.BRASS_INGOT.get(), 3)
);
helper.succeedWhen(() -> outputs.forEach(helper::assertContainerContains));
}
@GameTest(template = "arm_purgatory", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void armPurgatory(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(2, 3, 2);
BlockPos depot1Pos = new BlockPos(3, 2, 1);
DepotTileEntity depot1 = helper.getBlockEntity(AllTileEntities.DEPOT.get(), depot1Pos);
BlockPos depot2Pos = new BlockPos(1, 2, 1);
DepotTileEntity depot2 = helper.getBlockEntity(AllTileEntities.DEPOT.get(), depot2Pos);
helper.pullLever(lever);
helper.succeedWhen(() -> {
helper.assertSecondsPassed(5);
ItemStack held1 = depot1.getHeldItem();
boolean held1Empty = held1.isEmpty();
int held1Count = held1.getCount();
ItemStack held2 = depot2.getHeldItem();
boolean held2Empty = held2.isEmpty();
int held2Count = held2.getCount();
if (held1Empty && held2Empty)
helper.fail("No item present");
if (!held1Empty && held1Count != 1)
helper.fail("Unexpected count on depot 1: " + held1Count);
if (!held2Empty && held2Count != 1)
helper.fail("Unexpected count on depot 2: " + held2Count);
});
}
@GameTest(template = "attribute_filters", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void attributeFilters(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(2, 3, 1);
BlockPos end = new BlockPos(11, 2, 2);
Map<BlockPos, ItemStack> outputs = Map.of(
new BlockPos(3, 2, 1), new ItemStack(AllBlocks.BRASS_BLOCK.get()),
new BlockPos(4, 2, 1), new ItemStack(Items.APPLE),
new BlockPos(5, 2, 1), new ItemStack(Items.WATER_BUCKET),
new BlockPos(6, 2, 1), EnchantedBookItem.createForEnchantment(
new EnchantmentInstance(Enchantments.ALL_DAMAGE_PROTECTION, 1)
),
new BlockPos(7, 2, 1), Util.make(
new ItemStack(Items.NETHERITE_SWORD),
s -> s.setDamageValue(1)
),
new BlockPos(8, 2, 1), new ItemStack(Items.IRON_HELMET),
new BlockPos(9, 2, 1), new ItemStack(Items.COAL),
new BlockPos(10, 2, 1), new ItemStack(Items.POTATO)
);
helper.pullLever(lever);
helper.succeedWhen(() -> {
outputs.forEach(helper::assertContainerContains);
helper.assertContainerEmpty(end);
});
}
@GameTest(template = "belt_coaster", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void beltCoaster(CreateGameTestHelper helper) {
BlockPos input = new BlockPos(1, 5, 6);
BlockPos output = new BlockPos(3, 8, 6);
BlockPos lever = new BlockPos(1, 5, 5);
helper.pullLever(lever);
helper.succeedWhen(() -> {
long outputItems = helper.getTotalItems(output);
if (outputItems != 27)
helper.fail("Expected 27 items, got " + outputItems);
long remainingItems = helper.getTotalItems(input);
if (remainingItems != 2)
helper.fail("Expected 2 items remaining, got " + remainingItems);
});
}
@GameTest(template = "brass_tunnel_filtering")
public static void brassTunnelFiltering(CreateGameTestHelper helper) {
Map<BlockPos, ItemStack> outputs = Map.of(
new BlockPos(3, 2, 2), new ItemStack(Items.COPPER_INGOT, 13),
new BlockPos(4, 2, 3), new ItemStack(AllItems.ZINC_INGOT.get(), 4),
new BlockPos(4, 2, 4), new ItemStack(Items.IRON_INGOT, 2),
new BlockPos(4, 2, 5), new ItemStack(Items.GOLD_INGOT, 24),
new BlockPos(3, 2, 6), new ItemStack(Items.DIAMOND, 17)
);
BlockPos lever = new BlockPos(2, 3, 2);
helper.pullLever(lever);
helper.succeedWhen(() -> outputs.forEach(helper::assertContainerContains));
}
@GameTest(template = "brass_tunnel_prefer_nearest", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void brassTunnelPreferNearest(CreateGameTestHelper helper) {
List<BlockPos> tunnels = List.of(
new BlockPos(3, 3, 1),
new BlockPos(3, 3, 2),
new BlockPos(3, 3, 3)
);
List<BlockPos> out = List.of(
new BlockPos(5, 2, 1),
new BlockPos(5, 2, 2),
new BlockPos(5, 2, 3)
);
BlockPos lever = new BlockPos(2, 3, 2);
helper.pullLever(lever);
// tunnels reconnect and lose their modes
tunnels.forEach(tunnel -> helper.setTunnelMode(tunnel, SelectionMode.PREFER_NEAREST));
helper.succeedWhen(() ->
out.forEach(pos ->
helper.assertContainerContains(pos, AllBlocks.BRASS_CASING.get())
)
);
}
@GameTest(template = "brass_tunnel_round_robin", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void brassTunnelRoundRobin(CreateGameTestHelper helper) {
List<BlockPos> outputs = List.of(
new BlockPos(7, 3, 1),
new BlockPos(7, 3, 2),
new BlockPos(7, 3, 3)
);
brassTunnelModeTest(helper, SelectionMode.ROUND_ROBIN, outputs);
}
@GameTest(template = "brass_tunnel_split")
public static void brassTunnelSplit(CreateGameTestHelper helper) {
List<BlockPos> outputs = List.of(
new BlockPos(7, 2, 1),
new BlockPos(7, 2, 2),
new BlockPos(7, 2, 3)
);
brassTunnelModeTest(helper, SelectionMode.SPLIT, outputs);
}
private static void brassTunnelModeTest(CreateGameTestHelper helper, SelectionMode mode, List<BlockPos> outputs) {
BlockPos lever = new BlockPos(2, 3, 2);
List<BlockPos> tunnels = List.of(
new BlockPos(3, 3, 1),
new BlockPos(3, 3, 2),
new BlockPos(3, 3, 3)
);
helper.pullLever(lever);
tunnels.forEach(tunnel -> helper.setTunnelMode(tunnel, mode));
helper.succeedWhen(() -> {
long items = 0;
for (BlockPos out : outputs) {
helper.assertContainerContains(out, AllBlocks.BRASS_CASING.get());
items += helper.getTotalItems(out);
}
if (items != 10)
helper.fail("expected 10 items, got " + items);
});
}
@GameTest(template = "brass_tunnel_sync_input", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void brassTunnelSyncInput(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(1, 3, 2);
List<BlockPos> redstoneBlocks = List.of(
new BlockPos(3, 4, 1),
new BlockPos(3, 4, 2),
new BlockPos(3, 4, 3)
);
List<BlockPos> tunnels = List.of(
new BlockPos(5, 3, 1),
new BlockPos(5, 3, 2),
new BlockPos(5, 3, 3)
);
List<BlockPos> outputs = List.of(
new BlockPos(7, 2, 1),
new BlockPos(7, 2, 2),
new BlockPos(7, 2, 3)
);
helper.pullLever(lever);
tunnels.forEach(tunnel -> helper.setTunnelMode(tunnel, SelectionMode.SYNCHRONIZE));
helper.succeedWhen(() -> {
if (helper.secondsPassed() < 9) {
helper.setBlock(redstoneBlocks.get(0), Blocks.AIR);
helper.assertSecondsPassed(3);
outputs.forEach(helper::assertContainerEmpty);
helper.setBlock(redstoneBlocks.get(1), Blocks.AIR);
helper.assertSecondsPassed(6);
outputs.forEach(helper::assertContainerEmpty);
helper.setBlock(redstoneBlocks.get(2), Blocks.AIR);
helper.assertSecondsPassed(9);
} else {
outputs.forEach(out -> helper.assertContainerContains(out, AllBlocks.BRASS_CASING.get()));
}
});
}
@GameTest(template = "content_observer_counting")
public static void contentObserverCounting(CreateGameTestHelper helper) {
BlockPos chest = new BlockPos(3, 2, 1);
long totalChestItems = helper.getTotalItems(chest);
BlockPos chestNixiePos = new BlockPos(2, 3, 1);
NixieTubeTileEntity chestNixie = helper.getBlockEntity(AllTileEntities.NIXIE_TUBE.get(), chestNixiePos);
BlockPos doubleChest = new BlockPos(2, 2, 3);
long totalDoubleChestItems = helper.getTotalItems(doubleChest);
BlockPos doubleChestNixiePos = new BlockPos(1, 3, 3);
NixieTubeTileEntity doubleChestNixie = helper.getBlockEntity(AllTileEntities.NIXIE_TUBE.get(), doubleChestNixiePos);
helper.succeedWhen(() -> {
String chestNixieText = chestNixie.getFullText().getString();
long chestNixieReading = Long.parseLong(chestNixieText);
if (chestNixieReading != totalChestItems)
helper.fail("Chest nixie detected %s, expected %s".formatted(chestNixieReading, totalChestItems));
String doubleChestNixieText = doubleChestNixie.getFullText().getString();
long doubleChestNixieReading = Long.parseLong(doubleChestNixieText);
if (doubleChestNixieReading != totalDoubleChestItems)
helper.fail("Double chest nixie detected %s, expected %s".formatted(doubleChestNixieReading, totalDoubleChestItems));
});
}
@GameTest(template = "depot_display", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void depotDisplay(CreateGameTestHelper helper) {
BlockPos displayPos = new BlockPos(5, 3, 1);
List<DepotTileEntity> depots = Stream.of(
new BlockPos(2, 2, 1),
new BlockPos(1, 2, 1)
).map(pos -> helper.getBlockEntity(AllTileEntities.DEPOT.get(), pos)).toList();
List<BlockPos> levers = List.of(
new BlockPos(2, 5, 0),
new BlockPos(1, 5, 0)
);
levers.forEach(helper::pullLever);
FlapDisplayTileEntity display = helper.getBlockEntity(AllTileEntities.FLAP_DISPLAY.get(), displayPos).getController();
helper.succeedWhen(() -> {
for (int i = 0; i < 2; i++) {
FlapDisplayLayout line = display.getLines().get(i);
MutableComponent textComponent = Components.empty();
line.getSections().stream().map(FlapDisplaySection::getText).forEach(textComponent::append);
String text = textComponent.getString().toLowerCase(Locale.ROOT).trim();
DepotTileEntity depot = depots.get(i);
ItemStack item = depot.getHeldItem();
String name = Registry.ITEM.getKey(item.getItem()).getPath();
if (!name.equals(text))
helper.fail("Text mismatch: wanted [" + name + "], got: " + text);
}
});
}
@GameTest(template = "stockpile_switch")
public static void stockpileSwitch(CreateGameTestHelper helper) {
BlockPos chest = new BlockPos(1, 2, 1);
BlockPos lamp = new BlockPos(2, 3, 1);
helper.assertBlockProperty(lamp, RedstoneLampBlock.LIT, false);
IItemHandler chestStorage = helper.itemStorageAt(chest);
for (int i = 0; i < 18; i++) { // insert 18 stacks
ItemHandlerHelper.insertItem(chestStorage, new ItemStack(Items.DIAMOND, 64), false);
}
helper.succeedWhen(() -> helper.assertBlockProperty(lamp, RedstoneLampBlock.LIT, true));
}
@GameTest(template = "storages", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void storages(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(12, 3, 2);
BlockPos startChest = new BlockPos(13, 3, 1);
Object2LongMap<Item> originalContent = helper.getItemContent(startChest);
BlockPos endShulker = new BlockPos(1, 3, 1);
helper.pullLever(lever);
helper.succeedWhen(() -> helper.assertContentPresent(originalContent, endShulker));
}
@GameTest(template = "vault_comparator_output")
public static void vaultComparatorOutput(CreateGameTestHelper helper) {
BlockPos smallInput = new BlockPos(1, 4, 1);
BlockPos smallNixie = new BlockPos(3, 2, 1);
helper.assertNixiePower(smallNixie, 0);
helper.whenSecondsPassed(1, () -> helper.spawnItems(smallInput, Items.BREAD, 64 * 9));
BlockPos medInput = new BlockPos(1, 5, 4);
BlockPos medNixie = new BlockPos(4, 2, 4);
helper.assertNixiePower(medNixie, 0);
helper.whenSecondsPassed(2, () -> helper.spawnItems(medInput, Items.BREAD, 64 * 77));
BlockPos bigInput = new BlockPos(1, 6, 8);
BlockPos bigNixie = new BlockPos(5, 2, 7);
helper.assertNixiePower(bigNixie, 0);
helper.whenSecondsPassed(3, () -> helper.spawnItems(bigInput, Items.BREAD, 64 * 240));
helper.succeedWhen(() -> {
helper.assertNixiePower(smallNixie, 7);
helper.assertNixiePower(medNixie, 7);
helper.assertNixiePower(bigNixie, 7);
});
}
}

View file

@ -0,0 +1,67 @@
package com.simibubi.create.gametest.tests;
import com.simibubi.create.AllTileEntities;
import com.simibubi.create.content.schematics.SchematicExport;
import com.simibubi.create.content.schematics.block.SchematicannonTileEntity;
import com.simibubi.create.content.schematics.block.SchematicannonTileEntity.State;
import com.simibubi.create.content.schematics.item.SchematicItem;
import com.simibubi.create.gametest.infrastructure.CreateGameTestHelper;
import com.simibubi.create.gametest.infrastructure.GameTestGroup;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.animal.Sheep;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Blocks;
import static com.simibubi.create.gametest.infrastructure.CreateGameTestHelper.FIFTEEN_SECONDS;
@GameTestGroup(path = "misc")
public class TestMisc {
@GameTest(template = "schematicannon", timeoutTicks = FIFTEEN_SECONDS)
public static void schematicannon(CreateGameTestHelper helper) {
// load the structure
BlockPos whiteEndBottom = helper.absolutePos(new BlockPos(5, 2, 1));
BlockPos redEndTop = helper.absolutePos(new BlockPos(5, 4, 7));
ServerLevel level = helper.getLevel();
SchematicExport.saveSchematic(
SchematicExport.SCHEMATICS.resolve("uploaded/Deployer"), "schematicannon_gametest", true,
level, whiteEndBottom, redEndTop
);
ItemStack schematic = SchematicItem.create("schematicannon_gametest.nbt", "Deployer");
// deploy to pos
BlockPos anchor = helper.absolutePos(new BlockPos(1, 2, 1));
schematic.getOrCreateTag().putBoolean("Deployed", true);
schematic.getOrCreateTag().put("Anchor", NbtUtils.writeBlockPos(anchor));
// setup cannon
BlockPos cannonPos = new BlockPos(3, 2, 6);
SchematicannonTileEntity cannon = helper.getBlockEntity(AllTileEntities.SCHEMATICANNON.get(), cannonPos);
cannon.inventory.setStackInSlot(0, schematic);
// run
cannon.state = State.RUNNING;
cannon.statusMsg = "running";
helper.succeedWhen(() -> {
if (cannon.state != State.STOPPED) {
helper.fail("Schematicannon not done");
}
BlockPos lastBlock = new BlockPos(1, 4, 7);
helper.assertBlockPresent(Blocks.RED_WOOL, lastBlock);
});
}
@GameTest(template = "shearing")
public static void shearing(CreateGameTestHelper helper) {
BlockPos sheepPos = new BlockPos(2, 1, 2);
Sheep sheep = helper.getFirstEntity(EntityType.SHEEP, sheepPos);
sheep.shear(SoundSource.NEUTRAL);
helper.succeedWhen(() -> {
helper.assertItemEntityPresent(Items.WHITE_WOOL, sheepPos, 2);
});
}
}

View file

@ -0,0 +1,129 @@
package com.simibubi.create.gametest.tests;
import java.util.List;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.AllItems;
import com.simibubi.create.Create;
import com.simibubi.create.content.contraptions.itemAssembly.SequencedAssemblyRecipe;
import com.simibubi.create.content.contraptions.processing.ProcessingOutput;
import com.simibubi.create.gametest.infrastructure.CreateGameTestHelper;
import com.simibubi.create.gametest.infrastructure.GameTestGroup;
import com.simibubi.create.foundation.item.ItemHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestAssertException;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.alchemy.PotionUtils;
import net.minecraft.world.item.alchemy.Potions;
import net.minecraftforge.items.IItemHandler;
@GameTestGroup(path = "processing")
public class TestProcessing {
@GameTest(template = "brass_mixing", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void brassMixing(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(2, 3, 2);
BlockPos chest = new BlockPos(7, 3, 1);
helper.pullLever(lever);
helper.succeedWhen(() -> helper.assertContainerContains(chest, AllItems.BRASS_INGOT.get()));
}
@GameTest(template = "brass_mixing_2", timeoutTicks = CreateGameTestHelper.TWENTY_SECONDS)
public static void brassMixing2(CreateGameTestHelper helper) {
BlockPos basinLever = new BlockPos(3, 3, 1);
BlockPos armLever = new BlockPos(3, 3, 5);
BlockPos output = new BlockPos(1, 2, 3);
helper.pullLever(armLever);
helper.whenSecondsPassed(7, () -> helper.pullLever(armLever));
helper.whenSecondsPassed(10, () -> helper.pullLever(basinLever));
helper.succeedWhen(() -> helper.assertContainerContains(output, AllItems.BRASS_INGOT.get()));
}
@GameTest(template = "crushing_wheel_crafting", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void crushingWheelCrafting(CreateGameTestHelper helper) {
BlockPos chest = new BlockPos(1, 4, 3);
List<BlockPos> levers = List.of(
new BlockPos(2, 3, 2),
new BlockPos(6, 3, 2),
new BlockPos(3, 7, 3)
);
levers.forEach(helper::pullLever);
ItemStack expected = new ItemStack(AllBlocks.CRUSHING_WHEEL.get(), 2);
helper.succeedWhen(() -> helper.assertContainerContains(chest, expected));
}
@GameTest(template = "precision_mechanism_crafting", timeoutTicks = CreateGameTestHelper.TWENTY_SECONDS)
public static void precisionMechanismCrafting(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(6, 3, 6);
BlockPos output = new BlockPos(11, 3, 1);
helper.pullLever(lever);
SequencedAssemblyRecipe recipe = (SequencedAssemblyRecipe) helper.getLevel().getRecipeManager()
.byKey(Create.asResource("sequenced_assembly/precision_mechanism"))
.orElseThrow(() -> new GameTestAssertException("Precision Mechanism recipe not found"));
Item result = recipe.getResultItem().getItem();
Item[] possibleResults = recipe.resultPool.stream()
.map(ProcessingOutput::getStack)
.map(ItemStack::getItem)
.filter(item -> item != result)
.toArray(Item[]::new);
helper.succeedWhen(() -> {
helper.assertContainerContains(output, result);
helper.assertAnyContained(output, possibleResults);
});
}
@GameTest(template = "sand_washing", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void sandWashing(CreateGameTestHelper helper) {
BlockPos leverPos = new BlockPos(5, 3, 1);
helper.pullLever(leverPos);
BlockPos chestPos = new BlockPos(8, 3, 2);
helper.succeedWhen(() -> helper.assertContainerContains(chestPos, Items.CLAY_BALL));
}
@GameTest(template = "stone_cobble_sand_crushing", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void stoneCobbleSandCrushing(CreateGameTestHelper helper) {
BlockPos chest = new BlockPos(1, 6, 2);
BlockPos lever = new BlockPos(2, 3, 1);
helper.pullLever(lever);
ItemStack expected = new ItemStack(Items.SAND, 5);
helper.succeedWhen(() -> helper.assertContainerContains(chest, expected));
}
@GameTest(template = "track_crafting", timeoutTicks = CreateGameTestHelper.TEN_SECONDS)
public static void trackCrafting(CreateGameTestHelper helper) {
BlockPos output = new BlockPos(7, 3, 2);
BlockPos lever = new BlockPos(2, 3, 1);
helper.pullLever(lever);
ItemStack expected = new ItemStack(AllBlocks.TRACK.get(), 6);
helper.succeedWhen(() -> {
helper.assertContainerContains(output, expected);
IItemHandler handler = helper.itemStorageAt(output);
ItemHelper.extract(handler, stack -> stack.sameItem(expected), 6, false);
helper.assertContainerEmpty(output);
});
}
@GameTest(template = "water_filling_bottle")
public static void waterFillingBottle(CreateGameTestHelper helper) {
BlockPos lever = new BlockPos(3, 3, 3);
BlockPos output = new BlockPos(2, 2, 4);
ItemStack expected = PotionUtils.setPotion(new ItemStack(Items.POTION), Potions.WATER);
helper.pullLever(lever);
helper.succeedWhen(() -> helper.assertContainerContains(output, expected));
}
@GameTest(template = "wheat_milling")
public static void wheatMilling(CreateGameTestHelper helper) {
BlockPos output = new BlockPos(1, 2, 1);
BlockPos lever = new BlockPos(1, 7, 1);
helper.pullLever(lever);
ItemStack expected = new ItemStack(AllItems.WHEAT_FLOUR.get(), 3);
helper.succeedWhen(() -> helper.assertContainerContains(output, expected));
}
}

View file

@ -269,6 +269,8 @@
"create.schematicAndQuill.convert": "Save and Upload Immediately",
"create.schematicAndQuill.fallbackName": "My Schematic",
"create.schematicAndQuill.saved": "Saved as %1$s",
"create.schematicAndQuill.failed": "Failed to save schematic, check logs for details",
"create.schematicAndQuill.instant_failed": "Schematic instant-upload failed, check logs for details",
"create.schematic.invalid": "[!] Invalid Item - Use the Schematic Table instead",
"create.schematic.error": "Schematic failed to Load - Check Game Logs",
@ -920,7 +922,7 @@
"create.contraption.minecart_contraption_too_big": "This Cart Contraption seems too big to pick up",
"create.contraption.minecart_contraption_illegal_pickup": "A mystical force is binding this Cart Contraption to the world",
"enchantment.create.capacity.desc": "Increases Backtank air capacity.",
"enchantment.create.potato_recovery.desc": "Potato Cannon projectiles have a chance to be reused."

View file

@ -8,10 +8,13 @@
"ClientboundMapItemDataPacketMixin",
"ContraptionDriverInteractMixin",
"CustomItemUseEffectsMixin",
"MainMixin",
"MapItemSavedDataMixin",
"TestCommandMixin",
"accessor.AbstractProjectileDispenseBehaviorAccessor",
"accessor.DispenserBlockAccessor",
"accessor.FallingBlockEntityAccessor",
"accessor.GameTestHelperAccessor",
"accessor.LivingEntityAccessor",
"accessor.NbtAccounterAccessor",
"accessor.ServerLevelAccessor"