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 47e73e743..c2f3776eb 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 + } } } @@ -138,6 +150,14 @@ repositories { flatDir { dirs 'libs' } + maven { + // Location of maven for CC: Tweaked + name = "squiddev" + url = "https://squiddev.cc/maven/" + content { + includeGroup "org.squiddev" + } + } } dependencies { @@ -165,6 +185,11 @@ dependencies { compileOnly fg.deobf("top.theillusivec4.curios:curios-forge:${curios_minecraft_version}-${curios_version}:api") runtimeOnly fg.deobf("top.theillusivec4.curios:curios-forge:${curios_minecraft_version}-${curios_version}") + if (cc_tweaked_enable.toBoolean()) { + compileOnly fg.deobf("org.squiddev:cc-tweaked-${cc_tweaked_minecraft_version}:${cc_tweaked_version}:api") + runtimeOnly fg.deobf("org.squiddev:cc-tweaked-${cc_tweaked_minecraft_version}:${cc_tweaked_version}") + } + // implementation fg.deobf("curse.maven:druidcraft-340991:3101903") // implementation fg.deobf("com.ferreusveritas.dynamictrees:DynamicTrees-1.16.5:0.10.0-Beta25") // runtimeOnly fg.deobf("vazkii.arl:AutoRegLib:1.4-35.69") @@ -182,6 +207,12 @@ dependencies { } } +sourceSets.main.java { + if (!cc_tweaked_enable.toBoolean()) { + exclude 'com/simibubi/create/compat/computercraft/implementation/**' + } +} + sourceSets.main.resources { srcDir 'src/generated/resources' exclude '.cache/' diff --git a/gradle.properties b/gradle.properties index a2a50b835..e34eb5fcb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,6 +27,10 @@ jei_version = 9.7.0.209 curios_minecraft_version = 1.18.2 curios_version = 5.0.7.0 +cc_tweaked_enable = true +cc_tweaked_minecraft_version = 1.18.2 +cc_tweaked_version = 1.100.10 + # curseforge information projectId = 328085 curse_type = beta diff --git a/src/generated/resources/.cache/cache b/src/generated/resources/.cache/cache index cd3a59bbf..830218058 100644 --- a/src/generated/resources/.cache/cache +++ b/src/generated/resources/.cache/cache @@ -582,7 +582,7 @@ bf2b0310500213ff853c748c236eb5d01f61658e assets/create/blockstates/yellow_toolbo 7f39521b211441f5c3e06d60c5978cebe16cacfb assets/create/blockstates/zinc_block.json b7181bcd8182b2f17088e5aa881f374c9c65470c assets/create/blockstates/zinc_ore.json fcaad84ac4ebdb1e6d9301b77245ce855dbde503 assets/create/lang/en_ud.json -a0e7d027d022330c5358d75165a76383d86ba122 assets/create/lang/en_us.json +5bca3a0eafdf9c29f5d6b692fd395741e4a815e6 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 8407d688d..07280a38b 100644 --- a/src/generated/resources/assets/create/lang/en_us.json +++ b/src/generated/resources/assets/create/lang/en_us.json @@ -1188,6 +1188,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/Create.java b/src/main/java/com/simibubi/create/Create.java index df0b498c0..f2fd9fbb7 100644 --- a/src/main/java/com/simibubi/create/Create.java +++ b/src/main/java/com/simibubi/create/Create.java @@ -11,6 +11,7 @@ import com.google.gson.GsonBuilder; import com.mojang.logging.LogUtils; import com.simibubi.create.api.behaviour.BlockSpoutingBehaviour; import com.simibubi.create.compat.Mods; +import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.compat.curios.Curios; import com.simibubi.create.content.contraptions.TorquePropagator; import com.simibubi.create.content.contraptions.fluids.tank.BoilerHeaters; @@ -140,6 +141,7 @@ public class Create { ContraptionMovementSetting.registerDefaults(); AllArmInteractionPointTypes.register(); BlockSpoutingBehaviour.registerDefaults(); + ComputerCraftProxy.register(); ForgeMod.enableMilkFluid(); CopperRegistries.inject(); diff --git a/src/main/java/com/simibubi/create/api/event/TrackGraphMergeEvent.java b/src/main/java/com/simibubi/create/api/event/TrackGraphMergeEvent.java new file mode 100644 index 000000000..af58070d2 --- /dev/null +++ b/src/main/java/com/simibubi/create/api/event/TrackGraphMergeEvent.java @@ -0,0 +1,22 @@ +package com.simibubi.create.api.event; + +import com.simibubi.create.content.logistics.trains.TrackGraph; + +import net.minecraftforge.eventbus.api.Event; + +public class TrackGraphMergeEvent extends Event{ + private TrackGraph mergedInto, mergedFrom; + + public TrackGraphMergeEvent(TrackGraph from, TrackGraph into) { + mergedInto = into; + mergedFrom = from; + } + + public TrackGraph getGraphMergedInto() { + return mergedInto; + } + + public TrackGraph getGraphMergedFrom() { + return mergedFrom; + } +} diff --git a/src/main/java/com/simibubi/create/compat/Mods.java b/src/main/java/com/simibubi/create/compat/Mods.java index 55fe10952..36bd60e52 100644 --- a/src/main/java/com/simibubi/create/compat/Mods.java +++ b/src/main/java/com/simibubi/create/compat/Mods.java @@ -5,7 +5,10 @@ import java.util.function.Supplier; import com.simibubi.create.foundation.utility.Lang; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; import net.minecraftforge.fml.ModList; +import net.minecraftforge.registries.ForgeRegistries; /** * For compatibility with and without another mod present, we have to define load conditions of the specific code @@ -14,6 +17,8 @@ public enum Mods { DYNAMICTREES, TCONSTRUCT, CURIOS, + + COMPUTERCRAFT, STORAGEDRAWERS, XLPACKETS; @@ -51,4 +56,8 @@ public enum Mods { toExecute.get().run(); } } + + public Block getBlock(String id) { + return ForgeRegistries.BLOCKS.getValue(new ResourceLocation(asId(), id)); + } } diff --git a/src/main/java/com/simibubi/create/compat/computercraft/AbstractComputerBehaviour.java b/src/main/java/com/simibubi/create/compat/computercraft/AbstractComputerBehaviour.java new file mode 100644 index 000000000..a8620450b --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/AbstractComputerBehaviour.java @@ -0,0 +1,57 @@ +package com.simibubi.create.compat.computercraft; + +import com.simibubi.create.foundation.tileEntity.SmartTileEntity; +import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour; +import com.simibubi.create.foundation.tileEntity.behaviour.BehaviourType; + +import net.minecraft.nbt.CompoundTag; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; + +public class AbstractComputerBehaviour extends TileEntityBehaviour { + + public static final BehaviourType TYPE = new BehaviourType<>(); + + boolean hasAttachedComputer; + + public AbstractComputerBehaviour(SmartTileEntity te) { + super(te); + this.hasAttachedComputer = false; + } + + @Override + public void read(CompoundTag nbt, boolean clientPacket) { + hasAttachedComputer = nbt.getBoolean("HasAttachedComputer"); + super.read(nbt, clientPacket); + } + + @Override + public void write(CompoundTag nbt, boolean clientPacket) { + nbt.putBoolean("HasAttachedComputer", hasAttachedComputer); + super.write(nbt, clientPacket); + } + + public boolean isPeripheralCap(Capability cap) { + return false; + } + + public LazyOptional getPeripheralCapability() { + return LazyOptional.empty(); + } + + public void removePeripheral() {} + + public void setHasAttachedComputer(boolean hasAttachedComputer) { + this.hasAttachedComputer = hasAttachedComputer; + } + + public boolean hasAttachedComputer() { + return hasAttachedComputer; + } + + @Override + public BehaviourType getType() { + return TYPE; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/AttachedComputerPacket.java b/src/main/java/com/simibubi/create/compat/computercraft/AttachedComputerPacket.java new file mode 100644 index 000000000..3181217fe --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/AttachedComputerPacket.java @@ -0,0 +1,37 @@ +package com.simibubi.create.compat.computercraft; + +import com.simibubi.create.foundation.networking.TileEntityDataPacket; +import com.simibubi.create.foundation.tileEntity.SmartTileEntity; +import com.simibubi.create.foundation.tileEntity.SyncedTileEntity; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; + +public class AttachedComputerPacket extends TileEntityDataPacket { + + private final boolean hasAttachedComputer; + + public AttachedComputerPacket(BlockPos tilePos, boolean hasAttachedComputer) { + super(tilePos); + this.hasAttachedComputer = hasAttachedComputer; + } + + public AttachedComputerPacket(FriendlyByteBuf buffer) { + super(buffer); + this.hasAttachedComputer = buffer.readBoolean(); + } + + @Override + protected void writeData(FriendlyByteBuf buffer) { + buffer.writeBoolean(hasAttachedComputer); + } + + @Override + protected void handlePacket(SyncedTileEntity tile) { + if (tile instanceof SmartTileEntity smartTile) { + smartTile.getBehaviour(AbstractComputerBehaviour.TYPE) + .setHasAttachedComputer(hasAttachedComputer); + } + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/ComputerCraftProxy.java b/src/main/java/com/simibubi/create/compat/computercraft/ComputerCraftProxy.java new file mode 100644 index 000000000..e424bfdad --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/ComputerCraftProxy.java @@ -0,0 +1,30 @@ +package com.simibubi.create.compat.computercraft; + +import java.util.function.Function; + +import com.simibubi.create.compat.Mods; +import com.simibubi.create.compat.computercraft.implementation.ComputerBehaviour; +import com.simibubi.create.foundation.tileEntity.SmartTileEntity; + +public class ComputerCraftProxy { + + public static void register() { + fallbackFactory = FallbackComputerBehaviour::new; + Mods.COMPUTERCRAFT.executeIfInstalled(() -> ComputerCraftProxy::registerWithDependency); + } + + private static void registerWithDependency() { + /* Comment if computercraft.implementation is not in the source set */ + computerFactory = ComputerBehaviour::new; + } + + private static Function fallbackFactory; + private static Function computerFactory; + + public static AbstractComputerBehaviour behaviour(SmartTileEntity ste) { + if (computerFactory == null) + return fallbackFactory.apply(ste); + return computerFactory.apply(ste); + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/ComputerScreen.java b/src/main/java/com/simibubi/create/compat/computercraft/ComputerScreen.java new file mode 100644 index 000000000..2d55a2554 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/ComputerScreen.java @@ -0,0 +1,96 @@ +package com.simibubi.create.compat.computercraft; + +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.simibubi.create.compat.Mods; +import com.simibubi.create.foundation.gui.AbstractSimiScreen; +import com.simibubi.create.foundation.gui.AllGuiTextures; +import com.simibubi.create.foundation.gui.AllIcons; +import com.simibubi.create.foundation.gui.element.GuiGameElement; +import com.simibubi.create.foundation.gui.widget.AbstractSimiWidget; +import com.simibubi.create.foundation.gui.widget.ElementWidget; +import com.simibubi.create.foundation.gui.widget.IconButton; +import com.simibubi.create.foundation.utility.Lang; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class ComputerScreen extends AbstractSimiScreen { + + private final AllGuiTextures background = AllGuiTextures.COMPUTER; + + private final Supplier displayTitle; + private final RenderWindowFunction additional; + private final Screen previousScreen; + private final Supplier hasAttachedComputer; + + private AbstractSimiWidget computerWidget; + private IconButton confirmButton; + + public ComputerScreen(Component title, @Nullable RenderWindowFunction additional, Screen previousScreen, Supplier hasAttachedComputer) { + this(title, () -> title, additional, previousScreen, hasAttachedComputer); + } + + public ComputerScreen(Component title, Supplier displayTitle, @Nullable RenderWindowFunction additional, Screen previousScreen, Supplier hasAttachedComputer) { + super(title); + this.displayTitle = displayTitle; + this.additional = additional; + this.previousScreen = previousScreen; + this.hasAttachedComputer = hasAttachedComputer; + } + + @Override + public void tick() { + if (!hasAttachedComputer.get()) + minecraft.setScreen(previousScreen); + + super.tick(); + } + + @Override + protected void init() { + setWindowSize(background.width, background.height); + super.init(); + + int x = guiLeft; + int y = guiTop; + + Mods.COMPUTERCRAFT.executeIfInstalled(() -> () -> { + computerWidget = new ElementWidget(x + 33, y + 38) + .showingElement(GuiGameElement.of(Mods.COMPUTERCRAFT.getBlock("computer_advanced"))); + computerWidget.getToolTip().add(Lang.translate("gui.attached_computer.hint").component()); + addRenderableWidget(computerWidget); + }); + + confirmButton = new IconButton(x + background.width - 33, y + background.height - 24, AllIcons.I_CONFIRM); + confirmButton.withCallback(this::onClose); + addRenderableWidget(confirmButton); + } + + + + @Override + protected void renderWindow(PoseStack ms, int mouseX, int mouseY, float partialTicks) { + int x = guiLeft; + int y = guiTop; + + background.render(ms, x, y, this); + + font.draw(ms, displayTitle.get(), x + background.width / 2.0F - font.width(displayTitle.get()) / 2.0F, y + 4, 0x442000); + font.drawWordWrap(Lang.translate("gui.attached_computer.controlled").component(), x + 55, y + 32, 111, 0x7A7A7A); + + if (additional != null) + additional.render(ms, mouseX, mouseY, partialTicks, x, y, background); + } + + @FunctionalInterface + public interface RenderWindowFunction { + + void render(PoseStack ms, int mouseX, int mouseY, float partialTicks, int guiLeft, int guiTop, AllGuiTextures background); + + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/FallbackComputerBehaviour.java b/src/main/java/com/simibubi/create/compat/computercraft/FallbackComputerBehaviour.java new file mode 100644 index 000000000..2e1a3da77 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/FallbackComputerBehaviour.java @@ -0,0 +1,16 @@ +package com.simibubi.create.compat.computercraft; + +import com.simibubi.create.foundation.tileEntity.SmartTileEntity; + +public class FallbackComputerBehaviour extends AbstractComputerBehaviour { + + public FallbackComputerBehaviour(SmartTileEntity te) { + super(te); + } + + @Override + public boolean hasAttachedComputer() { + return false; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/ComputerBehaviour.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/ComputerBehaviour.java new file mode 100644 index 000000000..dfe1f504d --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/ComputerBehaviour.java @@ -0,0 +1,74 @@ +package com.simibubi.create.compat.computercraft.implementation; + +import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour; +import com.simibubi.create.compat.computercraft.implementation.peripherals.DisplayLinkPeripheral; +import com.simibubi.create.compat.computercraft.implementation.peripherals.SequencedGearshiftPeripheral; +import com.simibubi.create.compat.computercraft.implementation.peripherals.SpeedControllerPeripheral; +import com.simibubi.create.compat.computercraft.implementation.peripherals.SpeedGaugePeripheral; +import com.simibubi.create.compat.computercraft.implementation.peripherals.StationPeripheral; +import com.simibubi.create.compat.computercraft.implementation.peripherals.StressGaugePeripheral; +import com.simibubi.create.content.contraptions.relays.advanced.SpeedControllerTileEntity; +import com.simibubi.create.content.contraptions.relays.advanced.sequencer.SequencedGearshiftTileEntity; +import com.simibubi.create.content.contraptions.relays.gauge.SpeedGaugeTileEntity; +import com.simibubi.create.content.contraptions.relays.gauge.StressGaugeTileEntity; +import com.simibubi.create.content.logistics.block.display.DisplayLinkTileEntity; +import com.simibubi.create.content.logistics.trains.management.edgePoint.station.StationTileEntity; +import com.simibubi.create.foundation.tileEntity.SmartTileEntity; + +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.CapabilityToken; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.common.util.NonNullSupplier; + +public class ComputerBehaviour extends AbstractComputerBehaviour { + + protected static final Capability PERIPHERAL_CAPABILITY = + CapabilityManager.get(new CapabilityToken<>() { + }); + LazyOptional peripheral; + NonNullSupplier peripheralSupplier; + + public ComputerBehaviour(SmartTileEntity te) { + super(te); + this.peripheralSupplier = getPeripheralFor(te); + } + + public static NonNullSupplier getPeripheralFor(SmartTileEntity te) { + if (te instanceof SpeedControllerTileEntity scte) + return () -> new SpeedControllerPeripheral(scte, scte.targetSpeed); + if (te instanceof DisplayLinkTileEntity dlte) + return () -> new DisplayLinkPeripheral(dlte); + if (te instanceof SequencedGearshiftTileEntity sgte) + return () -> new SequencedGearshiftPeripheral(sgte); + if (te instanceof SpeedGaugeTileEntity sgte) + return () -> new SpeedGaugePeripheral(sgte); + if (te instanceof StressGaugeTileEntity sgte) + return () -> new StressGaugePeripheral(sgte); + if (te instanceof StationTileEntity ste) + return () -> new StationPeripheral(ste); + + throw new IllegalArgumentException("No peripheral available for " + te.getType() + .getRegistryName()); + } + + @Override + public boolean isPeripheralCap(Capability cap) { + return cap == PERIPHERAL_CAPABILITY; + } + + @Override + public LazyOptional getPeripheralCapability() { + if (peripheral == null || !peripheral.isPresent()) + peripheral = LazyOptional.of(peripheralSupplier); + return peripheral.cast(); + } + + @Override + public void removePeripheral() { + if (peripheral != null) + peripheral.invalidate(); + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/CreateLuaTable.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/CreateLuaTable.java new file mode 100644 index 000000000..3c957274e --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/CreateLuaTable.java @@ -0,0 +1,172 @@ +package com.simibubi.create.compat.computercraft.implementation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaTable; +import dan200.computercraft.api.lua.LuaValues; + +public class CreateLuaTable implements LuaTable { + + private final Map map; + + public CreateLuaTable() { + this.map = new HashMap<>(); + } + + public CreateLuaTable(Map map) { + this.map = new HashMap<>(map); + } + + public boolean getBoolean(String key) throws LuaException { + Object value = get(key); + + if (!(value instanceof Boolean)) + throw LuaValues.badField(key, "boolean", LuaValues.getType(value)); + + return (Boolean) value; + } + + public String getString(String key) throws LuaException { + Object value = get(key); + + if (!(value instanceof String)) + throw LuaValues.badField(key, "string", LuaValues.getType(value)); + + return (String) value; + } + + public CreateLuaTable getTable(String key) throws LuaException { + Object value = get(key); + + if (!(value instanceof Map)) + throw LuaValues.badField(key, "table", LuaValues.getType(value)); + + return new CreateLuaTable((Map) value); + } + + public Optional getOptBoolean(String key) throws LuaException { + Object value = get(key); + + if (value == null) + return Optional.empty(); + + if (!(value instanceof Boolean)) + throw LuaValues.badField(key, "boolean", LuaValues.getType(value)); + + return Optional.of((Boolean) value); + } + + public Set stringKeySet() throws LuaException { + Set stringSet = new HashSet<>(); + + for (Object key : keySet()) { + if (!(key instanceof String)) + throw new LuaException("key " + key + " is not string (got " + LuaValues.getType(key) + ")"); + + stringSet.add((String) key); + } + + return Collections.unmodifiableSet(stringSet); + } + + public Collection tableValues() throws LuaException { + List tables = new ArrayList<>(); + + for (int i = 1; i <= size(); i++) { + Object value = get((double) i); + + if (!(value instanceof Map)) + throw new LuaException("value " + value + " is not table (got " + LuaValues.getType(value) + ")"); + + tables.add(new CreateLuaTable((Map) value)); + } + + return Collections.unmodifiableList(tables); + } + + public Map getMap() { + return map; + } + + @Nullable + @Override + public Object put(Object key, Object value) { + return map.put(key, value); + } + + public void putBoolean(String key, boolean value) { + map.put(key, value); + } + + public void putDouble(String key, double value) { + map.put(key, value); + } + + public void putString(String key, String value) { + map.put(key, value); + } + + public void putTable(String key, CreateLuaTable value) { + map.put(key, value); + } + + public void putTable(int i, CreateLuaTable value) { + map.put(i, value); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object o) { + return map.containsKey(o); + } + + @Override + public boolean containsValue(Object o) { + return map.containsValue(o); + } + + @Override + public Object get(Object o) { + return map.get(o); + } + + @NotNull + @Override + public Set keySet() { + return map.keySet(); + } + + @NotNull + @Override + public Collection values() { + return map.values(); + } + + @NotNull + @Override + public Set> entrySet() { + return map.entrySet(); + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/DisplayLinkPeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/DisplayLinkPeripheral.java new file mode 100644 index 000000000..0e9275227 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/DisplayLinkPeripheral.java @@ -0,0 +1,108 @@ +package com.simibubi.create.compat.computercraft.implementation.peripherals; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.jetbrains.annotations.NotNull; + +import com.simibubi.create.content.logistics.block.display.DisplayLinkContext; +import com.simibubi.create.content.logistics.block.display.DisplayLinkTileEntity; +import com.simibubi.create.content.logistics.block.display.target.DisplayTargetStats; + +import dan200.computercraft.api.lua.LuaFunction; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +public class DisplayLinkPeripheral extends SyncedPeripheral { + + public static final String TAG_KEY = "ComputerSourceList"; + private final AtomicInteger cursorX = new AtomicInteger(); + private final AtomicInteger cursorY = new AtomicInteger(); + + public DisplayLinkPeripheral(DisplayLinkTileEntity tile) { + super(tile); + } + + @LuaFunction + public final void setCursorPos(int x, int y) { + cursorX.set(x - 1); + cursorY.set(y - 1); + } + + @LuaFunction + public final Object[] getCursorPos() { + return new Object[] {cursorX.get() + 1, cursorY.get() + 1}; + } + + @LuaFunction(mainThread = true) + public final Object[] getSize() { + DisplayTargetStats stats = tile.activeTarget.provideStats(new DisplayLinkContext(tile.getLevel(), tile)); + return new Object[]{stats.maxRows(), stats.maxColumns()}; + } + + @LuaFunction + public final boolean isColor() { + return false; + } + + @LuaFunction + public final boolean isColour() { + return false; + } + + @LuaFunction + public final void write(String text) { + ListTag tag = tile.getSourceConfig().getList(TAG_KEY, Tag.TAG_STRING); + + int x = cursorX.get(); + int y = cursorY.get(); + + for (int i = tag.size(); i <= y; i++) { + tag.add(StringTag.valueOf("")); + } + + StringBuilder builder = new StringBuilder(tag.getString(y)); + + builder.append(" ".repeat(Math.max(0, x - builder.length()))); + builder.replace(x, x + text.length(), text); + + tag.set(y, StringTag.valueOf(builder.toString())); + + synchronized (tile) { + tile.getSourceConfig().put(TAG_KEY, tag); + } + + cursorX.set(x + text.length()); + } + + @LuaFunction + public final void clearLine() { + ListTag tag = tile.getSourceConfig().getList(TAG_KEY, Tag.TAG_STRING); + + if (tag.size() > cursorY.get()) + tag.set(cursorY.get(), StringTag.valueOf("")); + + synchronized (tile) { + tile.getSourceConfig().put(TAG_KEY, tag); + } + } + + @LuaFunction + public final void clear() { + synchronized (tile) { + tile.getSourceConfig().put(TAG_KEY, new ListTag()); + } + } + + @LuaFunction(mainThread = true) + public final void update() { + tile.tickSource(); + } + + @NotNull + @Override + public String getType() { + return "Create_DisplayLink"; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SequencedGearshiftPeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SequencedGearshiftPeripheral.java new file mode 100644 index 000000000..8addb181e --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SequencedGearshiftPeripheral.java @@ -0,0 +1,54 @@ +package com.simibubi.create.compat.computercraft.implementation.peripherals; + +import org.jetbrains.annotations.NotNull; + +import com.simibubi.create.content.contraptions.relays.advanced.sequencer.Instruction; +import com.simibubi.create.content.contraptions.relays.advanced.sequencer.InstructionSpeedModifiers; +import com.simibubi.create.content.contraptions.relays.advanced.sequencer.SequencedGearshiftTileEntity; +import com.simibubi.create.content.contraptions.relays.advanced.sequencer.SequencerInstructions; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; + +public class SequencedGearshiftPeripheral extends SyncedPeripheral { + + public SequencedGearshiftPeripheral(SequencedGearshiftTileEntity tile) { + super(tile); + } + + @LuaFunction(mainThread = true) + public final void rotate(IArguments arguments) throws LuaException { + runInstruction(arguments, SequencerInstructions.TURN_ANGLE); + } + + @LuaFunction(mainThread = true) + public final void move(IArguments arguments) throws LuaException { + runInstruction(arguments, SequencerInstructions.TURN_DISTANCE); + } + + @LuaFunction + public final boolean isRunning() { + return !this.tile.isIdle(); + } + + private void runInstruction(IArguments arguments, SequencerInstructions instructionType) throws LuaException { + int speedModifier = arguments.count() > 1 ? arguments.getInt(1) : 1; + this.tile.getInstructions().clear(); + + this.tile.getInstructions().add(new Instruction( + instructionType, + InstructionSpeedModifiers.getByModifier(speedModifier), + Math.abs(arguments.getInt(0)))); + this.tile.getInstructions().add(new Instruction(SequencerInstructions.END)); + + this.tile.run(0); + } + + @NotNull + @Override + public String getType() { + return "Create_SequencedGearshift"; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SpeedControllerPeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SpeedControllerPeripheral.java new file mode 100644 index 000000000..2aba811f0 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SpeedControllerPeripheral.java @@ -0,0 +1,35 @@ +package com.simibubi.create.compat.computercraft.implementation.peripherals; + +import org.jetbrains.annotations.NotNull; + +import com.simibubi.create.content.contraptions.relays.advanced.SpeedControllerTileEntity; +import com.simibubi.create.foundation.tileEntity.behaviour.scrollvalue.ScrollValueBehaviour; + +import dan200.computercraft.api.lua.LuaFunction; + +public class SpeedControllerPeripheral extends SyncedPeripheral { + + private final ScrollValueBehaviour targetSpeed; + + public SpeedControllerPeripheral(SpeedControllerTileEntity tile, ScrollValueBehaviour targetSpeed) { + super(tile); + this.targetSpeed = targetSpeed; + } + + @LuaFunction(mainThread = true) + public final void setTargetSpeed(int speed) { + this.targetSpeed.setValue(speed); + } + + @LuaFunction + public final float getTargetSpeed() { + return this.targetSpeed.getValue(); + } + + @NotNull + @Override + public String getType() { + return "Create_RotationSpeedController"; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SpeedGaugePeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SpeedGaugePeripheral.java new file mode 100644 index 000000000..a13b02cd5 --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SpeedGaugePeripheral.java @@ -0,0 +1,26 @@ +package com.simibubi.create.compat.computercraft.implementation.peripherals; + +import org.jetbrains.annotations.NotNull; + +import com.simibubi.create.content.contraptions.relays.gauge.SpeedGaugeTileEntity; + +import dan200.computercraft.api.lua.LuaFunction; + +public class SpeedGaugePeripheral extends SyncedPeripheral { + + public SpeedGaugePeripheral(SpeedGaugeTileEntity tile) { + super(tile); + } + + @LuaFunction + public final float getSpeed() { + return this.tile.getSpeed(); + } + + @NotNull + @Override + public String getType() { + return "Create_Speedometer"; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StationPeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StationPeripheral.java new file mode 100644 index 000000000..3de8e979b --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StationPeripheral.java @@ -0,0 +1,269 @@ +package com.simibubi.create.compat.computercraft.implementation.peripherals; + +import java.util.Map; + +import javax.annotation.Nullable; + +import org.jetbrains.annotations.NotNull; + +import com.simibubi.create.compat.computercraft.implementation.CreateLuaTable; +import com.simibubi.create.content.logistics.trains.entity.Train; +import com.simibubi.create.content.logistics.trains.management.edgePoint.station.GlobalStation; +import com.simibubi.create.content.logistics.trains.management.edgePoint.station.StationTileEntity; +import com.simibubi.create.content.logistics.trains.management.edgePoint.station.TrainEditPacket; +import com.simibubi.create.content.logistics.trains.management.schedule.Schedule; +import com.simibubi.create.foundation.networking.AllPackets; +import com.simibubi.create.foundation.utility.Components; +import com.simibubi.create.foundation.utility.StringHelper; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CollectionTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraftforge.network.PacketDistributor; + +public class StationPeripheral extends SyncedPeripheral { + + public StationPeripheral(StationTileEntity tile) { + super(tile); + } + + @LuaFunction(mainThread = true) + public final void assemble() throws LuaException { + if (!tile.isAssembling()) + throw new LuaException("station must be in assembly mode"); + + tile.assemble(null); + + if (tile.getStation() == null || tile.getStation().getPresentTrain() == null) + throw new LuaException("failed to assemble train"); + + if (!tile.exitAssemblyMode()) + throw new LuaException("failed to exit assembly mode"); + } + + @LuaFunction(mainThread = true) + public final void disassemble() throws LuaException { + if (tile.isAssembling()) + throw new LuaException("station must not be in assembly mode"); + + getTrainOrThrow(); + + if (!tile.enterAssemblyMode(null)) + throw new LuaException("could not disassemble train"); + } + + @LuaFunction(mainThread = true) + public final void setAssemblyMode(boolean assemblyMode) throws LuaException { + if (assemblyMode) { + if (!tile.enterAssemblyMode(null)) + throw new LuaException("failed to enter assembly mode"); + } else { + if (!tile.exitAssemblyMode()) + throw new LuaException("failed to exit assembly mode"); + } + } + + @LuaFunction + public final boolean isInAssemblyMode() { + return tile.isAssembling(); + } + + @LuaFunction + public final String getStationName() throws LuaException { + GlobalStation station = tile.getStation(); + if (station == null) + throw new LuaException("station is not connected to a track"); + + return station.name; + } + + @LuaFunction(mainThread = true) + public final void setStationName(String name) throws LuaException { + if (!tile.updateName(name)) + throw new LuaException("could not set station name"); + } + + @LuaFunction + public final boolean isTrainPresent() throws LuaException { + GlobalStation station = tile.getStation(); + if (station == null) + throw new LuaException("station is not connected to a track"); + + return station.getPresentTrain() != null; + } + + @LuaFunction + public final boolean isTrainImminent() throws LuaException { + GlobalStation station = tile.getStation(); + if (station == null) + throw new LuaException("station is not connected to a track"); + + return station.getImminentTrain() != null; + } + + @LuaFunction + public final boolean isTrainEnroute() throws LuaException { + GlobalStation station = tile.getStation(); + if (station == null) + throw new LuaException("station is not connected to a track"); + + return station.getNearestTrain() != null; + } + + @LuaFunction + public final String getTrainName() throws LuaException { + Train train = getTrainOrThrow(); + return train.name.getString(); + } + + @LuaFunction(mainThread = true) + public final void setTrainName(String name) throws LuaException { + Train train = getTrainOrThrow(); + train.name = Components.literal(name); + AllPackets.channel.send(PacketDistributor.ALL.noArg(), new TrainEditPacket.TrainEditReturnPacket(train.id, name, train.icon.getId())); + } + + @LuaFunction + public final boolean hasSchedule() throws LuaException { + Train train = getTrainOrThrow(); + return train.runtime.getSchedule() != null; + } + + @LuaFunction + public final CreateLuaTable getSchedule() throws LuaException { + Train train = getTrainOrThrow(); + + Schedule schedule = train.runtime.getSchedule(); + if (schedule == null) + throw new LuaException("train doesn't have a schedule"); + + return fromCompoundTag(schedule.write()); + } + + @LuaFunction(mainThread = true) + public final void setSchedule(IArguments arguments) throws LuaException { + Train train = getTrainOrThrow(); + Schedule schedule = Schedule.fromTag(toCompoundTag(new CreateLuaTable(arguments.getTable(0)))); + boolean autoSchedule = train.runtime.getSchedule() == null || train.runtime.isAutoSchedule; + train.runtime.setSchedule(schedule, autoSchedule); + } + + private @NotNull Train getTrainOrThrow() throws LuaException { + GlobalStation station = tile.getStation(); + if (station == null) + throw new LuaException("station is not connected to a track"); + + Train train = station.getPresentTrain(); + if (train == null) + throw new LuaException("there is no train present"); + + return train; + } + + private static @NotNull CreateLuaTable fromCompoundTag(CompoundTag tag) throws LuaException { + return (CreateLuaTable) fromNBTTag(null, tag); + } + + private static @NotNull Object fromNBTTag(@Nullable String key, Tag tag) throws LuaException { + byte type = tag.getId(); + + if (type == Tag.TAG_BYTE && key != null && key.equals("Count")) + return ((NumericTag) tag).getAsByte(); + else if (type == Tag.TAG_BYTE) + return ((NumericTag) tag).getAsByte() != 0; + else if (type == Tag.TAG_SHORT || type == Tag.TAG_INT || type == Tag.TAG_LONG) + return ((NumericTag) tag).getAsLong(); + else if (type == Tag.TAG_FLOAT || type == Tag.TAG_DOUBLE) + return ((NumericTag) tag).getAsDouble(); + else if (type == Tag.TAG_STRING) + return tag.getAsString(); + else if (type == Tag.TAG_LIST || type == Tag.TAG_BYTE_ARRAY || type == Tag.TAG_INT_ARRAY || type == Tag.TAG_LONG_ARRAY) { + CreateLuaTable list = new CreateLuaTable(); + CollectionTag listTag = (CollectionTag) tag; + + for (int i = 0; i < listTag.size(); i++) { + list.put(i + 1, fromNBTTag(null, listTag.get(i))); + } + + return list; + + } else if (type == Tag.TAG_COMPOUND) { + CreateLuaTable table = new CreateLuaTable(); + CompoundTag compoundTag = (CompoundTag) tag; + + for (String compoundKey : compoundTag.getAllKeys()) { + table.put( + StringHelper.camelCaseToSnakeCase(compoundKey), + fromNBTTag(compoundKey, compoundTag.get(compoundKey)) + ); + } + + return table; + } + + throw new LuaException("unknown tag type " + tag.getType().getName()); + } + + private static @NotNull CompoundTag toCompoundTag(CreateLuaTable table) throws LuaException { + return (CompoundTag) toNBTTag(null, table.getMap()); + } + + private static @NotNull Tag toNBTTag(@Nullable String key, Object value) throws LuaException { + if (value instanceof Boolean v) + return ByteTag.valueOf(v); + else if (value instanceof Byte || (key != null && key.equals("count"))) + return ByteTag.valueOf(((Number) value).byteValue()); + else if (value instanceof Number v) { + // If number is numerical integer + if (v.intValue() == v.doubleValue()) + return IntTag.valueOf(v.intValue()); + else + return DoubleTag.valueOf(v.doubleValue()); + + } else if (value instanceof String v) + return StringTag.valueOf(v); + else if (value instanceof Map v && v.containsKey(1.0)) { // List + ListTag list = new ListTag(); + for (Object o : v.values()) { + list.add(toNBTTag(null, o)); + } + + return list; + + } else if (value instanceof Map v) { // Table/Map + CompoundTag compound = new CompoundTag(); + for (Object objectKey : v.keySet()) { + if (!(objectKey instanceof String compoundKey)) + throw new LuaException("table key is not of type string"); + + compound.put( + // Items serialize their resource location as "id" and not as "Id". + // This check is needed to see if the 'i' should be left lowercase or not. + // Items store "count" in the same compound tag, so we can check for its presence to see if this is a serialized item + compoundKey.equals("id") && v.containsKey("count") ? "id" : StringHelper.snakeCaseToCamelCase(compoundKey), + toNBTTag(compoundKey, v.get(compoundKey)) + ); + } + + return compound; + } + + throw new LuaException("unknown object type " + value.getClass().getName()); + } + + @NotNull + @Override + public String getType() { + return "Create_Station"; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StressGaugePeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StressGaugePeripheral.java new file mode 100644 index 000000000..b712bd6da --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/StressGaugePeripheral.java @@ -0,0 +1,31 @@ +package com.simibubi.create.compat.computercraft.implementation.peripherals; + +import org.jetbrains.annotations.NotNull; + +import com.simibubi.create.content.contraptions.relays.gauge.StressGaugeTileEntity; + +import dan200.computercraft.api.lua.LuaFunction; + +public class StressGaugePeripheral extends SyncedPeripheral { + + public StressGaugePeripheral(StressGaugeTileEntity tile) { + super(tile); + } + + @LuaFunction + public final float getStress() { + return this.tile.getNetworkStress(); + } + + @LuaFunction + public final float getStressCapacity() { + return this.tile.getNetworkCapacity(); + } + + @NotNull + @Override + public String getType() { + return "Create_Stressometer"; + } + +} diff --git a/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SyncedPeripheral.java b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SyncedPeripheral.java new file mode 100644 index 000000000..a6371063d --- /dev/null +++ b/src/main/java/com/simibubi/create/compat/computercraft/implementation/peripherals/SyncedPeripheral.java @@ -0,0 +1,50 @@ +package com.simibubi.create.compat.computercraft.implementation.peripherals; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.simibubi.create.compat.computercraft.AttachedComputerPacket; +import com.simibubi.create.compat.computercraft.implementation.ComputerBehaviour; +import com.simibubi.create.foundation.networking.AllPackets; +import com.simibubi.create.foundation.tileEntity.SmartTileEntity; + +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraftforge.network.PacketDistributor; + +public abstract class SyncedPeripheral implements IPeripheral { + + protected final T tile; + private final AtomicInteger computers = new AtomicInteger(); + + public SyncedPeripheral(T tile) { + this.tile = tile; + } + + @Override + public void attach(@NotNull IComputerAccess computer) { + computers.incrementAndGet(); + updateTile(); + } + + @Override + public void detach(@NotNull IComputerAccess computer) { + computers.decrementAndGet(); + updateTile(); + } + + private void updateTile() { + boolean hasAttachedComputer = computers.get() > 0; + + tile.getBehaviour(ComputerBehaviour.TYPE).setHasAttachedComputer(hasAttachedComputer); + AllPackets.channel.send(PacketDistributor.ALL.noArg(), new AttachedComputerPacket(tile.getBlockPos(), hasAttachedComputer)); + } + + @Override + public boolean equals(@Nullable IPeripheral other) { + return this == other; + } + +} diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/gantry/GantryContraptionEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/gantry/GantryContraptionEntity.java index c7bef330b..d3cd16ded 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/gantry/GantryContraptionEntity.java +++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/gantry/GantryContraptionEntity.java @@ -202,6 +202,7 @@ public class GantryContraptionEntity extends AbstractContraptionEntity { } @Override + @OnlyIn(Dist.CLIENT) public void applyLocalTransforms(PoseStack matrixStack, float partialTicks) {} public void updateClientMotion() { diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java index da5c00300..5d541ad75 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java +++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java @@ -25,6 +25,7 @@ import net.minecraft.tags.BlockTags; import net.minecraft.tags.FluidTags; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; @@ -39,6 +40,7 @@ import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; import net.minecraft.world.phys.AABB; +import net.minecraftforge.common.ForgeConfig; import net.minecraftforge.common.Tags; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.fluids.FluidStack; @@ -54,6 +56,7 @@ public class OpenEndedPipe extends FlowSource { registerEffectHandler(new MilkEffectHandler()); registerEffectHandler(new WaterEffectHandler()); registerEffectHandler(new LavaEffectHandler()); + registerEffectHandler(new TeaEffectHandler()); } private Level world; @@ -443,4 +446,23 @@ public class OpenEndedPipe extends FlowSource { } } + public static class TeaEffectHandler implements IEffectHandler { + @Override + public boolean canApplyEffects(OpenEndedPipe pipe, FluidStack fluid) { + return fluid.getFluid().isSame(AllFluids.TEA.get()); + } + + @Override + public void applyEffects(OpenEndedPipe pipe, FluidStack fluid) { + Level world = pipe.getWorld(); + if (world.getGameTime() % 5 != 0) + return; + List entities = world + .getEntitiesOfClass(LivingEntity.class, pipe.getAOE(), LivingEntity::isAffectedByPotions); + for (LivingEntity entity : entities) { + entity.addEffect(new MobEffectInstance(MobEffects.DIG_SPEED, 21, 0, false, false, false)); + } + } + } + } 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/fluids/actors/SpoutRenderer.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/SpoutRenderer.java index 2ecd2fe59..45fe2475a 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/SpoutRenderer.java +++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/SpoutRenderer.java @@ -38,14 +38,24 @@ public class SpoutRenderer extends SafeBlockEntityRenderer { .getValue(partialTicks); if (!fluidStack.isEmpty() && level != 0) { + boolean top = fluidStack.getFluid() + .getAttributes() + .isLighterThanAir(); + level = Math.max(level, 0.175f); float min = 2.5f / 16f; float max = min + (11 / 16f); float yOffset = (11 / 16f) * level; + ms.pushPose(); - ms.translate(0, yOffset, 0); - FluidRenderer.renderFluidBox(fluidStack, min, min - yOffset, min, max, min, max, buffer, ms, light, - false); + if (!top) ms.translate(0, yOffset, 0); + else ms.translate(0, max - min, 0); + + FluidRenderer.renderFluidBox(fluidStack, + min, min - yOffset, min, + max, min, max, + buffer, ms, light, false); + ms.popPose(); } diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/EncasedPipeBlock.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/EncasedPipeBlock.java index 54badb0c1..923a138af 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/EncasedPipeBlock.java +++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/EncasedPipeBlock.java @@ -13,6 +13,8 @@ import java.util.function.Supplier; import com.simibubi.create.AllBlockEntityTypes; import com.simibubi.create.AllBlocks; +import com.simibubi.create.content.contraptions.components.structureMovement.ITransformableBlock; +import com.simibubi.create.content.contraptions.components.structureMovement.StructureTransform; import com.simibubi.create.content.contraptions.fluids.FluidPropagator; import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour; import com.simibubi.create.content.contraptions.relays.elementary.EncasedBlock; @@ -36,7 +38,9 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.PipeBlock; +import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; @@ -46,7 +50,8 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.ticks.TickPriority; -public class EncasedPipeBlock extends Block implements IWrenchable, ISpecialBlockItemRequirement, IBE, EncasedBlock { +public class EncasedPipeBlock extends Block + implements IWrenchable, ISpecialBlockItemRequirement, IBE, EncasedBlock, ITransformableBlock { public static final Map FACING_TO_PROPERTY_MAP = PipeBlock.PROPERTY_BY_DIRECTION; private final Supplier casing; @@ -173,4 +178,20 @@ public class EncasedPipeBlock extends Block implements IWrenchable, ISpecialBloc EncasedPipeBlock.transferSixWayProperties(state, defaultBlockState())); FluidTransportBehaviour.loadFlows(level, pos); } + + @Override + public BlockState rotate(BlockState pState, Rotation pRotation) { + return FluidPipeBlockRotation.rotate(pState, pRotation); + } + + @Override + public BlockState mirror(BlockState pState, Mirror pMirror) { + return FluidPipeBlockRotation.mirror(pState, pMirror); + } + + @Override + public BlockState transform(BlockState state, StructureTransform transform) { + return FluidPipeBlockRotation.transform(state, transform); + } + } diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java index 26697260d..d464b4586 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java +++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java @@ -8,6 +8,8 @@ import javax.annotation.Nullable; import com.simibubi.create.AllBlockEntityTypes; import com.simibubi.create.AllBlocks; +import com.simibubi.create.content.contraptions.components.structureMovement.ITransformableBlock; +import com.simibubi.create.content.contraptions.components.structureMovement.StructureTransform; import com.simibubi.create.content.contraptions.fluids.FluidPropagator; import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour; import com.simibubi.create.content.contraptions.relays.elementary.BracketedBlockEntityBehaviour; @@ -36,7 +38,9 @@ import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.PipeBlock; +import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.SimpleWaterloggedBlock; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; @@ -50,8 +54,8 @@ import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.ticks.TickPriority; -public class FluidPipeBlock extends PipeBlock - implements SimpleWaterloggedBlock, IWrenchableWithBracket, IBE, EncasableBlock { +public class FluidPipeBlock extends PipeBlock implements SimpleWaterloggedBlock, IWrenchableWithBracket, + IBE, EncasableBlock, ITransformableBlock { private static final VoxelShape OCCLUSION_BOX = Block.box(4, 4, 4, 12, 12, 12); @@ -337,4 +341,20 @@ public class FluidPipeBlock extends PipeBlock public VoxelShape getOcclusionShape(BlockState pState, BlockGetter pLevel, BlockPos pPos) { return OCCLUSION_BOX; } + + @Override + public BlockState rotate(BlockState pState, Rotation pRotation) { + return FluidPipeBlockRotation.rotate(pState, pRotation); + } + + @Override + public BlockState mirror(BlockState pState, Mirror pMirror) { + return FluidPipeBlockRotation.mirror(pState, pMirror); + } + + @Override + public BlockState transform(BlockState state, StructureTransform transform) { + return FluidPipeBlockRotation.transform(state, transform); + } + } diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlockRotation.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlockRotation.java new file mode 100644 index 000000000..c60a3a2a7 --- /dev/null +++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlockRotation.java @@ -0,0 +1,49 @@ +package com.simibubi.create.content.contraptions.fluids.pipes; + +import java.util.Map; + +import com.simibubi.create.content.contraptions.components.structureMovement.StructureTransform; +import com.simibubi.create.foundation.utility.Iterate; + +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.PipeBlock; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; + +public class FluidPipeBlockRotation { + + public static final Map FACING_TO_PROPERTY_MAP = PipeBlock.PROPERTY_BY_DIRECTION; + + public static BlockState rotate(BlockState state, Rotation rotation) { + BlockState rotated = state; + for (Direction direction : Iterate.horizontalDirections) + rotated = rotated.setValue(FACING_TO_PROPERTY_MAP.get(rotation.rotate(direction)), + state.getValue(FACING_TO_PROPERTY_MAP.get(direction))); + return rotated; + } + + public static BlockState mirror(BlockState state, Mirror mirror) { + BlockState mirrored = state; + for (Direction direction : Iterate.horizontalDirections) + mirrored = mirrored.setValue(FACING_TO_PROPERTY_MAP.get(mirror.mirror(direction)), + state.getValue(FACING_TO_PROPERTY_MAP.get(direction))); + return mirrored; + } + + public static BlockState transform(BlockState state, StructureTransform transform) { + if (transform.mirror != null) + state = mirror(state, transform.mirror); + + if (transform.rotationAxis == Direction.Axis.Y) + return rotate(state, transform.rotation); + + BlockState rotated = state; + for (Direction direction : Iterate.directions) + rotated = rotated.setValue(FACING_TO_PROPERTY_MAP.get(transform.rotateFacing(direction)), + state.getValue(FACING_TO_PROPERTY_MAP.get(direction))); + return rotated; + } + +} 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 d3cb6f53e..09b25e0aa 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 @@ -44,7 +44,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; @@ -214,7 +215,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/contraptions/relays/advanced/SpeedControllerBlockEntity.java b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/SpeedControllerBlockEntity.java index 28f5c8368..80dd29b5b 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/SpeedControllerBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/SpeedControllerBlockEntity.java @@ -2,6 +2,11 @@ package com.simibubi.create.content.contraptions.relays.advanced; import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour; +import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.content.contraptions.RotationPropagator; import com.simibubi.create.content.contraptions.base.KineticBlockEntity; import com.simibubi.create.content.contraptions.components.motor.KineticScrollValueBehaviour; @@ -20,11 +25,14 @@ import net.minecraft.core.Direction; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; public class SpeedControllerBlockEntity extends KineticBlockEntity { public static final int DEFAULT_SPEED = 16; - protected ScrollValueBehaviour targetSpeed; + public ScrollValueBehaviour targetSpeed; + public AbstractComputerBehaviour computerBehaviour; boolean hasBracket; @@ -50,6 +58,7 @@ public class SpeedControllerBlockEntity extends KineticBlockEntity { targetSpeed.value = DEFAULT_SPEED; targetSpeed.withCallback(i -> this.updateTargetRotation()); behaviours.add(targetSpeed); + behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this)); registerAwardables(behaviours, AllAdvancements.SPEED_CONTROLLER); } @@ -125,6 +134,20 @@ public class SpeedControllerBlockEntity extends KineticBlockEntity { .isHorizontal(); } + @NotNull + @Override + public LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) { + if (computerBehaviour.isPeripheralCap(cap)) + return computerBehaviour.getPeripheralCapability(); + return super.getCapability(cap, side); + } + + @Override + public void invalidateCaps() { + super.invalidateCaps(); + computerBehaviour.removePeripheral(); + } + private class ControllerValueBoxTransform extends ValueBoxTransform.Sided { @Override diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/ConfigureSequencedGearshiftPacket.java b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/ConfigureSequencedGearshiftPacket.java index 3834762d8..190053b92 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/ConfigureSequencedGearshiftPacket.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/ConfigureSequencedGearshiftPacket.java @@ -35,6 +35,9 @@ public class ConfigureSequencedGearshiftPacket extends BlockEntityConfigurationP @Override protected void applySettings(SequencedGearshiftBlockEntity be) { + if (be.computerBehaviour.hasAttachedComputer()) + return; + be.run(-1); be.instructions = Instruction.deserializeAll(instructions); be.sendData(); diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/Instruction.java b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/Instruction.java index 206cb444f..38af66e29 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/Instruction.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/Instruction.java @@ -19,8 +19,12 @@ public class Instruction { } public Instruction(SequencerInstructions instruction, int value) { + this(instruction, InstructionSpeedModifiers.FORWARD, value); + } + + public Instruction(SequencerInstructions instruction, InstructionSpeedModifiers speedModifier, int value) { this.instruction = instruction; - speedModifier = InstructionSpeedModifiers.FORWARD; + this.speedModifier = speedModifier; this.value = value; } diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/InstructionSpeedModifiers.java b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/InstructionSpeedModifiers.java index 93713d075..b1a1f236a 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/InstructionSpeedModifiers.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/InstructionSpeedModifiers.java @@ -1,6 +1,7 @@ package com.simibubi.create.content.contraptions.relays.advanced.sequencer; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import com.simibubi.create.foundation.utility.Components; @@ -36,4 +37,11 @@ public enum InstructionSpeedModifiers { return options; } + public static InstructionSpeedModifiers getByModifier(int modifier) { + return Arrays.stream(InstructionSpeedModifiers.values()) + .filter(speedModifier -> speedModifier.value == modifier) + .findAny() + .orElse(InstructionSpeedModifiers.FORWARD); + } + } diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftBlockEntity.java b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftBlockEntity.java index 453f5966c..717aa7ecd 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftBlockEntity.java @@ -1,9 +1,17 @@ package com.simibubi.create.content.contraptions.relays.advanced.sequencer; +import java.util.List; import java.util.Vector; +import javax.annotation.Nullable; + +import org.jetbrains.annotations.NotNull; + +import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour; +import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.content.contraptions.base.KineticBlockEntity; import com.simibubi.create.content.contraptions.relays.encased.SplitShaftBlockEntity; +import com.simibubi.create.foundation.blockEntity.BlockEntityBehaviour; import com.simibubi.create.foundation.utility.NBTHelper; import net.minecraft.core.BlockPos; @@ -12,6 +20,8 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; public class SequencedGearshiftBlockEntity extends SplitShaftBlockEntity { @@ -21,6 +31,8 @@ public class SequencedGearshiftBlockEntity extends SplitShaftBlockEntity { float currentInstructionProgress; int timer; boolean poweredPreviously; + + public AbstractComputerBehaviour computerBehaviour; public record SequenceContext(SequencerInstructions instruction, double relativeValue) { @@ -61,6 +73,12 @@ public class SequencedGearshiftBlockEntity extends SplitShaftBlockEntity { poweredPreviously = false; } + @Override + public void addBehaviours(List behaviours) { + super.addBehaviours(behaviours); + behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this)); + } + @Override public void tick() { super.tick(); @@ -103,6 +121,8 @@ public class SequencedGearshiftBlockEntity extends SplitShaftBlockEntity { } public void onRedstoneUpdate(boolean isPowered, boolean isRunning) { + if (computerBehaviour.hasAttachedComputer()) + return; if (!poweredPreviously && isPowered) risingFlank(); poweredPreviously = isPowered; @@ -136,7 +156,7 @@ public class SequencedGearshiftBlockEntity extends SplitShaftBlockEntity { } } - protected void run(int instructionIndex) { + public void run(int instructionIndex) { Instruction instruction = getInstruction(instructionIndex); if (instruction == null || instruction.instruction == SequencerInstructions.END) { if (getModifier() != 0) @@ -193,6 +213,20 @@ public class SequencedGearshiftBlockEntity extends SplitShaftBlockEntity { super.read(compound, clientPacket); } + @NotNull + @Override + public LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) { + if (computerBehaviour.isPeripheralCap(cap)) + return computerBehaviour.getPeripheralCapability(); + return super.getCapability(cap, side); + } + + @Override + public void invalidateCaps() { + super.invalidateCaps(); + computerBehaviour.removePeripheral(); + } + @Override public float getRotationSpeedModifier(Direction face) { if (isVirtual()) @@ -208,4 +242,8 @@ public class SequencedGearshiftBlockEntity extends SplitShaftBlockEntity { .getSpeedModifier(); } + public Vector getInstructions() { + return this.instructions; + } + } diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftScreen.java b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftScreen.java index ef80d1cb2..6d703a69c 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftScreen.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/advanced/sequencer/SequencedGearshiftScreen.java @@ -4,6 +4,7 @@ import java.util.Vector; import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.AllBlocks; +import com.simibubi.create.compat.computercraft.ComputerScreen; import com.simibubi.create.foundation.gui.AbstractSimiScreen; import com.simibubi.create.foundation.gui.AllGuiTextures; import com.simibubi.create.foundation.gui.AllIcons; @@ -15,7 +16,6 @@ import com.simibubi.create.foundation.networking.AllPackets; import com.simibubi.create.foundation.utility.Components; import com.simibubi.create.foundation.utility.Lang; -import net.minecraft.core.BlockPos; import net.minecraft.nbt.ListTag; import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; @@ -25,22 +25,26 @@ public class SequencedGearshiftScreen extends AbstractSimiScreen { private final ItemStack renderedItem = AllBlocks.SEQUENCED_GEARSHIFT.asStack(); private final AllGuiTextures background = AllGuiTextures.SEQUENCER; private IconButton confirmButton; + private SequencedGearshiftBlockEntity be; private ListTag compareTag; private Vector instructions; - private BlockPos pos; private Vector> inputs; public SequencedGearshiftScreen(SequencedGearshiftBlockEntity be) { super(Lang.translateDirect("gui.sequenced_gearshift.title")); this.instructions = be.instructions; - this.pos = be.getBlockPos(); + this.be = be; compareTag = Instruction.serializeAll(instructions); } @Override protected void init() { + if (be.computerBehaviour.hasAttachedComputer()) + minecraft.setScreen( + new ComputerScreen(title, this::renderAdditional, this, be.computerBehaviour::hasAttachedComputer)); + setWindowSize(background.width, background.height); setWindowOffset(-20, 0); super.init(); @@ -55,8 +59,7 @@ public class SequencedGearshiftScreen extends AbstractSimiScreen { for (int row = 0; row < instructions.size(); row++) initInputsOfRow(row, x, y); - confirmButton = - new IconButton(x + background.width - 33, y + background.height - 24, AllIcons.I_CONFIRM); + confirmButton = new IconButton(x + background.width - 33, y + background.height - 24, AllIcons.I_CONFIRM); confirmButton.withCallback(() -> { onClose(); }); @@ -127,6 +130,15 @@ public class SequencedGearshiftScreen extends AbstractSimiScreen { modifier.setState(instruction.speedModifier.ordinal()); } + @Override + public void tick() { + super.tick(); + + if (be.computerBehaviour.hasAttachedComputer()) + minecraft.setScreen( + new ComputerScreen(title, this::renderAdditional, this, be.computerBehaviour::hasAttachedComputer)); + } + @Override protected void renderWindow(PoseStack ms, int mouseX, int mouseY, float partialTicks) { int x = guiLeft; @@ -134,6 +146,13 @@ public class SequencedGearshiftScreen extends AbstractSimiScreen { background.render(ms, x, y, this); + for (int row = 0; row < instructions.capacity(); row++) { + AllGuiTextures toDraw = AllGuiTextures.SEQUENCER_EMPTY; + int yOffset = toDraw.height * row; + + toDraw.render(ms, x, y + 14 + yOffset, this); + } + for (int row = 0; row < instructions.capacity(); row++) { AllGuiTextures toDraw = AllGuiTextures.SEQUENCER_EMPTY; int yOffset = toDraw.height * row; @@ -157,9 +176,13 @@ public class SequencedGearshiftScreen extends AbstractSimiScreen { } font.draw(ms, title, x + (background.width - 8) / 2 - font.width(title) / 2, y + 4, 0x592424); + renderAdditional(ms, mouseX, mouseY, partialTicks, x, y, background); + } - GuiGameElement.of(renderedItem) - .at(x + background.width + 6, y + background.height - 56, -200) + private void renderAdditional(PoseStack ms, int mouseX, int mouseY, float partialTicks, int guiLeft, int guiTop, + AllGuiTextures background) { + GuiGameElement.of(renderedItem).at(guiLeft + background.width + 6, guiTop + background.height - 56, 100) .scale(5) .render(ms); } @@ -172,7 +195,8 @@ public class SequencedGearshiftScreen extends AbstractSimiScreen { ListTag serialized = Instruction.serializeAll(instructions); if (serialized.equals(compareTag)) return; - AllPackets.getChannel().sendToServer(new ConfigureSequencedGearshiftPacket(pos, serialized)); + AllPackets.getChannel() + .sendToServer(new ConfigureSequencedGearshiftPacket(be.getBlockPos(), serialized)); } @Override diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/GaugeBlockEntity.java b/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/GaugeBlockEntity.java index c22749cb2..682052bdc 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/GaugeBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/GaugeBlockEntity.java @@ -12,7 +12,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -public class GaugeBlockEntity extends KineticBlockEntity implements IHaveGoggleInformation { +public abstract class GaugeBlockEntity extends KineticBlockEntity implements IHaveGoggleInformation { public float dialTarget; public float dialState; @@ -52,4 +52,5 @@ public class GaugeBlockEntity extends KineticBlockEntity implements IHaveGoggleI return true; } + } diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/SpeedGaugeBlockEntity.java b/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/SpeedGaugeBlockEntity.java index 2efbd7d16..2ae2188fd 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/SpeedGaugeBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/SpeedGaugeBlockEntity.java @@ -2,24 +2,41 @@ package com.simibubi.create.content.contraptions.relays.gauge; import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour; +import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.content.contraptions.base.IRotate.SpeedLevel; +import com.simibubi.create.foundation.blockEntity.BlockEntityBehaviour; import com.simibubi.create.foundation.config.AllConfigs; import com.simibubi.create.foundation.utility.Color; import com.simibubi.create.foundation.utility.Lang; import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; public class SpeedGaugeBlockEntity extends GaugeBlockEntity { + public AbstractComputerBehaviour computerBehaviour; + public SpeedGaugeBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); } + @Override + public void addBehaviours(List behaviours) { + super.addBehaviours(behaviours); + behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this)); + } + @Override public void onSpeedChanged(float prevSpeed) { super.onSpeedChanged(prevSpeed); @@ -62,4 +79,19 @@ public class SpeedGaugeBlockEntity extends GaugeBlockEntity { .forGoggles(tooltip); return true; } + + @NotNull + @Override + public LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) { + if (computerBehaviour.isPeripheralCap(cap)) + return computerBehaviour.getPeripheralCapability(); + return super.getCapability(cap, side); + } + + @Override + public void invalidateCaps() { + super.invalidateCaps(); + computerBehaviour.removePeripheral(); + } + } diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/StressGaugeBlockEntity.java b/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/StressGaugeBlockEntity.java index 2244fe051..fb7dc9941 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/StressGaugeBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/contraptions/relays/gauge/StressGaugeBlockEntity.java @@ -2,6 +2,11 @@ package com.simibubi.create.content.contraptions.relays.gauge; import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour; +import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.content.contraptions.base.IRotate.StressImpact; import com.simibubi.create.foundation.advancement.AllAdvancements; import com.simibubi.create.foundation.blockEntity.BlockEntityBehaviour; @@ -13,14 +18,19 @@ import com.simibubi.create.foundation.utility.LangBuilder; import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; public class StressGaugeBlockEntity extends GaugeBlockEntity { + public AbstractComputerBehaviour computerBehaviour; + static BlockPos lastSent; public StressGaugeBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { @@ -30,6 +40,7 @@ public class StressGaugeBlockEntity extends GaugeBlockEntity { @Override public void addBehaviours(List behaviours) { super.addBehaviours(behaviours); + behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this)); registerAwardables(behaviours, AllAdvancements.STRESSOMETER, AllAdvancements.STRESSOMETER_MAXED); } @@ -141,4 +152,18 @@ public class StressGaugeBlockEntity extends GaugeBlockEntity { award(AllAdvancements.STRESSOMETER_MAXED); } + @NotNull + @Override + public LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) { + if (computerBehaviour.isPeripheralCap(cap)) + return computerBehaviour.getPeripheralCapability(); + return super.getCapability(cap, side); + } + + @Override + public void invalidateCaps() { + super.invalidateCaps(); + computerBehaviour.removePeripheral(); + } + } diff --git a/src/main/java/com/simibubi/create/content/logistics/block/belts/tunnel/BrassTunnelBlockEntity.java b/src/main/java/com/simibubi/create/content/logistics/block/belts/tunnel/BrassTunnelBlockEntity.java index 4795d42a4..026f017c6 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/belts/tunnel/BrassTunnelBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/belts/tunnel/BrassTunnelBlockEntity.java @@ -26,6 +26,7 @@ import com.simibubi.create.foundation.blockEntity.behaviour.filtering.FilteringB import com.simibubi.create.foundation.blockEntity.behaviour.filtering.SidedFilteringBehaviour; import com.simibubi.create.foundation.blockEntity.behaviour.scrollvalue.INamedIconOptions; import com.simibubi.create.foundation.blockEntity.behaviour.scrollvalue.ScrollOptionBehaviour; +import com.simibubi.create.foundation.config.AllConfigs; import com.simibubi.create.foundation.gui.AllIcons; import com.simibubi.create.foundation.utility.BlockHelper; import com.simibubi.create.foundation.utility.Components; @@ -174,7 +175,7 @@ public class BrassTunnelBlockEntity extends BeltTunnelBlockEntity implements IHa return; if (selectionMode.get() != SelectionMode.SYNCHRONIZE || syncedOutputActive) { - distributionProgress = 10; + distributionProgress = AllConfigs.server().logistics.brassTunnelTimer.get(); sendData(); } return; diff --git a/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotBlockEntity.java b/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotBlockEntity.java index 02fd38104..dc9748b71 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/depot/DepotBlockEntity.java @@ -7,6 +7,7 @@ import com.simibubi.create.foundation.blockEntity.SmartBlockEntity; 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 DepotBlockEntity extends SmartBlockEntity { 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/display/AllDisplayBehaviours.java b/src/main/java/com/simibubi/create/content/logistics/block/display/AllDisplayBehaviours.java index 9ac06e8fe..bdebd1da9 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/display/AllDisplayBehaviours.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/display/AllDisplayBehaviours.java @@ -9,6 +9,8 @@ import java.util.Map; import javax.annotation.Nullable; import com.simibubi.create.Create; +import com.simibubi.create.compat.Mods; +import com.simibubi.create.content.logistics.block.display.source.ComputerDisplaySource; import com.simibubi.create.content.logistics.block.display.source.DeathCounterDisplaySource; import com.simibubi.create.content.logistics.block.display.source.DisplaySource; import com.simibubi.create.content.logistics.block.display.source.EnchantPowerDisplaySource; @@ -241,5 +243,14 @@ public class AllDisplayBehaviours { assignBlockEntity(register(Create.asResource("scoreboard_display_source"), new ScoreboardDisplaySource()), BlockEntityType.COMMAND_BLOCK); assignBlockEntity(register(Create.asResource("enchant_power_display_source"), new EnchantPowerDisplaySource()), BlockEntityType.ENCHANTING_TABLE); assignBlock(register(Create.asResource("redstone_power_display_source"), new RedstonePowerDisplaySource()), Blocks.TARGET); + + Mods.COMPUTERCRAFT.executeIfInstalled(() -> () -> { + DisplayBehaviour computerDisplaySource = register(Create.asResource("computer_display_source"), new ComputerDisplaySource()); + + assignTile(computerDisplaySource, new ResourceLocation(Mods.COMPUTERCRAFT.asId(), "wired_modem_full")); + assignTile(computerDisplaySource, new ResourceLocation(Mods.COMPUTERCRAFT.asId(), "computer_normal")); + assignTile(computerDisplaySource, new ResourceLocation(Mods.COMPUTERCRAFT.asId(), "computer_advanced")); + assignTile(computerDisplaySource, new ResourceLocation(Mods.COMPUTERCRAFT.asId(), "computer_command")); + }); } } diff --git a/src/main/java/com/simibubi/create/content/logistics/block/display/DisplayLinkBlockEntity.java b/src/main/java/com/simibubi/create/content/logistics/block/display/DisplayLinkBlockEntity.java index 43fb1b603..54ae8af36 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/display/DisplayLinkBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/display/DisplayLinkBlockEntity.java @@ -2,6 +2,11 @@ package com.simibubi.create.content.logistics.block.display; import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour; +import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.content.logistics.block.display.source.DisplaySource; import com.simibubi.create.content.logistics.block.display.target.DisplayTarget; import com.simibubi.create.foundation.advancement.AllAdvancements; @@ -18,6 +23,8 @@ import net.minecraft.nbt.NbtUtils; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; public class DisplayLinkBlockEntity extends SmartBlockEntity { @@ -31,8 +38,9 @@ public class DisplayLinkBlockEntity extends SmartBlockEntity { public LerpedFloat glow; private boolean sendPulse; - + public int refreshTicks; + public AbstractComputerBehaviour computerBehaviour; public DisplayLinkBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); @@ -44,10 +52,16 @@ public class DisplayLinkBlockEntity extends SmartBlockEntity { glow.chase(0, 0.5f, Chaser.EXP); } + @Override + public void addBehaviours(List behaviours) { + behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this)); + registerAwardables(behaviours, AllAdvancements.DISPLAY_LINK, AllAdvancements.DISPLAY_BOARD); + } + @Override public void tick() { super.tick(); - + if (isVirtual()) { glow.tickChaser(); return; @@ -59,9 +73,9 @@ public class DisplayLinkBlockEntity extends SmartBlockEntity { glow.tickChaser(); return; } - + refreshTicks++; - if (refreshTicks < activeSource.getPassiveRefreshTicks()) + if (refreshTicks < activeSource.getPassiveRefreshTicks() || !activeSource.shouldPassiveReset()) return; tickSource(); } @@ -114,13 +128,8 @@ public class DisplayLinkBlockEntity extends SmartBlockEntity { activeSource.transferData(context, activeTarget, targetLine); sendPulse = true; sendData(); - - award(AllAdvancements.DISPLAY_LINK); - } - @Override - public void addBehaviours(List behaviours) { - registerAwardables(behaviours, AllAdvancements.DISPLAY_LINK, AllAdvancements.DISPLAY_BOARD); + award(AllAdvancements.DISPLAY_LINK); } @Override @@ -133,7 +142,7 @@ public class DisplayLinkBlockEntity extends SmartBlockEntity { protected void write(CompoundTag tag, boolean clientPacket) { super.write(tag, clientPacket); writeGatheredData(tag); - if (clientPacket && activeTarget != null) + if (clientPacket && activeTarget != null) tag.putString("TargetType", activeTarget.id.toString()); if (clientPacket && sendPulse) { sendPulse = false; @@ -173,6 +182,21 @@ public class DisplayLinkBlockEntity extends SmartBlockEntity { sourceConfig = data.copy(); } + @NotNull + @Override + public LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) { + if (computerBehaviour.isPeripheralCap(cap)) + return computerBehaviour.getPeripheralCapability(); + + return super.getCapability(cap, side); + } + + @Override + public void invalidateCaps() { + super.invalidateCaps(); + computerBehaviour.removePeripheral(); + } + public void target(BlockPos targetPosition) { this.targetOffset = targetPosition.subtract(worldPosition); } diff --git a/src/main/java/com/simibubi/create/content/logistics/block/display/source/ComputerDisplaySource.java b/src/main/java/com/simibubi/create/content/logistics/block/display/source/ComputerDisplaySource.java new file mode 100644 index 000000000..e3e988143 --- /dev/null +++ b/src/main/java/com/simibubi/create/content/logistics/block/display/source/ComputerDisplaySource.java @@ -0,0 +1,33 @@ +package com.simibubi.create.content.logistics.block.display.source; + +import java.util.ArrayList; +import java.util.List; + +import com.simibubi.create.content.logistics.block.display.DisplayLinkContext; +import com.simibubi.create.content.logistics.block.display.target.DisplayTargetStats; +import com.simibubi.create.foundation.utility.Components; + +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.MutableComponent; + +public class ComputerDisplaySource extends DisplaySource { + + @Override + public List provideText(DisplayLinkContext context, DisplayTargetStats stats) { + List components = new ArrayList<>(); + ListTag tag = context.sourceConfig().getList("ComputerSourceList", Tag.TAG_STRING); + + for (int i = 0; i < tag.size(); i++) { + components.add(Components.literal(tag.getString(i))); + } + + return components; + } + + @Override + public boolean shouldPassiveReset() { + return false; + } + +} diff --git a/src/main/java/com/simibubi/create/content/logistics/block/display/source/DisplaySource.java b/src/main/java/com/simibubi/create/content/logistics/block/display/source/DisplaySource.java index f7470e161..3fff48f59 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/display/source/DisplaySource.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/display/source/DisplaySource.java @@ -49,6 +49,10 @@ public abstract class DisplaySource extends DisplayBehaviour { return 100; }; + public boolean shouldPassiveReset() { + return true; + } + protected String getTranslationKey() { return id.getPath(); } diff --git a/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeBlockEntity.java b/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeBlockEntity.java index 8d1fe44fe..3cefc7e76 100644 --- a/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/logistics/block/redstone/NixieTubeBlockEntity.java @@ -123,6 +123,10 @@ public class NixieTubeBlockEntity extends SmartBlockEntity { customText = Optional.empty(); } + public int getRedstoneStrength() { + return redstoneStrength; + } + // @Override diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/TrackPropagator.java b/src/main/java/com/simibubi/create/content/logistics/trains/TrackPropagator.java index c755b4613..e401a04cf 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/TrackPropagator.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/TrackPropagator.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Set; import com.simibubi.create.Create; +import com.simibubi.create.api.event.TrackGraphMergeEvent; import com.simibubi.create.content.logistics.trains.TrackNodeLocation.DiscoveredLocation; import com.simibubi.create.content.logistics.trains.management.edgePoint.signal.SignalPropagator; @@ -16,6 +17,7 @@ import net.minecraft.util.Mth; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.MinecraftForge; public class TrackPropagator { @@ -135,6 +137,7 @@ public class TrackPropagator { if (graph == null) graph = other; else { + MinecraftForge.EVENT_BUS.post(new TrackGraphMergeEvent(other, graph)); other.transferAll(graph); manager.removeGraphAndGroup(other); sync.graphRemoved(other); diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/entity/Carriage.java b/src/main/java/com/simibubi/create/content/logistics/trains/entity/Carriage.java index c19ce6429..45d129f94 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/entity/Carriage.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/entity/Carriage.java @@ -108,7 +108,7 @@ public class Carriage { DimensionalCarriageEntity dimensional = getDimensional(level); dimensional.alignEntity(entity); - dimensional.removeAndSaveEntity(entity, false); + dimensional.removeAndSaveEntity(entity, true); } public DimensionalCarriageEntity getDimensional(Level level) { @@ -739,8 +739,8 @@ public class Carriage { } - private void dismountPlayer(ServerLevel sLevel, ServerPlayer sp, Integer seat, boolean portal) { - if (!portal) { + private void dismountPlayer(ServerLevel sLevel, ServerPlayer sp, Integer seat, boolean capture) { + if (!capture) { sp.stopRiding(); return; } diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/entity/Train.java b/src/main/java/com/simibubi/create/content/logistics/trains/entity/Train.java index 7190ccfe0..b5125ba80 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/entity/Train.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/entity/Train.java @@ -17,10 +17,6 @@ import java.util.function.Consumer; import javax.annotation.Nullable; -import com.simibubi.create.content.logistics.trains.track.AbstractBogeyBlockEntity; - -import net.minecraft.world.level.block.entity.BlockEntity; - import org.apache.commons.lang3.mutable.MutableBoolean; import org.apache.commons.lang3.mutable.MutableObject; @@ -46,6 +42,7 @@ import com.simibubi.create.content.logistics.trains.management.edgePoint.station import com.simibubi.create.content.logistics.trains.management.edgePoint.station.StationBlockEntity; import com.simibubi.create.content.logistics.trains.management.schedule.ScheduleRuntime; import com.simibubi.create.content.logistics.trains.management.schedule.ScheduleRuntime.State; +import com.simibubi.create.content.logistics.trains.track.AbstractBogeyBlockEntity; import com.simibubi.create.foundation.advancement.AllAdvancements; import com.simibubi.create.foundation.config.AllConfigs; import com.simibubi.create.foundation.networking.AllPackets; @@ -70,6 +67,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Explosion.BlockInteraction; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.fluids.FluidStack; @@ -90,6 +88,7 @@ public class Train { public boolean honk = false; public UUID id; + @Nullable public UUID owner; public TrackGraph graph; public Navigation navigation; @@ -1108,7 +1107,8 @@ public class Train { public CompoundTag write(DimensionPalette dimensions) { CompoundTag tag = new CompoundTag(); tag.putUUID("Id", id); - tag.putUUID("Owner", owner); + if (owner != null) + tag.putUUID("Owner", owner); if (graph != null) tag.putUUID("Graph", graph.id); tag.put("Carriages", NBTHelper.writeCompoundList(carriages, c -> c.write(dimensions))); @@ -1154,7 +1154,7 @@ public class Train { public static Train read(CompoundTag tag, Map trackNetworks, DimensionPalette dimensions) { UUID id = tag.getUUID("Id"); - UUID owner = tag.getUUID("Owner"); + UUID owner = tag.contains("Owner") ? tag.getUUID("Owner") : null; UUID graphId = tag.contains("Graph") ? tag.getUUID("Graph") : null; TrackGraph graph = graphId == null ? null : trackNetworks.get(graphId); List carriages = new ArrayList<>(); diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/entity/TrainPacket.java b/src/main/java/com/simibubi/create/content/logistics/trains/entity/TrainPacket.java index d7039017f..f16014085 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/entity/TrainPacket.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/entity/TrainPacket.java @@ -37,7 +37,10 @@ public class TrainPacket extends SimplePacketBase { if (!add) return; - UUID owner = buffer.readUUID(); + UUID owner = null; + if (buffer.readBoolean()) + owner = buffer.readUUID(); + List carriages = new ArrayList<>(); List carriageSpacing = new ArrayList<>(); @@ -75,7 +78,9 @@ public class TrainPacket extends SimplePacketBase { if (!add) return; - buffer.writeUUID(train.owner); + buffer.writeBoolean(train.owner != null); + if (train.owner != null) + buffer.writeUUID(train.owner); buffer.writeVarInt(train.carriages.size()); for (Carriage carriage : train.carriages) { 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 29853d561..ece16fcef 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 @@ -90,11 +90,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); @@ -108,11 +108,11 @@ public class FlapDisplaySection { spinning[i + 1] &= continueSpin; if (spinningTicks > max) spinning[i] = false; - + if (spinning[i]) spinningFlaps++; } - + return spinningFlaps; } @@ -170,10 +170,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/logistics/trains/management/display/GlobalTrainDisplayData.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/display/GlobalTrainDisplayData.java index 605d6c03f..24a4ed405 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/display/GlobalTrainDisplayData.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/display/GlobalTrainDisplayData.java @@ -30,10 +30,11 @@ public class GlobalTrainDisplayData { } public static List prepare(String filter, int maxLines) { + String regex = filter.isBlank() ? filter : "\\Q" + filter.replace("*", "\\E.*\\Q") + "\\E"; return statusByDestination.entrySet() .stream() .filter(e -> e.getKey() - .matches(filter.replace("*", ".*"))) + .matches(regex)) .flatMap(e -> e.getValue() .stream()) .sorted() diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/AbstractStationScreen.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/AbstractStationScreen.java index 2231b98fa..dea096157 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/AbstractStationScreen.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/AbstractStationScreen.java @@ -7,6 +7,7 @@ import com.jozufozu.flywheel.core.PartialModel; import com.jozufozu.flywheel.util.transform.TransformStack; import com.mojang.blaze3d.vertex.PoseStack; import com.simibubi.create.CreateClient; +import com.simibubi.create.compat.computercraft.ComputerScreen; import com.simibubi.create.content.logistics.trains.entity.Carriage; import com.simibubi.create.content.logistics.trains.entity.Train; import com.simibubi.create.content.logistics.trains.entity.TrainIconType; @@ -15,6 +16,7 @@ import com.simibubi.create.foundation.gui.AllGuiTextures; import com.simibubi.create.foundation.gui.AllIcons; import com.simibubi.create.foundation.gui.element.GuiGameElement; import com.simibubi.create.foundation.gui.widget.IconButton; +import com.simibubi.create.foundation.utility.Components; import net.minecraft.world.level.block.state.properties.BlockStateProperties; @@ -39,6 +41,10 @@ public abstract class AbstractStationScreen extends AbstractSimiScreen { @Override protected void init() { + if (te.computerBehaviour.hasAttachedComputer()) + minecraft.setScreen(new ComputerScreen(title, () -> Components.literal(station.name), + this::renderAdditional, this, te.computerBehaviour::hasAttachedComputer)); + setWindowSize(background.width, background.height); super.init(); clearWidgets(); @@ -71,17 +77,29 @@ public abstract class AbstractStationScreen extends AbstractSimiScreen { return w; } + @Override + public void tick() { + super.tick(); + + if (te.computerBehaviour.hasAttachedComputer()) + minecraft.setScreen(new ComputerScreen(title, () -> Components.literal(station.name), + this::renderAdditional, this, te.computerBehaviour::hasAttachedComputer)); + } + @Override protected void renderWindow(PoseStack ms, int mouseX, int mouseY, float partialTicks) { int x = guiLeft; int y = guiTop; background.render(ms, x, y, this); + renderAdditional(ms, mouseX, mouseY, partialTicks, x, y, background); + } + private void renderAdditional(PoseStack ms, int mouseX, int mouseY, float partialTicks, int guiLeft, int guiTop, AllGuiTextures background) { ms.pushPose(); TransformStack msr = TransformStack.cast(ms); msr.pushPose() - .translate(x + background.width + 4, y + background.height + 4, 100) + .translate(guiLeft + background.width + 4, guiTop + background.height + 4, 100) .scale(40) .rotateX(-22) .rotateY(63); diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationBlockEntity.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationBlockEntity.java index b162d47ff..f809c156d 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationBlockEntity.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationBlockEntity.java @@ -9,13 +9,18 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.UUID; +import java.util.function.Consumer; import javax.annotation.Nullable; +import org.jetbrains.annotations.NotNull; + import com.simibubi.create.AllBlocks; import com.simibubi.create.AllItems; import com.simibubi.create.AllSoundEvents; import com.simibubi.create.Create; +import com.simibubi.create.compat.computercraft.AbstractComputerBehaviour; +import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.content.contraptions.components.actors.DoorControlBehaviour; import com.simibubi.create.content.contraptions.components.structureMovement.AssemblyException; import com.simibubi.create.content.contraptions.components.structureMovement.ITransformableBlockEntity; @@ -23,6 +28,7 @@ import com.simibubi.create.content.contraptions.components.structureMovement.Str import com.simibubi.create.content.logistics.block.depot.DepotBehaviour; import com.simibubi.create.content.logistics.block.display.DisplayLinkBlock; import com.simibubi.create.content.logistics.trains.AbstractBogeyBlock; +import com.simibubi.create.content.logistics.trains.GraphLocation; import com.simibubi.create.content.logistics.trains.ITrackBlock; import com.simibubi.create.content.logistics.trains.TrackEdge; import com.simibubi.create.content.logistics.trains.TrackGraph; @@ -49,6 +55,7 @@ import com.simibubi.create.foundation.networking.AllPackets; import com.simibubi.create.foundation.utility.Iterate; import com.simibubi.create.foundation.utility.Lang; import com.simibubi.create.foundation.utility.NBTHelper; +import com.simibubi.create.foundation.utility.VecHelper; import com.simibubi.create.foundation.utility.WorldAttached; import com.simibubi.create.foundation.utility.animation.LerpedFloat; import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser; @@ -63,9 +70,11 @@ import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundSource; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.SoundType; @@ -90,6 +99,7 @@ public class StationBlockEntity extends SmartBlockEntity implements ITransformab protected int failedCarriageIndex; protected AssemblyException lastException; protected DepotBehaviour depotBehaviour; + public AbstractComputerBehaviour computerBehaviour; // for display UUID imminentTrain; @@ -122,6 +132,7 @@ public class StationBlockEntity extends SmartBlockEntity implements ITransformab depotBehaviour.addSubBehaviours(behaviours); registerAwardables(behaviours, AllAdvancements.CONTRAPTION_ACTORS, AllAdvancements.TRAIN, AllAdvancements.LONG_TRAIN, AllAdvancements.CONDUCTOR); + behaviours.add(computerBehaviour = ComputerCraftProxy.behaviour(this)); } @Override @@ -343,6 +354,63 @@ public class StationBlockEntity extends SmartBlockEntity implements ITransformab return true; } + public boolean enterAssemblyMode(@Nullable ServerPlayer sender) { + if (isAssembling()) + return false; + + tryDisassembleTrain(sender); + if (!tryEnterAssemblyMode()) + return false; + + BlockState newState = getBlockState().setValue(StationBlock.ASSEMBLING, true); + level.setBlock(getBlockPos(), newState, 3); + refreshBlockState(); + refreshAssemblyInfo(); + + updateStationState(station -> station.assembling = true); + GlobalStation station = getStation(); + if (station != null) { + for (Train train : Create.RAILWAYS.sided(level).trains.values()) { + if (train.navigation.destination != station) + continue; + + GlobalStation preferredDestination = train.runtime.startCurrentInstruction(); + train.navigation.startNavigation(preferredDestination != null ? preferredDestination : station, Double.MAX_VALUE, false); + } + } + + return true; + } + + public boolean exitAssemblyMode() { + if (!isAssembling()) + return false; + + cancelAssembly(); + BlockState newState = getBlockState().setValue(StationBlock.ASSEMBLING, false); + level.setBlock(getBlockPos(), newState, 3); + refreshBlockState(); + + return updateStationState(station -> station.assembling = false); + } + + public boolean tryDisassembleTrain(@Nullable ServerPlayer sender) { + GlobalStation station = getStation(); + if (station == null) + return false; + + Train train = station.getPresentTrain(); + if (train == null) + return false; + + BlockPos trackPosition = edgePoint.getGlobalPosition(); + if (!train.disassemble(getAssemblyDirection(), trackPosition.above())) + return false; + + dropSchedule(sender); + return true; + } + public boolean isAssembling() { BlockState state = getBlockState(); return state.hasProperty(StationBlock.ASSEMBLING) && state.getValue(StationBlock.ASSEMBLING); @@ -370,6 +438,42 @@ public class StationBlockEntity extends SmartBlockEntity implements ITransformab return true; } + public void dropSchedule(@Nullable ServerPlayer sender) { + GlobalStation station = getStation(); + if (station == null) + return; + + Train train = station.getPresentTrain(); + if (train == null) + return; + + ItemStack schedule = train.runtime.returnSchedule(); + if (schedule.isEmpty()) + return; + if (sender != null && sender.getMainHandItem().isEmpty()) { + sender.getInventory() + .placeItemBackInInventory(schedule); + return; + } + + Vec3 v = VecHelper.getCenterOf(getBlockPos()); + ItemEntity itemEntity = new ItemEntity(getLevel(), v.x, v.y, v.z, schedule); + itemEntity.setDeltaMovement(Vec3.ZERO); + getLevel().addFreshEntity(itemEntity); + } + + private boolean updateStationState(Consumer updateState) { + GlobalStation station = getStation(); + GraphLocation graphLocation = edgePoint.determineGraphLocation(); + if (station == null || graphLocation == null) + return false; + + updateState.accept(station); + Create.RAILWAYS.sync.pointAdded(graphLocation.graph, station); + Create.RAILWAYS.markTracksDirty(); + return true; + } + public void refreshAssemblyInfo() { if (!edgePoint.hasValidTrack()) return; @@ -450,6 +554,14 @@ public class StationBlockEntity extends SmartBlockEntity implements ITransformab map.put(worldPosition, BoundingBox.fromCorners(startPosition, trackEnd)); } + public boolean updateName(String name) { + if (!updateStationState(station -> station.name = name)) + return false; + notifyUpdate(); + + return true; + } + public boolean isValidBogeyOffset(int i) { if ((i < 3 || bogeyCount == 0) && i != 0) return false; @@ -744,12 +856,20 @@ public class StationBlockEntity extends SmartBlockEntity implements ITransformab } @Override - public LazyOptional getCapability(Capability cap, Direction side) { + public @NotNull LazyOptional getCapability(@NotNull Capability cap, Direction side) { if (isItemHandlerCap(cap)) return depotBehaviour.getItemCapability(cap, side); + if (computerBehaviour.isPeripheralCap(cap)) + return computerBehaviour.getPeripheralCapability(); return super.getCapability(cap, side); } + @Override + public void invalidateCaps() { + super.invalidateCaps(); + computerBehaviour.removePeripheral(); + } + private void applyAutoSchedule() { ItemStack stack = getAutoSchedule(); if (!AllItems.SCHEDULE.isIn(stack)) diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationEditPacket.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationEditPacket.java index 5f545f510..7b3bee2f0 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationEditPacket.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/edgePoint/station/StationEditPacket.java @@ -1,21 +1,14 @@ package com.simibubi.create.content.logistics.trains.management.edgePoint.station; -import com.simibubi.create.Create; import com.simibubi.create.content.contraptions.components.actors.DoorControl; -import com.simibubi.create.content.logistics.trains.GraphLocation; -import com.simibubi.create.content.logistics.trains.entity.Train; import com.simibubi.create.foundation.networking.BlockEntityConfigurationPacket; -import com.simibubi.create.foundation.utility.VecHelper; import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Mth; -import net.minecraft.world.entity.item.ItemEntity; -import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.Vec3; public class StationEditPacket extends BlockEntityConfigurationPacket { @@ -101,22 +94,15 @@ public class StationEditPacket extends BlockEntityConfigurationPacket T enumData(String key, Class enumClass) { T[] enumConstants = enumClass.getEnumConstants(); return enumConstants[data.getInt(key) % enumConstants.length]; } - + protected String textData(String key) { return data.getString(key); } - + protected int intData(String key) { return data.getInt(key); } - + } diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/FluidThresholdCondition.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/FluidThresholdCondition.java index 0c3f25f55..f9ab12a18 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/FluidThresholdCondition.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/FluidThresholdCondition.java @@ -68,7 +68,8 @@ public class FluidThresholdCondition extends CargoThresholdCondition { @Override protected void readAdditional(CompoundTag tag) { super.readAdditional(tag); - compareStack = ItemStack.of(tag.getCompound("Bucket")); + if (tag.contains("Bucket")) + compareStack = ItemStack.of(tag.getCompound("Bucket")); } @Override @@ -139,4 +140,4 @@ public class FluidThresholdCondition extends CargoThresholdCondition { Math.max(0, getThreshold() + offset), Lang.translateDirect("schedule.condition.threshold.buckets")); } -} \ No newline at end of file +} diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ItemThresholdCondition.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ItemThresholdCondition.java index 78905d201..81122ad73 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ItemThresholdCondition.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ItemThresholdCondition.java @@ -69,7 +69,8 @@ public class ItemThresholdCondition extends CargoThresholdCondition { @Override protected void readAdditional(CompoundTag tag) { super.readAdditional(tag); - stack = ItemStack.of(tag.getCompound("Item")); + if (tag.contains("Item")) + stack = ItemStack.of(tag.getCompound("Item")); } @Override @@ -131,4 +132,4 @@ public class ItemThresholdCondition extends CargoThresholdCondition { Math.max(0, getThreshold() + offset), Lang.translateDirect("schedule.condition.threshold." + (inStacks() ? "stacks" : "items"))); } -} \ No newline at end of file +} diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/RedstoneLinkCondition.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/RedstoneLinkCondition.java index 9bb8f4190..896c9a23f 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/RedstoneLinkCondition.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/RedstoneLinkCondition.java @@ -107,7 +107,8 @@ public class RedstoneLinkCondition extends ScheduleWaitCondition { @Override protected void readAdditional(CompoundTag tag) { - freq = Couple.deserializeEach(tag.getList("Frequency", Tag.TAG_COMPOUND), c -> Frequency.of(ItemStack.of(c))); + if (tag.contains("Frequency")) + freq = Couple.deserializeEach(tag.getList("Frequency", Tag.TAG_COMPOUND), c -> Frequency.of(ItemStack.of(c))); } @Override @@ -118,7 +119,7 @@ public class RedstoneLinkCondition extends ScheduleWaitCondition { .titled(Lang.translateDirect("schedule.condition.redstone_link.frequency_state")), "Inverted"); } - + @Override public MutableComponent getWaitingStatus(Level level, Train train, CompoundTag tag) { return Lang.translateDirect("schedule.condition.redstone_link.status"); diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ScheduleWaitCondition.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ScheduleWaitCondition.java index a8e307d6a..7da150cce 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ScheduleWaitCondition.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/condition/ScheduleWaitCondition.java @@ -16,16 +16,17 @@ import net.minecraft.world.level.Level; public abstract class ScheduleWaitCondition extends ScheduleDataEntry { public abstract boolean tickCompletion(Level level, Train train, CompoundTag context); - + protected void requestStatusToUpdate(CompoundTag context) { context.putInt("StatusVersion", context.getInt("StatusVersion") + 1); } - + public final CompoundTag write() { CompoundTag tag = new CompoundTag(); + CompoundTag dataCopy = data.copy(); + writeAdditional(dataCopy); tag.putString("Id", getId().toString()); - tag.put("Data", data.copy()); - writeAdditional(tag); + tag.put("Data", dataCopy); return tag; } @@ -43,11 +44,14 @@ public abstract class ScheduleWaitCondition extends ScheduleDataEntry { } ScheduleWaitCondition condition = supplier.get(); - condition.data = tag.getCompound("Data"); + // Left around for migration purposes. Data added in writeAdditional has moved into the "Data" tag condition.readAdditional(tag); + CompoundTag data = tag.getCompound("Data"); + condition.readAdditional(data); + condition.data = data; return condition; } public abstract MutableComponent getWaitingStatus(Level level, Train train, CompoundTag tag); -} \ No newline at end of file +} diff --git a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/destination/ScheduleInstruction.java b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/destination/ScheduleInstruction.java index cc3554094..6e1e56354 100644 --- a/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/destination/ScheduleInstruction.java +++ b/src/main/java/com/simibubi/create/content/logistics/trains/management/schedule/destination/ScheduleInstruction.java @@ -16,9 +16,10 @@ public abstract class ScheduleInstruction extends ScheduleDataEntry { public final CompoundTag write() { CompoundTag tag = new CompoundTag(); + CompoundTag dataCopy = data.copy(); + writeAdditional(dataCopy); tag.putString("Id", getId().toString()); - tag.put("Data", data.copy()); - writeAdditional(tag); + tag.put("Data", dataCopy); return tag; } @@ -36,9 +37,12 @@ public abstract class ScheduleInstruction extends ScheduleDataEntry { } ScheduleInstruction scheduleDestination = supplier.get(); - scheduleDestination.data = tag.getCompound("Data"); + // Left around for migration purposes. Data added in writeAdditional has moved into the "Data" tag scheduleDestination.readAdditional(tag); + CompoundTag data = tag.getCompound("Data"); + scheduleDestination.readAdditional(data); + scheduleDestination.data = data; return scheduleDestination; } -} \ 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 bd42779aa..6dae799f7 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.SchematicTableBlockEntity; -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 952c25d92..f268143e2 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,8 @@ 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.AllItems; import com.simibubi.create.AllKeys; @@ -15,34 +10,29 @@ 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.SchematicExport; +import com.simibubi.create.content.schematics.SchematicExport.SchematicExportResult; 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; import com.simibubi.create.foundation.utility.VecHelper; import com.simibubi.create.foundation.utility.outliner.Outliner; +import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; 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 +42,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 +202,31 @@ 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.getChannel().sendToServer(new InstantSchematicPacket(filename, origin, bounds)); - + AllPackets.getChannel() + .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 +234,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/events/ClientEvents.java b/src/main/java/com/simibubi/create/events/ClientEvents.java index 3e3d4a611..a2baade8a 100644 --- a/src/main/java/com/simibubi/create/events/ClientEvents.java +++ b/src/main/java/com/simibubi/create/events/ClientEvents.java @@ -295,14 +295,14 @@ public class ClientEvents { if (AllFluids.CHOCOLATE.get() .isSame(fluid)) { - event.scaleFarPlaneDistance(1f / 32f); + event.scaleFarPlaneDistance(1f / 32f * AllConfigs.CLIENT.chocolateTransparencyMultiplier.getF()); event.setCanceled(true); return; } if (AllFluids.HONEY.get() .isSame(fluid)) { - event.scaleFarPlaneDistance(1f / 8f); + event.scaleFarPlaneDistance(1f / 8f * AllConfigs.CLIENT.honeyTransparencyMultiplier.getF()); event.setCanceled(true); return; } 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 676a47d1f..5c532a142 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/config/CClient.java b/src/main/java/com/simibubi/create/foundation/config/CClient.java index 449c10a3f..1b0c8cb7e 100644 --- a/src/main/java/com/simibubi/create/foundation/config/CClient.java +++ b/src/main/java/com/simibubi/create/foundation/config/CClient.java @@ -31,7 +31,14 @@ public class CClient extends ConfigBase { public final ConfigInt ingameMenuConfigButtonOffsetX = i(-4, Integer.MIN_VALUE, Integer.MAX_VALUE, "ingameMenuConfigButtonOffsetX", Comments.ingameMenuConfigButtonOffsetX); public final ConfigBool ignoreFabulousWarning = b(false, "ignoreFabulousWarning", - Comments.ignoreFabulousWarning); + Comments.ignoreFabulousWarning); + + // custom fluid fog + public final ConfigGroup fluidFogSettings = group(1, "fluidFogSettings", Comments.fluidFogSettings); + public final ConfigFloat honeyTransparencyMultiplier = + f(1, .125f, 256, "honey", Comments.honeyTransparencyMultiplier); + public final ConfigFloat chocolateTransparencyMultiplier = + f(1, .125f, 256, "chocolate", Comments.chocolateTransparencyMultiplier); //overlay group public final ConfigGroup overlay = group(1, "goggleOverlay", @@ -149,6 +156,9 @@ public class CClient extends ConfigBase { static String mountedZoomMultiplier = "How far away the Camera should zoom when seated on a train"; static String showTrackGraphOnF3 = "Display nodes and edges of a Railway Network while f3 debug mode is active"; static String showExtendedTrackGraphOnF3 = "Additionally display materials of a Rail Network while f3 debug mode is active"; + static String fluidFogSettings = "Configure your vision range when submerged in Create's custom fluids"; + static String honeyTransparencyMultiplier = "The vision range through honey will be multiplied by this factor"; + static String chocolateTransparencyMultiplier = "The vision range though chocolate will be multiplied by this factor"; } } diff --git a/src/main/java/com/simibubi/create/foundation/config/CLogistics.java b/src/main/java/com/simibubi/create/foundation/config/CLogistics.java index a786847e3..97435638f 100644 --- a/src/main/java/com/simibubi/create/foundation/config/CLogistics.java +++ b/src/main/java/com/simibubi/create/foundation/config/CLogistics.java @@ -8,6 +8,7 @@ public class CLogistics extends ConfigBase { public final ConfigInt linkRange = i(256, 1, "linkRange", Comments.linkRange); public final ConfigInt displayLinkRange = i(64, 1, "displayLinkRange", Comments.displayLinkRange); public final ConfigInt vaultCapacity = i(20, 1, "vaultCapacity", Comments.vaultCapacity); + public final ConfigInt brassTunnelTimer = i(10, 1, 10, "brassTunnelTimer", Comments.brassTunnelTimer); @Override public String getName() { @@ -24,6 +25,7 @@ public class CLogistics extends ConfigBase { "The amount of ticks a portable storage interface waits for transfers until letting contraptions move along."; static String mechanicalArmRange = "Maximum distance in blocks a Mechanical Arm can reach across."; static String vaultCapacity = "The total amount of stacks a vault can hold per block in size."; + static String brassTunnelTimer = "The amount of ticks a brass tunnel waits between distributions."; } } diff --git a/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java b/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java index f9429281b..2333dd062 100644 --- a/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java +++ b/src/main/java/com/simibubi/create/foundation/gui/AllGuiTextures.java @@ -177,7 +177,7 @@ public enum AllGuiTextures implements ScreenElement { SPEECH_TOOLTIP_BACKGROUND("widgets", 0, 24, 8, 8), SPEECH_TOOLTIP_COLOR("widgets", 8, 24, 8, 8), - + TRAIN_HUD_SPEED_BG("widgets", 0, 190, 182, 5), TRAIN_HUD_SPEED("widgets", 0, 185, 182, 5), TRAIN_HUD_THROTTLE("widgets", 0, 195, 182, 5), @@ -189,7 +189,10 @@ public enum AllGuiTextures implements ScreenElement { TRAIN_PROMPT("widgets", 0, 230, 256, 16), // PlacementIndicator - PLACEMENT_INDICATOR_SHEET("placement_indicator", 0, 0, 16, 256); + PLACEMENT_INDICATOR_SHEET("placement_indicator", 0, 0, 16, 256), + + // ComputerCraft + COMPUTER("computer", 200, 102); ; 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/networking/AllPackets.java b/src/main/java/com/simibubi/create/foundation/networking/AllPackets.java index f26b66c1a..4220962fa 100644 --- a/src/main/java/com/simibubi/create/foundation/networking/AllPackets.java +++ b/src/main/java/com/simibubi/create/foundation/networking/AllPackets.java @@ -8,6 +8,7 @@ import java.util.function.Function; import java.util.function.Supplier; import com.simibubi.create.Create; +import com.simibubi.create.compat.computercraft.AttachedComputerPacket; import com.simibubi.create.content.contraptions.components.actors.controls.ContraptionDisableActorPacket; import com.simibubi.create.content.contraptions.components.structureMovement.ContraptionBlockChangedPacket; import com.simibubi.create.content.contraptions.components.structureMovement.ContraptionColliderLockPacket; @@ -202,7 +203,8 @@ public enum AllPackets { SET_FIRE_IMMUNE(NetheriteDivingHandler.SetFireImmunePacket.class, NetheriteDivingHandler.SetFireImmunePacket::new, PLAY_TO_CLIENT), CONTRAPTION_COLLIDER_LOCK(ContraptionColliderLockPacket.class, ContraptionColliderLockPacket::new, PLAY_TO_CLIENT), - + ATTACHED_COMPUTER(AttachedComputerPacket.class, AttachedComputerPacket::new, PLAY_TO_CLIENT), + ; public static final ResourceLocation CHANNEL_NAME = Create.asResource("main"); diff --git a/src/main/java/com/simibubi/create/foundation/ponder/content/PonderIndex.java b/src/main/java/com/simibubi/create/foundation/ponder/content/PonderIndex.java index c947baa05..ce9678e95 100644 --- a/src/main/java/com/simibubi/create/foundation/ponder/content/PonderIndex.java +++ b/src/main/java/com/simibubi/create/foundation/ponder/content/PonderIndex.java @@ -3,6 +3,7 @@ package com.simibubi.create.foundation.ponder.content; import com.simibubi.create.AllBlocks; import com.simibubi.create.AllItems; import com.simibubi.create.Create; +import com.simibubi.create.compat.Mods; import com.simibubi.create.content.logistics.trains.TrackMaterial; import com.simibubi.create.content.logistics.trains.track.TrackBlock; import com.simibubi.create.foundation.config.AllConfigs; @@ -23,7 +24,9 @@ import com.simibubi.create.foundation.ponder.content.trains.TrainSignalScenes; import com.simibubi.create.foundation.ponder.content.trains.TrainStationScenes; import com.tterrag.registrate.util.entry.BlockEntry; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.registries.RegistryObject; @@ -596,6 +599,12 @@ public class PonderIndex { .add(Blocks.COMMAND_BLOCK) .add(Blocks.TARGET); + Mods.COMPUTERCRAFT.executeIfInstalled(() -> () -> { + Block computer = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(Mods.COMPUTERCRAFT.asId(), "computer_advanced")); + if (computer != null) + PonderRegistry.TAGS.forTag(PonderTag.DISPLAY_SOURCES).add(computer); + }); + PonderRegistry.TAGS.forTag(PonderTag.DISPLAY_TARGETS) .add(AllBlocks.ORANGE_NIXIE_TUBE) .add(AllBlocks.DISPLAY_BOARD) 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/foundation/utility/StringHelper.java b/src/main/java/com/simibubi/create/foundation/utility/StringHelper.java new file mode 100644 index 000000000..78521b4e5 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/utility/StringHelper.java @@ -0,0 +1,46 @@ +package com.simibubi.create.foundation.utility; + +import java.util.Locale; + +public class StringHelper { + + public static String snakeCaseToCamelCase(String text) { + StringBuilder builder = new StringBuilder(); + builder.append(text.substring(0, 1).toUpperCase(Locale.ROOT)); + + for (int i = 1; i < text.length(); i++) { + int j = text.indexOf('_', i); + + if (j == -1) { + builder.append(text.substring(i)); + break; + } + + builder.append(text.substring(i, j).toLowerCase(Locale.ROOT)); + builder.append(text.substring(j + 1, j + 2).toUpperCase(Locale.ROOT)); + + i = j + 1; + } + + return builder.toString(); + } + + public static String camelCaseToSnakeCase(String text) { + StringBuilder builder = new StringBuilder(); + + for (char c : text.toCharArray()) { + if (Character.isUpperCase(c)) { + builder.append('_'); + builder.append(Character.toLowerCase(c)); + } else { + builder.append(c); + } + } + + if (builder.length() > 0 && builder.charAt(0) == '_') + builder.deleteCharAt(0); + + return builder.toString(); + } + +} 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 e60ca6cd0..de2d84281 100644 --- a/src/main/resources/assets/create/lang/default/interface.json +++ b/src/main/resources/assets/create/lang/default/interface.json @@ -322,6 +322,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/resources/assets/create/textures/gui/computer.png b/src/main/resources/assets/create/textures/gui/computer.png new file mode 100644 index 000000000..293bedad7 Binary files /dev/null and b/src/main/resources/assets/create/textures/gui/computer.png differ diff --git a/src/main/resources/create.mixins.json b/src/main/resources/create.mixins.json index ede90b11b..c36fcab35 100644 --- a/src/main/resources/create.mixins.json +++ b/src/main/resources/create.mixins.json @@ -13,10 +13,13 @@ "EnchantmentMixin", "EntityMixin", "LavaSwimmingMixin", + "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 000000000..62320b6d4 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/contraptions/arrow_dispenser.nbt differ 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 000000000..45e0887b8 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/contraptions/crop_farming.nbt differ 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 000000000..85f7e9666 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/contraptions/mounted_fluid_drain.nbt differ 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 000000000..dead6de25 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/contraptions/mounted_item_extract.nbt differ 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 000000000..64329baa3 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/contraptions/ploughing.nbt differ 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 000000000..60b165139 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/contraptions/redstone_contacts.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/contraptions/train_observer.nbt b/src/main/resources/data/create/structures/gametest/contraptions/train_observer.nbt new file mode 100644 index 000000000..84b841857 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/contraptions/train_observer.nbt differ 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 000000000..b7a9d2b54 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/fluids/3_pipe_combine.nbt differ 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 000000000..88f648db6 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/fluids/3_pipe_split.nbt differ 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 000000000..f42e51fe6 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/fluids/hose_pulley_transfer.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_in.nbt b/src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_in.nbt new file mode 100644 index 000000000..fef3d5ff4 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_in.nbt differ 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 000000000..cfa909345 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/fluids/in_world_pumping_out.nbt differ 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 000000000..4edf9e7bd Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/fluids/steam_engine.nbt differ 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 000000000..ab4e2beaf Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/andesite_tunnel_split.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/items/arm_purgatory.nbt b/src/main/resources/data/create/structures/gametest/items/arm_purgatory.nbt new file mode 100644 index 000000000..fa1aa5fcf Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/arm_purgatory.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/items/attribute_filters.nbt b/src/main/resources/data/create/structures/gametest/items/attribute_filters.nbt new file mode 100644 index 000000000..40e3c9614 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/attribute_filters.nbt differ 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 000000000..df3cb2642 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/belt_coaster.nbt differ 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 000000000..c877ddd0a Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_filtering.nbt differ 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 000000000..bdbbacd9e Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_prefer_nearest.nbt differ 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 000000000..310e8b882 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_round_robin.nbt differ 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 000000000..ec6eaf4e3 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_single_split.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/items/brass_tunnel_split.nbt b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_split.nbt new file mode 100644 index 000000000..059f61080 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_split.nbt differ 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 000000000..284d4e143 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/brass_tunnel_sync_input.nbt differ 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 000000000..61719d373 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/content_observer_counting.nbt differ 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 000000000..f266d3e7e Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/depot_display.nbt differ 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 000000000..65d268da8 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/stockpile_switch.nbt differ 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 000000000..f42e7a870 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/storages.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/items/vault_comparator_output.nbt b/src/main/resources/data/create/structures/gametest/items/vault_comparator_output.nbt new file mode 100644 index 000000000..51b8830d6 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/items/vault_comparator_output.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/misc/schematicannon.nbt b/src/main/resources/data/create/structures/gametest/misc/schematicannon.nbt new file mode 100644 index 000000000..f31d452c6 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/misc/schematicannon.nbt differ 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 000000000..2656d5a5b Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/misc/shearing.nbt differ 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 000000000..a10e5e468 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/brass_mixing.nbt differ 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 000000000..44de42dfe Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/brass_mixing_2.nbt differ 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 000000000..099e093a9 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/crushing_wheel_crafting.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/processing/iron_compacting.nbt b/src/main/resources/data/create/structures/gametest/processing/iron_compacting.nbt new file mode 100644 index 000000000..6a0ece131 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/iron_compacting.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/processing/precision_mechanism_crafting.nbt b/src/main/resources/data/create/structures/gametest/processing/precision_mechanism_crafting.nbt new file mode 100644 index 000000000..3820415b9 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/precision_mechanism_crafting.nbt differ 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 000000000..75ade7053 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/sand_washing.nbt differ 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 000000000..d56e9423d Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/stone_cobble_sand_crushing.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/processing/track_crafting.nbt b/src/main/resources/data/create/structures/gametest/processing/track_crafting.nbt new file mode 100644 index 000000000..6aa705296 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/track_crafting.nbt differ 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 000000000..d2b527bdf Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/water_filling_bottle.nbt differ diff --git a/src/main/resources/data/create/structures/gametest/processing/wheat_milling.nbt b/src/main/resources/data/create/structures/gametest/processing/wheat_milling.nbt new file mode 100644 index 000000000..3511d4d05 Binary files /dev/null and b/src/main/resources/data/create/structures/gametest/processing/wheat_milling.nbt differ diff --git a/wiki/Lua-Display-Link.md b/wiki/Lua-Display-Link.md new file mode 100644 index 000000000..d75969da1 --- /dev/null +++ b/wiki/Lua-Display-Link.md @@ -0,0 +1,88 @@ +| Method | Description | +|---------------------------------------|------------------------------------------------------| +| [`setCursorPos(x, y)`](#setCursorPos) | Sets the cursor position | +| [`getCursorPos()`](#getCursorPos) | Gets the current cursor position | +| [`getSize()`](#getSize) | Gets the display size of the connected target | +| [`isColor()`](#isColor) | Whether the connected display target supports color | +| [`isColour()`](#isColour) | Whether the connected display target supports colour | +| [`write(text)`](#writetext) | Writes text at the current cursor position | +| [`clearLine()`](#clearLine) | Clears the line at the current cursor position | +| [`clear()`](#clear) | Clears the whole display | +| [`update()`](#update) | Pushes an update to the display target | + +--- +### `setCursorPos(x, y)` +Sets the cursor position. Can be outside the bounds of the connected display. + +**Parameters** +- _x:_ `number` The cursor x position. +- _y:_ `number` The cursor y position. + +--- +### `getCursorPos()` +Gets the current cursor position. + +**Returns** +- `number` The cursor x position. +- `number` The cursor y position. + +--- +### `getSize()` +Gets the size of the connected display target. + +**Returns** +- `number` The width of the display. +- `number` The height of the display. + +--- +### `isColor()` +Checks whether the connected display target supports color. + +**Returns** +- `boolean` Whether the display supports color. + +--- +### `isColour()` +Checks whether the connected display target supports colour. + +**Returns** +- `boolean` Whether the display supports colour. + +--- +### `write(text)` +Writes text at the current cursor position, moving the cursor to the end of the text. +This only writes to an internal buffer. For the changes to show up on the display [`update()`](#update) must be used. +If the cursor is outside the bounds of the connected display, the text will not show up on the display. + +This will overwrite any text currently at the cursor position. + +**Parameters** +- _text:_ `string` The text to write. + +**See also** +- [`update()`](#update) To push the changes to the display target. + +--- +### `clearLine()` +Clears the line at the current cursor position. + +**See also** +- [`update()`](#update) To push the changes to the display target. + +--- +### `clear()` +Clears the whole display. + +**See also** +- [`update()`](#update) To push the changes to the display target. + +--- +### `update()` +Pushes any changes to the connected display target. +Any changes made are only made to an internal buffer. +For them to show up on the display they must be pushed to the display using this function. +This allows for this peripheral to be better multithreaded and for users to be able to change multiple lines at once by +using multiple [`write(text)`](#writetext) calls and then one [`update()`](#update) call. + +**See also** +- [`write(text)`](#writetext) To write text to the display target. diff --git a/wiki/Lua-Rotation-Speed-Controller.md b/wiki/Lua-Rotation-Speed-Controller.md new file mode 100644 index 000000000..ffd8ca785 --- /dev/null +++ b/wiki/Lua-Rotation-Speed-Controller.md @@ -0,0 +1,18 @@ +| Method | Description | +|-------------------------------------------------|----------------------------------------| +| [`setTargetSpeed(speed)`](#setTargetSpeedspeed) | Sets the target rotation speed | +| [`getTargetSpeed()`](#getTargetSpeed) | Gets the current target rotation speed | + +--- +### `setTargetSpeed(speed)` +Sets the rotation speed controller's target speed. + +**Parameters** +- _speed:_ `number` The target speed in RPM. Must be an integer within the range of [-256..256]. Values outside of this range will be clamped. + +--- +### `getTargetSpeed()` +Gets the rotation speed controller's current target speed. + +**Returns** +- `number` The current target rotation speed in RPM. diff --git a/wiki/Lua-Sequenced-Gearshift.md b/wiki/Lua-Sequenced-Gearshift.md new file mode 100644 index 000000000..9e7813322 --- /dev/null +++ b/wiki/Lua-Sequenced-Gearshift.md @@ -0,0 +1,28 @@ +| Method | Description | +|--------------------------------------------------------|--------------------------------------------------------------| +| [`rotate(angle, [modifier])`](#rotateangle-modifier) | Rotates shaft by a set angle | +| [`move(distance, [modifier])`](#movedistance-modifier) | Rotates shaft to move Piston/Pulley/Gantry by a set distance | +| [`isRunning()`](#isRunning) | Whether the gearshift is currently spinning | + +--- +### `rotate(angle, [modifier])` +Rotates connected components by a set angle. + +**Parameters** +- _angle:_ `number` Angle to rotate the shaft by in degrees. Must be a positive integer. To do backwards rotation, set _modifier_ to a negative value. +- _modifier?:_ `number = 1` Speed modifier which can be used to reverse rotation. Must be an integer within the range of [-2..2]. Values out of this range are ignored and the default of 1 is used. + +--- +### `move(distance, [modifier])` +Rotates connected components to move connected piston, pulley or gantry contractions by a set distance. + +**Parameters** +- _distance:_ `number` Distance to move connected piston, pulley or gantry contraptions by. Must be a positive integer. To do backwards movement, set _modifier_ to a negative value. +- _modifier?:_ `number = 1` Speed modifier which can be used to reverse direction. Must be an integer within the range of [-2..2]. Values out of this range are ignored and the default of 1 is used. + +--- +### `isRunning()` +Checks if the sequenced gearshift is currently spinning. + +**Returns** +- `boolean` Whether the sequenced gearshift is currently spinning. diff --git a/wiki/Lua-Speedometer.md b/wiki/Lua-Speedometer.md new file mode 100644 index 000000000..868714e0d --- /dev/null +++ b/wiki/Lua-Speedometer.md @@ -0,0 +1,10 @@ +| Method | Description | +|---------------------------|---------------------------------| +| [`getSpeed()`](#getSpeed) | Gets the current rotation speed | + +--- +### `getSpeed()` +Gets the current rotation speed of the attached components. + +**Returns** +- `number` The current rotation speed in RPM. diff --git a/wiki/Lua-Stressometer.md b/wiki/Lua-Stressometer.md new file mode 100644 index 000000000..0e609f5d0 --- /dev/null +++ b/wiki/Lua-Stressometer.md @@ -0,0 +1,18 @@ +| Method | Description | +|---------------------------------------------|--------------------------------| +| [`getStress()`](#getStress) | Gets the current stress level | +| [`getStressCapacity()`](#getStressCapacity) | Gets the total stress capacity | + +--- +### `getStress()` +Gets the connected network's current stress level. + +**Returns** +- `number` The current stress level in SU. + +--- +### `getStressCapacity()` +Gets the connected network's total stress capacity. + +**Returns** +- `number` The total stress capacity in SU. diff --git a/wiki/Lua-Train-Schedule.md b/wiki/Lua-Train-Schedule.md new file mode 100644 index 000000000..e221985ae --- /dev/null +++ b/wiki/Lua-Train-Schedule.md @@ -0,0 +1,195 @@ +Train schedules are represented by a table in Lua. The table contains a list of entries where each entry has a single instruction and multiple conditions. +Each instruction and condition has a `data` table that stores specific data about the instruction or condition. + +```lua +schedule = { + cyclic = true, -- Does the schedule repeat itself after the end has been reached? + entries = { -- List of entries, each entry contains a single instruction and multiple conditions. + { + instruction = { + id = "create:destination", -- The different instructions are described below. + data = { -- Data that is stored about the instruction. Different for each instruction type. + text = "Station 1", + }, + }, + conditions = { -- List of lists of conditions. The outer list is the "OR" list + { -- and the inner lists are "AND" lists. + { + id = "create:delay", -- The different conditions are described below. + data = { -- Data that is stored about the condition. Different for each condition type. + value = 5, + time_unit = 1, + }, + }, + { + id = "create:powered", + data = {}, + }, + }, + { + { + id = "create:time_of_day", + data = { + rotation = 0, + hour = 14, + minute = 0, + }, + }, + }, + }, + }, + }, +} +``` +--- +## Instructions +| ID | Description | +|----------------------------------------------|---------------------------------| +| [`"create:destination"`](#createdestination) | Move to a certain train station | +| [`"create:rename"`](#createrename) | Change the schedule title | +| [`"create:throttle"`](#createthrottle) | Change the train's throttle | + +--- +### `"create:destination"` +Moves the train to the chosen train station. This instruction must have at least one condition. + +**Data** +- _text:_ `string` The name of the station to travel to. Can include * as a wildcard. + +--- +### `"create:rename"` +Renames the schedule. This name shows up on display link targets. This instruction cannot have conditions. + +**Data** +- _text:_ `string` The name to rename the schedule to. + +--- +### `"create:throttle"` +Changes the throttle of the train. This instruction cannot have conditions. + +**Data** +- _value:_ `number` The throttle to set the train to. Must be an integer within the range of [5..100]. + +--- +## Conditions +Conditions are stored in a list of lists of conditions. The inner lists contain conditions that get `AND`'ed together. They must all be met for that group to be true. +The outer list contains the `AND`'ed groups of conditions that get `OR`'ed together. Only one of the groups needs to be true for the schedule to move onto the next instruction. + +| ID | Description | +|-----------------------------------------------------|-----------------------------------------------------| +| [`"create:delay"`](#createdelay) | Wait for a certain delay | +| [`"create:time_of_day"`](#createtimeofday) | Wait for a specific time of day | +| [`"create:fluid_threshold"`](#createfluidthreshold) | Wait for a certain amount of fluid to be on board | +| [`"create:item_threshold"`](#createitemthreshold) | Wait for a certain amount of items to be on board | +| [`"create:redstone_link"`](#createredstonelink) | Wait for a redstone link to be powered | +| [`"create:player_count"`](#createplayercount) | Wait for a certain amount of players to be on board | +| [`"create:idle"`](#createidle) | Wait for cargo loading inactivity | +| [`"create:unloaded"`](#createunloaded) | Wait for the current chunk to be unloaded | +| [`"create:powered"`](#createpowered) | Wait for the station to be powered | + +--- +### `"create:delay"` +Wait for a set delay. Can be measured in ticks, seconds or minutes. + +**Data** +- _value:_ `number` The amount of time to wait for. +- _time_unit:_ `number` The unit of time. 0 for ticks, 1 for seconds and 2 for minutes. + +--- +### `"create:time_of_day"` +Wait for a time of day, then repeat at a specified interval. + +**Data** +- _hour:_ `number` The hour of the day to wait for in a 24-hour format. Must be an integer within the range of [0..23]. +- _minute:_ `number` The minute of the hour to wait for. Must be an integer within the range of [0..59]. +- _rotation:_ `number` The interval to repeat at after the time of day has been met. Check the rotation table below for valid values. Must be an integer within the range of [0..9]. + +**Rotation** + +| Rotation | Time Interval | +|----------|------------------| +| 0 | Every Day | +| 1 | Every 12 Hours | +| 2 | Every 6 Hours | +| 3 | Every 4 Hours | +| 4 | Every 3 Hours | +| 5 | Every 2 Hours | +| 6 | Every Hour | +| 7 | Every 45 Minutes | +| 8 | Every 30 Minutes | +| 9 | Every 15 Minutes | + +--- +### `"create:fluid_threshold"` +Wait for a certain amount of a specific fluid to be loaded onto the train. + +**Data** +- _bucket:_ `table` The bucket item of the fluid. +- _threshold:_ `number` The threshold in number of buckets of fluid. Must be a positive integer. +- _operator:_ `number` Whether the condition should wait for the train to be loaded above the threshold, below the threshold or exactly at the threshold. 0 for greater than, 1 for less than, 2 for equal to. +- _measure:_ `number` The unit to measure the fluid in. This condition supports buckets as the only unit. Set to 0. + +**See also** +- [Items](#items) How items are represented in Lua. + +--- +### `"create:item_threshold"` +Wait for a certain amount of a specific item to be loaded onto the train. + +**Data** +- _item:_ `table` The item. +- _threshold:_ `number` The threshold of items. Must be a positive integer. +- _operator:_ `number` Whether the condition should wait for the train to be loaded above the threshold, below the threshold or exactly at the threshold. 0 for greater than, 1 for less than, 2 for equal to. +- _measure:_ `number` The unit to measure the items in. 0 for items. 1 for stacks of items. + +**See also** +- [Items](#items) How items are represented in Lua. + +--- +### `"create:redstone_link"` +Wait for a redstone link to be powered. + +**Data** +- _frequency:_ `{ table... }` A list of the two items making up the redstone link frequency. +- _inverted:_ `number` Whether the redstone link should be powered or not to meet the condition. 0 for powered. 1 for not powered. + +**See also** +- [Items](#items) How items are represented in Lua. + +--- +### `"create:player_count"` +Wait for a certain amount of players to be seated on the train. + +**Data** +- _count:_ `number` The number of players to be seated on the train. Must be a positive integer. +- _exact:_ `number` Whether the seated player count has to be exact to meet the condition. 0 for the exact amount of players seated, 1 for a greater than or equal amount of seated players. + +--- +### `"create:idle"` +Wait for a period of inactivity in loading or unloading the train. Can be measured in ticks, seconds or minutes. + +**Data** +- _value:_ `number` The amount of idle time to meet the condition. Must be a positive integer. +- _time_unit:_ `number` The unit of time. 0 for ticks, 1 for seconds and 2 for minutes. + +--- +### `"create:unloaded"` +Wait for the chunk the train is in to be unloaded. + +--- +### `"create:powered"` +Wait for the station to be powered with a redstone signal. + +--- +## Items +In Lua, items are represented with an ID and a count. + +```lua +item = { + id = "minecraft:stone", + count = 1, +} +``` + +- _id:_ `string` The ID of the item. +- _count:_ `number` The amount of items in the stack. For the purposes of working with train schedules the count should always be 1. Must be an integer. diff --git a/wiki/Lua-Train-Station.md b/wiki/Lua-Train-Station.md new file mode 100644 index 000000000..c8a39ae45 --- /dev/null +++ b/wiki/Lua-Train-Station.md @@ -0,0 +1,179 @@ +| Method | Description | +|-----------------------------------------------------------------|----------------------------------------------------| +| [`assemble()`](#assemble) | Assembles a new train at the station | +| [`disassemble()`](#disassemble) | Disassembles the currently present train | +| [`setAssemblyMode(assemblyMode)`](#setAssemblyModeassemblyMode) | Sets the station's assembly mode | +| [`isInAssemblyMode()`](#isInAssemblyMode) | Whether the station is in assembly mode | +| [`getStationName()`](#getStationName) | Gets the station's current name | +| [`setStationName(name)`](#setStationNamename) | Sets the station's name | +| [`isTrainPresent()`](#isTrainPresent) | Whether a train is present at the station | +| [`isTrainImminent()`](#isTrainImminent) | Whether a train is imminent to the station | +| [`isTrainEnroute()`](#isTrainEnroute) | Whether a train is enroute to the station | +| [`getTrainName()`](#getTrainName) | Gets the currently present train's name | +| [`setTrainName(name)`](#setTrainNamename) | Sets the currently present train's name | +| [`hasSchedule()`](#hasSchedule) | Whether the currently present train has a schedule | +| [`getSchedule()`](#getSchedule) | Gets the currently present train's schedule | +| [`setSchedule(schedule)`](#setScheduleschedule) | Sets the currently present train's schedule | + +--- +### `assemble()` +Assembles a new train at the station. The station must be in assembly mode prior to calling this function. +This function also causes the station to exit assembly mode after the train is done assembing. + +**Throws** +- If the station is not in assembly mode. +- If the station is not connected to a track. +- If the train failed to assemble. +- If the station failed to exit assembly mode. + +**See also** +- [`setAssemblyMode(assemblyMode)`](#setAssemblyModeassemblyMode) To set the assembly mode of the station. + +--- +### `disassemble()` +Disassembles the station's currently present train. The station must not be in assembly mode. + +**Throws** +- If the station is in assembly mode. +- If the station is not connected to a track. +- If there is currently no train present at the station. +- If the train failed to disassemble. + +**See also** +- [`setAssemblyMode(assemblyMode)`](#setAssemblyModeassemblyMode) To set the assembly mode of the station. + +--- +### `setAssemblyMode(assemblyMode)` +Sets the station's assembly mode. + +**Parameters** +- _assemblyMode:_ `boolean` Whether the station should be in assembly mode. + +**Throws** +- If the station fails to enter or exit assembly mode. +- If the station is not connected to a track. + +--- +### `isInAssemblyMode()` +Checks whether the station is in assembly mode. + +**Returns** +- `boolean` Whether the station is in assembly mode. + +--- +### `getStationName()` +Gets the station's current name. + +**Returns** +- `string` The station's current name. + +**Throws** +- If the station is not connected to a track. + +--- +### `setStationName(name)` +Sets the station's name. + +**Parameters** +- _name:_ `string` What to set the station's name to. + +**Throws** +- If the station name fails to be set. +- If the station is not connected to a track. + +--- +### `isTrainPresent()` +Checks whether a train is currently present at the station. + +**Returns** +- `boolean` Whether a train is present at the station. + +**Throws** +- If the station is not connected to a track. + +--- +### `isTrainImminent()` +Checks whether a train is imminently arriving at the station. +Imminent is defined as being within 30 blocks of the station. +This will not be true if the train has arrived and stopped at the station. + +**Returns** +- `boolean` Whether a train is imminent to the station. + +**Throws** +- If the station is not connected to a track. + +**See also** +- [`isTrainPresent()`](#isTrainPresent) To check if a train is present at the station. + +--- +### `isTrainEnroute()` +Checks whether a train is enroute and navigating to the station. + +**Returns** +- `boolean` Whether a train is enroute to the station. + +**Throws** +- If the station is not connected to a track. + +--- +### `getTrainName()` +Gets the currently present train's name. + +**Returns** +- `string` The currently present train's name. + +**Throws** +- If the station is not connected to a track. +- If there is currently no train present at the station. + +--- +### `setTrainName(name)` +Sets the currently present train's name. + +**Parameters** +- _name:_ `string` What to set the currently present train's name to. + +**Throws** +- If the station is not connected to a track. +- If there is currently no train present at the station. + +--- +### `hasSchedule()` +Checks whether the currently present train has a schedule. + +**Returns** +- `boolean` Whether the currently present train has a schedule. + +**Throws** +- If the station is not connected to a track. +- If there is currently no train present at the station. + +--- +### `getSchedule()` +Gets the currently present train's schedule. + +**Returns** +- `table` The train's schedule + +**Throws** +- If the station is not connected to a track. +- If there is currently no train present at the station. +- If the present train doesn't have a schedule. + +**See also** +- [Lua Train Schedules](#Lua-Train-Schedules) How train schedules are represented in Lua. + +--- +### `setSchedule(schedule)` +Sets the currently present train's schedule. This will overwrite the currently set schedule. + +**Parameters** +- _schedule:_ `table` The schedule to set the present train to. + +**Throws** +- If the station is not connected to a track. +- If there is currently no train present at the station. + +**See also** +- [Lua Train Schedules](#Lua-Train-Schedules) How train schedules are represented in Lua. diff --git a/wiki/README.md b/wiki/README.md new file mode 100644 index 000000000..cf91b97ae --- /dev/null +++ b/wiki/README.md @@ -0,0 +1,2 @@ +Just before this PR is about to be merged this /wiki folder will be removed from the PR and the pages will be added to +the wiki section of the Create GitHub under API Reference