From beb61708a0f80ed808385cc3ebcbeef97b3bed90 Mon Sep 17 00:00:00 2001 From: TropheusJ <60247969+TropheusJ@users.noreply.github.com> Date: Thu, 11 May 2023 09:00:32 -0400 Subject: [PATCH] Add GameTests by TropheusJ (#4496) --- .github/workflows/gametest.yml | 23 + build.gradle | 12 + src/generated/resources/.cache/cache | 2 +- .../resources/assets/create/lang/en_us.json | 2 + .../fluids/actors/HosePulleyFluidHandler.java | 4 + .../itemAssembly/SequencedAssemblyRecipe.java | 5 +- .../block/depot/DepotTileEntity.java | 5 + .../block/redstone/NixieTubeTileEntity.java | 4 + .../display/FlapDisplaySection.java | 14 +- .../content/schematics/SchematicExport.java | 75 +++ .../schematics/ServerSchematicLoader.java | 88 ++-- .../client/SchematicAndQuillHandler.java | 83 +--- .../client/SchematicPromptScreen.java | 1 - .../foundation/command/AllCommands.java | 12 +- .../foundation/command/CreateTestCommand.java | 111 +++++ .../create/foundation/mixin/MainMixin.java | 53 ++ .../foundation/mixin/TestCommandMixin.java | 46 ++ .../accessor/GameTestHelperAccessor.java | 17 + .../foundation/utility/FilesHelper.java | 9 +- .../create/gametest/CreateGameTests.java | 39 ++ .../com/simibubi/create/gametest/TESTING.md | 15 + .../infrastructure/CreateGameTestHelper.java | 452 ++++++++++++++++++ .../infrastructure/CreateTestFunction.java | 122 +++++ .../infrastructure/GameTestGroup.java | 25 + .../gametest/tests/TestContraptions.java | 98 ++++ .../create/gametest/tests/TestFluids.java | 152 ++++++ .../create/gametest/tests/TestItems.java | 331 +++++++++++++ .../create/gametest/tests/TestMisc.java | 67 +++ .../create/gametest/tests/TestProcessing.java | 129 +++++ .../assets/create/lang/default/interface.json | 4 +- src/main/resources/create.mixins.json | 3 + .../gametest/contraptions/arrow_dispenser.nbt | Bin 0 -> 1054 bytes .../gametest/contraptions/crop_farming.nbt | Bin 0 -> 2276 bytes .../contraptions/mounted_fluid_drain.nbt | Bin 0 -> 1368 bytes .../contraptions/mounted_item_extract.nbt | Bin 0 -> 1436 bytes .../gametest/contraptions/ploughing.nbt | Bin 0 -> 855 bytes .../contraptions/redstone_contacts.nbt | Bin 0 -> 1501 bytes .../gametest/contraptions/train_observer.nbt | Bin 0 -> 3331 bytes .../gametest/fluids/3_pipe_combine.nbt | Bin 0 -> 1141 bytes .../gametest/fluids/3_pipe_split.nbt | Bin 0 -> 1128 bytes .../gametest/fluids/hose_pulley_transfer.nbt | Bin 0 -> 3467 bytes .../gametest/fluids/in_world_pumping_in.nbt | Bin 0 -> 971 bytes .../gametest/fluids/in_world_pumping_out.nbt | Bin 0 -> 853 bytes .../gametest/fluids/steam_engine.nbt | Bin 0 -> 1594 bytes .../gametest/items/andesite_tunnel_split.nbt | Bin 0 -> 1260 bytes .../gametest/items/arm_purgatory.nbt | Bin 0 -> 1132 bytes .../gametest/items/attribute_filters.nbt | Bin 0 -> 2151 bytes .../gametest/items/belt_coaster.nbt | Bin 0 -> 3459 bytes .../gametest/items/brass_tunnel_filtering.nbt | Bin 0 -> 1650 bytes .../items/brass_tunnel_prefer_nearest.nbt | Bin 0 -> 1371 bytes .../items/brass_tunnel_round_robin.nbt | Bin 0 -> 1556 bytes .../items/brass_tunnel_single_split.nbt | Bin 0 -> 1209 bytes .../gametest/items/brass_tunnel_split.nbt | Bin 0 -> 1564 bytes .../items/brass_tunnel_sync_input.nbt | Bin 0 -> 1715 bytes .../items/content_observer_counting.nbt | Bin 0 -> 933 bytes .../gametest/items/depot_display.nbt | Bin 0 -> 1526 bytes .../gametest/items/stockpile_switch.nbt | Bin 0 -> 504 bytes .../structures/gametest/items/storages.nbt | Bin 0 -> 1789 bytes .../items/vault_comparator_output.nbt | Bin 0 -> 1908 bytes .../gametest/misc/schematicannon.nbt | Bin 0 -> 1320 bytes .../structures/gametest/misc/shearing.nbt | Bin 0 -> 1078 bytes .../gametest/processing/brass_mixing.nbt | Bin 0 -> 2154 bytes .../gametest/processing/brass_mixing_2.nbt | Bin 0 -> 2846 bytes .../processing/crushing_wheel_crafting.nbt | Bin 0 -> 2715 bytes .../gametest/processing/iron_compacting.nbt | Bin 0 -> 1865 bytes .../precision_mechanism_crafting.nbt | Bin 0 -> 2413 bytes .../gametest/processing/sand_washing.nbt | Bin 0 -> 1370 bytes .../processing/stone_cobble_sand_crushing.nbt | Bin 0 -> 2508 bytes .../gametest/processing/track_crafting.nbt | Bin 0 -> 2098 bytes .../processing/water_filling_bottle.nbt | Bin 0 -> 1797 bytes .../gametest/processing/wheat_milling.nbt | Bin 0 -> 986 bytes 71 files changed, 1881 insertions(+), 122 deletions(-) create mode 100644 .github/workflows/gametest.yml create mode 100644 src/main/java/com/simibubi/create/content/schematics/SchematicExport.java create mode 100644 src/main/java/com/simibubi/create/foundation/command/CreateTestCommand.java create mode 100644 src/main/java/com/simibubi/create/foundation/mixin/MainMixin.java create mode 100644 src/main/java/com/simibubi/create/foundation/mixin/TestCommandMixin.java create mode 100644 src/main/java/com/simibubi/create/foundation/mixin/accessor/GameTestHelperAccessor.java create mode 100644 src/main/java/com/simibubi/create/gametest/CreateGameTests.java create mode 100644 src/main/java/com/simibubi/create/gametest/TESTING.md create mode 100644 src/main/java/com/simibubi/create/gametest/infrastructure/CreateGameTestHelper.java create mode 100644 src/main/java/com/simibubi/create/gametest/infrastructure/CreateTestFunction.java create mode 100644 src/main/java/com/simibubi/create/gametest/infrastructure/GameTestGroup.java create mode 100644 src/main/java/com/simibubi/create/gametest/tests/TestContraptions.java create mode 100644 src/main/java/com/simibubi/create/gametest/tests/TestFluids.java create mode 100644 src/main/java/com/simibubi/create/gametest/tests/TestItems.java create mode 100644 src/main/java/com/simibubi/create/gametest/tests/TestMisc.java create mode 100644 src/main/java/com/simibubi/create/gametest/tests/TestProcessing.java create mode 100644 src/main/resources/data/create/structures/gametest/contraptions/arrow_dispenser.nbt create mode 100644 src/main/resources/data/create/structures/gametest/contraptions/crop_farming.nbt create mode 100644 src/main/resources/data/create/structures/gametest/contraptions/mounted_fluid_drain.nbt create mode 100644 src/main/resources/data/create/structures/gametest/contraptions/mounted_item_extract.nbt create mode 100644 src/main/resources/data/create/structures/gametest/contraptions/ploughing.nbt create mode 100644 src/main/resources/data/create/structures/gametest/contraptions/redstone_contacts.nbt create mode 100644 src/main/resources/data/create/structures/gametest/contraptions/train_observer.nbt create mode 100644 src/main/resources/data/create/structures/gametest/fluids/3_pipe_combine.nbt create mode 100644 src/main/resources/data/create/structures/gametest/fluids/3_pipe_split.nbt create mode 100644 src/main/resources/data/create/structures/gametest/fluids/hose_pulley_transfer.nbt create mode 100644 src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_in.nbt create mode 100644 src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_out.nbt create mode 100644 src/main/resources/data/create/structures/gametest/fluids/steam_engine.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/andesite_tunnel_split.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/arm_purgatory.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/attribute_filters.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/belt_coaster.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/brass_tunnel_filtering.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/brass_tunnel_prefer_nearest.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/brass_tunnel_round_robin.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/brass_tunnel_single_split.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/brass_tunnel_split.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/brass_tunnel_sync_input.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/content_observer_counting.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/depot_display.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/stockpile_switch.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/storages.nbt create mode 100644 src/main/resources/data/create/structures/gametest/items/vault_comparator_output.nbt create mode 100644 src/main/resources/data/create/structures/gametest/misc/schematicannon.nbt create mode 100644 src/main/resources/data/create/structures/gametest/misc/shearing.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/brass_mixing.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/brass_mixing_2.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/crushing_wheel_crafting.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/iron_compacting.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/precision_mechanism_crafting.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/sand_washing.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/stone_cobble_sand_crushing.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/track_crafting.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/water_filling_bottle.nbt create mode 100644 src/main/resources/data/create/structures/gametest/processing/wheat_milling.nbt diff --git a/.github/workflows/gametest.yml b/.github/workflows/gametest.yml new file mode 100644 index 000000000..1ec05830a --- /dev/null +++ b/.github/workflows/gametest.yml @@ -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 diff --git a/build.gradle b/build.gradle index d8e3a2d87..05c9bdb9a 100644 --- a/build.gradle +++ b/build.gradle @@ -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 + } } } diff --git a/src/generated/resources/.cache/cache b/src/generated/resources/.cache/cache index d50c09db9..e7c958a52 100644 --- a/src/generated/resources/.cache/cache +++ b/src/generated/resources/.cache/cache @@ -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 diff --git a/src/generated/resources/assets/create/lang/en_us.json b/src/generated/resources/assets/create/lang/en_us.json index 2603c07fc..3064a4f83 100644 --- a/src/generated/resources/assets/create/lang/en_us.json +++ b/src/generated/resources/assets/create/lang/en_us.json @@ -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", diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/HosePulleyFluidHandler.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/HosePulleyFluidHandler.java index 063a52eae..b5883e2d7 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/HosePulleyFluidHandler.java +++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/HosePulleyFluidHandler.java @@ -125,4 +125,8 @@ public class HosePulleyFluidHandler implements IFluidHandler { return internalTank.isFluidValid(tank, stack); } + public SmartFluidTank getInternalTank() { + return internalTank; + } + } diff --git a/src/main/java/com/simibubi/create/content/contraptions/itemAssembly/SequencedAssemblyRecipe.java b/src/main/java/com/simibubi/create/content/contraptions/itemAssembly/SequencedAssemblyRecipe.java index 980d506eb..e3e64cf4f 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/itemAssembly/SequencedAssemblyRecipe.java +++ b/src/main/java/com/simibubi/create/content/contraptions/itemAssembly/SequencedAssemblyRecipe.java @@ -43,7 +43,8 @@ public class SequencedAssemblyRecipe implements Recipe { protected List> sequence; protected int loops; protected ProcessingOutput transitionalItem; - protected List resultPool; + + public final List resultPool; public SequencedAssemblyRecipe(ResourceLocation recipeId, SequencedAssemblyRecipeSerializer serializer) { this.id = recipeId; @@ -213,7 +214,7 @@ public class SequencedAssemblyRecipe implements Recipe { public boolean isSpecial() { return true; } - + @Override public RecipeType getType() { return AllRecipeTypes.SEQUENCED_ASSEMBLY.getType(); diff --git a/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotTileEntity.java b/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotTileEntity.java index 308237dec..8203bebaa 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotTileEntity.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotTileEntity.java @@ -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(); + } } diff --git a/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeTileEntity.java b/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeTileEntity.java index 28afb42ed..adeb4ba96 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeTileEntity.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeTileEntity.java @@ -123,6 +123,10 @@ public class NixieTubeTileEntity extends SmartTileEntity { customText = Optional.empty(); } + public int getRedstoneStrength() { + return redstoneStrength; + } + // @Override diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/display/FlapDisplaySection.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/display/FlapDisplaySection.java index 4481a80ca..51b5f5ef4 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/display/FlapDisplaySection.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/display/FlapDisplaySection.java @@ -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(";")); } -} \ No newline at end of file +} diff --git a/src/main/java/com/simibubi/create/content/schematics/SchematicExport.java b/src/main/java/com/simibubi/create/content/schematics/SchematicExport.java new file mode 100644 index 000000000..6bad9f5a6 --- /dev/null +++ b/src/main/java/com/simibubi/create/content/schematics/SchematicExport.java @@ -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) { + } +} diff --git a/src/main/java/com/simibubi/create/content/schematics/ServerSchematicLoader.java b/src/main/java/com/simibubi/create/content/schematics/ServerSchematicLoader.java index ffee13229..ffe6b5df9 100644 --- a/src/main/java/com/simibubi/create/content/schematics/ServerSchematicLoader.java +++ b/src/main/java/com/simibubi/create/content/schematics/ServerSchematicLoader.java @@ -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 stream = Files.list(dir)) { + List files = stream.toList(); + if (files.size() < getConfig().maxSchematics.get()) + return true; + Optional 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 list = Files.list(Paths.get(playerPath))) { - count = list.count(); - } - - if (count >= getConfig().maxSchematics.get()) { - Stream list2 = Files.list(Paths.get(playerPath)); - Optional 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); } } diff --git a/src/main/java/com/simibubi/create/content/schematics/client/SchematicAndQuillHandler.java b/src/main/java/com/simibubi/create/content/schematics/client/SchematicAndQuillHandler.java index ff5c0a3de..ee3912882 100644 --- a/src/main/java/com/simibubi/create/content/schematics/client/SchematicAndQuillHandler.java +++ b/src/main/java/com/simibubi/create/content/schematics/client/SchematicAndQuillHandler.java @@ -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; } -} \ No newline at end of file +} diff --git a/src/main/java/com/simibubi/create/content/schematics/client/SchematicPromptScreen.java b/src/main/java/com/simibubi/create/content/schematics/client/SchematicPromptScreen.java index 3fb353c40..4f9723252 100644 --- a/src/main/java/com/simibubi/create/content/schematics/client/SchematicPromptScreen.java +++ b/src/main/java/com/simibubi/create/content/schematics/client/SchematicPromptScreen.java @@ -109,5 +109,4 @@ public class SchematicPromptScreen extends AbstractSimiScreen { CreateClient.SCHEMATIC_AND_QUILL_HANDLER.saveSchematic(nameField.getValue(), convertImmediately); onClose(); } - } diff --git a/src/main/java/com/simibubi/create/foundation/command/AllCommands.java b/src/main/java/com/simibubi/create/foundation/command/AllCommands.java index 8664b0a5e..6942b6e5e 100644 --- a/src/main/java/com/simibubi/create/foundation/command/AllCommands.java +++ b/src/main/java/com/simibubi/create/foundation/command/AllCommands.java @@ -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 util = buildUtilityCommands(); - LiteralCommandNode createRoot = dispatcher.register(Commands.literal("create") + LiteralArgumentBuilder 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 createRoot = dispatcher.register(root); createRoot.addChild(buildRedirect("u", util)); diff --git a/src/main/java/com/simibubi/create/foundation/command/CreateTestCommand.java b/src/main/java/com/simibubi/create/foundation/command/CreateTestCommand.java new file mode 100644 index 000000000..f42edbed2 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/command/CreateTestCommand.java @@ -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 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 getSuggestions(CommandContext 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 findInDir(Path dir, SuggestionsBuilder builder) { + try (Stream 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(); + } +} diff --git a/src/main/java/com/simibubi/create/foundation/mixin/MainMixin.java b/src/main/java/com/simibubi/create/foundation/mixin/MainMixin.java new file mode 100644 index 000000000..da309104b --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/mixin/MainMixin.java @@ -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. + *

+ * 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; + } +} diff --git a/src/main/java/com/simibubi/create/foundation/mixin/TestCommandMixin.java b/src/main/java/com/simibubi/create/foundation/mixin/TestCommandMixin.java new file mode 100644 index 000000000..f78936a54 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/mixin/TestCommandMixin.java @@ -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; + } +} diff --git a/src/main/java/com/simibubi/create/foundation/mixin/accessor/GameTestHelperAccessor.java b/src/main/java/com/simibubi/create/foundation/mixin/accessor/GameTestHelperAccessor.java new file mode 100644 index 000000000..3c3c280d6 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/mixin/accessor/GameTestHelperAccessor.java @@ -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); +} diff --git a/src/main/java/com/simibubi/create/foundation/utility/FilesHelper.java b/src/main/java/com/simibubi/create/foundation/utility/FilesHelper.java index a4b3c8b78..536ffa943 100644 --- a/src/main/java/com/simibubi/create/foundation/utility/FilesHelper.java +++ b/src/main/java/com/simibubi/create/foundation/utility/FilesHelper.java @@ -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; } diff --git a/src/main/java/com/simibubi/create/gametest/CreateGameTests.java b/src/main/java/com/simibubi/create/gametest/CreateGameTests.java new file mode 100644 index 000000000..f50450caa --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/CreateGameTests.java @@ -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 generateTests() { + return CreateTestFunction.getTestsFrom(testHolders); + } +} diff --git a/src/main/java/com/simibubi/create/gametest/TESTING.md b/src/main/java/com/simibubi/create/gametest/TESTING.md new file mode 100644 index 000000000..3dec254c9 --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/TESTING.md @@ -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. diff --git a/src/main/java/com/simibubi/create/gametest/infrastructure/CreateGameTestHelper.java b/src/main/java/com/simibubi/create/gametest/infrastructure/CreateGameTestHelper.java new file mode 100644 index 000000000..51b86f548 --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/infrastructure/CreateGameTestHelper.java @@ -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 getBlockEntity(BlockEntityType 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 getControllerBlockEntity(BlockEntityType 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 getBehavior(BlockPos pos, BehaviourType 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 getFirstEntity(EntityType type, BlockPos pos) { + List 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 List getEntitiesBetween(EntityType type, BlockPos pos1, BlockPos pos2) { + BoundingBox box = BoundingBox.fromCorners(absolutePos(pos1), absolutePos(pos2)); + List entities = getLevel().getEntities(type, e -> box.isInside(e.blockPosition())); + return (List) entities; + } + + + // transfer - fluids + + public IFluidHandler fluidStorageAt(BlockPos pos) { + BlockEntity be = getBlockEntity(pos); + if (be == null) + fail("BlockEntity not present"); + Optional 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 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 getItemContent(BlockPos pos) { + IItemHandler handler = itemStorageAt(pos); + Object2LongMap 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 content, BlockPos pos) { + IItemHandler handler = itemStorageAt(pos); + Object2LongMap 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 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); + } +} diff --git a/src/main/java/com/simibubi/create/gametest/infrastructure/CreateTestFunction.java b/src/main/java/com/simibubi/create/gametest/infrastructure/CreateTestFunction.java new file mode 100644 index 000000000..16cfcd163 --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/infrastructure/CreateTestFunction.java @@ -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 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 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 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 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)); + } +} diff --git a/src/main/java/com/simibubi/create/gametest/infrastructure/GameTestGroup.java b/src/main/java/com/simibubi/create/gametest/infrastructure/GameTestGroup.java new file mode 100644 index 000000000..cb24dc5ce --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/infrastructure/GameTestGroup.java @@ -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; +} diff --git a/src/main/java/com/simibubi/create/gametest/tests/TestContraptions.java b/src/main/java/com/simibubi/create/gametest/tests/TestContraptions.java new file mode 100644 index 000000000..524475898 --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/tests/TestContraptions.java @@ -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 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 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"); +// } +} diff --git a/src/main/java/com/simibubi/create/gametest/tests/TestFluids.java b/src/main/java/com/simibubi/create/gametest/tests/TestFluids.java new file mode 100644 index 000000000..0624e29d5 --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/tests/TestFluids.java @@ -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)); + } + }); + } +} diff --git a/src/main/java/com/simibubi/create/gametest/tests/TestItems.java b/src/main/java/com/simibubi/create/gametest/tests/TestItems.java new file mode 100644 index 000000000..43dc317e3 --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/tests/TestItems.java @@ -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 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 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 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 tunnels = List.of( + new BlockPos(3, 3, 1), + new BlockPos(3, 3, 2), + new BlockPos(3, 3, 3) + ); + List 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 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 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 outputs) { + BlockPos lever = new BlockPos(2, 3, 2); + List 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 redstoneBlocks = List.of( + new BlockPos(3, 4, 1), + new BlockPos(3, 4, 2), + new BlockPos(3, 4, 3) + ); + List tunnels = List.of( + new BlockPos(5, 3, 1), + new BlockPos(5, 3, 2), + new BlockPos(5, 3, 3) + ); + List 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 depots = Stream.of( + new BlockPos(2, 2, 1), + new BlockPos(1, 2, 1) + ).map(pos -> helper.getBlockEntity(AllTileEntities.DEPOT.get(), pos)).toList(); + List 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 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); + }); + } +} diff --git a/src/main/java/com/simibubi/create/gametest/tests/TestMisc.java b/src/main/java/com/simibubi/create/gametest/tests/TestMisc.java new file mode 100644 index 000000000..829bd3d4b --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/tests/TestMisc.java @@ -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); + }); + } +} diff --git a/src/main/java/com/simibubi/create/gametest/tests/TestProcessing.java b/src/main/java/com/simibubi/create/gametest/tests/TestProcessing.java new file mode 100644 index 000000000..ca2767c7d --- /dev/null +++ b/src/main/java/com/simibubi/create/gametest/tests/TestProcessing.java @@ -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 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)); + } +} diff --git a/src/main/resources/assets/create/lang/default/interface.json b/src/main/resources/assets/create/lang/default/interface.json index 86e0d8237..af689e00f 100644 --- a/src/main/resources/assets/create/lang/default/interface.json +++ b/src/main/resources/assets/create/lang/default/interface.json @@ -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." diff --git a/src/main/resources/create.mixins.json b/src/main/resources/create.mixins.json index dc5b8e9a8..c316ea3bd 100644 --- a/src/main/resources/create.mixins.json +++ b/src/main/resources/create.mixins.json @@ -8,10 +8,13 @@ "ClientboundMapItemDataPacketMixin", "ContraptionDriverInteractMixin", "CustomItemUseEffectsMixin", + "MainMixin", "MapItemSavedDataMixin", + "TestCommandMixin", "accessor.AbstractProjectileDispenseBehaviorAccessor", "accessor.DispenserBlockAccessor", "accessor.FallingBlockEntityAccessor", + "accessor.GameTestHelperAccessor", "accessor.LivingEntityAccessor", "accessor.NbtAccounterAccessor", "accessor.ServerLevelAccessor" diff --git a/src/main/resources/data/create/structures/gametest/contraptions/arrow_dispenser.nbt b/src/main/resources/data/create/structures/gametest/contraptions/arrow_dispenser.nbt new file mode 100644 index 0000000000000000000000000000000000000000..62320b6d4b0a0299c227279e39f56425a6fbc8e2 GIT binary patch literal 1054 zcmV+(1mXK1iwFP!00000|HYV1Z__Xs$Di2F)^-e#5C=X0CxpZap-DSTyl7ltV$uX$ zCNK56Xjxnwcilip`zCw@&U`11NL+E+y35St7X}iQsy4~b@o!J^f8uoj&fgmI*mqCgg0Hkn{3-BrqXo%Y>XQ6LPjp$eATLTPEae znUJ$(Le7>6InP`iXX0VH&eX$@1coLsU1x^e^R9F5VY<#M9)=_^G=X6OACllqQ0D>A z`atK9?~a8SZe4)cv-(QtsZ_@~^q%rDFG_vBebJtT?V6_Co+^j+t>I7%?co;}Z5_$W zp*d*2?ri3;va6M7=53n7+HRp_RqgR=(w^XU4@fzL8-)@^WaC6?sXV&f@(nypsCr--wU2|3dQXNEYhdYDkO7~LeXM~a}S9hah$PsKqAhTiM0a~akfmX z9k|X|J8+$`7`x6sCe{vIXDsS5XM#BQuh|X)(Vqu>eG&Acn&{$aUT&JXZq38QdO@sq%Oykt6YB-B>rE4!X@WCDoUvXI%N0YMeN3zuxXxHF zppG-v3uwgIGO=DjBhHqI^#a!!>jkbemSNY~$HaPp>x|_*=1frMtG%E-#Tw+Vr?pV+ zls_Lly#M9-<8NO-eBb`@j_w1uz6i2)+3tsIX z2(H>8xLdbYH#|%%`>|Rr?}Y>=mi^fEr3ub7!I>e>SoUK%Vu-VkiDln)#GqGgHoN0nH zOK@h0vyTa-gdxs8#&upD^Gawf+Pmat57;YyBH-?#zbjr=Q+~c*@TTqU0~qWmITot@ z39kqFSWeA-kv#%C?)K$s))@Q2nnl?;$rk*_2mR&m^pkiJ5Y7(iL;CP19m3d)`hfuEeSMt%i0O znT)~B>FlFC75nqwVw{n#0OMz61KIiI#k=wkUdqIQ1)wv!uT^OZt~w?66aanbm=)+V YuKAu&O(|=$i2VY;0P~qV=wcWE0HKW#$^ZZW literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/contraptions/crop_farming.nbt b/src/main/resources/data/create/structures/gametest/contraptions/crop_farming.nbt new file mode 100644 index 0000000000000000000000000000000000000000..45e0887b89e925ed64f98c0499d84fddcde4b0d0 GIT binary patch literal 2276 zcmaJ?eK-?p8}AKCanvQuu4dq7p;cWZ4qJk*qWdjcsY6 z`COCcqnhQ?T4?aa(MzhCah=ckc6z$@Gjmu=+vb zO4ZQ1og$S@Ia$dqg(#O5#51B~UrGW6H&`eemzrZ9Zr=fx^mrYE3FPeE2x-RjVTDgN z*uvwNpO4LaxZ3USEZCiv9&n-xJ2xK^$o$=;+gsvBO@C2}h%#&fqU(xoc8XfyNgE6W zJ{9CAw(PE>eWp)iPdWUujmDU23=^FKwzr89eGZ2z__D|wZMow={@n1C_-Xd#hzNt) zr5e4F_O9^CAQ2*twz6IOASOFRzCU7qoN*34O%Cj2STrf4un{z*MU{B0s_b!6D5ZC% z3?azaHp&aTN~#pJf0W3mPv87Dhl}XjrzT}L&ReSET@oj|267pa85urno+Y0iV9oE} z#VWBkcHy4R5UdsFc1rWI{?Ht(Zv-gtIP-hL{d+PC;9YO}3q8x^H5Pb*EiOK_21du0 ze3&U~O$1S+$bu;cyCTZk2S&Yk7`?kyG+^LgLXLMVAR9P3q=izdMw>_{o2Fm6)ZoU1 zO6jaz(1iO`1!*-MBJB9j?zRy#|Y7Tx_rVpvJ_j*<9Hp;tZ z8)worb)7iN+v@xYqyxyK8$S|nnWP=^wpr7eWd7J@MZ9H)BS37dDgS{Gug+oNQT@%9 zQY0z%+ptBfgZ96TFQqAm*>As>zcju)Ejw}F*#E4u#8M9H+*{UiJ4N&Q_eCwyzfStF zV$**^{;B(0%GW9_*y2u{F2#>y<3ysU^T&`YwYQBUUxPf_@gwIgowX^_W=$v3ZS`s} zi9lQyytqByihdgE3-RxQMvP43PoIxl7W}m=5>0uFuuP-(>&J8)O(ll5d0_UMnH7-R z+YqG_e&r2~Qyy6+%9+NEx@1*iSNf#Q0VdP>I!OcjP$>_J*A$e220_xYWG#eibnphN{Ti>W8pV0yuLS~aB6n!Tox;^o58MX{ zYtaBbVO%b;)fPa#yx^lohF@b~kswAs-Iryz`-NW!#VwV_GluDim_W*nbrRq=r{-$r zoCwlb|9R^{x%J^of$ka?V#>F*NPvW`2jAzcL-vGd6Cx&%w8}mzYSA{XeRO|N4}f{I zl4F}-d8%4{vHP>qFvI6~4y)FA=^gZ>JN>`Mf$3PqVQoLg=82jQ4d+qb8ZV@Vt?B<) zNB5~CfXe#u%T4+zutcAcU(4<0dPj76itjnA-6o=a-cV6ANLR%o&=-M$Dix(~&_$Mt zxQg=fd;Q_zMRBx|TR^MWZ~)cHBvtb(lDt4x?=PfklCm$V0BUlQQJYm@jo08>d}3&`WJI)d*=c%po}tLaNu23X>jF+U}R~q zOab6bUGypmgN(Mm{FvwMxP7lUVM75#45fQ@{B(FM)&5m5|0o$h{upi@c9G^k8-1FW zNxv|CUdx2cyz@xyANWB<$-e~A-6d26(biA7YMB_ny|Y?v5fSS$qLcGeG`^3=XCK{7 zklq~Wu@AlMBK{18sbwi%?%+Q4b(5uH_W~_0U-_&s9A?B=?eZJyn38ta2-%|>d&WC@ zL|Lv4G)GVQx&&W0gYguM; zJm8)EbAQ@g7`jA1Occ#x`7u~`!~H#~yQdE1TX=IoB@lv6n9Plw|nuDyg$ylJNw zgQ5Dbk5s8)k|JOO$zwNSxm$7#{d;+%O(|I_BpLNcape^^n45XgiQ~L_mTM z#2dMjW*Uy@mN+|y-xLIgzUoHx$_q%-hw3&RVK>;{y?!60(F}K#b8Ik?Ke#MHv0L?y(>;M1& literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/contraptions/mounted_fluid_drain.nbt b/src/main/resources/data/create/structures/gametest/contraptions/mounted_fluid_drain.nbt new file mode 100644 index 0000000000000000000000000000000000000000..85f7e96666344db1dca7a3a1f94b83d325dde982 GIT binary patch literal 1368 zcmV-e1*iHSiwFP!00000|HW9%ZyQAvfBR*x*9jCs3kW2*Ac4dYBvjERuG2JWOJe1t zEeOe)tS9y;>z&o^Y}|0l1*t+p91v$N`~mz24dNf*fP}aK5?9nC7gXNt?riL}cjHtM zShB{u``(-1`})0EI{*zZBla0J06>0g+1G4`nhx$YZ}J_gxM7FtPLaw@LlSPtw(Z=Q=zT#A|cz z_4vq_Hxbwt7es=v@KRSw7A&k1&s$-UAihge9E~>dcY_`gRBM3I;Zd^R^83e$%JOEa zy8KeQ>WGPj^yyO@bk>6x+>oN?)hG^V*zJ2UZGgVR6BA1r zVE_cpG-kP9%~LfN~Q{rlBvR@WX`H*v#^&2DJfcDZ3mS0+Q;(V zr49TUB{wqWaiG3I#fXOo7T9eP5SNLg+sD$dxsBW_x3FNb)$5^+?D6Wc+)rQcA-ogh zfNK`iwna#zXyrJ8)ZBJM?s@kJ(vsXZrL2XFvW8sna7Gzu1sC+p1qW(3IP+*|fYWjX zJEUu9Cyl>-e}J6o(Fl@Q)wbgx@Q#weL$gDoNJ(o@OFc%V`c1#j{8UFixX*kfaIFc} z<_O1vjOJ@!w*Gqaj}O+rdv9@cTR6WvuyB_|;*O6*pHQUs0L5Ox^@ zC(hk?$b24Q$WpYmHOl$}30<3H@qI65y)Mp%0|%NrxU{)1aKzcrSe1`9$dJPGXpFic z*%w#oFc3#*uQkMZC7`5^ivcyVfa0wkHcTANQu9WJBajmfr(!n+#1KTdPh6@&y~9T| zq&*C>JwC=}l%ihP*8Csy`17rw^snGU?~8Z7{{HP>jd*q*FK2nYp5@W{uktuTrEn~d zFFq;@s6Hc)7tz}leVn>>SsgUkFu=k%KqJ0L!DiPsI{ zVc>u%4~0c#sn7e= z*!P*=pPC)YpI4`&yhlkWRi^vJDB!g-yw${DMLZURscgeUwkI1sU;(X4bO|pTg)}zi z!{=izr|{+boD*N}R|&rH_&J4q|8O3@S)0u_%K)8L-fcHd`DQWm!1ZF`4vKA}n7WlS zb;((19DnoT@at7)p-G>T;7--^rL)gMTI$z?3kxVy4~R#Fpz;lC(FQXaCW@Xz!n_eV z>bfM7O@S%vgdATD+Q~ZK6k$wlsNqAYlNJ~#%VT58lMGw5snn1n zp@=3~aAbE={|cH`G#~+m+9;XVHW-2jIhZ{z1RtiCx!vjcT85_J#92I8G}BBbvd8;< z$&CBNix87Z9xT{x{_hYpCpK?QZAY7+41C;DWDAAMdK;<)l}I`Qrmp#Dd{NW al@%h$Jv{g@?jyy03;zPIr8f956953px~}yA literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/contraptions/mounted_item_extract.nbt b/src/main/resources/data/create/structures/gametest/contraptions/mounted_item_extract.nbt new file mode 100644 index 0000000000000000000000000000000000000000..dead6de25da871b2abf64d47d2a6be6e8fdf137e GIT binary patch literal 1436 zcmYk+c{tR090%~^2?u{1F3}5>3*5{W=5Ni^!(ezRC?t$q=f!Sq3O~ift<$oGl25!m*kh_ zu0cyvn)%%hx9rAhFJa0EOkDGF-rsKtiQ{|6=G_c^KG!PC>M+WRmVUMxrw-4t_^(a| zuap0Jxv`@4&;d)}3_%`;(K6?6I0Pcc%<|#J2dP3& zvAGrfxD*+*jTC=k!`I)$i9g(H;Keh9_TU)9h}cFKgwGinDwz$C|XeF;GnP9tlgS-5k(K^+H5EMX1)bHWl!YC9bC zUjdV!*=c>Bz`*}2sIe41Y7GVoGFK}{7?;TQKtWO306}V|7l7^g1_FRd%$IM~A6A^2uA~ z`u|!T#M|?D|07>4uB3%|1%SCdBN`NjC9nf++cP9lF2-5jxSP2d(<1{OD_f$0V=wle z=AE)|th0>O1*bClD8lo0H8C{5zLbh^hlXfyfLhxLrxxYBsSA~=i0C8C2(8IG))`YZ zr^tRvr_Ab-Z6PDYmZ$xCL`s;lOEx#JBtg$WbF79BJ99mYhBmKei&CA(p3@sWB7ayb zONut=*Udi=UaNODlyBU8W-{DopHW}z>NX+AxVWv;G)E0$IwS=1l);vUP56v825;m? z6_1W=V$j{L{Ot!^HH1cArTV)vFs{ZjQ8XB6%S7absP&u%P*UFkp_&U8$6-RA#ycI- zpl&Cmgs3Eo%jMu1m!XCd2-thi1bsIt$FIw^u!02x?lcucM8QXnT{#}RjGwkBNLP(LTM#0Fyi&nP_mV?dg?~}GOfMrp5lA<_P^981N*kSq zhrHOX-FT8|j`aehd}wpJx^?u%xeLorSfBIai)jWO7o|orb@pA4f|ru-dZl3+D?4sQ Z7J7=Ei_C{#{wCU3@=n*T1Cg`<;2)&xug?Gg literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/contraptions/ploughing.nbt b/src/main/resources/data/create/structures/gametest/contraptions/ploughing.nbt new file mode 100644 index 0000000000000000000000000000000000000000..64329baa3259f70a483a3eb1996c476336de0db0 GIT binary patch literal 855 zcmV-d1E~BTiwFP!00000|D~7RZrU&u$4@>Cfv)Q=R%x0h?K#@3PW#X*P2Du4?&BtM zlLJ^yDgul=EoqvuGFa#JtSh70|0HAvI>NOld zgt=srVF}e^0KJQhCzs_=y?H-?px~w6w0;CYN=YTwJK8j!gT{Bz1Qw0wz@s%b9<8zQ zXpN1>ci_<)8;{o5c(lgGv-Z(JW8={p8;{o5ctQsrt+DZFjg3cZY&^t)M{8_6T4UqU z8XM0tPlLxdXgmjv@1O}R8qa~pbKvnDcsvIl-+{+(;Q<&zkY7jwWKpJX$cOovLrXChHw5#Cp&IaKR6wH~SU z`(m;9-FD)ezVr-aq<6VE445h^Cufbjf;TgR`IINpQP?pW6cnLNmO^ zol?VsDqAX_g;ek84J*OBiQq}~DyA9FC-ImG6>*tn9rtBD5JE>2?|jl!7J_)3!QS_j{e_Wu2&$EV;amcSc{kp#kRU hy3#v`G&VogIDSJVJx5V>mTLZ=@E5WN-FH(D008NZsiFV? literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/contraptions/redstone_contacts.nbt b/src/main/resources/data/create/structures/gametest/contraptions/redstone_contacts.nbt new file mode 100644 index 0000000000000000000000000000000000000000..60b165139b62885e7b7da2e2357fe042de86572b GIT binary patch literal 1501 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/fluids/3_pipe_combine.nbt b/src/main/resources/data/create/structures/gametest/fluids/3_pipe_combine.nbt new file mode 100644 index 0000000000000000000000000000000000000000..b7a9d2b54f87abfcfe51fb84266e230661efedd9 GIT binary patch literal 1141 zcmV-*1d96~iwFP!00000|J9gHkK05T#~;7OaTYBgUL-yOHzZCKEo`<_1*&C5XbW7z ztUa5I*6~=Lant1#@c}q;7j8A8mkAh)|i;J#>A{OCT6WMG21{kYA*H znVu?a>l>8a!Rxnpy^YuJN&pAk3D)$=weUf)U?~?&URJdJeadEYg>DC6vl!lpa)hI| z&Xa=0Q%TZ`5Jq3)<(Q`u7l8nV_tZ}(WI^E&JEKuf&gCt-$mAvVceLO|Dxt+ttA1|| zj8z-zfj7x0jgMFAyVpYf{&tiZ>{P_KCH5f9dLfl=INTW%V^CWphl7gzoRt6l@1}wPbc}EDFd1E(1_^UGn zm*RNgQhKTwikx~-S4|Z)n8q5}zM@ZPE@<4-icx7r749cyBM@v!cfm?1^{Q-nN83W9 zIZ0VWl4({fGMot$UgQz2^Y7zE{`qaoL^BJVJ!XL+&?=C(>p%osWC@*KUJt1_aUVN;Ou z+$dK z*v|;^(j0MGL0;A6d)z2H?sYBSOKg;HEZ?KQ+xBqH6tixeq1Uz_Y8v|HKV62l?T2oT zq5l76=)uD-L!FmyC^QUx{(btLjl67l*KJp*tWsN76;8*tzh&oYHz6h zyRkGo_0RE~gwHBBU+Z@I7*t+xFs9Xcx6yV~R^5cpW;DJ6(H*MY+PcGz17fcpUGkI( zu%50vJ6yXpTh1v>n!Qx9lumSXS)E|Cq}uLZJ+CC!cXn}QvWvc_^<9U@Nxa-G4hr(#(~jYBFQ6~3&vBdJ-@(zjgqb^ H4jBLd3e7JB literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/fluids/3_pipe_split.nbt b/src/main/resources/data/create/structures/gametest/fluids/3_pipe_split.nbt new file mode 100644 index 0000000000000000000000000000000000000000..88f648db6b679ee8e855201a36e9601f52e5d188 GIT binary patch literal 1128 zcmV-u1eg0CiwFP!00000|J9hwZrer_h7Yfzs1!+yUVuJAb_Kc$nj*H-AW0L~sM@6M zMjex5X<&*&aE4Kwm5>L>GTW}(F8T;v^o6?YvS&z+q)_C9Nfg5-V9+Fg9)9PaGdG0* zLvRIqMjZf9e}nq$4I!YZWRg(<>KKB5obc#Wgn04g5S)w){nF}iz!Q>4TEEezv3h9i z9vY{I#x-cH9?V)}V%8cHv(}iHwZ_D3_h8l<6SLNsn6<{ltTiU)ReSW%n3%Q3#H=+Y zX00(XyFHk-#>A{OCT6WMF>8&9+3UfqH6~`QF)?e6iCJq*%zh7MtuZlcjfq)nOw3yI zDsu=w?m)N~N%n-^r9@Wn!Q&YdGO0#N5BzbFWyx7d4WlC>gjQSN|Cq%X z7_%9ClTKM$kFNOOF-v2Hes2H%#q#Bgzr^0>|1Lwg@_-2WZORwv zk;1lqLfJh$|BUCGc>bjXaKIhom|nRSJ}Bla<$}qxiq^kN*>tAR?Eq{R!-r9haP-DW zQm}X;NqQQ>=m$I<@KoX?5Ww(`dUQzU6s}@08s+3f-dNyEXotTy=S7O~uK(9Is9BAM zv1&s-@WvUX@xfBvyLO2GitS-jTv)-Pny3{^?A? zrPy9Ll^!XEBB!cX*ec`lFts(ZeNUgzT+q0u6{FIMD&1RVBM@vsx4{%s30K+jskVhi zGm^51B$KR|XV??QyvQS3*Wdj{{rPRvM6(K<9ae!M(JGNo>O=&aX9=C0Vg*YUHS=ip zqs}}wOTk?Y53NnTfBla?U)$6nhU0~twKYc>pskx`ETb(`o7EU9z1Q&RQtV>b6r?;i z3OZ|Qf87+llh`a4e>Wz#^1rRgn?*3_ZE_!rKC2O?3CXIi)X0w z-VKF@p>MxV|9Yb?8{T!>6)LOL(p9C?vFvYIx!R3l8(vUv9yYvj-_4utF}0;`?8eA; zS-g42y4fBcJG{A!b>r{Wo2@N?_QrX|n;{G`l29qB`f)T2z%56zkLzznJj-Z4iHKn7 z6pajza*j9DPpbijnGsyc_=|FM4#1H-1Mh?+f(GCpb1C^8T$$s3Rma`Vr5V&e!F>{b zt6Y4o-qYKl@`Q~ssrI{#cSq&6KH<|Tjn^Qp0XsH`y&}5gDHC8lT?M!K?ytY6)|sm-yI7m-qVH*a)!}iu*xU_IeO2Mq7w1W{x@^JOp*yVz u4oPHp0d%hN`}CMP58Yvmyk(DZV04>E@`&bw@f63N-{60~0sz$x82|vk9z3!D literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/fluids/hose_pulley_transfer.nbt b/src/main/resources/data/create/structures/gametest/fluids/hose_pulley_transfer.nbt new file mode 100644 index 0000000000000000000000000000000000000000..f42e51fe69c5b98e3404ae8c7c137e096db2b8c7 GIT binary patch literal 3467 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_out.nbt b/src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_out.nbt new file mode 100644 index 0000000000000000000000000000000000000000..cfa909345d2c68f3f4ddd38ee3bf207e3030ea50 GIT binary patch literal 853 zcmV-b1FHNViwFP!00000|Gkz$Pt-sZ$6vQQ%WfAU2Q~4cC%*&j+6K78zJeYXUlOMtp@uu-(cdBjf>f8T+CME zVzwF=bF&7s)wr0g#>H$kE@rE7F|!)XR^wu}8W*$GxR|Zxx*GH*e0f)#F>iu-n#Lc~ z1mLH8ngQF&5Th?=W^R6+eyU>PX%>;v3!sTyX=-ThKS&Hm?MR;kYISgQxWob zHyb2bK&Ip%52>$v&q#ci#^*FH()fbLJ6Dy(p~v6#OcV0J<9koelJw#zbAfbvnR`sWPzv1Z!yxRjVgrjd_Vbmk@B6 z^L@R*MeYGxk%Lqly7^V1_+CuAp2@;{Ri(4#@<&N2G}6-VHc!i2wh;h(Oy?`M8?bb0 z>Z-NP^FHbarzTR?law8g6gxZ~pqV7JuCcKqQ%zTR8#=vsAfk1iAl$gX98z*}A6gc` z|7lh_92T8p(qWa0Iu0?(^vNZMWz5x<)C~GxF#LCi<%yZC4*VWhBBd(dphmJE%VCw5 zxx(DoW1>W%m*ND15JV1;mt&9*NIZQ{Z#tz5SC1cdKffj=} f!re0Av@NS#^BuaSgiPr^`vAWIBT%)Z_6q<2sZpW3 literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/fluids/steam_engine.nbt b/src/main/resources/data/create/structures/gametest/fluids/steam_engine.nbt new file mode 100644 index 0000000000000000000000000000000000000000..4edf9e7bd5db6f09a2baf4f2a9a99aca3835199e GIT binary patch literal 1594 zcmZw9c{mdc0LO7hBr7eVWsg{6bIdY^wD5WfHPnzoZZVM#mfUC3R@(NyntKQ9*nXBib&?GV8+49m!gRMV40y5Oe{t2WGL&f;d&ja@r@iC3`HnK-ax=YI#e!Vi$y=w8we1T+l6L5TFFTZxRfum9ei+LFxPVDuSwIDodZ^RhdxxE)A6aN*3;l z!)+oa(Ea0{`%`siK4pVcV>%!F`~5bescQ6}^hftt=BM-rRIB*I`U$$^{y%tcSn=?U zICR4c1-}Nhvm5(&dR_=9nUFn5@!R3aEJ^HE zkANR8Nm!cEK-`cC{uk!h2{F<>Cd_3QOfi<= zA^%ugF9Mn6Xo7_h-JJr$iF!?+UV5}*+pddNa0}yu!Rr0vQEu{}TZf`Xd!VDz=iPV8 zy6Vag)Pd~YIo`3&Ux#F_8E2vmeDhrqHMxVry}KUdfKFLIdC_r0)%t+vEXicTfBjtE zJBo2=dp%Vv@FslY-IB@X$11=r-{K)VlJPy57i>6~40ouo1N)S5?5bc#oeE2AjXQKW zYHUJjAQOR~Q(qYjfceC2ayE`y-Og(I#%gFDpy*msqA|zm;LeoyO3ARfAS}Dd0qiw$ zl&A#F@h_+PR=T37e9pu4$P;O28R=_sCuk3x$L4>%SXqdS5GGTR^)B%Xj$*55LU7#(Fj9t(LQQeAae zqD{1|$O`LBNGJ4#e3wi_C>lxDu0x_K;SvrQ%!aexd%iw4%T z3wIYxy{xDA<#m~T;Dvk8g>fvM@Ckv+xI0D=2ccs^`LkCTlkrYtv?Web&Wh7q4lPBv zQ2wP+y*+>q^o!^^A$GAVG3`~Gz)X_QbmP&H4T3ad`*hamm1?52RYiL6f<{)lIpjsb zCFiaoTyi4NG(g9)by|d8s({`zDn;<(``ota5CBKkm1eTf?E}B|izsq%o(JezcpH4x zI8@Ua{nf(pKvL8fQPT0-nXK$6Mk%V8dic$F ScvEt^L`u0MR>2=7CG|I3tP`#P literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/andesite_tunnel_split.nbt b/src/main/resources/data/create/structures/gametest/items/andesite_tunnel_split.nbt new file mode 100644 index 0000000000000000000000000000000000000000..ab4e2beafd62635ec547bc180917ea8cb46c5b36 GIT binary patch literal 1260 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 AI{*Lx literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/belt_coaster.nbt b/src/main/resources/data/create/structures/gametest/items/belt_coaster.nbt new file mode 100644 index 0000000000000000000000000000000000000000..df3cb2642aea36757daca5824b4cdf0ef1fa6859 GIT binary patch literal 3459 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 UwN7R0hZfLV!@k|X1@iIz2apVvIRF3v literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/brass_tunnel_filtering.nbt b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_filtering.nbt new file mode 100644 index 0000000000000000000000000000000000000000..c877ddd0aa2ae5c48689090ddbb76b45d01afff8 GIT binary patch literal 1650 zcmZ{idr%UH7KgEXP?)*bLTFVQe6T9I7(TKTHIWS^+fvJSv_4pTl_sl^s6nfhMk=Og zEo4@T`K+`8Wo1)P#5QGpgqWEkH?f<#KCn%_&EAFCQ1AOsi@^%od9^uPRHS@Qx7`ak=heb(QFaXbGujGYkxtI1DALPqEdNXP_z87;vki^d?QhaU}-){C$TTUp&6fF8WEM$;rO3tLUtqUSD6E%rZiLc;1X9WeNyr*(3F z70ILEqH@ck4;~ZBI>!;;j7I>@+?2iEI_Oe-s;wt)>yb)zUXRKBELb%{J}~ac}9_w6ZH6wJisM1a>XEOz^vHEC8fpq;529`wa%~z znO!V19X#f}Mwn!uOgBw+N;|ckQcpGC%4)UVTSLLJlepZ1=m9!~r!tn71GG09L9I4{QBg^coJUcQJJj z9@W}4t>%1B{(_FL05 z=st@@3(YH_=JfpBLJCAMF-D@@@<>V<*{FFo%AqG?T*va?Tj(dka-DvPVJi%X1U2No zxfyEI$Ouy3u|+?z>9YW^m$o67bj0afh=T?Y`LRx60vD}zek2)r9^Jj~0S^80k?D&iBZ^P84)MRVnE_+w2Dt?5S!%UX7 zX=|^z%`Q$omp8zX2=id{C2MyQLUlB#A%6^`U80njs1rP^u1&B~3ZssAu?L(-u7Axd z`Z4cNqv!9?{voaprw-AZDe_C}M&sqw(iv#tx=IWUpXhm$;nXa7s2zUZra#L6HUleF zpSx!yYZ^WVP7zJ(SFTT{@UBm I3jqN98|nihcK`qY literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/brass_tunnel_prefer_nearest.nbt b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_prefer_nearest.nbt new file mode 100644 index 0000000000000000000000000000000000000000..bdbbacd9e4e2262b3f8a723117bdd57e9314e991 GIT binary patch literal 1371 zcmV-h1*G~PiwFP!00000|LvLWZre5#hL2=Pu@$@Bunz06J1J0LZQ1|~)y%t~WoRvHtt(wLZC3udJ;F)NLUS!qnnN@HSPKMxCyiCJk(%t~WoRvHttXThvA zCT68EF)NLUS!qnnz6G<=n3$Eu#H=(XW~DJP2NujqV`5es6SLBon3cxFT(Y#79gW7Z z(6|r7V!KG19~7WLyj$YOde`>CjQYpQdaiXjAh zBu}zAc!x)a??VXh`>8RfEzBQvm`x}78jZ)qAhq>gZ%X1H!6Trh>}zdd;~op!~0 zvh`R(7#(DnSYblUK?(tDf&}|XD&)pqC8b{IC-;$)L~aY?O7>JPo>#CmJ|%okMYUe|@Ioqi za$bmpWyhS&Reg@z%k&0&h^uv{E8G$6G&^(P8Ezw(B0=MmWWEp%3aU1x+1zr~ip$#f z@i~V3Uc7(N3(kW5l&o@t<4_;xBtbaBeonWE<5EU9qYT&JsiOq*vl z$<#F&Md>QUmr2;dKj!o@VMV^){57^|PMOJ9+x(Ah<9tfnxL>es;Hz!ERND+TY}2jj zJ7D$|14?5m29(BB%6T2kI!)hUrH|RSVD>GTeG6vaf;q5Y4lI}h3+BLrInXfgtVl|Q zpmLr+8{#3BEUB($t!{2jnh;l?73K1Of~c91h=M;$8RJ9jon6VL3p~LKif3iKTfS+@ zS%If()n%g)PU$7x>QvM0d-cLoUhv3#R=t94HEuI_&|Pd!AiOyW4D zPaTFaQBM-Z<_KGvdRP?d$BlOc?U8q{@D@+me2zX+!>?VLF)kL^kGR1B1BsZ@E^k+d zydHb{`GR0KBTfY;a*JtYZ{6`vH88;)BToc9li{4zC#BXPb*Dqz2Z`Jt*R$Xny61eg dS;;#39TDUs76Mph;PLZM_y-{=f~*}N003Ejo*V!G literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/brass_tunnel_round_robin.nbt b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_round_robin.nbt new file mode 100644 index 0000000000000000000000000000000000000000..310e8b882a89b2e74f572f201557e2801898b526 GIT binary patch literal 1556 zcmY+Cdpr{g6vtIqT!ci%bV>Ti!aNosQC_ztv=*A@9>|#Drm09MVYa3*73z~BuP~4D z+H^JZSk$B!Ml0F_X@o7;O?^JMPv`SF=X1{Y{LcC3oNd_~<$l=e^bKC@JE#`RG~hIP zXY5`a$WrX}w$&);qMREL>{d5z5^DTbyCPH3ZRcV9jaDaCa2UQe>>+n%OFk%YZx-%q z%6q#*0Gs`Pc1b<|J-g)V=<$q9UZ<2b?>__Oexmqs<}dAcq`&v~@kJ2L7ZvVKysB$!f*qpKn)QFLw*%Jb|B^|CK4p5@^RsF| zo(ngAP+pNZwYHx=#T5T-nOI~B#WL~;o=M9ORJ_hQqQJ|dEauJ=S75<$b5P^w21U>9 zs0D!I6+v+-O7b@ERC_)VD7EtV*^mbj1u0}EfBc^XXNI$?*Q&oVLW`=`OhR?i#j2c1 zu~aG@$XtqYJrmw3_u-9h0Wvb~=7gyFYV>|H7xyimw>TI_(4|WQZYqx{i<={hnF|q* z_GO4H_OI57&LV?Yp&5-0FQ2*8b;myl@04$tW}uf3dQSvzjM?-hz5Qs+p^E7&;dP9a z(TY=G>2kXKm4|96TPYKVowrOTn$c23mN&hXZGm}tLNIH*G*^(s5hxM`l*aqAnCN4d z(Q)LG16F-cKgzWfh^xw*B%d;M$nT5k$BoueYLHY{qC#L%w*Fv_pd) z)w7$(LidC^9nu_KpQKk`sz?{mKXpQ!1(qugp->K=M47%p=R|JB7`k2cI zlq7M1C0L)7;!fO!EaNJ<5u?Bp_AS7$A{HX|Ep0nsvOF!I3_|tt1<<`w;RxrB3|-r% z(?iYQ{{)dM)y58_uGI}3}Q(w9FUgH~H3l>Kyk_jxz;aKui&8M`SkUAvwDDN=EK&D=6K~ zqgZrYh~!Cuys!D^-&NPOQ|mF?%OPw2k>av;nsd70C^Zo60AO-3)jlC~Ceg|n*)%+x zIWQB!allgx3oBoZUeEC_Voccakp~AI7(M}tN~JV<=KMsB z&@LK5(lHZ z+LrjPTi(={r&lw+K1XX^u;Mg3uGdXkJVn7SY~?J5w`h>@kc)xFhI$7qTLgyEh$spg z$L0E~!OxM2ur4UusUMB$nGFM0+cLP|CX4Z0_7ZyiSa+(h=z6?i*&wkky+)>Nf(nw= zop%pxg8Rcv6Rg!?$`zVys@+RaO30ia8;}jJ->i`A2MRJvgBQ7zKl)Kuh8-3`f@JTW pXR#*{Dyd2cg%MTldcF?)#U3Bn{s#xyU-2J8Hq3df%1q_t{sXYT0tEm7 literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/brass_tunnel_single_split.nbt b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_single_split.nbt new file mode 100644 index 0000000000000000000000000000000000000000..ec6eaf4e38066244c32acca40039a44207f153c9 GIT binary patch literal 1209 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/brass_tunnel_sync_input.nbt b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_sync_input.nbt new file mode 100644 index 0000000000000000000000000000000000000000..284d4e143faeaa380bc47b0f6ed718998f2dc4f5 GIT binary patch literal 1715 zcmYL|dpz6s9>xvFr4_A9N8)ZOQ`%P|k*;;Cp?c72sj<2wii8BMTS?P!ET>{3%czo| zSdo=nG!0s5X&mhcKXTl9q#A42RXT!_ik*cz>Dt%X_pkTsd40am^T+e~IM9rA{(L*j zbxT=uD9fSt`^ras-1~X6^_H)xsDu)oVDsrMNOV zZEU;lx=JfR%p1WD_a0l1`s-~7W`@A9Ca7nXnbg`n z>mE+Pd@x$N*yL{roGUZg6weTf zgjsnU#0o=__aa>blFg8)oA9G7DiAu}I@qxGjp~iVhWvYCNPJ@X+Z%&h6pUH8GciB5 zS~!GN6c(k z zW^j3z-;y{X3=M3_Pu2MXk$c!r4-iQWd?5f%eaUah?)tJl=Va3c20|aglYr&p!329T zw;oprQe`Nzc|;wH`qpC<)=+Pfs=4JCpW^KdpZO7A@z({?d4Bl3w7q-J#V5ctSx06?!Ytd}JxiKVFosgnGFLh>xuuyRFVUc-To(k%C@}!_9J_A^kRb`@ z`N?V{?Rm>SgOT*HO=4|bWL;qdMr{n3@b&43+PaZI^F&)$O{vWBeS4FOOpxyT%8UWm z5}6CEULSi+oMFb~nJ%dLl10qQRIK(Fcmh86r@iM*n(!p%k`Z89B6EUy>tm^6r8zS} zR~Ux5WDFeo7|ReV&6vI)WBp2GP?*LSn0GGgt@ioZ+i#skwro)KBih#&UT&C%W#%E= z%!|%FxE1Gy>cIzgD-zxyi@Rge*#rMX;2^sz|L7oRvo0`M2dDOLvN$WBK1@wL@1}^p z-raJWiGD*5C6$x)R&`$0zW%1E=EDB&U3bCC=j3Ym%Rx~fJ?2}~)5O0GvkbCptkC-H z9yc3CyjWwD@a?4(!2wBWZsl=@AZN-weA~OY-8|5y&#pn{YL6KWn#8*rR-}^0dc0(! z<(Ifwtxyar;X5juZdY-F02IJ%Z5Ts}4c-2XEzc)e#MMZ7XfnLM9cNS5i7gLm&EYRr z#&iT64yhpi=Sq})AAxNGtvBqb3m8d-{3^k^Uygq4&;5tqb#H9t*{B~E$A4RFxH~i4 w5s6NH6wv4vgP0;c@r|lY=hP`S6ZJg{N2ff@jgv{mw|8{XpF24f!gX~12a2de3jhEB literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/content_observer_counting.nbt b/src/main/resources/data/create/structures/gametest/items/content_observer_counting.nbt new file mode 100644 index 0000000000000000000000000000000000000000..61719d37359cf244bc72255ae2c6f715fd681dd9 GIT binary patch literal 933 zcmV;W16uqaiwFP!00000|J|5RZ__Xoz+YmgP1B8m#+b&jN5o;1IPHSa5YoiN5StKi zsod1LjMW5W7W9QJyg3Ix`> zG@A83eZu-Mp_||wjw8wvovn+@uUAi=e*bd$@Y|2@=VJt38si{{CUtee4!W@JGb#t$ z?FM`plJj;LM5i9qj^YvXsSC~l>CkWj?|0!*2K#X&b5$Nsy-tpv(vq1vQa@efULQp>*u}Bt8 zqO}U4d~Z<&qwB3*+S@;vm=dz)5;LAiSMvIsCz47pKap^jUIU!{zO#(=T{G+S=Gtsp`zMr(iq7R%oKwN*R^0RyQL^IlGXVd`wVduwxf&EV z0#ve6%If6)n{~?Q_2sNLmayJj#(L`(x|?4q^$JF}Qn10f1DDLCDn@r!!X?gCjLsRd zU0~7q&R*cGm__G1dx5iJ7M<@FIV(ozY?V5vD;B}p-iV8Ma7@p{zMR}=$$&0+vu|Pb zb4$q2)^6*`cI(20)i-3Uxt+|!d)YLNg0q0O#i&CooboktE0{&+r0X@hNp3s(+;+@! zvrBqX%*`(CNjamNN47zYP-%_I8QnZW2F}R985uZZ17~dDj18Qzfitdft`A8_g`mkB zui*lIMFzBd_g6Oyr*sdz*DRJBfAX&)SH_FSl+i9YJrZ(x!mag)pFHkFF%u_H93p=% zu8f5R8|6Y_O60piMXLI1z}!rpu9q}xjwQjuxZkhLRE87hLkc*gy_xK_H{8tw*OIP; z^fp2}Ea;#7rgXC#k2)c3%K>D-UKqzL*TFH(b+BYZLeP9p1bIst58?=5WgC70DfN8- HtrP$NQ47_{ literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/depot_display.nbt b/src/main/resources/data/create/structures/gametest/items/depot_display.nbt new file mode 100644 index 0000000000000000000000000000000000000000..f266d3e7eb5fa6be6a29bdee7bb88c75a2cfe5dc GIT binary patch literal 1526 zcmYk5dpr{g6vt(Yx!r7QWmz?uydT9T$>#mIc~ugwJH)&rA#AgYP#U+El()H4UUv(T zOWu#nb8eE?NFFhd)E0UmyDFc1JD>CUem>`X&hK;1AHO4$1QPicLQcj%v{BQ&?YT?R zdRYOCjiMLJv1Bpox%|!HrN4^u9;*hPch+#9vJQWkL+l~e%}dFyvXfzOjR0MN_~l8i zFh);&Q^NM(g*U`I0fR^br(%e5h(|P!!>iX>_Ewk&1c-E|@US8=?_Z*W{Kq{3Wb6Rd zmMA%M5zvx+TX1$<$IVvfaEIrm1M03jp5f7)MJXEum?3~>HZcScti$xwBLti_P)(!& zzmmmnBWP>T%??7pceiSjAE%UJ+x_v|s?#?nwzXSRFxhP_k{K+p|H2Xw)&E{muaVrM*4q@$s@qrfHzFo)w5l3oUd9%UkvYv6?U z&~xH=gKX=N5&)mxlTlx|EH%!H&Vc_Mqo19Rzh@(?J6hrzNt&)rt7;H_%BpJE(2Us} zV}CjF5SxC#kgR{@337*D-A&{}!kAc2%G%?>%KrI0`k z4oolid(wib{W}xH%?p-qiB_lbg~6zeUGk!?ZYYZB*NkPGVlLv*7~cuM{%Rs9HUY!` zq>g}tjreDPV6*RtzeP2<`qn3#=9Uxt1csX+D-1xdjUP&VbfQo3qiuJ1_}epfyA7*1 zL^notilqN^7Ett4W&xiqamSZEGbQw~S|)ai0uQC3Q13!ITBl2wQ1VXaA%0OHA(Tg7 zvl^Y&lo@T0aJx$iSe1)uj$%E4&@z-P0g*n;ZhQY?=gW@g_Eq}gixVLxivA#*EVlL) z6cB7!FJb0eBSmP8Yxl2_n$|#qo2O-oSRjoyA%J4_W7{qGKoDJ|8FrW`3IzMT*44Wi zVcYFDHkwmPkx8#u(5Ho=`WLP&NRRk*I>}rTx~A?Y^NvUzBwKqXc%OPP>%f;VxaymB z+DZ1xNM8zhD$Z- zLIqxj@i#Tnl3HCy}do#u{@Ex`y4Pxi!%(Mv7SUg+e)wQ@MlthYrdUq-> zfAl! zEj!`>`eG;DQM}peVlOdMW(!5qVu0NlwMtjFA75^>XrasN5*5pAF*BLeNuxaW27bu1 zrAP9Qo^%LqIxRccYkxr1to!^v`W7?`ZQi%jztHq$ChH;nB z@FN+BD)SP{?!3U(!Qmx_pD!|paH8=f@Yt|dCT>=x)K(=1l!(Z`f{5$Y literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/stockpile_switch.nbt b/src/main/resources/data/create/structures/gametest/items/stockpile_switch.nbt new file mode 100644 index 0000000000000000000000000000000000000000..65d268da8af1359dae76671dc6e51a73e606f4f3 GIT binary patch literal 504 zcmVK*7rrnLqO_D5*{jsG%P~HCp0XA)oZ3$EP}-&SUiHoBUmDWB_dcN zf(8BiztZp#4U5q52@M-$6(G!-a!Bt9>-~Ubz|{uc)yiP$WJNdS4n2*yz3xhkXr{X2 z6I~TB`>r&hW02n3Cqr!C+S^lWQn^r`-J550gqz^VI(JmO#OM@`$TGA)-fBVas ub*{`tm6B`-_~(;p(stTo`y(+9i?_IwB!8mw${K*hANU6j@^JTK2LJ$9i}8y9 literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/items/storages.nbt b/src/main/resources/data/create/structures/gametest/items/storages.nbt new file mode 100644 index 0000000000000000000000000000000000000000..f42e7a870fa3acc0859ac3992c23e34cd912d8cf GIT binary patch literal 1789 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 F0{{zrfiwUB literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/misc/shearing.nbt b/src/main/resources/data/create/structures/gametest/misc/shearing.nbt new file mode 100644 index 0000000000000000000000000000000000000000..2656d5a5bb6af8c5bde319507d74cb6bdad0e2f4 GIT binary patch literal 1078 zcmV-61j+j!iwFP!00000|HYWUZzDw%fZxu{I<|91!W{*oKmpMp0lFFq5+_zjM2=Vi zgZLqtu|2j|+1=Tj9iMffxq^;@KY;>@tDpc0DA1jtLxPqHiH3#(@gE#-c0EowyyKCO z4oj=`c)xk`y|=T{J~sgx!1Cx!PyjGTXwMC(i&RHC5_uEHK@(`&*G&j_6_3&;)Dw|N zg;x!~aWGYS_tqOxB67`>3@2dVW=m@o^{2WFL2AHuc`kOY0hB=Fx*SHMNT_m&8q`{e zoThsF#&=Ku`q9sCQ)nD#LUA3*)Bx+iy4}Mzer2~`e&?yj&vt(N_Rfdje4T85Q3v)K zPXt^!;ptE&TY1K3=~iE+(;V!zCzB`@1I0)B#j!|*iUu!?WE{&`Pw{jtVD7j5Q}*d^ zxQ3m!_3vqkyeSeaPcP4e7(xKeeICdA@vSHwV>uf8QVqngg)xmbTsll|PGg+P`?0_h zZ?^imRGCSuOc;u)Khlv(^LS6ncqnHkV`MLx$n6Nrnhr$C)eh4m6fgq@q5qOaR{{o!DBsX71ap$ zWGs~_+L0J(BP@mi!I+T>o(rIUoW7#4D8m4^;EB}5-7oUk{p0uHeB+o|hgu}%#xg%` zwiNEi2U2G?b$c{`c4S`I-n(|Yu-kd-UU}}m4|d<0P>b-aEHvB4YTdsdz~w!jcA~*) zH#?G2V;*a-PXsrWBv3aGLU}VVP`w|^!6`0ipDt1q0MF==%{_KlS*7j2%CjAt;uiA@ ziC4=RHqhcjA;hhr#eBvxCh%a02SYs==3oL3XUn*pE#q>wjLX?FE@$H5Y#EocWn9jd zaXDMY<-E8b9*oP`GA?J!xSTEHa<+TkZ8sfRQ5aHby4%)^;^I5Q7t=HbjLoYyiQ3$2BDHLcr5 z>4|~DUPNI3z_EYnVVfc1-hk((DYDGv^EQJ=3YyUD;48aOrq4u1@LU7aYAWimKEq%t zmg6zLT8ASZ=lElo%c-7NBuuemnk+7|8MdGE(PG7`=POEn+8dePXZgA|jQ_&((SKam wBj3gF|6TW=g)ZhllsZ4WbzOz_SeL?jn`{22X|v6L7VDqD-^u(jmt+wD09Gj#`2YX_ literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/processing/brass_mixing.nbt b/src/main/resources/data/create/structures/gametest/processing/brass_mixing.nbt new file mode 100644 index 0000000000000000000000000000000000000000..a10e5e46876093a671575928b528a5390ebc32d5 GIT binary patch literal 2154 zcma)+e>fA`AIG;At1H~Pm15ETQhHo8Z73?#(~2mRX`z-e8>5i&d*t!0?CVD~W~(OT zrthL{EI;Q**N}y<`8izL_RPrdRc7^_?yY<8f8X=R>zwC&&htL!bI$vGj7S>*U)z@% znkeiYXIpEjt|yiGkiZ=pUD6n$jqE+u!4!DJMsPq zr~Sle0d96E;faQCFd_&6GtMRw32cpEG_+QD`CXh%|7e(C~ny9)@nJV8}|uXB(rRZ`m_!zXCTbm?Xw?5^ z_J;11iskN=FJ<+7obW+Cc;?*8%Q|t})9=1Ot8+pXKrPO%S(Vh_;fH$!slq#1pTHPi z;^GHd{}{13|DY$C5%NPviof|~Rt(|eAZ4bQ+-FWV?C@=Ioi>d^K48tw$$sxYZsU7p zJztr_m6fk3yH=DxsZ@D18jUCeZqiAM^lQ2=5IfqIze9;uuFcl6gi&v*<1eN!J64>B zklAMBPz-T6n2%5X zZGS^eW}dBq@@0EK6E#n1&DqE?e;;PsG+;nQkEXamm5f1_G{R|WAbBiQxS>Y>{>LTTWcS_xZ}!8>t<9bs4Z#uq16OY6k7hEcbwcF+C&{*vF6}^ zyjYU_xpPW|4T$A(j8#bps;PGb9RImq_OD=;fyY~e3w^hQCmK|uH*&gRtcUVS$Y>); ze|SNH#rTYxg4jJ4QpzD4_BlmFk)zV=pusKSc3~m%X+IO>cMpMyUz|`PW`Jl%_9!JkD_3I5N za3Zq@?K+S4sP6F;RS5lI;`NdenDzcoNPO%Iq(<=kTmj&GmKS*XtvBi+L>dtf zw~*GeHAZtQ!J!RZ+5X(kgZFo=`ovg9K6B<)g)Ok7bI<=8kx1e?iq9Rd=p7l_`a?~X zK-|7q{5tzs|CUt~I=P%lT9TcS-X!wGsnIp3Ta8nAoa-QJxY@~0u-B*64s;XZT|Gi<#;Pj=eyIUEabn1HAQh<1>r~Hst|g> zB2x05^f-!}en0y*N&R{|6F}taV2YpJE~){H3QAcbV-1Gita zptjijHYpcW-=eJ*8SY5I0P*lFbS1@do_1tI<_u)YQtoi3RhwUk{JbZ$ukCvFRuuJJ z5qtUx!*!TL&sXqq6@K!D2CE_BQ62)fvulOaYiCcBL@XWaSDKx6(!@z?zdT;vCdx^vjG(2q(kezL-UUh^7WUX433u(-t1E685r_jec8YG)KZ+) zVwPB4v;u%NLMwpZpaYNq(BJSQS|=`lHg}RB1;s6l3Y|!qBCy4VmmyqTZGG?9$ox!@ z#5nzz`fA_K|MMs|(38C;c`of(pQ$2B>h1P?-(3rg>>9%-q{+kA!zV+PS=tT%eO=0+ zIym=Hb4|e)78$~J_k!(&JubHioiAkc1q=pA#Zi`BVq{U_iE&fsOLA;8 z-m~=6L9@zW_Rz5T^0fzM3F8&$~X)&&Jx0skvT(H->h!l%u_+RN!XXQ46C-0H)ru+`>HUd+H&~u&DFW?Sa u%OV8YEvI5)t?q4qHDnyJxu>RX#mu8U5+y*Ppd6)@Qa6RR@m$PN0N{TlEk(xw literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/processing/brass_mixing_2.nbt b/src/main/resources/data/create/structures/gametest/processing/brass_mixing_2.nbt new file mode 100644 index 0000000000000000000000000000000000000000..44de42dfea2bf58181979644850faef5d3bc52fc GIT binary patch literal 2846 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 GIr1;~(w`3i literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/processing/crushing_wheel_crafting.nbt b/src/main/resources/data/create/structures/gametest/processing/crushing_wheel_crafting.nbt new file mode 100644 index 0000000000000000000000000000000000000000..099e093a93f170665dd3b83372af89ffbaad555c GIT binary patch literal 2715 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/processing/sand_washing.nbt b/src/main/resources/data/create/structures/gametest/processing/sand_washing.nbt new file mode 100644 index 0000000000000000000000000000000000000000..75ade7053ccdf27359f701f6bc19068a02178478 GIT binary patch literal 1370 zcmY+>dpOez9LMo4L$a1qC!VH~Yns_w#_Acxd0kFNXFFMRzgY?xkPMdi*qoM z4$Z>R#HR4H_@SLl?qOz3x#XB+PS1IsQ%~RL_2>Ja&-2}zwrSmex7NZK4DUzpNG$hv ziM}6%SCen4edD|9BBwbl1#IWN&yF)=M1&b_dz`9jZ;Y>+-@zNKkawT(K2j@BO))-R ztzrTa@19#I&y_F;G4q=Eb3vy?O)2>fVU!Nz}hlTaWJ^s2aUM6 zsKqJi_DKtWrKzP!&*I}iMknZrBVh2vchK#u8-gFsEbc?lQU9a5MyRS%zawk{_{1L$ z2z0dDzoX%PYJs0y%+IZ?S^~AAC-QHBjP@gt%^*-PF|Jb(7QAch8srPhE{Wu_hEo^92j_#48o zJ1-nvYqnVLT2L<*VJ*1>b#DaA1XXunV#Ofi^#l~kEP zpFT)k75Ow-YB**}Y&&oc&;dOtZBiU4|F-6;zpuNkZ{qs~OC=>ry`T&pqwcWhOBBOO zTdv#+KApeq4>*5-3m0g;OU8Qp&cFO3Y>+rd7WT76P zwd``lrBk$g(^H+X`ge0sew1OWrRTe)pBxN=@VzGER)c)|;~{l#Ok#Af=yJ zR`gUvW|i?0rfnloep7Zd6Y4sQp&}X!2Ba*om2|M@UayjJSKPrk>2aXK)=wOb=GG16 zKS1A8bt%1aYPyot{{j|@^!V!9G|rxZ#CXd)*vaku)r{%d>GwsI_L5ndG`MNuvdYjo zweVP9FzzEnSm!$~X^GS^4$8WNAa`{tEg`L9+DdgHMiAy4BOHbduW#`C%1oqS~x?QLKp7-dDF>aV4~vv6P$X3?x@41fkMLj6QKr zYh-g?*D&n8=D+&vfMLeG0+gtb@#Y3Xl%u;#ptT@>uEy-#O32PRqAtg?+8QHyOp6$J z*M^R;9JR^h)sILN*IMwVmjg2>vtyS$DGTNnxAA%(?Odn{qLkL_EaAz~^XEp_Ph-bW zq=_dbo(zotytj!KmuX=!+$Z`wd5=kfnf&1>dr;43X8`aZD7!3eG1rB}D-)&%jA@h$ ztEzh%yzOOc&u{#q_X3tywB2p@tD&si_-EQ#A8LO?j1!vPHi9v#bxbL;71D5$ymeqm z--#;dXm(hzr;>~!;WFO6{%0L6PD`FK3^V2?>Ek{KU|45;*oEgB*B!KSXJ$)m0#8s} vUw;Ahs1bSGgLlmo8A(>1Xkrl1z literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/processing/stone_cobble_sand_crushing.nbt b/src/main/resources/data/create/structures/gametest/processing/stone_cobble_sand_crushing.nbt new file mode 100644 index 0000000000000000000000000000000000000000..d56e9423d7fb0a826f2d386fb36c16d6f4519f4a GIT binary patch literal 2508 zcmY*adpy(o8$Y(4;mi~#IxV+jmGg6FGR!4uq+U{vxs_w6xr|ss8*`mHCHy3e5uH$M zxviX`VQF(>A#z#Bahp03ne9ZzZ>rOI{l0&^U+>rRd7jVfd3~Pe{r+laDtz$-x_xZy`W;u>Cl=w!J!@yfnzf8T6~TB^|z$NpL8@;Dbi-CfQ5oBdG8_x@$& zNOtw0-s`=)UdOI0xxLc)8+~6no*36WUS#c}z1ILh$#}S>xhYKUjaHsN`p&G0vthpG ztjP0`vTwJ;rAA1fibnlvRgAeM8E)k&+8jBTV;05YJo(9F%1LCxIqS>K54R5-xCDag zg@PPsP!?LLdn}A`{Vb~EOb|FLv*`OT|-rE(+0e{PnW3AIbC z?Cm+wJ9dRwjW2_U89%(f8X6XTGbgjHQAu(q48L&sv2MOMSZ1ZIG7KcCAoy8;sa+M%IB zg#(#xGeOx3eDQ7!y^&-bmWHU+UJ;2=tEett`R#%RT!pDLyIB19Dm)x&S zn0KvOM<@D`NBi7QcwoGURmEwm_lIdt((JjHktT;T!Suh(OsqJN^UG<-Ulf`?5>-Ox zs`VuzTg}-#hL+pp-dp>^Fy2oPr;De<63j$a)l1Q*5rS7lqadYYl0~8LqD`-PzTc&n zn@7~MdbCe0yX@Rh0VhYmyptdRrjF@HXW^Vw_}qYTR9h3e;4EAhNO#EQSpM6#6bj&%b!;hS@{wepIHSxfOF`c4hs}qCL z!dU)0yKcI^!{qU6dSUj&lGiHCHa{O=h^BOk}(lM7u{eD&8tE3ij3}geo4A>sp|9Qtbdt7pxG3JMk49vFae) z7m#G}M{Mrphn7AV953kgJX12;7fD+td#AoQ7&Z3WRJNjB%L>Ji%T?1m%g}E*^}tPm zu3ygwLcK89B0VVIa+~nwQmu7NJdyV^Ma(y`lA`TA8s& zu(Ca5_$G{=Sc3D*IdLRyS*uNh!P|V@m{|?XG)wr@rn7K}wthbjyHr;VOb9sB4D4Yn z9(+3tC}_5+e?s25*imaW^sXbEmE3PrKM`Rbi*1NV)BK*!sm*nZa1&f)Dld@n^Ff%v z_i}GLg6J9+KBdjD!h=Ia?coehUkBr%KUo@ov4^}$$O|==tznHK;lL~o8kkVLT5lnz zT`+joO-=mpa0r9m>f0Lp92@OTEf2QqHw$I2|m=? zo}k~3(-4(ZYh6?30lj)ZlS48$86wF&R)R5xJ7v5yBbcw1`q}CAw(Wi!WLo|t=zmoG zw$!;rxeNMmFm}^cF?vkGj(k4uJUM1pnSW!(W?q_Npz!l!eTd=53XVMuOgF4w$osTY zJE=fG%pt6@0{_QUYYTIeyI6K!K(p3nXHOhyWsdYAKH^r%bXj4Ypc8#z_z}}H@oo$w z5f(BLbm{JYE^EJ39>9_!TPGVr>viRrDBV#gjB&KXW1p_63kWyNhZ?9JrtGos$=#_d zTf}Ri>)SkKdlPdy_=^0JuaMSZsKEaM3*S@80NEc@qR6*T^;M}^ild9=Ff0z^@Q27T1ML&iay*n#}q3~LQ;b&Wn|%Wb?> zMO4O{zs80bJ<>Jqnb~Bd%SXlanvfXrKC^Jli>=(7!zDa2cQ&ke9$n$mr&n_lDD>v~ zpbh(%hJ&A)YkPOD{rA4IQ;qS9?sCU;9ru|BFFHTmVcn||{Zzv(Z@YF}>h4>$p2tA) zGiPbG^y!O9rE&NRbFh?xIW#1az`qx80}^X{+)x!8eM<>k-E#2o44Tpn{yjG1+0~@G zM@3A8ra6n>Xhx#`m7!&G@07>6@1_{8FZQ(16dN^oBx=@S40^oIGhSJgI*GqD6H(q> z;n8*Q10lw8)UE9U(ZhTx<44k>)AYNpS#-$(1M8OSskgdM>nYi{;A~Hgln@Z1rml^S z_F-98cGojh3bUa`Rd!``RQpO+@dnL!X&|a;{EtF>NZS62fND dUWrxhw29}b6FC=!l@Q)KH!Ye?X9@SWH00_ ziey^4U4FJXCqDEZtNH}V=4DJ9JL3x$(6^v)xf;7Hd;CnAO(`>&dCDOA?u&a0eJLa4^UiXEbcyLCQzP_XW1b3Or}#8$ zDf^`?%lPLY$^Omg(aTD5rjSQpL0n|QgHm;Yzm_W*u6ho`xp%$@Rfl zrdzHDxZP-)-%Zd{mNMj~Ks?T5m$y$#KJ#cm;^in%bfi+ka3 z0>Ko2(^xYg_*(-(HhtEAQy8`>EZW-W<#0F&*MaUkJ{-j8QG8vA;6A2hoQMqJLo|+s ziRE!d*ykH>23adVO}V+E(KZ1g(>K5aR5>VWPf~v9kK$#+kSK=*5j2W9sV5ZEEU+jc*;HC_jrGrCFjd zzfkslZB&YDYm})zVl-FQ85(z?aCUbK`5`fGh2j?_QXJXR;qWk9O-bL+ShYo`EFBC` z=&r>pWJ8Ghw@a_4z-1lJKv5|vJ3+NefhGM#7|j96y9+UA|i>Cz`LwV ztQINRvLF%@$GI;3og?AWo29wrgYp^LWG_zqu9yA$oezio5I;trd`7-ERL&}@acyqZ z!PE3E9pSJm8Wx+V*#7?N#qL$uqlYZl)}{^c~GcOHdd&=rgBe zP1A{=xmxJeSNU{L&N1u{?xf+>)s;s_d~M6(nQ_P=e1Bg1(_4v}qiyYJ2TCeqTMLf4 zp}Y?FLW=12K@QwyJ(-O=z>kO0?VX@6byOj&y$qN%%lI>3fgm7>t|fy70c8SRpR%eK zN6#8;WyqP*LtoFhDGIW!Hk#buIv~QXXiO}yNKz%p z;>|yPKMxH!sf^2$mL#9fwa~@_aCrc$08jml(*QOkl-^V=s@NL8%}-U&8BMJbOg_wM z6-=AAZi>y<>7sWr_QKhQo1LwwrjUUdy>&zENZOs$nOZwTPN$Xazt@VjSL2=EdU}Is z@*@qZ^+j5ut*>ewxt#N1?tDF(keQzH7&*{=k=I7)D%j$`;qeFfdc%EFi^FR#KsXpx zYX^H3rD)&$N^AGR?EE2dAkDwIeAwROdE6e;d=ls?$j%HEEJ_{>wb?#byU|3#aov&b=?Xg2UT}Q=)XV(jMzqoZ(if->~dV zesGA`vNraz?&Vwrwo&V1w11`3TX}Zk{HkM%6ih!0Lc8VASszF zQ~DhUHfae%>|61Ti8R`biBxEanS4!Pq5{NoNQn3h*ZS0om&Q@X11XkN;C_G#d`dO* z`SBP48o+@iN;*wde()ZNUD-^OnONeVC6MPtu$1`>sXQJ16||xzwK3q*zruERd2fV! z<(6Bs7x1NH7Y&u}ijsc~w=;_C<*-cqqS+o)AvtH(xcd^zxBXUrfK7k^+D6}CYgc$U=|Z4BV=(2xKjSXe4*9>B{-H~36V6=zu`%ce8nAj z1OMED^&GZr`0SM=cax62wyaXV;WIn*pkTkx|BGIEN;_Rg+eQa!;opkXza>1vp1v?y zWnFt~m52*=;QF4t!R#Oy^Vi`_dI_J?Wr!vI36;jI1yX97|d-Vr_YeDP-DlD+g0uj*T1eg$FZ&P^<;w{0Q?8%TOS7i literal 0 HcmV?d00001 diff --git a/src/main/resources/data/create/structures/gametest/processing/water_filling_bottle.nbt b/src/main/resources/data/create/structures/gametest/processing/water_filling_bottle.nbt new file mode 100644 index 0000000000000000000000000000000000000000..d2b527bdf19541a5142d5ef1c134ce64a2ed3408 GIT binary patch literal 1797 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 IhYS(`04c)v00000 literal 0 HcmV?d00001