}
String subprotocol = headers.get( HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL );
- WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
+ WebSocketClientHandshaker handshaker = new NoOriginWebSocketHanshakder(
uri, WebSocketVersion.V13, subprotocol, true, headers,
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
);
diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerSide.java b/src/main/java/dan200/computercraft/core/computer/ComputerSide.java
index 85fcfab57..897ae91b9 100644
--- a/src/main/java/dan200/computercraft/core/computer/ComputerSide.java
+++ b/src/main/java/dan200/computercraft/core/computer/ComputerSide.java
@@ -5,14 +5,11 @@
*/
package dan200.computercraft.core.computer;
-import net.minecraft.core.Direction;
-
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
- * A side on a computer. Unlike {@link Direction}, this is relative to the direction the computer is
- * facing..
+ * A side on a computer. This is relative to the direction the computer is facing.
*/
public enum ComputerSide
{
diff --git a/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadExecutor.java b/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadExecutor.java
index c70f98cc7..2cccf3933 100644
--- a/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadExecutor.java
+++ b/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadExecutor.java
@@ -10,7 +10,6 @@ import dan200.computercraft.api.peripheral.IWorkMonitor;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
-import net.minecraft.world.level.block.entity.BlockEntity;
import java.util.ArrayDeque;
import java.util.Queue;
@@ -28,7 +27,7 @@ import java.util.concurrent.TimeUnit;
* this tick. At the beginning of the tick, we execute as many {@link MainThread} tasks as possible, until our
* time-frame or the global time frame has expired.
*
- * Then, when other objects (such as {@link BlockEntity}) are ticked, we update how much time we've used using
+ * Then, when other objects (such as block entities or entities) are ticked, we update how much time we've used via
* {@link IWorkMonitor#trackWork(long, TimeUnit)}.
*
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as
diff --git a/src/main/java/dan200/computercraft/core/terminal/Terminal.java b/src/main/java/dan200/computercraft/core/terminal/Terminal.java
index a5b532d95..a9271bf83 100644
--- a/src/main/java/dan200/computercraft/core/terminal/Terminal.java
+++ b/src/main/java/dan200/computercraft/core/terminal/Terminal.java
@@ -7,8 +7,6 @@ package dan200.computercraft.core.terminal;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.shared.util.Palette;
-import net.minecraft.nbt.CompoundTag;
-import net.minecraft.network.FriendlyByteBuf;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -16,23 +14,23 @@ import java.nio.ByteBuffer;
public class Terminal
{
- private static final String BASE_16 = "0123456789abcdef";
+ protected static final String BASE_16 = "0123456789abcdef";
- private int width;
- private int height;
- private final boolean colour;
+ protected int width;
+ protected int height;
+ protected final boolean colour;
- private int cursorX = 0;
- private int cursorY = 0;
- private boolean cursorBlink = false;
- private int cursorColour = 0;
- private int cursorBackgroundColour = 15;
+ protected int cursorX = 0;
+ protected int cursorY = 0;
+ protected boolean cursorBlink = false;
+ protected int cursorColour = 0;
+ protected int cursorBackgroundColour = 15;
- private TextBuffer[] text;
- private TextBuffer[] textColour;
- private TextBuffer[] backgroundColour;
+ protected TextBuffer[] text;
+ protected TextBuffer[] textColour;
+ protected TextBuffer[] backgroundColour;
- private final Palette palette;
+ protected final Palette palette;
private final @Nullable Runnable onChanged;
@@ -320,110 +318,6 @@ public class Terminal
if( onChanged != null ) onChanged.run();
}
- public synchronized void write( FriendlyByteBuf buffer )
- {
- buffer.writeInt( cursorX );
- buffer.writeInt( cursorY );
- buffer.writeBoolean( cursorBlink );
- buffer.writeByte( cursorBackgroundColour << 4 | cursorColour );
-
- for( int y = 0; y < height; y++ )
- {
- TextBuffer text = this.text[y];
- TextBuffer textColour = this.textColour[y];
- TextBuffer backColour = backgroundColour[y];
-
- for( int x = 0; x < width; x++ ) buffer.writeByte( text.charAt( x ) & 0xFF );
- for( int x = 0; x < width; x++ )
- {
- buffer.writeByte( getColour(
- backColour.charAt( x ), Colour.BLACK ) << 4 |
- getColour( textColour.charAt( x ), Colour.WHITE )
- );
- }
- }
-
- palette.write( buffer );
- }
-
- public synchronized void read( FriendlyByteBuf buffer )
- {
- cursorX = buffer.readInt();
- cursorY = buffer.readInt();
- cursorBlink = buffer.readBoolean();
-
- byte cursorColour = buffer.readByte();
- cursorBackgroundColour = (cursorColour >> 4) & 0xF;
- this.cursorColour = cursorColour & 0xF;
-
- for( int y = 0; y < height; y++ )
- {
- TextBuffer text = this.text[y];
- TextBuffer textColour = this.textColour[y];
- TextBuffer backColour = backgroundColour[y];
-
- for( int x = 0; x < width; x++ ) text.setChar( x, (char) (buffer.readByte() & 0xFF) );
- for( int x = 0; x < width; x++ )
- {
- byte colour = buffer.readByte();
- backColour.setChar( x, BASE_16.charAt( (colour >> 4) & 0xF ) );
- textColour.setChar( x, BASE_16.charAt( colour & 0xF ) );
- }
- }
-
- palette.read( buffer );
- setChanged();
- }
-
- public synchronized CompoundTag writeToNBT( CompoundTag nbt )
- {
- nbt.putInt( "term_cursorX", cursorX );
- nbt.putInt( "term_cursorY", cursorY );
- nbt.putBoolean( "term_cursorBlink", cursorBlink );
- nbt.putInt( "term_textColour", cursorColour );
- nbt.putInt( "term_bgColour", cursorBackgroundColour );
- for( int n = 0; n < height; n++ )
- {
- nbt.putString( "term_text_" + n, text[n].toString() );
- nbt.putString( "term_textColour_" + n, textColour[n].toString() );
- nbt.putString( "term_textBgColour_" + n, backgroundColour[n].toString() );
- }
-
- palette.writeToNBT( nbt );
- return nbt;
- }
-
- public synchronized void readFromNBT( CompoundTag nbt )
- {
- cursorX = nbt.getInt( "term_cursorX" );
- cursorY = nbt.getInt( "term_cursorY" );
- cursorBlink = nbt.getBoolean( "term_cursorBlink" );
- cursorColour = nbt.getInt( "term_textColour" );
- cursorBackgroundColour = nbt.getInt( "term_bgColour" );
-
- for( int n = 0; n < height; n++ )
- {
- text[n].fill( ' ' );
- if( nbt.contains( "term_text_" + n ) )
- {
- text[n].write( nbt.getString( "term_text_" + n ) );
- }
- textColour[n].fill( BASE_16.charAt( cursorColour ) );
- if( nbt.contains( "term_textColour_" + n ) )
- {
- textColour[n].write( nbt.getString( "term_textColour_" + n ) );
- }
- backgroundColour[n].fill( BASE_16.charAt( cursorBackgroundColour ) );
- if( nbt.contains( "term_textBgColour_" + n ) )
- {
- backgroundColour[n].write( nbt.getString( "term_textBgColour_" + n ) );
- }
- }
-
- palette.readFromNBT( nbt );
- setChanged();
- }
-
public static int getColour( char c, Colour def )
{
if( c >= '0' && c <= '9' ) return c - '0';
diff --git a/src/main/java/dan200/computercraft/shared/CommonHooks.java b/src/main/java/dan200/computercraft/shared/CommonHooks.java
index 6a089b86c..af73d538b 100644
--- a/src/main/java/dan200/computercraft/shared/CommonHooks.java
+++ b/src/main/java/dan200/computercraft/shared/CommonHooks.java
@@ -7,7 +7,7 @@ package dan200.computercraft.shared;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.apis.http.NetworkUtils;
-import dan200.computercraft.core.filesystem.ResourceMount;
+import dan200.computercraft.shared.computer.core.ResourceMount;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.metrics.ComputerMBean;
diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
index 9dcf69357..fac637aff 100644
--- a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
+++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
@@ -365,8 +365,7 @@ public final class CommandComputerCraft
private static final List DEFAULT_FIELDS = Arrays.asList(
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.COUNT ),
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.NONE ),
- new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.AVG ),
- new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.MAX )
+ new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.AVG )
);
private static int displayTimings( CommandSourceStack source, AggregatedMetric sortField, List fields ) throws CommandSyntaxException
diff --git a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java b/src/main/java/dan200/computercraft/shared/computer/core/ResourceMount.java
similarity index 98%
rename from src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java
rename to src/main/java/dan200/computercraft/shared/computer/core/ResourceMount.java
index 01bbac46d..852cf8766 100644
--- a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ResourceMount.java
@@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.core.filesystem;
+package dan200.computercraft.shared.computer.core;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
@@ -11,6 +11,7 @@ import com.google.common.io.ByteStreams;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
+import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.shared.util.IoUtil;
import net.minecraft.ResourceLocationException;
import net.minecraft.resources.ResourceLocation;
diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
index 47fad09fc..0e4927d40 100644
--- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
@@ -15,12 +15,12 @@ import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.MetricsObserver;
-import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
+import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
-import dan200.computercraft.shared.network.client.TerminalState;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -41,7 +41,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment
private final MetricsObserver metrics;
private final Computer computer;
- private final Terminal terminal;
+ private final NetworkedTerminal terminal;
private final AtomicBoolean terminalChanged = new AtomicBoolean( false );
private boolean changedLastFrame;
@@ -54,7 +54,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment
ServerContext context = ServerContext.get( level.getServer() );
instanceID = context.registry().getUnusedInstanceID();
- terminal = new Terminal( terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged );
+ terminal = new NetworkedTerminal( terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged );
metrics = context.metrics().createMetricObserver( this );
computer = new Computer( context.computerContext(), this, terminal, computerID );
diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java
index ee460de3d..6c738a508 100644
--- a/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java
@@ -5,13 +5,17 @@
*/
package dan200.computercraft.shared.computer.core;
+import com.google.common.annotations.VisibleForTesting;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.ComputerCraftAPIImpl;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.core.ComputerContext;
+import dan200.computercraft.core.computer.ComputerThread;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.computer.mainthread.MainThread;
+import dan200.computercraft.core.lua.CobaltLuaMachine;
+import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import dan200.computercraft.shared.util.IDAssigner;
@@ -41,6 +45,9 @@ public final class ServerContext
{
private static final LevelResource FOLDER = new LevelResource( ComputerCraft.MOD_ID );
+ @VisibleForTesting
+ public static ILuaMachine.Factory luaMachine = CobaltLuaMachine::new;
+
private static @Nullable ServerContext instance;
private final MinecraftServer server;
@@ -57,7 +64,11 @@ public final class ServerContext
this.server = server;
storageDir = server.getWorldPath( FOLDER );
mainThread = new MainThread();
- context = new ComputerContext( new Environment( server ), ComputerCraft.computerThreads, mainThread );
+ context = new ComputerContext(
+ new Environment( server ),
+ new ComputerThread( ComputerCraft.computerThreads ),
+ mainThread, luaMachine
+ );
idAssigner = new IDAssigner( storageDir.resolve( "ids.json" ) );
}
diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java
index 6ea17b28b..ae7050d4c 100644
--- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java
+++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java
@@ -11,7 +11,8 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import dan200.computercraft.shared.computer.menu.ServerInputState;
-import dan200.computercraft.shared.network.client.TerminalState;
+import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.util.SingleIntArray;
import net.minecraft.world.entity.player.Player;
@@ -34,7 +35,7 @@ public abstract class ContainerComputerBase extends AbstractContainerMenu implem
private final @Nullable ServerComputer computer;
private final @Nullable ServerInputState input;
- private final @Nullable Terminal terminal;
+ private final @Nullable NetworkedTerminal terminal;
private final ItemStack displayStack;
diff --git a/src/main/java/dan200/computercraft/shared/computer/menu/ComputerMenu.java b/src/main/java/dan200/computercraft/shared/computer/menu/ComputerMenu.java
index 0711a1bec..f2c7f829a 100644
--- a/src/main/java/dan200/computercraft/shared/computer/menu/ComputerMenu.java
+++ b/src/main/java/dan200/computercraft/shared/computer/menu/ComputerMenu.java
@@ -6,7 +6,7 @@
package dan200.computercraft.shared.computer.menu;
import dan200.computercraft.shared.computer.core.ServerComputer;
-import dan200.computercraft.shared.network.client.TerminalState;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import net.minecraft.world.inventory.AbstractContainerMenu;
/**
diff --git a/src/main/java/dan200/computercraft/shared/computer/terminal/NetworkedTerminal.java b/src/main/java/dan200/computercraft/shared/computer/terminal/NetworkedTerminal.java
new file mode 100644
index 000000000..83298de32
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/computer/terminal/NetworkedTerminal.java
@@ -0,0 +1,129 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.shared.computer.terminal;
+
+import dan200.computercraft.core.terminal.Terminal;
+import dan200.computercraft.core.terminal.TextBuffer;
+import dan200.computercraft.shared.util.Colour;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.FriendlyByteBuf;
+
+public class NetworkedTerminal extends Terminal
+{
+ public NetworkedTerminal( int width, int height, boolean colour )
+ {
+ super( width, height, colour );
+ }
+
+ public NetworkedTerminal( int width, int height, boolean colour, Runnable changedCallback )
+ {
+ super( width, height, colour, changedCallback );
+ }
+
+ public synchronized void write( FriendlyByteBuf buffer )
+ {
+ buffer.writeInt( cursorX );
+ buffer.writeInt( cursorY );
+ buffer.writeBoolean( cursorBlink );
+ buffer.writeByte( cursorBackgroundColour << 4 | cursorColour );
+
+ for( int y = 0; y < height; y++ )
+ {
+ TextBuffer text = this.text[y];
+ TextBuffer textColour = this.textColour[y];
+ TextBuffer backColour = backgroundColour[y];
+
+ for( int x = 0; x < width; x++ ) buffer.writeByte( text.charAt( x ) & 0xFF );
+ for( int x = 0; x < width; x++ )
+ {
+ buffer.writeByte( getColour(
+ backColour.charAt( x ), Colour.BLACK ) << 4 |
+ getColour( textColour.charAt( x ), Colour.WHITE )
+ );
+ }
+ }
+
+ palette.write( buffer );
+ }
+
+ public synchronized void read( FriendlyByteBuf buffer )
+ {
+ cursorX = buffer.readInt();
+ cursorY = buffer.readInt();
+ cursorBlink = buffer.readBoolean();
+
+ byte cursorColour = buffer.readByte();
+ cursorBackgroundColour = (cursorColour >> 4) & 0xF;
+ this.cursorColour = cursorColour & 0xF;
+
+ for( int y = 0; y < height; y++ )
+ {
+ TextBuffer text = this.text[y];
+ TextBuffer textColour = this.textColour[y];
+ TextBuffer backColour = backgroundColour[y];
+
+ for( int x = 0; x < width; x++ ) text.setChar( x, (char) (buffer.readByte() & 0xFF) );
+ for( int x = 0; x < width; x++ )
+ {
+ byte colour = buffer.readByte();
+ backColour.setChar( x, BASE_16.charAt( (colour >> 4) & 0xF ) );
+ textColour.setChar( x, BASE_16.charAt( colour & 0xF ) );
+ }
+ }
+
+ palette.read( buffer );
+ setChanged();
+ }
+
+ public synchronized CompoundTag writeToNBT( CompoundTag nbt )
+ {
+ nbt.putInt( "term_cursorX", cursorX );
+ nbt.putInt( "term_cursorY", cursorY );
+ nbt.putBoolean( "term_cursorBlink", cursorBlink );
+ nbt.putInt( "term_textColour", cursorColour );
+ nbt.putInt( "term_bgColour", cursorBackgroundColour );
+ for( int n = 0; n < height; n++ )
+ {
+ nbt.putString( "term_text_" + n, text[n].toString() );
+ nbt.putString( "term_textColour_" + n, textColour[n].toString() );
+ nbt.putString( "term_textBgColour_" + n, backgroundColour[n].toString() );
+ }
+
+ palette.writeToNBT( nbt );
+ return nbt;
+ }
+
+ public synchronized void readFromNBT( CompoundTag nbt )
+ {
+ cursorX = nbt.getInt( "term_cursorX" );
+ cursorY = nbt.getInt( "term_cursorY" );
+ cursorBlink = nbt.getBoolean( "term_cursorBlink" );
+ cursorColour = nbt.getInt( "term_textColour" );
+ cursorBackgroundColour = nbt.getInt( "term_bgColour" );
+
+ for( int n = 0; n < height; n++ )
+ {
+ text[n].fill( ' ' );
+ if( nbt.contains( "term_text_" + n ) )
+ {
+ text[n].write( nbt.getString( "term_text_" + n ) );
+ }
+ textColour[n].fill( BASE_16.charAt( cursorColour ) );
+ if( nbt.contains( "term_textColour_" + n ) )
+ {
+ textColour[n].write( nbt.getString( "term_textColour_" + n ) );
+ }
+ backgroundColour[n].fill( BASE_16.charAt( cursorBackgroundColour ) );
+ if( nbt.contains( "term_textBgColour_" + n ) )
+ {
+ backgroundColour[n].write( nbt.getString( "term_textBgColour_" + n ) );
+ }
+ }
+
+ palette.readFromNBT( nbt );
+ setChanged();
+ }
+}
diff --git a/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java b/src/main/java/dan200/computercraft/shared/computer/terminal/TerminalState.java
similarity index 92%
rename from src/main/java/dan200/computercraft/shared/network/client/TerminalState.java
rename to src/main/java/dan200/computercraft/shared/computer/terminal/TerminalState.java
index fc7f905d4..dd755d819 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/TerminalState.java
+++ b/src/main/java/dan200/computercraft/shared/computer/terminal/TerminalState.java
@@ -3,9 +3,8 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.shared.network.client;
+package dan200.computercraft.shared.computer.terminal;
-import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.util.IoUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
@@ -42,12 +41,12 @@ public class TerminalState
private ByteBuf compressed;
- public TerminalState( @Nullable Terminal terminal )
+ public TerminalState( @Nullable NetworkedTerminal terminal )
{
this( terminal, true );
}
- public TerminalState( @Nullable Terminal terminal, boolean compress )
+ public TerminalState( @Nullable NetworkedTerminal terminal, boolean compress )
{
this.compress = compress;
@@ -115,17 +114,17 @@ public class TerminalState
return buffer == null ? 0 : buffer.readableBytes();
}
- public void apply( Terminal terminal )
+ public void apply( NetworkedTerminal terminal )
{
if( buffer == null ) throw new NullPointerException( "buffer" );
terminal.resize( width, height );
terminal.read( new FriendlyByteBuf( buffer ) );
}
- public Terminal create()
+ public NetworkedTerminal create()
{
if( buffer == null ) throw new NullPointerException( "Terminal does not exist" );
- Terminal terminal = new Terminal( width, height, colour );
+ NetworkedTerminal terminal = new NetworkedTerminal( width, height, colour );
terminal.read( new FriendlyByteBuf( buffer ) );
return terminal;
}
diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java b/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java
index 707f876ce..250d2892d 100644
--- a/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java
+++ b/src/main/java/dan200/computercraft/shared/computer/upload/UploadResult.java
@@ -13,8 +13,6 @@ public enum UploadResult
CONSUMED,
ERROR;
- public static final Component SUCCESS_TITLE = Component.translatable( "gui.computercraft.upload.success" );
-
public static final Component FAILED_TITLE = Component.translatable( "gui.computercraft.upload.failed" );
public static final Component COMPUTER_OFF_MSG = Component.translatable( "gui.computercraft.upload.failed.computer_off" );
public static final Component TOO_MUCH_MSG = Component.translatable( "gui.computercraft.upload.failed.too_much" );
diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java
index bdcedb696..8491ab44e 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java
@@ -6,6 +6,7 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
diff --git a/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java
index 5aff75662..20fd67b38 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/MonitorClientMessage.java
@@ -5,6 +5,7 @@
*/
package dan200.computercraft.shared.network.client;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
import net.minecraft.client.Minecraft;
diff --git a/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java b/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java
index 8e5e8b01b..650af2f72 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/PocketComputerDataMessage.java
@@ -7,8 +7,9 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.pocket.PocketComputerData;
-import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerState;
+import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.network.FriendlyByteBuf;
@@ -29,7 +30,7 @@ public class PocketComputerDataMessage implements NetworkMessage
instanceId = computer.getInstanceID();
state = computer.getState();
lightState = computer.getLight();
- terminal = sendTerminal ? computer.getTerminalState() : new TerminalState( (Terminal) null );
+ terminal = sendTerminal ? computer.getTerminalState() : new TerminalState( (NetworkedTerminal) null );
}
public PocketComputerDataMessage( FriendlyByteBuf buf )
diff --git a/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java
index 56499ce8e..3c55bbf8b 100644
--- a/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java
+++ b/src/main/java/dan200/computercraft/shared/network/container/ComputerContainerData.java
@@ -7,7 +7,7 @@ package dan200.computercraft.shared.network.container;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
-import dan200.computercraft.shared.network.client.TerminalState;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.item.ItemStack;
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java
index e320f514e..71d48e44a 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/EnergyMethods.java
@@ -25,6 +25,7 @@ import javax.annotation.Nonnull;
* :::
*
* @cc.module energy_storage
+ * @cc.since 1.94.0
*/
public class EnergyMethods implements GenericPeripheral
{
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java
index 73d56246c..ba2790646 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/FluidMethods.java
@@ -35,6 +35,7 @@ import static dan200.computercraft.shared.peripheral.generic.methods.ArgumentHel
* Methods for interacting with tanks and other fluid storage blocks.
*
* @cc.module fluid_storage
+ * @cc.since 1.94.0
*/
public class FluidMethods implements GenericPeripheral
{
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
index 649fbbc4e..7111cb44e 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
@@ -37,6 +37,7 @@ import static dan200.computercraft.shared.peripheral.generic.methods.ArgumentHel
* Methods for interacting with inventories.
*
* @cc.module inventory
+ * @cc.since 1.94.0
*/
public class InventoryMethods implements GenericPeripheral
{
@@ -170,6 +171,7 @@ public class InventoryMethods implements GenericPeripheral
* end
* print(total)
* }
+ * @cc.since 1.96.0
*/
@LuaFunction( mainThread = true )
public static int getItemLimit( IItemHandler inventory, int slot ) throws LuaException
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java
index 1437bad99..3bf0a3e55 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java
@@ -9,7 +9,8 @@ import com.mojang.blaze3d.platform.GlStateManager;
import dan200.computercraft.client.util.DirectBuffers;
import dan200.computercraft.client.util.DirectVertexBuffer;
import dan200.computercraft.core.terminal.Terminal;
-import dan200.computercraft.shared.network.client.TerminalState;
+import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import net.minecraft.core.BlockPos;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
@@ -36,7 +37,7 @@ public final class ClientMonitor
public int tboUniform;
public DirectVertexBuffer backgroundBuffer;
public DirectVertexBuffer foregroundBuffer;
- private Terminal terminal;
+ private NetworkedTerminal terminal;
private boolean terminalChanged;
public ClientMonitor( TileMonitor origin )
@@ -182,7 +183,7 @@ public final class ClientMonitor
{
if( state.hasTerminal() )
{
- if( terminal == null ) terminal = new Terminal( state.width, state.height, state.colour );
+ if( terminal == null ) terminal = new NetworkedTerminal( state.width, state.height, state.colour );
state.apply( terminal );
terminalChanged = true;
}
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java
index dc389f0b7..3e7d5f8a4 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorWatcher.java
@@ -8,7 +8,7 @@ package dan200.computercraft.shared.peripheral.monitor;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.MonitorClientMessage;
-import dan200.computercraft.shared.network.client.TerminalState;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java
index 2e7efc0de..8a75a3a56 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ServerMonitor.java
@@ -7,6 +7,7 @@ package dan200.computercraft.shared.peripheral.monitor;
import com.google.common.annotations.VisibleForTesting;
import dan200.computercraft.core.terminal.Terminal;
+import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.util.TickScheduler;
import javax.annotation.Nullable;
@@ -18,7 +19,7 @@ public class ServerMonitor
private final boolean colour;
private int textScale = 2;
- private @Nullable Terminal terminal;
+ private @Nullable NetworkedTerminal terminal;
private final AtomicBoolean resized = new AtomicBoolean( false );
private final AtomicBoolean changed = new AtomicBoolean( false );
@@ -46,7 +47,7 @@ public class ServerMonitor
if( terminal == null )
{
- terminal = new Terminal( termWidth, termHeight, colour, this::markChanged );
+ terminal = new NetworkedTerminal( termWidth, termHeight, colour, this::markChanged );
markChanged();
}
else
@@ -91,7 +92,7 @@ public class ServerMonitor
@Nullable
@VisibleForTesting
- public Terminal getTerminal()
+ public NetworkedTerminal getTerminal()
{
return terminal;
}
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java
index b89b699ec..cd0874425 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java
@@ -11,7 +11,7 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.common.TileGeneric;
-import dan200.computercraft.shared.network.client.TerminalState;
+import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.util.CapabilityUtil;
import dan200.computercraft.shared.util.TickScheduler;
import net.minecraft.core.BlockPos;
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java
index a45e9edcd..9d3c57157 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java
@@ -8,6 +8,7 @@ package dan200.computercraft.shared.peripheral.printer;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.common.TileGeneric;
+import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.media.items.ItemPrintout;
import dan200.computercraft.shared.util.*;
import net.minecraft.core.BlockPos;
@@ -62,7 +63,7 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
SidedCaps.ofNullable( facing -> facing == null ? new InvWrapper( this ) : new SidedInvWrapper( this, facing ) );
private LazyOptional peripheralCap;
- private final Terminal page = new Terminal( ItemPrintout.LINE_MAX_LENGTH, ItemPrintout.LINES_PER_PAGE, true );
+ private final NetworkedTerminal page = new NetworkedTerminal( ItemPrintout.LINE_MAX_LENGTH, ItemPrintout.LINES_PER_PAGE, true );
private String pageTitle = "";
private boolean printing = false;
diff --git a/src/main/java/dan200/computercraft/shared/util/NBTUtil.java b/src/main/java/dan200/computercraft/shared/util/NBTUtil.java
index 2896f285c..c4180e620 100644
--- a/src/main/java/dan200/computercraft/shared/util/NBTUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/NBTUtil.java
@@ -17,6 +17,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -177,7 +178,7 @@ public final class NBTUtil
{
MessageDigest digest = MessageDigest.getInstance( "MD5" );
DataOutput output = new DataOutputStream( new DigestOutputStream( digest ) );
- NbtIo.write( tag, output );
+ writeTag( output, "", tag );
byte[] hash = digest.digest();
return ENCODING.encode( hash );
}
@@ -188,6 +189,37 @@ public final class NBTUtil
}
}
+ /**
+ * An alternative version of {@link NbtIo#write(CompoundTag, DataOutput)}, which sorts keys. This
+ * should make the output slightly more deterministic.
+ *
+ * @param output The output to write to.
+ * @param name The name of the key we're writing. Should be {@code ""} for the root node.
+ * @param tag The tag to write.
+ * @throws IOException If the underlying stream throws.
+ * @see NbtIo#write(CompoundTag, DataOutput)
+ * @see CompoundTag#write(DataOutput)
+ */
+ private static void writeTag( DataOutput output, String name, Tag tag ) throws IOException
+ {
+ output.writeByte( tag.getId() );
+ if( tag.getId() == 0 ) return;
+ output.writeUTF( name );
+
+ if( tag instanceof CompoundTag compound )
+ {
+ String[] keys = compound.getAllKeys().toArray( new String[0] );
+ Arrays.sort( keys );
+ for( String key : keys ) writeTag( output, key, compound.get( key ) );
+
+ output.writeByte( 0 );
+ }
+ else
+ {
+ tag.write( output );
+ }
+ }
+
private static final class DigestOutputStream extends OutputStream
{
private final MessageDigest digest;
diff --git a/src/main/resources/assets/computercraft/lang/en_us.json b/src/main/resources/assets/computercraft/lang/en_us.json
index 33de587af..ed420e7a0 100644
--- a/src/main/resources/assets/computercraft/lang/en_us.json
+++ b/src/main/resources/assets/computercraft/lang/en_us.json
@@ -111,13 +111,10 @@
"gui.computercraft.tooltip.computer_id": "Computer ID: %s",
"gui.computercraft.tooltip.disk_id": "Disk ID: %s",
"gui.computercraft.tooltip.turn_on": "Turn this computer on",
- "gui.computercraft.tooltip.turn_on.key": "Hold Ctrl+R",
"gui.computercraft.tooltip.turn_off": "Turn this computer off",
"gui.computercraft.tooltip.turn_off.key": "Hold Ctrl+S",
"gui.computercraft.tooltip.terminate": "Stop the currently running code",
"gui.computercraft.tooltip.terminate.key": "Hold Ctrl+T",
- "gui.computercraft.upload.success": "Upload Succeeded",
- "gui.computercraft.upload.success.msg": "%d files uploaded.",
"gui.computercraft.upload.failed": "Upload Failed",
"gui.computercraft.upload.failed.computer_off": "You must turn the computer on before uploading files.",
"gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.",
diff --git a/src/main/resources/data/computercraft/lua/bios.lua b/src/main/resources/data/computercraft/lua/bios.lua
index d31e20335..a653c97a6 100644
--- a/src/main/resources/data/computercraft/lua/bios.lua
+++ b/src/main/resources/data/computercraft/lua/bios.lua
@@ -3,7 +3,7 @@
-- Ideally we'd use require, but that is part of the shell, and so is not
-- available to the BIOS or any APIs. All APIs load this using dofile, but that
-- has not been defined at this point.
-local expect
+local expect, field
do
local h = fs.open("rom/modules/main/cc/expect.lua", "r")
@@ -11,7 +11,8 @@ do
h.close()
if not f then error(err) end
- expect = f().expect
+ local res = f()
+ expect, field = res.expect, res.field
end
if _VERSION == "Lua 5.1" then
@@ -716,9 +717,17 @@ local tEmpty = {}
function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
expect(1, sPath, "string")
expect(2, sLocation, "string")
- expect(3, bIncludeFiles, "boolean", "nil")
- expect(4, bIncludeDirs, "boolean", "nil")
+ local bIncludeHidden = nil
+ if type(bIncludeFiles) == "table" then
+ bIncludeDirs = field(bIncludeFiles, "include_dirs", "boolean", "nil")
+ bIncludeHidden = field(bIncludeFiles, "include_hidden", "boolean", "nil")
+ bIncludeFiles = field(bIncludeFiles, "include_files", "boolean", "nil")
+ else
+ expect(3, bIncludeFiles, "boolean", "nil")
+ expect(4, bIncludeDirs, "boolean", "nil")
+ end
+ bIncludeHidden = bIncludeHidden ~= false
bIncludeFiles = bIncludeFiles ~= false
bIncludeDirs = bIncludeDirs ~= false
local sDir = sLocation
@@ -755,7 +764,9 @@ function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
local tFiles = fs.list(sDir)
for n = 1, #tFiles do
local sFile = tFiles[n]
- if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName then
+ if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName and (
+ bIncludeHidden or sFile:sub(1, 1) ~= "." or sName:sub(1, 1) == "."
+ ) then
local bIsDir = fs.isDir(fs.combine(sDir, sFile))
local sResult = string.sub(sFile, #sName + 1)
if bIsDir then
@@ -902,7 +913,7 @@ settings.define("paint.default_extension", {
settings.define("list.show_hidden", {
default = false,
- description = [[Show hidden files (those starting with "." in the Lua REPL)]],
+ description = [[Show hidden files (those starting with "." in the Lua REPL).]],
type = "boolean",
})
@@ -937,6 +948,11 @@ settings.define("bios.strict_globals", {
description = "Prevents assigning variables into a program's environment. Make sure you use the local keyword or assign to _G explicitly.",
type = "boolean",
})
+settings.define("shell.autocomplete_hidden", {
+ default = false,
+ description = [[Autocomplete hidden files and folders (those starting with ".").]],
+ type = "boolean",
+})
if term.isColour() then
settings.define("bios.use_multishell", {
diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.md b/src/main/resources/data/computercraft/lua/rom/help/changelog.md
index 50c78ded4..a2691c8b6 100644
--- a/src/main/resources/data/computercraft/lua/rom/help/changelog.md
+++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.md
@@ -1,10 +1,28 @@
+# New features in CC: Tweaked 1.101.0
+
+* Improvee Dutch translation (Quezler)
+* Better reporting of fatal computer timeouts in the server log.
+* Convert detail providers into a registry, allowing peripheral mods to read item/block details.
+* Redesign the metrics system. `/computercraft track` now allows computing aggregates (total, max, avg) on any metric, not just computer time.
+* File drag-and-drop now queues a `file_transfer` event on the computer. The
+ built-in shell or the `import` program must now be running to upload files.
+* The `peripheral` now searches for remote peripherals using any peripheral with the `peripheral_hub` type, not just wired modems.
+* Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72)
+* Add `shell.autocomplete_hidden` setting. (IvoLeal72)
+
+Several bug fixes:
+* Prevent `edit`'s "Run" command scrolling the terminal output on smaller
+ screens.
+* Remove some non-determinism in computing item's `nbt` hash.
+* Don't set the `Origin` header on outgoing websocket requests.
+
# New features in CC: Tweaked 1.100.10
* Mention WAV support in speaker help (MCJack123).
* Add http programs to the path, even when http is not enabled.
Several bug fixes:
-* Fix example in textutils.pagedTabulate docs (IvoLeal72).
+* Fix example in `textutils.pagedTabulate` docs (IvoLeal72).
* Fix help program treating the terminal one line longer than it was.
* Send block updates to client when turtle moves (roland-a).
* Resolve several monitor issues when running Occulus shaders.
@@ -236,7 +254,7 @@ And several bug fixes:
# New features in CC: Tweaked 1.96.0
* Use lightGrey for folders within the "list" program.
-* Add getLimit to inventory peripherals.
+* Add `getItemLimit` to inventory peripherals.
* Expose the generic peripheral system to the public API.
* Add cc.expect.range (Lupus590).
* Allow calling cc.expect directly (MCJack123).
diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md
index 50ad6318a..874b04d8a 100644
--- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md
+++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md
@@ -1,12 +1,19 @@
-New features in CC: Tweaked 1.100.10
+New features in CC: Tweaked 1.101.0
-* Mention WAV support in speaker help (MCJack123).
-* Add http programs to the path, even when http is not enabled.
+* Improvee Dutch translation (Quezler)
+* Better reporting of fatal computer timeouts in the server log.
+* Convert detail providers into a registry, allowing peripheral mods to read item/block details.
+* Redesign the metrics system. `/computercraft track` now allows computing aggregates (total, max, avg) on any metric, not just computer time.
+* File drag-and-drop now queues a `file_transfer` event on the computer. The
+ built-in shell or the `import` program must now be running to upload files.
+* The `peripheral` now searches for remote peripherals using any peripheral with the `peripheral_hub` type, not just wired modems.
+* Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72)
+* Add `shell.autocomplete_hidden` setting. (IvoLeal72)
Several bug fixes:
-* Fix example in textutils.pagedTabulate docs (IvoLeal72).
-* Fix help program treating the terminal one line longer than it was.
-* Send block updates to client when turtle moves (roland-a).
-* Resolve several monitor issues when running Occulus shaders.
+* Prevent `edit`'s "Run" command scrolling the terminal output on smaller
+ screens.
+* Remove some non-determinism in computing item's `nbt` hash.
+* Don't set the `Origin` header on outgoing websocket requests.
Type "help changelog" to see the full version history.
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua
index fd8fa2686..fc8e8fa66 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua
@@ -27,9 +27,9 @@ application or development builds of [FFmpeg].
@see guide!speaker_audio Gives a more general introduction to audio processing and the speaker.
@see speaker.playAudio To play the decoded audio data.
+@since 1.100.0
@usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio
is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program.
-@since 1.100.0
```lua
local dfpwm = require("cc.audio.dfpwm")
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
index 4bdc24674..86a061c42 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
@@ -34,7 +34,11 @@ local completion = require "cc.completion"
-- @tparam string text Current text to complete.
-- @treturn { string... } A list of suffixes of matching files.
local function file(shell, text)
- return fs.complete(text, shell.dir(), true, false)
+ return fs.complete(text, shell.dir(), {
+ include_files = true,
+ include_dirs = false,
+ include_hidden = settings.get("shell.autocomplete_hidden"),
+ })
end
--- Complete the name of a directory relative to the current working directory.
@@ -43,7 +47,11 @@ end
-- @tparam string text Current text to complete.
-- @treturn { string... } A list of suffixes of matching directories.
local function dir(shell, text)
- return fs.complete(text, shell.dir(), false, true)
+ return fs.complete(text, shell.dir(), {
+ include_files = false,
+ include_dirs = true,
+ include_hidden = settings.get("shell.autocomplete_hidden"),
+ })
end
--- Complete the name of a file or directory relative to the current working
@@ -55,7 +63,11 @@ end
-- @tparam[opt] boolean add_space Whether to add a space after the completed item.
-- @treturn { string... } A list of suffixes of matching files and directories.
local function dirOrFile(shell, text, previous, add_space)
- local results = fs.complete(text, shell.dir(), true, true)
+ local results = fs.complete(text, shell.dir(), {
+ include_files = true,
+ include_dirs = true,
+ include_hidden = settings.get("shell.autocomplete_hidden"),
+ })
if add_space then
for n = 1, #results do
local result = results[n]
diff --git a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua
index 5331854d9..dd9b9f282 100644
--- a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua
+++ b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua
@@ -55,19 +55,22 @@ term.redirect(current)
term.setTextColor(term.isColour() and colours.yellow or colours.white)
term.setBackgroundColor(colours.black)
term.setCursorBlink(false)
-local _, y = term.getCursorPos()
-local _, h = term.getSize()
if not ok then
printError(err)
end
-if ok and y >= h then
- term.scroll(1)
+
+local message = "Press any key to continue."
+if ok then message = "Program finished. " .. message end
+local _, y = term.getCursorPos()
+local w, h = term.getSize()
+local wrapped = require("cc.strings").wrap(message, w)
+
+local start_y = h - #wrapped + 1
+if y >= start_y then term.scroll(y - start_y + 1) end
+for i = 1, #wrapped do
+ term.setCursorPos(1, start_y + i - 1)
+ term.write(wrapped[i])
end
-term.setCursorPos(1, h)
-if ok then
- write("Program finished. ")
-end
-write("Press any key to continue")
os.pullEvent('key')
]]
diff --git a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua
index f2b650d03..6add0424f 100644
--- a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua
+++ b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua
@@ -329,9 +329,14 @@ function shell.programs(include_hidden)
end
local function completeProgram(sLine)
+ local bIncludeHidden = settings.get("shell.autocomplete_hidden")
if #sLine > 0 and (sLine:find("/") or sLine:find("\\")) then
-- Add programs from the root
- return fs.complete(sLine, sDir, true, false)
+ return fs.complete(sLine, sDir, {
+ include_files = true,
+ include_dirs = false,
+ include_hidden = bIncludeHidden,
+ })
else
local tResults = {}
@@ -349,7 +354,11 @@ local function completeProgram(sLine)
end
-- Add all subdirectories. We don't include files as they will be added in the block below
- local tDirs = fs.complete(sLine, sDir, false, false)
+ local tDirs = fs.complete(sLine, sDir, {
+ include_files = false,
+ include_dirs = false,
+ include_hidden = bIncludeHidden,
+ })
for i = 1, #tDirs do
local sResult = tDirs[i]
if not tSeen[sResult] then
diff --git a/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java b/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java
index 77701b5d1..a57cc9a3d 100644
--- a/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java
+++ b/src/test/java/dan200/computercraft/core/apis/http/options/AddressRuleTest.java
@@ -12,6 +12,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.net.InetSocketAddress;
import java.util.Collections;
+import java.util.OptionalInt;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -21,8 +22,8 @@ public class AddressRuleTest
public void matchesPort()
{
Iterable rules = Collections.singletonList( AddressRule.parse(
- "127.0.0.1", 8080,
- new PartialOptions( Action.ALLOW, null, null, null, null )
+ "127.0.0.1", OptionalInt.of( 8080 ),
+ Action.ALLOW.toPartial()
) );
assertEquals( apply( rules, "localhost", 8080 ).action, Action.ALLOW );
diff --git a/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java b/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java
index fe495a7b4..ddcd50db9 100644
--- a/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java
+++ b/src/test/java/dan200/computercraft/core/terminal/TerminalTest.java
@@ -9,9 +9,6 @@ import dan200.computercraft.api.lua.LuaValues;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.test.core.CallCounter;
import dan200.computercraft.test.core.terminal.TerminalMatchers;
-import io.netty.buffer.Unpooled;
-import net.minecraft.nbt.CompoundTag;
-import net.minecraft.network.FriendlyByteBuf;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
@@ -597,92 +594,6 @@ class TerminalTest
callCounter.assertNotCalled();
}
- @Test
- void testPacketBufferRoundtrip()
- {
- Terminal writeTerminal = new Terminal( 2, 1, true );
-
- blit( writeTerminal, "hi", "11", "ee" );
- writeTerminal.setCursorPos( 2, 5 );
- writeTerminal.setTextColour( 3 );
- writeTerminal.setBackgroundColour( 5 );
-
- FriendlyByteBuf packetBuffer = new FriendlyByteBuf( Unpooled.buffer() );
- writeTerminal.write( packetBuffer );
-
- CallCounter callCounter = new CallCounter();
- Terminal readTerminal = new Terminal( 2, 1, true, callCounter );
- packetBuffer.writeBytes( packetBuffer );
- readTerminal.read( packetBuffer );
-
- assertThat( readTerminal, allOf(
- textMatches( new String[] { "hi", } ),
- textColourMatches( new String[] { "11", } ),
- backgroundColourMatches( new String[] { "ee", } )
- ) );
-
- assertEquals( 2, readTerminal.getCursorX() );
- assertEquals( 5, readTerminal.getCursorY() );
- assertEquals( 3, readTerminal.getTextColour() );
- assertEquals( 5, readTerminal.getBackgroundColour() );
- callCounter.assertCalledTimes( 1 );
- }
-
- @Test
- void testNbtRoundtrip()
- {
- Terminal writeTerminal = new Terminal( 10, 5, true );
- blit( writeTerminal, "hi", "11", "ee" );
- writeTerminal.setCursorPos( 2, 5 );
- writeTerminal.setTextColour( 3 );
- writeTerminal.setBackgroundColour( 5 );
-
- CompoundTag nbt = new CompoundTag();
- writeTerminal.writeToNBT( nbt );
-
- CallCounter callCounter = new CallCounter();
- Terminal readTerminal = new Terminal( 2, 1, true, callCounter );
-
- readTerminal.readFromNBT( nbt );
-
- assertThat( readTerminal, allOf(
- textMatches( new String[] { "hi", } ),
- textColourMatches( new String[] { "11", } ),
- backgroundColourMatches( new String[] { "ee", } )
- ) );
-
- assertEquals( 2, readTerminal.getCursorX() );
- assertEquals( 5, readTerminal.getCursorY() );
- assertEquals( 3, readTerminal.getTextColour() );
- assertEquals( 5, readTerminal.getBackgroundColour() );
- callCounter.assertCalledTimes( 1 );
- }
-
- @Test
- void testReadWriteNBTEmpty()
- {
- Terminal terminal = new Terminal( 0, 0, true );
-
- CompoundTag nbt = new CompoundTag();
- terminal.writeToNBT( nbt );
-
- CallCounter callCounter = new CallCounter();
- terminal = new Terminal( 0, 1, true, callCounter );
- terminal.readFromNBT( nbt );
-
- assertThat( terminal, allOf(
- textMatches( new String[] { "", } ),
- textColourMatches( new String[] { "", } ),
- backgroundColourMatches( new String[] { "", } )
- ) );
-
- assertEquals( 0, terminal.getCursorX() );
- assertEquals( 0, terminal.getCursorY() );
- assertEquals( 0, terminal.getTextColour() );
- assertEquals( 15, terminal.getBackgroundColour() );
- callCounter.assertCalledTimes( 1 );
- }
-
@Test
void testGetColour()
{
diff --git a/src/test/java/dan200/computercraft/core/filesystem/ResourceMountTest.java b/src/test/java/dan200/computercraft/shared/computer/core/ResourceMountTest.java
similarity index 97%
rename from src/test/java/dan200/computercraft/core/filesystem/ResourceMountTest.java
rename to src/test/java/dan200/computercraft/shared/computer/core/ResourceMountTest.java
index daf93b599..e88632472 100644
--- a/src/test/java/dan200/computercraft/core/filesystem/ResourceMountTest.java
+++ b/src/test/java/dan200/computercraft/shared/computer/core/ResourceMountTest.java
@@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.core.filesystem;
+package dan200.computercraft.shared.computer.core;
import dan200.computercraft.api.filesystem.IMount;
import net.minecraft.Util;
diff --git a/src/test/java/dan200/computercraft/shared/computer/terminal/NetworkedTerminalTest.java b/src/test/java/dan200/computercraft/shared/computer/terminal/NetworkedTerminalTest.java
new file mode 100644
index 000000000..8614b67c0
--- /dev/null
+++ b/src/test/java/dan200/computercraft/shared/computer/terminal/NetworkedTerminalTest.java
@@ -0,0 +1,113 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.shared.computer.terminal;
+
+import dan200.computercraft.api.lua.LuaValues;
+import dan200.computercraft.core.terminal.Terminal;
+import dan200.computercraft.test.core.CallCounter;
+import io.netty.buffer.Unpooled;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.FriendlyByteBuf;
+import org.junit.jupiter.api.Test;
+
+import static dan200.computercraft.test.core.terminal.TerminalMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class NetworkedTerminalTest
+{
+ @Test
+ void testPacketBufferRoundtrip()
+ {
+ var writeTerminal = new NetworkedTerminal( 2, 1, true );
+
+ blit( writeTerminal, "hi", "11", "ee" );
+ writeTerminal.setCursorPos( 2, 5 );
+ writeTerminal.setTextColour( 3 );
+ writeTerminal.setBackgroundColour( 5 );
+
+ FriendlyByteBuf packetBuffer = new FriendlyByteBuf( Unpooled.buffer() );
+ writeTerminal.write( packetBuffer );
+
+ CallCounter callCounter = new CallCounter();
+ var readTerminal = new NetworkedTerminal( 2, 1, true, callCounter );
+ packetBuffer.writeBytes( packetBuffer );
+ readTerminal.read( packetBuffer );
+
+ assertThat( readTerminal, allOf(
+ textMatches( new String[] { "hi", } ),
+ textColourMatches( new String[] { "11", } ),
+ backgroundColourMatches( new String[] { "ee", } )
+ ) );
+
+ assertEquals( 2, readTerminal.getCursorX() );
+ assertEquals( 5, readTerminal.getCursorY() );
+ assertEquals( 3, readTerminal.getTextColour() );
+ assertEquals( 5, readTerminal.getBackgroundColour() );
+ callCounter.assertCalledTimes( 1 );
+ }
+
+ @Test
+ void testNbtRoundtrip()
+ {
+ var writeTerminal = new NetworkedTerminal( 10, 5, true );
+ blit( writeTerminal, "hi", "11", "ee" );
+ writeTerminal.setCursorPos( 2, 5 );
+ writeTerminal.setTextColour( 3 );
+ writeTerminal.setBackgroundColour( 5 );
+
+ CompoundTag nbt = new CompoundTag();
+ writeTerminal.writeToNBT( nbt );
+
+ CallCounter callCounter = new CallCounter();
+ var readTerminal = new NetworkedTerminal( 2, 1, true, callCounter );
+
+ readTerminal.readFromNBT( nbt );
+
+ assertThat( readTerminal, allOf(
+ textMatches( new String[] { "hi", } ),
+ textColourMatches( new String[] { "11", } ),
+ backgroundColourMatches( new String[] { "ee", } )
+ ) );
+
+ assertEquals( 2, readTerminal.getCursorX() );
+ assertEquals( 5, readTerminal.getCursorY() );
+ assertEquals( 3, readTerminal.getTextColour() );
+ assertEquals( 5, readTerminal.getBackgroundColour() );
+ callCounter.assertCalledTimes( 1 );
+ }
+
+ @Test
+ void testReadWriteNBTEmpty()
+ {
+ var terminal = new NetworkedTerminal( 0, 0, true );
+
+ CompoundTag nbt = new CompoundTag();
+ terminal.writeToNBT( nbt );
+
+ CallCounter callCounter = new CallCounter();
+ terminal = new NetworkedTerminal( 0, 1, true, callCounter );
+ terminal.readFromNBT( nbt );
+
+ assertThat( terminal, allOf(
+ textMatches( new String[] { "", } ),
+ textColourMatches( new String[] { "", } ),
+ backgroundColourMatches( new String[] { "", } )
+ ) );
+
+ assertEquals( 0, terminal.getCursorX() );
+ assertEquals( 0, terminal.getCursorY() );
+ assertEquals( 0, terminal.getTextColour() );
+ assertEquals( 15, terminal.getBackgroundColour() );
+ callCounter.assertCalledTimes( 1 );
+ }
+
+ private static void blit( Terminal terminal, String text, String fg, String bg )
+ {
+ terminal.blit( LuaValues.encode( text ), LuaValues.encode( fg ), LuaValues.encode( bg ) );
+ }
+}
diff --git a/src/test/java/dan200/computercraft/shared/network/client/TerminalStateTest.java b/src/test/java/dan200/computercraft/shared/computer/terminal/TerminalStateTest.java
similarity index 84%
rename from src/test/java/dan200/computercraft/shared/network/client/TerminalStateTest.java
rename to src/test/java/dan200/computercraft/shared/computer/terminal/TerminalStateTest.java
index 01a4bd325..3b99ec573 100644
--- a/src/test/java/dan200/computercraft/shared/network/client/TerminalStateTest.java
+++ b/src/test/java/dan200/computercraft/shared/computer/terminal/TerminalStateTest.java
@@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.shared.network.client;
+package dan200.computercraft.shared.computer.terminal;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
@@ -23,7 +23,7 @@ public class TerminalStateTest
@RepeatedTest( 5 )
public void testCompressed()
{
- Terminal terminal = randomTerminal();
+ var terminal = randomTerminal();
FriendlyByteBuf buffer = new FriendlyByteBuf( Unpooled.directBuffer() );
new TerminalState( terminal, true ).write( buffer );
@@ -35,7 +35,7 @@ public class TerminalStateTest
@RepeatedTest( 5 )
public void testUncompressed()
{
- Terminal terminal = randomTerminal();
+ var terminal = randomTerminal();
FriendlyByteBuf buffer = new FriendlyByteBuf( Unpooled.directBuffer() );
new TerminalState( terminal, false ).write( buffer );
@@ -44,10 +44,10 @@ public class TerminalStateTest
assertEquals( 0, buffer.readableBytes() );
}
- private static Terminal randomTerminal()
+ private static NetworkedTerminal randomTerminal()
{
Random random = new Random();
- Terminal terminal = new Terminal( 10, 5, true );
+ NetworkedTerminal terminal = new NetworkedTerminal( 10, 5, true );
for( int y = 0; y < terminal.getHeight(); y++ )
{
TextBuffer buffer = terminal.getLine( y );
@@ -70,14 +70,14 @@ public class TerminalStateTest
}
}
- private static Terminal read( FriendlyByteBuf buffer )
+ private static NetworkedTerminal read( FriendlyByteBuf buffer )
{
TerminalState state = new TerminalState( buffer );
assertTrue( state.colour );
if( !state.hasTerminal() ) return null;
- Terminal other = new Terminal( state.width, state.height, true );
+ var other = new NetworkedTerminal( state.width, state.height, true );
state.apply( other );
return other;
}
diff --git a/src/test/resources/test-rom/spec/apis/fs_spec.lua b/src/test/resources/test-rom/spec/apis/fs_spec.lua
index 32503598e..7bcab9e86 100644
--- a/src/test/resources/test-rom/spec/apis/fs_spec.lua
+++ b/src/test/resources/test-rom/spec/apis/fs_spec.lua
@@ -10,6 +10,46 @@ describe("The fs library", function()
expect.error(fs.complete, "", "", 1):eq("bad argument #3 (expected boolean, got number)")
expect.error(fs.complete, "", "", true, 1):eq("bad argument #4 (expected boolean, got number)")
end)
+
+ describe("include_hidden", function()
+ local dir = "tmp/hidden"
+ local function setup_tree()
+ fs.delete(dir)
+ fs.makeDir(dir)
+ fs.open(dir .. "/file.txt", "w").close()
+ fs.open(dir .. "/.hidden.txt", "w").close()
+ end
+
+ it("hides hidden files", function()
+ setup_tree()
+ local opts = { include_files = true, include_dirs = false, include_hidden = false }
+
+ expect(fs.complete("", dir, opts)):same { "../", "file.txt" }
+ expect(fs.complete(dir .. "/", "", opts)):same { "file.txt" }
+ end)
+
+ it("shows hidden files when typing a dot", function()
+ setup_tree()
+ local opts = { include_files = true, include_dirs = false, include_hidden = false }
+
+ expect(fs.complete(".", dir, opts)):same { "./", "hidden.txt" }
+ expect(fs.complete(dir .. "/.", "", opts)):same { "hidden.txt" }
+
+ -- Also test
+ expect(fs.complete(dir .. "/file", "", opts)):same { ".txt" }
+ expect(fs.complete(dir .. "/file.", "", opts)):same { "txt" }
+ expect(fs.complete("file", dir, opts)):same { ".txt" }
+ expect(fs.complete("file.", dir, opts)):same { "txt" }
+ end)
+
+ it("shows hidden files when include_hidden is true", function()
+ setup_tree()
+ local opts = { include_files = true, include_dirs = false, include_hidden = true }
+
+ expect(fs.complete("", dir, opts)):same { "../", ".hidden.txt", "file.txt" }
+ expect(fs.complete(dir .. "/", "", opts)):same { ".hidden.txt", "file.txt" }
+ end)
+ end)
end)
describe("fs.isDriveRoot", function()
diff --git a/src/test/resources/test-rom/spec/apis/os_spec.lua b/src/test/resources/test-rom/spec/apis/os_spec.lua
index 6fd7838b3..a9a620d50 100644
--- a/src/test/resources/test-rom/spec/apis/os_spec.lua
+++ b/src/test/resources/test-rom/spec/apis/os_spec.lua
@@ -61,9 +61,9 @@ describe("The os library", function()
-- TODO: Java 16 apparently no longer treats TextStyle.FULL as full and will render Sun instead of Sunday.
exp_code("%a", "Sun")
- -- exp_code("%A", "Sunday")
+ exp_code("%A", "Sunday")
exp_code("%b", "Oct")
- -- exp_code("%B", "October")
+ exp_code("%B", "October")
exp_code("%c", "Sun Oct 1 23:12:17 2000")
exp_code("%C", "20")
exp_code("%d", "01")
diff --git a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt
index ad36ae258..3e94dea31 100644
--- a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt
+++ b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/KotlinLuaMachine.kt
@@ -5,11 +5,9 @@ import dan200.computercraft.api.lua.ILuaContext
import dan200.computercraft.core.lua.ILuaMachine
import dan200.computercraft.core.lua.MachineEnvironment
import dan200.computercraft.core.lua.MachineResult
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.*
import java.io.InputStream
+import kotlin.coroutines.CoroutineContext
/**
* An [ILuaMachine] which runs Kotlin functions instead.
@@ -26,7 +24,7 @@ abstract class KotlinLuaMachine(environment: MachineEnvironment) : ILuaMachine,
queueEvent(eventName, arguments)
} else {
val task = getTask()
- if (task != null) CoroutineScope(Dispatchers.Unconfined + CoroutineName("Computer")).launch { task() }
+ if (task != null) CoroutineScope(NeverDispatcher() + CoroutineName("Computer")).launch { task() }
}
return MachineResult.OK
@@ -38,4 +36,20 @@ abstract class KotlinLuaMachine(environment: MachineEnvironment) : ILuaMachine,
* Get the next task to execute on this computer.
*/
protected abstract fun getTask(): (suspend KotlinLuaMachine.() -> Unit)?
+
+ /**
+ * A [CoroutineDispatcher] which only allows resuming from the computer thread. In practice, this means the only
+ * way to yield is with [pullEvent].
+ */
+ private class NeverDispatcher : CoroutineDispatcher() {
+ private val expectedGroup = Thread.currentThread().threadGroup
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ if (Thread.currentThread().threadGroup != expectedGroup) {
+ throw UnsupportedOperationException("Cannot perform arbitrary yields")
+ }
+
+ block.run()
+ }
+ }
}
diff --git a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt
index 28ab11c3a..066045b87 100644
--- a/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt
+++ b/src/testFixtures/kotlin/dan200/computercraft/test/core/computer/LuaTaskContext.kt
@@ -4,9 +4,11 @@ import dan200.computercraft.api.lua.ILuaAPI
import dan200.computercraft.api.lua.ILuaContext
import dan200.computercraft.api.lua.MethodResult
import dan200.computercraft.api.lua.ObjectArguments
+import dan200.computercraft.core.apis.OSAPI
import dan200.computercraft.core.apis.PeripheralAPI
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.time.Duration
/**
* The context for tasks which consume Lua objects.
@@ -40,6 +42,19 @@ interface LuaTaskContext {
/** Call a peripheral method. */
suspend fun LuaTaskContext.callPeripheral(name: String, method: String, vararg args: Any?): Array? =
getApi().call(context, ObjectArguments(name, method, *args)).await()
+
+ /**
+ * Sleep for the given duration. This uses the internal computer clock, so won't be accurate.
+ */
+ suspend fun LuaTaskContext.sleep(duration: Duration) {
+ val timer = getApi().startTimer(duration.inWholeMilliseconds / 1000.0)
+ while (true) {
+ val event = pullEvent("timer")
+ if (event[0] == "timer" && event[1] is Number && (event[1] as Number).toInt() == timer) {
+ return
+ }
+ }
+ }
}
/** Get a registered API. */
diff --git a/src/testMod/java/dan200/computercraft/export/Exporter.java b/src/testMod/java/dan200/computercraft/export/Exporter.java
index 7147a39f2..398c1fadc 100644
--- a/src/testMod/java/dan200/computercraft/export/Exporter.java
+++ b/src/testMod/java/dan200/computercraft/export/Exporter.java
@@ -10,8 +10,11 @@ import com.google.common.io.RecursiveDeleteOption;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mojang.blaze3d.systems.RenderSystem;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import dan200.computercraft.ComputerCraft;
-import dan200.computercraft.ingame.mod.TestMod;
import net.minecraft.client.Minecraft;
import net.minecraft.core.NonNullList;
import net.minecraft.network.chat.Component;
@@ -19,10 +22,6 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.*;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.client.event.ClientChatEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.registries.ForgeRegistries;
import java.io.File;
@@ -37,19 +36,24 @@ import java.util.Set;
/**
* Provides a {@literal /ccexport } command which exports icons and recipes for all ComputerCraft items.
*/
-@Mod.EventBusSubscriber( modid = TestMod.MOD_ID, value = Dist.CLIENT )
public class Exporter
{
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
- @SubscribeEvent
- public static void onClientCommands( ClientChatEvent event )
+ public static void register( CommandDispatcher dispatcher )
{
- String prefix = "/ccexport";
- if( !event.getMessage().startsWith( prefix ) ) return;
- event.setCanceled( true );
+ dispatcher.register(
+ LiteralArgumentBuilder.literal( "ccexport" )
+ .then( RequiredArgumentBuilder.argument( "path", StringArgumentType.string() )
+ .executes( c -> {
+ run( c.getArgument( "name", String.class ) );
+ return 0;
+ } ) ) );
+ }
- Path output = new File( event.getMessage().substring( prefix.length() ).trim() ).getAbsoluteFile().toPath();
+ private static void run( String path )
+ {
+ Path output = new File( path ).getAbsoluteFile().toPath();
if( !Files.isDirectory( output ) )
{
Minecraft.getInstance().gui.getChat().addMessage( Component.literal( "Output path does not exist" ) );
diff --git a/src/testMod/java/dan200/computercraft/ingame/api/ComputerState.java b/src/testMod/java/dan200/computercraft/gametest/api/ComputerState.java
similarity index 93%
rename from src/testMod/java/dan200/computercraft/ingame/api/ComputerState.java
rename to src/testMod/java/dan200/computercraft/gametest/api/ComputerState.java
index 34f64ad4d..d4b82a347 100644
--- a/src/testMod/java/dan200/computercraft/ingame/api/ComputerState.java
+++ b/src/testMod/java/dan200/computercraft/gametest/api/ComputerState.java
@@ -3,9 +3,9 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.ingame.api;
+package dan200.computercraft.gametest.api;
-import dan200.computercraft.ingame.mod.TestAPI;
+import dan200.computercraft.gametest.core.TestAPI;
import net.minecraft.gametest.framework.GameTestAssertException;
import net.minecraft.gametest.framework.GameTestSequence;
diff --git a/src/testMod/java/dan200/computercraft/gametest/api/GameTestHolder.java b/src/testMod/java/dan200/computercraft/gametest/api/GameTestHolder.java
new file mode 100644
index 000000000..be4684cb6
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/gametest/api/GameTestHolder.java
@@ -0,0 +1,24 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest.api;
+
+import net.minecraft.gametest.framework.GameTest;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks classes containing {@linkplain GameTest game tests}.
+ *
+ * This is used by Forge to automatically load and test classes.
+ */
+@Target( ElementType.TYPE )
+@Retention( RetentionPolicy.RUNTIME )
+public @interface GameTestHolder
+{
+}
diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/CCTestCommand.java b/src/testMod/java/dan200/computercraft/gametest/core/CCTestCommand.java
similarity index 76%
rename from src/testMod/java/dan200/computercraft/ingame/mod/CCTestCommand.java
rename to src/testMod/java/dan200/computercraft/gametest/core/CCTestCommand.java
index 1bd85bf0e..de82eafe1 100644
--- a/src/testMod/java/dan200/computercraft/ingame/mod/CCTestCommand.java
+++ b/src/testMod/java/dan200/computercraft/gametest/core/CCTestCommand.java
@@ -3,17 +3,16 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.ingame.mod;
+package dan200.computercraft.gametest.core;
import com.mojang.brigadier.CommandDispatcher;
import dan200.computercraft.ComputerCraft;
+import dan200.computercraft.mixin.gametest.TestCommandAccessor;
import net.minecraft.ChatFormatting;
-import net.minecraft.client.Minecraft;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestRegistry;
import net.minecraft.gametest.framework.StructureUtils;
-import net.minecraft.gametest.framework.TestCommand;
import net.minecraft.gametest.framework.TestFunction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
@@ -24,10 +23,10 @@ import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.level.block.entity.StructureBlockEntity;
import net.minecraft.world.level.storage.LevelResource;
-import net.minecraftforge.fml.loading.FMLLoader;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.nio.file.Path;
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice;
import static net.minecraft.commands.Commands.literal;
@@ -37,6 +36,8 @@ import static net.minecraft.commands.Commands.literal;
*/
class CCTestCommand
{
+ public static final LevelResource LOCATION = new LevelResource( ComputerCraft.MOD_ID );
+
public static void register( CommandDispatcher dispatcher )
{
dispatcher.register( choice( "cctest" )
@@ -49,7 +50,7 @@ class CCTestCommand
for( TestFunction function : GameTestRegistry.getAllTestFunctions() )
{
- TestCommand.exportTestStructure( context.getSource(), function.getStructureName() );
+ TestCommandAccessor.callExportTestStructure( context.getSource(), function.getStructureName() );
}
return 0;
} ) )
@@ -57,17 +58,11 @@ class CCTestCommand
for( TestFunction function : GameTestRegistry.getAllTestFunctions() )
{
dispatcher.execute( "test import " + function.getTestName(), context.getSource() );
- TestCommand.exportTestStructure( context.getSource(), function.getStructureName() );
+ TestCommandAccessor.callExportTestStructure( context.getSource(), function.getStructureName() );
}
return 0;
} ) )
- .then( literal( "promote" ).executes( context -> {
- if( !FMLLoader.getDist().isClient() ) return error( context.getSource(), "Cannot run on server" );
-
- promote();
- return 0;
- } ) )
.then( literal( "marker" ).executes( context -> {
ServerPlayer player = context.getSource().getPlayerOrException();
BlockPos pos = StructureUtils.findNearestStructureBlock( player.blockPosition(), 15, player.getLevel() );
@@ -99,7 +94,7 @@ class CCTestCommand
{
try
{
- Copier.replicate( TestMod.sourceDir.resolve( "computers" ), server.getWorldPath( new LevelResource( ComputerCraft.MOD_ID ) ) );
+ Copier.replicate( getSourceComputerPath(), getWorldComputerPath( server ) );
}
catch( IOException e )
{
@@ -111,7 +106,7 @@ class CCTestCommand
{
try
{
- Copier.replicate( server.getWorldPath( new LevelResource( ComputerCraft.MOD_ID ) ), TestMod.sourceDir.resolve( "computers" ) );
+ Copier.replicate( getWorldComputerPath( server ), getSourceComputerPath() );
}
catch( IOException e )
{
@@ -119,20 +114,14 @@ class CCTestCommand
}
}
- private static void promote()
+ private static Path getWorldComputerPath( MinecraftServer server )
{
- try
- {
- Copier.replicate(
- Minecraft.getInstance().gameDirectory.toPath().resolve( "screenshots" ),
- TestMod.sourceDir.resolve( "screenshots" ),
- x -> !x.toFile().getName().endsWith( ".diff.png" )
- );
- }
- catch( IOException e )
- {
- throw new UncheckedIOException( e );
- }
+ return server.getWorldPath( LOCATION ).resolve( "computer" ).resolve( "0" );
+ }
+
+ private static Path getSourceComputerPath()
+ {
+ return TestHooks.sourceDir.resolve( "computer" );
}
private static int error( CommandSourceStack source, String message )
diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/ClientHooks.java b/src/testMod/java/dan200/computercraft/gametest/core/ClientHooks.java
similarity index 96%
rename from src/testMod/java/dan200/computercraft/ingame/mod/ClientHooks.java
rename to src/testMod/java/dan200/computercraft/gametest/core/ClientHooks.java
index a946dc107..78638ad66 100644
--- a/src/testMod/java/dan200/computercraft/ingame/mod/ClientHooks.java
+++ b/src/testMod/java/dan200/computercraft/gametest/core/ClientHooks.java
@@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.ingame.mod;
+package dan200.computercraft.gametest.core;
import net.minecraft.client.CloudStatus;
import net.minecraft.client.Minecraft;
@@ -17,7 +17,7 @@ import net.minecraftforge.fml.common.Mod;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-@Mod.EventBusSubscriber( modid = TestMod.MOD_ID, value = Dist.CLIENT )
+@Mod.EventBusSubscriber( modid = "cctest", value = Dist.CLIENT )
public final class ClientHooks
{
private static final Logger LOG = LogManager.getLogger( TestHooks.class );
diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/Copier.java b/src/testMod/java/dan200/computercraft/gametest/core/Copier.java
similarity index 97%
rename from src/testMod/java/dan200/computercraft/ingame/mod/Copier.java
rename to src/testMod/java/dan200/computercraft/gametest/core/Copier.java
index 439ba152e..df198af9d 100644
--- a/src/testMod/java/dan200/computercraft/ingame/mod/Copier.java
+++ b/src/testMod/java/dan200/computercraft/gametest/core/Copier.java
@@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.ingame.mod;
+package dan200.computercraft.gametest.core;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/TestAPI.java b/src/testMod/java/dan200/computercraft/gametest/core/TestAPI.java
similarity index 94%
rename from src/testMod/java/dan200/computercraft/ingame/mod/TestAPI.java
rename to src/testMod/java/dan200/computercraft/gametest/core/TestAPI.java
index 67e8876e2..c939f6900 100644
--- a/src/testMod/java/dan200/computercraft/ingame/mod/TestAPI.java
+++ b/src/testMod/java/dan200/computercraft/gametest/core/TestAPI.java
@@ -3,22 +3,22 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.ingame.mod;
+package dan200.computercraft.gametest.core;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.IComputerSystem;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
-import dan200.computercraft.ingame.api.ComputerState;
-import dan200.computercraft.ingame.api.TestExtensionsKt;
+import dan200.computercraft.gametest.api.ComputerState;
+import dan200.computercraft.gametest.api.TestExtensionsKt;
import net.minecraft.gametest.framework.GameTestSequence;
import java.util.Optional;
/**
* API exposed to computers to help write tests.
- *
+ *
* Note, we extend this API within startup file of computers (see {@code cctest.lua}).
*
* @see TestExtensionsKt#thenComputerOk(GameTestSequence, String, String) To check tests on the computer have passed.
diff --git a/src/testMod/java/dan200/computercraft/gametest/core/TestHooks.java b/src/testMod/java/dan200/computercraft/gametest/core/TestHooks.java
new file mode 100644
index 000000000..b106a325e
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/gametest/core/TestHooks.java
@@ -0,0 +1,60 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest.core;
+
+import dan200.computercraft.api.ComputerCraftAPI;
+import dan200.computercraft.gametest.api.Times;
+import dan200.computercraft.shared.computer.core.ServerContext;
+import net.minecraft.core.BlockPos;
+import net.minecraft.gametest.framework.GameTestRunner;
+import net.minecraft.gametest.framework.GameTestTicker;
+import net.minecraft.gametest.framework.StructureUtils;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.GameRules;
+import net.minecraft.world.level.Level;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class TestHooks
+{
+ public static final Logger LOGGER = LoggerFactory.getLogger( TestHooks.class );
+
+ public static final Path sourceDir = Paths.get( System.getProperty( "cctest.sources" ) ).normalize().toAbsolutePath();
+
+ public static void init()
+ {
+ ServerContext.luaMachine = ManagedComputers.INSTANCE;
+ ComputerCraftAPI.registerAPIFactory( TestAPI::new );
+ StructureUtils.testStructuresDir = sourceDir.resolve( "structures" ).toString();
+ }
+
+ public static void onServerStarted( MinecraftServer server )
+ {
+ GameRules rules = server.getGameRules();
+ rules.getRule( GameRules.RULE_DAYLIGHT ).set( false, server );
+
+ ServerLevel world = server.getLevel( Level.OVERWORLD );
+ if( world != null ) world.setDayTime( Times.NOON );
+
+ LOGGER.info( "Cleaning up after last run" );
+ GameTestRunner.clearAllTests( server.overworld(), new BlockPos( 0, -60, 0 ), GameTestTicker.SINGLETON, 200 );
+
+ // Delete server context and add one with a mutable machine factory. This allows us to set the factory for
+ // specific test batches without having to reset all computers.
+ for( var computer : ServerContext.get( server ).registry().getComputers() )
+ {
+ var label = computer.getLabel() == null ? "#" + computer.getID() : computer.getLabel();
+ LOGGER.warn( "Unexpected computer {}", label );
+ }
+
+ LOGGER.info( "Importing files" );
+ CCTestCommand.importFiles( server );
+ }
+}
diff --git a/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java b/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java
new file mode 100644
index 000000000..0244d86ff
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java
@@ -0,0 +1,144 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest.core;
+
+import dan200.computercraft.export.Exporter;
+import dan200.computercraft.gametest.api.GameTestHolder;
+import net.minecraft.gametest.framework.GameTest;
+import net.minecraft.gametest.framework.GameTestRegistry;
+import net.minecraft.gametest.framework.StructureUtils;
+import net.minecraft.gametest.framework.TestFunction;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraftforge.client.event.RegisterClientCommandsEvent;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.event.RegisterCommandsEvent;
+import net.minecraftforge.event.RegisterGameTestsEvent;
+import net.minecraftforge.event.server.ServerStartedEvent;
+import net.minecraftforge.eventbus.api.EventPriority;
+import net.minecraftforge.fml.ModList;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
+import net.minecraftforge.forgespi.language.ModFileScanData;
+import net.minecraftforge.gametest.PrefixGameTestTemplate;
+import org.objectweb.asm.Type;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+@Mod( "cctest" )
+public class TestMod
+{
+ public TestMod()
+ {
+ TestHooks.init();
+
+ var bus = MinecraftForge.EVENT_BUS;
+ bus.addListener( EventPriority.LOW, ( ServerStartedEvent e ) -> TestHooks.onServerStarted( e.getServer() ) );
+ bus.addListener( ( RegisterCommandsEvent e ) -> CCTestCommand.register( e.getDispatcher() ) );
+ bus.addListener( ( RegisterClientCommandsEvent e ) -> Exporter.register( e.getDispatcher() ) );
+
+ var modBus = FMLJavaModLoadingContext.get().getModEventBus();
+ modBus.addListener( ( RegisterGameTestsEvent event ) -> {
+ var holder = Type.getType( GameTestHolder.class );
+ ModList.get().getAllScanData().stream()
+ .map( ModFileScanData::getAnnotations )
+ .flatMap( Collection::stream )
+ .filter( a -> holder.equals( a.annotationType() ) )
+ .forEach( x -> registerClass( x.clazz().getClassName(), event::register ) );
+ } );
+ }
+
+
+ private static Class> loadClass( String name )
+ {
+ try
+ {
+ return Class.forName( name, true, TestMod.class.getClassLoader() );
+ }
+ catch( ReflectiveOperationException e )
+ {
+ throw new RuntimeException( e );
+ }
+ }
+
+ private static void registerClass( String className, Consumer fallback )
+ {
+ var klass = loadClass( className );
+ for( var method : klass.getDeclaredMethods() )
+ {
+ var testInfo = method.getAnnotation( GameTest.class );
+ if( testInfo == null )
+ {
+ fallback.accept( method );
+ continue;
+ }
+
+ GameTestRegistry.getAllTestFunctions().add( turnMethodIntoTestFunction( method, testInfo ) );
+ GameTestRegistry.getAllTestClassNames().add( className );
+ }
+ }
+
+ /**
+ * Custom implementation of {@link GameTestRegistry#turnMethodIntoTestFunction(Method)} which makes
+ * {@link GameTest#template()} behave the same as Fabric, namely in that it points to a {@link ResourceLocation},
+ * rather than a test-class-specific structure.
+ *
+ * This effectively acts as a global version of {@link PrefixGameTestTemplate}, just one which doesn't require Forge
+ * to be present.
+ *
+ * @param method The method to register.
+ * @param testInfo The test info.
+ * @return The constructed test function.
+ */
+ private static TestFunction turnMethodIntoTestFunction( Method method, GameTest testInfo )
+ {
+ var className = method.getDeclaringClass().getSimpleName().toLowerCase( Locale.ROOT );
+ var testName = className + "." + method.getName().toLowerCase( Locale.ROOT );
+ return new TestFunction(
+ testInfo.batch(),
+ testName,
+ testInfo.template().isEmpty() ? testName : testInfo.template(),
+ StructureUtils.getRotationForRotationSteps( testInfo.rotationSteps() ), testInfo.timeoutTicks(), testInfo.setupTicks(),
+ testInfo.required(), testInfo.requiredSuccesses(), testInfo.attempts(),
+ turnMethodIntoConsumer( method )
+ );
+ }
+
+ private static Consumer turnMethodIntoConsumer( Method method )
+ {
+ return value -> {
+ try
+ {
+ Object instance = null;
+ if( !Modifier.isStatic( method.getModifiers() ) )
+ {
+ instance = method.getDeclaringClass().getConstructor().newInstance();
+ }
+
+ method.invoke( instance, value );
+ }
+ catch( InvocationTargetException e )
+ {
+ if( e.getCause() instanceof RuntimeException )
+ {
+ throw (RuntimeException) e.getCause();
+ }
+ else
+ {
+ throw new RuntimeException( e.getCause() );
+ }
+ }
+ catch( ReflectiveOperationException e )
+ {
+ throw new RuntimeException( e );
+ }
+ };
+ }
+}
diff --git a/src/testMod/java/dan200/computercraft/ingame/Computer_Test.kt b/src/testMod/java/dan200/computercraft/ingame/Computer_Test.kt
deleted file mode 100644
index 9ba41826f..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/Computer_Test.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package dan200.computercraft.ingame
-
-import dan200.computercraft.ComputerCraft
-import dan200.computercraft.ingame.api.modifyBlock
-import dan200.computercraft.ingame.api.sequence
-import net.minecraft.core.BlockPos
-import net.minecraft.gametest.framework.GameTest
-import net.minecraft.gametest.framework.GameTestHelper
-import net.minecraft.world.level.block.LeverBlock
-import net.minecraft.world.level.block.RedstoneLampBlock
-import net.minecraftforge.gametest.GameTestHolder
-
-@GameTestHolder(ComputerCraft.MOD_ID)
-class Computer_Test {
- /**
- * Ensures redstone signals do not travel through computers.
- *
- * @see [#548](https://github.com/cc-tweaked/CC-Tweaked/issues/548)
- */
- @GameTest
- fun No_through_signal(context: GameTestHelper) = context.sequence {
- val lamp = BlockPos(2, 2, 4)
- val lever = BlockPos(2, 2, 0)
- this
- .thenExecute {
- context.assertBlockState(lamp, { !it.getValue(RedstoneLampBlock.LIT) }, { "Lamp should not be lit" })
- context.modifyBlock(lever) { x -> x.setValue(LeverBlock.POWERED, true) }
- }
- .thenIdle(3)
- .thenExecute {
- context.assertBlockState(
- lamp,
- { !it.getValue(RedstoneLampBlock.LIT) },
- { "Lamp should still not be lit" },
- )
- }
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/CraftOs_Test.kt b/src/testMod/java/dan200/computercraft/ingame/CraftOs_Test.kt
deleted file mode 100644
index 6740501ab..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/CraftOs_Test.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package dan200.computercraft.ingame
-
-import dan200.computercraft.ComputerCraft
-import dan200.computercraft.ingame.api.sequence
-import dan200.computercraft.ingame.api.thenComputerOk
-import net.minecraft.gametest.framework.GameTest
-import net.minecraft.gametest.framework.GameTestHelper
-import net.minecraftforge.gametest.GameTestHolder
-
-@GameTestHolder(ComputerCraft.MOD_ID)
-class CraftOs_Test {
- /**
- * Sends a rednet message to another a computer and back again.
- */
- @GameTest(timeoutTicks = 200)
- fun Sends_basic_rednet_messages(context: GameTestHelper) = context.sequence { thenComputerOk("main") }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/Disk_Drive_Test.kt b/src/testMod/java/dan200/computercraft/ingame/Disk_Drive_Test.kt
deleted file mode 100644
index 46b284612..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/Disk_Drive_Test.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package dan200.computercraft.ingame
-
-import dan200.computercraft.ComputerCraft
-import dan200.computercraft.ingame.api.sequence
-import dan200.computercraft.ingame.api.thenComputerOk
-import net.minecraft.core.BlockPos
-import net.minecraft.gametest.framework.GameTest
-import net.minecraft.gametest.framework.GameTestHelper
-import net.minecraft.world.item.Items
-import net.minecraftforge.gametest.GameTestHolder
-
-@GameTestHolder(ComputerCraft.MOD_ID)
-class Disk_Drive_Test {
- /**
- * Ensure audio disks exist and we can play them.
- *
- * @see [#688](https://github.com/cc-tweaked/CC-Tweaked/issues/688)
- */
- @GameTest(timeoutTicks = Modem_Test.TIMEOUT)
- fun Audio_disk(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- @GameTest(timeoutTicks = Modem_Test.TIMEOUT)
- fun Ejects_disk(helper: GameTestHelper) = helper.sequence {
- val stackAt = BlockPos(2, 2, 2)
- this
- .thenComputerOk()
- .thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) }
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/Modem_Test.kt b/src/testMod/java/dan200/computercraft/ingame/Modem_Test.kt
deleted file mode 100644
index 6c518ee21..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/Modem_Test.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package dan200.computercraft.ingame
-
-import dan200.computercraft.ComputerCraft
-import dan200.computercraft.ingame.api.sequence
-import dan200.computercraft.ingame.api.thenComputerOk
-import dan200.computercraft.shared.Registry
-import dan200.computercraft.shared.peripheral.modem.wired.BlockCable
-import net.minecraft.core.BlockPos
-import net.minecraft.gametest.framework.GameTest
-import net.minecraft.gametest.framework.GameTestHelper
-import net.minecraftforge.gametest.GameTestHolder
-
-@GameTestHolder(ComputerCraft.MOD_ID)
-class Modem_Test {
- @GameTest(timeoutTicks = TIMEOUT)
- fun Have_peripherals(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- @GameTest(timeoutTicks = TIMEOUT)
- fun Gains_peripherals(helper: GameTestHelper) = helper.sequence {
- val position = BlockPos(2, 2, 2)
- this
- .thenComputerOk(marker = "initial")
- .thenExecute {
- helper.setBlock(
- position,
- BlockCable.correctConnections(
- helper.level,
- helper.absolutePos(position),
- Registry.ModBlocks.CABLE.get().defaultBlockState().setValue(BlockCable.CABLE, true),
- ),
- )
- }
- .thenComputerOk()
- }
-
- /**
- * Sends a modem message to another computer on the same network
- */
- @GameTest(timeoutTicks = TIMEOUT)
- fun Transmits_messages(context: GameTestHelper) = context.sequence { thenComputerOk("receive") }
-
- companion object {
- const val TIMEOUT = 200
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/Monitor_Test.kt b/src/testMod/java/dan200/computercraft/ingame/Monitor_Test.kt
deleted file mode 100644
index 52813cf3a..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/Monitor_Test.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package dan200.computercraft.ingame
-
-import dan200.computercraft.ComputerCraft
-import dan200.computercraft.ingame.api.*
-import dan200.computercraft.shared.Capabilities
-import dan200.computercraft.shared.Registry
-import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer
-import dan200.computercraft.shared.peripheral.monitor.TileMonitor
-import net.minecraft.commands.arguments.blocks.BlockInput
-import net.minecraft.core.BlockPos
-import net.minecraft.gametest.framework.GameTest
-import net.minecraft.gametest.framework.GameTestHelper
-import net.minecraft.nbt.CompoundTag
-import net.minecraft.world.level.block.Blocks
-import net.minecraftforge.gametest.GameTestHolder
-import java.util.*
-
-@GameTestHolder(ComputerCraft.MOD_ID)
-class Monitor_Test {
- @GameTest
- fun Ensures_valid_on_place(context: GameTestHelper) = context.sequence {
- val pos = BlockPos(2, 2, 2)
- val tag = CompoundTag()
- tag.putInt("Width", 2)
- tag.putInt("Height", 2)
-
- val toSet = BlockInput(
- Registry.ModBlocks.MONITOR_ADVANCED.get().defaultBlockState(),
- Collections.emptySet(),
- tag,
- )
-
- context.setBlock(pos, Blocks.AIR.defaultBlockState())
- context.setBlock(pos, toSet)
-
- this
- .thenIdle(2)
- .thenExecute {
- val tile = context.getBlockEntity(pos)
- if (tile !is TileMonitor) {
- context.fail("Expected tile to be monitor, is $tile", pos)
- return@thenExecute
- }
-
- if (tile.width != 1 || tile.height != 1) {
- context.fail("Tile has width and height of ${tile.width}x${tile.height}, but should be 1x1", pos)
- }
- }
- }
-
- private fun looksAcceptable(helper: GameTestHelper, renderer: MonitorRenderer) = helper.sequence {
- this
- .thenExecute {
- ComputerCraft.monitorRenderer = renderer
- helper.positionAtArmorStand()
-
- // Get the monitor and peripheral. This forces us to create a server monitor at this location.
- val monitor = helper.getBlockEntity(BlockPos(2, 2, 3), Registry.ModBlockEntities.MONITOR_ADVANCED.get())
- monitor.getCapability(Capabilities.CAPABILITY_PERIPHERAL)
-
- val terminal = monitor.cachedServerMonitor!!.terminal!!
- terminal.write("Hello, world!")
- terminal.setCursorPos(1, 2)
- terminal.textColour = 2
- terminal.backgroundColour = 3
- terminal.write("Some coloured text")
- }
- .thenScreenshot()
- }
-
- // @GameTest(batch = "Monitor_Test.Looks_acceptable", template = LOOKS_ACCEPTABLE)
- fun Looks_acceptable(helper: GameTestHelper) = looksAcceptable(helper, renderer = MonitorRenderer.TBO)
-
- // @GameTest(batch = "Monitor_Test.Looks_acceptable_dark", template = LOOKS_ACCEPTABLE_DARK)
- fun Looks_acceptable_dark(helper: GameTestHelper) = looksAcceptable(helper, renderer = MonitorRenderer.TBO)
-
- // @GameTest(batch = "Monitor_Test.Looks_acceptable_vbo", template = LOOKS_ACCEPTABLE)
- fun Looks_acceptable_vbo(helper: GameTestHelper) = looksAcceptable(helper, renderer = MonitorRenderer.VBO)
-
- // @GameTest(batch = "Monitor_Test.Looks_acceptable_dark_vbo", template = LOOKS_ACCEPTABLE_DARK)
- fun Looks_acceptable_dark_vbo(helper: GameTestHelper) = looksAcceptable(helper, renderer = MonitorRenderer.VBO)
-
- private companion object {
- const val LOOKS_ACCEPTABLE = "looks_acceptable"
- const val LOOKS_ACCEPTABLE_DARK = "looks_acceptable_dark"
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/Printout_Test.kt b/src/testMod/java/dan200/computercraft/ingame/Printout_Test.kt
deleted file mode 100644
index 4975b1902..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/Printout_Test.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package dan200.computercraft.ingame
-
-import dan200.computercraft.ingame.api.positionAtArmorStand
-import dan200.computercraft.ingame.api.sequence
-import dan200.computercraft.ingame.api.thenScreenshot
-import net.minecraft.gametest.framework.GameTestHelper
-
-class Printout_Test {
- // @GameTest(batch = "Printout_Test.In_frame_at_night", timeoutTicks = Timeouts.CLIENT_TIMEOUT)
- fun In_frame_at_night(helper: GameTestHelper) = helper.sequence {
- this
- .thenExecute { helper.positionAtArmorStand() }
- .thenScreenshot()
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/Turtle_Test.kt b/src/testMod/java/dan200/computercraft/ingame/Turtle_Test.kt
deleted file mode 100644
index 9147a7950..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/Turtle_Test.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-package dan200.computercraft.ingame
-
-import dan200.computercraft.ComputerCraft
-import dan200.computercraft.api.detail.BasicItemDetailProvider
-import dan200.computercraft.api.detail.DetailRegistries
-import dan200.computercraft.ingame.api.*
-import dan200.computercraft.ingame.api.Timeouts.COMPUTER_TIMEOUT
-import dan200.computercraft.shared.media.items.ItemPrintout
-import net.minecraft.gametest.framework.GameTest
-import net.minecraft.gametest.framework.GameTestHelper
-import net.minecraft.world.item.ItemStack
-import net.minecraftforge.gametest.GameTestHolder
-
-@GameTestHolder(ComputerCraft.MOD_ID)
-class Turtle_Test {
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Unequip_refreshes_peripheral(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can sheer sheep (and drop items)
- *
- * @see [#537](https://github.com/cc-tweaked/CC-Tweaked/issues/537)
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Shears_sheep(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can place lava.
- *
- * @see [#518](https://github.com/cc-tweaked/CC-Tweaked/issues/518)
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Place_lava(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can place when waterlogged.
- *
- * @see [#385](https://github.com/cc-tweaked/CC-Tweaked/issues/385)
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Place_waterlogged(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can pick up lava
- *
- * @see [#297](https://github.com/cc-tweaked/CC-Tweaked/issues/297)
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Gather_lava(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can hoe dirt.
- *
- * @see [#258](https://github.com/cc-tweaked/CC-Tweaked/issues/258)
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Hoe_dirt(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can place monitors
- *
- * @see [#691](https://github.com/cc-tweaked/CC-Tweaked/issues/691)
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Place_monitor(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can place into compostors. These are non-typical inventories, so
- * worth testing.
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Use_compostors(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can be cleaned in cauldrons.
- *
- * Currently not required as turtles can no longer right-click cauldrons.
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT, required = false)
- fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence { thenComputerOk() }
-
- /**
- * Checks turtles can use IDetailProviders by getting details for a printed page.
- */
- @GameTest(timeoutTicks = COMPUTER_TIMEOUT)
- fun Item_detail_provider(helper: GameTestHelper) = helper.sequence {
- this
- .thenComputerOk(marker = "initial")
- .thenExecute {
- // Register a dummy provider for printout items
- DetailRegistries.ITEM_STACK.addProvider(
- object : BasicItemDetailProvider("printout", ItemPrintout::class.java) {
- override fun provideDetails(data: MutableMap, stack: ItemStack, item: ItemPrintout) {
- data["type"] = item.type.toString()
- }
- },
- )
- }
- .thenComputerOk()
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt b/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt
deleted file mode 100644
index 608f4aa22..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-package dan200.computercraft.ingame.api
-
-import dan200.computercraft.ingame.mod.ImageUtils
-import dan200.computercraft.ingame.mod.TestMod
-import net.minecraft.client.Minecraft
-import net.minecraft.client.Screenshot
-import net.minecraft.commands.arguments.blocks.BlockInput
-import net.minecraft.core.BlockPos
-import net.minecraft.gametest.framework.GameTestAssertException
-import net.minecraft.gametest.framework.GameTestAssertPosException
-import net.minecraft.gametest.framework.GameTestHelper
-import net.minecraft.gametest.framework.GameTestSequence
-import net.minecraft.resources.ResourceLocation
-import net.minecraft.world.entity.decoration.ArmorStand
-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.minecraftforge.registries.ForgeRegistries
-import java.nio.file.Files
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.function.Supplier
-import javax.imageio.ImageIO
-
-object Times {
- const val NOON: Long = 6000
-}
-
-/**
- * Custom timeouts for various test types.
- */
-object Timeouts {
- private const val SECOND: Int = 20
-
- const val COMPUTER_TIMEOUT: Int = SECOND * 15
-
- const val CLIENT_TIMEOUT: Int = SECOND * 20
-}
-
-/**
- * Wait until a computer has finished running and check it is OK.
- */
-fun GameTestSequence.thenComputerOk(name: String? = null, marker: String = ComputerState.DONE): GameTestSequence {
- val label = parent.testName + (if (name == null) "" else ".$name")
- return this.thenWaitUntil {
- val computer = ComputerState.get(label)
- if (computer == null || !computer.isDone(marker)) throw GameTestAssertException("Computer '$label' has not reached $marker yet.")
- }.thenExecute {
- ComputerState.get(label).check(marker)
- }
-}
-
-/**
- * Run a task on the client
- */
-fun GameTestSequence.thenOnClient(task: ClientTestHelper.() -> Unit): GameTestSequence {
- var future: CompletableFuture? = null
- return this
- .thenExecute { future = Minecraft.getInstance().submit(Supplier { task(ClientTestHelper()) }) }
- .thenWaitUntil { if (!future!!.isDone) throw GameTestAssertException("Not done task yet") }
- .thenExecute {
- try {
- future!!.get()
- } catch (e: ExecutionException) {
- throw e.cause ?: e
- }
- }
-}
-
-/**
- * Idle for one tick to allow the client to catch up, then take a screenshot.
- */
-fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence {
- val suffix = if (name == null) "" else "-$name"
- val fullName = "${parent.testName}$suffix"
-
- var counter = 0
- val hasScreenshot = AtomicBoolean()
-
- return this
- // Wait until all chunks have been rendered and we're idle for an extended period.
- .thenExecute { counter = 0 }
- .thenWaitUntil {
- val renderer = Minecraft.getInstance().levelRenderer
- if (renderer.chunkRenderDispatcher != null && renderer.hasRenderedAllChunks()) {
- val idleFor = ++counter
- if (idleFor <= 20) throw GameTestAssertException("Only idle for $idleFor ticks")
- } else {
- counter = 0
- throw GameTestAssertException("Waiting for client to finish rendering")
- }
- }
- // Now disable the GUI, take a screenshot and reenable it. We sleep either side to give the client time to do
- // its thing.
- .thenExecute {
- Minecraft.getInstance().options.hideGui = true
- hasScreenshot.set(false)
- }
- .thenIdle(5) // Some delay before/after to ensure the render thread has caught up.
- .thenOnClient { screenshot("$fullName.png") { hasScreenshot.set(true) } }
- .thenWaitUntil { if (!hasScreenshot.get()) throw GameTestAssertException("Screenshot does not exist") }
- .thenExecute {
- Minecraft.getInstance().options.hideGui = false
-
- val screenshotsPath = Minecraft.getInstance().gameDirectory.toPath().resolve("screenshots")
- val screenshotPath = screenshotsPath.resolve("$fullName.png")
- val originalPath = TestMod.sourceDir.resolve("screenshots").resolve("$fullName.png")
-
- if (!Files.exists(originalPath)) throw GameTestAssertException("$fullName does not exist. Use `/cctest promote' to create it.")
-
- val screenshot = ImageIO.read(screenshotPath.toFile())
- ?: throw GameTestAssertException("Error reading screenshot from $screenshotPath")
- val original = ImageIO.read(originalPath.toFile())
-
- if (screenshot.width != original.width || screenshot.height != original.height) {
- throw GameTestAssertException("$fullName screenshot is ${screenshot.width}x${screenshot.height} but original is ${original.width}x${original.height}")
- }
-
- ImageUtils.writeDifference(screenshotsPath.resolve("$fullName.diff.png"), screenshot, original)
- if (!ImageUtils.areSame(screenshot, original)) throw GameTestAssertException("Images are different.")
- }
-}
-
-val GameTestHelper.testName: String get() = testInfo.testName
-
-val GameTestHelper.structureName: String get() = testInfo.structureName
-
-/**
- * Modify a block state within the test.
- */
-fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
- setBlock(pos, modify(getBlockState(pos)))
-}
-
-fun GameTestHelper.sequence(run: GameTestSequence.() -> GameTestSequence) {
- run(startSequence()).thenSucceed()
-}
-
-private fun getName(type: BlockEntityType<*>): ResourceLocation = ForgeRegistries.BLOCK_ENTITY_TYPES.getKey(type)!!
-
-fun GameTestHelper.getBlockEntity(pos: BlockPos, type: BlockEntityType): T {
- val tile = getBlockEntity(pos)
- @Suppress("UNCHECKED_CAST")
- return when {
- tile == null -> throw GameTestAssertPosException("Expected ${getName(type)}, but no tile was there", absolutePos(pos), pos, 0)
- tile.type != type -> throw GameTestAssertPosException("Expected ${getName(type)} but got ${getName(tile.type)}", absolutePos(pos), pos, 0)
- else -> tile as T
- }
-}
-
-/**
- * Set a block within the test structure.
- */
-fun GameTestHelper.setBlock(pos: BlockPos, state: BlockInput) = state.place(level, absolutePos(pos), 3)
-
-/**
- * Position the player at an armor stand.
- */
-fun GameTestHelper.positionAtArmorStand() {
- val entities = level.getEntities(null, bounds) { it.name.string == structureName }
- if (entities.size <= 0 || entities[0] !is ArmorStand) throw GameTestAssertException("Cannot find armor stand")
-
- val stand = entities[0] as ArmorStand
- val player = level.randomPlayer ?: throw GameTestAssertException("Player does not exist")
-
- player.connection.teleport(stand.x, stand.y, stand.z, stand.yRot, stand.xRot)
-}
-
-class ClientTestHelper {
- val minecraft: Minecraft = Minecraft.getInstance()
-
- fun screenshot(name: String, callback: () -> Unit = {}) {
- Screenshot.grab(
- minecraft.gameDirectory,
- name,
- minecraft.mainRenderTarget,
- ) {
- TestMod.log.info(it.string)
- callback()
- }
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/ImageUtils.java b/src/testMod/java/dan200/computercraft/ingame/mod/ImageUtils.java
deleted file mode 100644
index 6739fe7d7..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/mod/ImageUtils.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * This file is part of ComputerCraft - http://www.computercraft.info
- * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
- * Send enquiries to dratcliffe@gmail.com
- */
-package dan200.computercraft.ingame.mod;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.nio.file.Path;
-
-public final class ImageUtils
-{
- private static final Logger LOG = LogManager.getLogger( ImageUtils.class );
-
- /**
- * Allow 0.3% of pixels to fail. This allows for slight differences at the edges.
- */
- private static final double PIXEL_THRESHOLD = 0.003;
-
- /**
- * Maximum possible distance between two colours. Floating point differences means we need some fuzziness here.
- */
- public static final int DISTANCE_THRESHOLD = 5;
-
- private ImageUtils()
- {
- }
-
- public static boolean areSame( BufferedImage left, BufferedImage right )
- {
- int width = left.getWidth(), height = left.getHeight();
- if( width != right.getWidth() || height != right.getHeight() ) return false;
-
- int failed = 0, threshold = (int) (width * height * PIXEL_THRESHOLD);
- for( int x = 0; x < width; x++ )
- {
- for( int y = 0; y < height; y++ )
- {
- int l = left.getRGB( x, y ), r = right.getRGB( x, y );
- if( (l & 0xFFFFFF) != (r & 0xFFFFFF) && distance( l, r, 0 ) + distance( l, r, 8 ) + distance( l, r, 16 ) >= DISTANCE_THRESHOLD )
- {
- failed++;
- }
- }
- }
-
- if( failed > 0 ) LOG.warn( "{} pixels failed comparing (threshold is {})", failed, threshold );
- return failed <= threshold;
- }
-
- public static void writeDifference( Path path, BufferedImage left, BufferedImage right ) throws IOException
- {
- int width = left.getWidth(), height = left.getHeight();
-
- BufferedImage copy = new BufferedImage( width, height, left.getType() );
- for( int x = 0; x < width; x++ )
- {
- for( int y = 0; y < height; y++ )
- {
- int l = left.getRGB( x, y ), r = right.getRGB( x, y );
- copy.setRGB( x, y, difference( l, r, 0 ) | difference( l, r, 8 ) | difference( l, r, 16 ) | 0xFF000000 );
- }
- }
-
- ImageIO.write( copy, "png", path.toFile() );
- }
-
- private static int difference( int l, int r, int shift )
- {
- return Math.abs( ((l >> shift) & 0xFF) - ((r >> shift) & 0xFF) ) << shift;
- }
-
- private static int distance( int l, int r, int shift )
- {
- return Math.abs( ((l >> shift) & 0xFF) - ((r >> shift) & 0xFF) );
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/TestHooks.java b/src/testMod/java/dan200/computercraft/ingame/mod/TestHooks.java
deleted file mode 100644
index d5338ac97..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/mod/TestHooks.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * This file is part of ComputerCraft - http://www.computercraft.info
- * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
- * Send enquiries to dratcliffe@gmail.com
- */
-package dan200.computercraft.ingame.mod;
-
-import dan200.computercraft.ingame.api.Times;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.level.ServerLevel;
-import net.minecraft.world.level.GameRules;
-import net.minecraft.world.level.Level;
-import net.minecraftforge.event.RegisterCommandsEvent;
-import net.minecraftforge.event.server.ServerStartedEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-@Mod.EventBusSubscriber( modid = TestMod.MOD_ID )
-public class TestHooks
-{
- private static final Logger LOG = LogManager.getLogger( TestHooks.class );
-
- @SubscribeEvent
- public static void onRegisterCommands( RegisterCommandsEvent event )
- {
- LOG.info( "Starting server, registering command helpers." );
- CCTestCommand.register( event.getDispatcher() );
- }
-
- @SubscribeEvent
- public static void onServerStarted( ServerStartedEvent event )
- {
- MinecraftServer server = event.getServer();
- GameRules rules = server.getGameRules();
- rules.getRule( GameRules.RULE_DAYLIGHT ).set( false, server );
-
- ServerLevel world = event.getServer().getLevel( Level.OVERWORLD );
- if( world != null ) world.setDayTime( Times.NOON );
-
- // LOG.info( "Cleaning up after last run" );
- // CommandSourceStack source = server.createCommandSourceStack();
- // GameTestRunner.clearAllTests( source.getLevel(), getStart( source ), GameTestTicker.SINGLETON, 200 );
-
- LOG.info( "Importing files" );
- CCTestCommand.importFiles( server );
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/TestMod.java b/src/testMod/java/dan200/computercraft/ingame/mod/TestMod.java
deleted file mode 100644
index fffd15ef0..000000000
--- a/src/testMod/java/dan200/computercraft/ingame/mod/TestMod.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * This file is part of ComputerCraft - http://www.computercraft.info
- * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
- * Send enquiries to dratcliffe@gmail.com
- */
-package dan200.computercraft.ingame.mod;
-
-import dan200.computercraft.api.ComputerCraftAPI;
-import net.minecraft.gametest.framework.StructureUtils;
-import net.minecraftforge.fml.common.Mod;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-@Mod( TestMod.MOD_ID )
-public class TestMod
-{
- public static final Path sourceDir = Paths.get( "../../src/testMod/server-files" ).normalize().toAbsolutePath();
-
- public static final String MOD_ID = "cctest";
-
- public static final Logger log = LogManager.getLogger( MOD_ID );
-
- public TestMod()
- {
- log.info( "CC: Test initialised" );
- ComputerCraftAPI.registerAPIFactory( TestAPI::new );
-
- StructureUtils.testStructuresDir = sourceDir.resolve( "structures" ).toString();
- }
-}
diff --git a/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestHelperAccessor.java b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestHelperAccessor.java
new file mode 100644
index 000000000..8e66a929f
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestHelperAccessor.java
@@ -0,0 +1,23 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.mixin.gametest;
+
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.gametest.framework.GameTestInfo;
+import net.minecraft.world.phys.AABB;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin( GameTestHelper.class )
+public interface GameTestHelperAccessor
+{
+ @Invoker
+ AABB callGetBounds();
+
+ @Accessor
+ GameTestInfo getTestInfo();
+}
diff --git a/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestInfoAccessor.java b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestInfoAccessor.java
new file mode 100644
index 000000000..b7e2a1846
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestInfoAccessor.java
@@ -0,0 +1,17 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.mixin.gametest;
+
+import net.minecraft.gametest.framework.GameTestInfo;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin( GameTestInfo.class )
+public interface GameTestInfoAccessor
+{
+ @Invoker( "getTick" )
+ long computercraft$getTick();
+}
diff --git a/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestSequenceAccessor.java b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestSequenceAccessor.java
new file mode 100644
index 000000000..9e9dad1ab
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestSequenceAccessor.java
@@ -0,0 +1,18 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.mixin.gametest;
+
+import net.minecraft.gametest.framework.GameTestInfo;
+import net.minecraft.gametest.framework.GameTestSequence;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin( GameTestSequence.class )
+public interface GameTestSequenceAccessor
+{
+ @Accessor
+ GameTestInfo getParent();
+}
diff --git a/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestSequenceMixin.java b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestSequenceMixin.java
new file mode 100644
index 000000000..efc7293c7
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/mixin/gametest/GameTestSequenceMixin.java
@@ -0,0 +1,58 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.mixin.gametest;
+
+import dan200.computercraft.gametest.core.TestHooks;
+import net.minecraft.gametest.framework.GameTestAssertException;
+import net.minecraft.gametest.framework.GameTestInfo;
+import net.minecraft.gametest.framework.GameTestSequence;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Overwrite;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin( GameTestSequence.class )
+class GameTestSequenceMixin
+{
+ @Shadow
+ @Final
+ GameTestInfo parent;
+
+ /**
+ * Override {@link GameTestSequence#tickAndContinue(long)} to catch non-{@link GameTestAssertException} failures.
+ *
+ * @param ticks The current tick.
+ * @author Jonathan Coates
+ * @reason There's no sense doing this in a more compatible way for game tests.
+ */
+ @Overwrite
+ public void tickAndContinue( long ticks )
+ {
+ try
+ {
+ tick( ticks );
+ }
+ catch( GameTestAssertException ignored )
+ {
+ // Mimic the original behaviour.
+ }
+ catch( AssertionError e )
+ {
+ parent.fail( e );
+ }
+ catch( Exception e )
+ {
+ // Fail the test, rather than crashing the server.
+ TestHooks.LOGGER.error( "{} threw unexpected exception", parent.getTestName(), e );
+ parent.fail( e );
+ }
+ }
+
+ @Shadow
+ private void tick( long tick )
+ {
+ }
+}
diff --git a/src/testMod/java/dan200/computercraft/mixin/gametest/TestCommandAccessor.java b/src/testMod/java/dan200/computercraft/mixin/gametest/TestCommandAccessor.java
new file mode 100644
index 000000000..70ce8ee55
--- /dev/null
+++ b/src/testMod/java/dan200/computercraft/mixin/gametest/TestCommandAccessor.java
@@ -0,0 +1,21 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.mixin.gametest;
+
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.gametest.framework.TestCommand;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin( TestCommand.class )
+public interface TestCommandAccessor
+{
+ @Invoker
+ static int callExportTestStructure( CommandSourceStack source, String structure )
+ {
+ return 0;
+ }
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt
new file mode 100644
index 000000000..581c5a712
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/Computer_Test.kt
@@ -0,0 +1,67 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.core.apis.RedstoneAPI
+import dan200.computercraft.core.computer.ComputerSide
+import dan200.computercraft.gametest.api.*
+import dan200.computercraft.test.core.computer.getApi
+import net.minecraft.core.BlockPos
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestHelper
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.world.level.block.LeverBlock
+import net.minecraft.world.level.block.RedstoneLampBlock
+
+@GameTestHolder
+class Computer_Test {
+ /**
+ * Ensures redstone signals do not travel through computers.
+ *
+ * @see [#548](https://github.com/cc-tweaked/CC-Tweaked/issues/548)
+ */
+ @GameTest
+ fun No_through_signal(context: GameTestHelper) = context.sequence {
+ val lamp = BlockPos(2, 2, 4)
+ val lever = BlockPos(2, 2, 0)
+ thenExecute {
+ context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should not be lit")
+ context.modifyBlock(lever) { x -> x.setValue(LeverBlock.POWERED, true) }
+ }
+ thenIdle(3)
+ thenExecute { context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should still not be lit") }
+ }
+
+ /**
+ * Similar to the above, but with a repeater before the computer
+ */
+ @GameTest
+ fun No_through_signal_reverse(context: GameTestHelper) = context.sequence {
+ val lamp = BlockPos(2, 2, 4)
+ val lever = BlockPos(2, 2, 0)
+ thenExecute {
+ context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should not be lit")
+ context.modifyBlock(lever) { x -> x.setValue(LeverBlock.POWERED, true) }
+ }
+ thenIdle(3)
+ thenExecute { context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should still not be lit") }
+ }
+
+ @GameTest
+ fun Set_and_destroy(context: GameTestHelper) = context.sequence {
+ val lamp = BlockPos(2, 2, 3)
+
+ thenOnComputer { getApi().setOutput(ComputerSide.BACK, true) }
+ thenIdle(3)
+ thenExecute { context.assertBlockHas(lamp, RedstoneLampBlock.LIT, true, "Lamp should be lit") }
+ thenExecute { context.setBlock(BlockPos(2, 2, 2), Blocks.AIR) }
+ thenIdle(4)
+ thenExecute { context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should not be lit") }
+ }
+
+ // TODO: More redstone connectivity tests!
+ // TODO: Computer peripherals (including command computers)
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/CraftOs_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/CraftOs_Test.kt
new file mode 100644
index 000000000..0ec7e3427
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/CraftOs_Test.kt
@@ -0,0 +1,22 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.gametest.api.GameTestHolder
+import dan200.computercraft.gametest.api.Timeouts
+import dan200.computercraft.gametest.api.sequence
+import dan200.computercraft.gametest.api.thenComputerOk
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestHelper
+
+@GameTestHolder
+class CraftOs_Test {
+ /**
+ * Sends a rednet message to another a computer and back again.
+ */
+ @GameTest(timeoutTicks = Timeouts.COMPUTER_TIMEOUT)
+ fun Sends_basic_rednet_messages(context: GameTestHelper) = context.sequence { thenComputerOk("main") }
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/Disk_Drive_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/Disk_Drive_Test.kt
new file mode 100644
index 000000000..3076f925f
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/Disk_Drive_Test.kt
@@ -0,0 +1,43 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.gametest.api.GameTestHolder
+import dan200.computercraft.gametest.api.sequence
+import dan200.computercraft.gametest.api.thenOnComputer
+import dan200.computercraft.test.core.assertArrayEquals
+import net.minecraft.core.BlockPos
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestHelper
+import net.minecraft.world.item.Items
+
+@GameTestHolder
+class Disk_Drive_Test {
+ /**
+ * Ensure audio disks exist and we can play them.
+ *
+ * @see [#688](https://github.com/cc-tweaked/CC-Tweaked/issues/688)
+ */
+ @GameTest
+ fun Audio_disk(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ callPeripheral("right", "hasAudio")
+ .assertArrayEquals(true, message = "Disk has audio")
+
+ callPeripheral("right", "getAudioTitle")
+ .assertArrayEquals("C418 - 13", message = "Correct audio title")
+ }
+ }
+
+ @GameTest
+ fun Ejects_disk(helper: GameTestHelper) = helper.sequence {
+ val stackAt = BlockPos(2, 2, 2)
+ thenOnComputer { callPeripheral("right", "ejectDisk") }
+ thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) }
+ }
+
+ // TODO: Ejecting disk unmounts and closes files
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/Loot_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/Loot_Test.kt
new file mode 100644
index 000000000..dd5b52121
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/Loot_Test.kt
@@ -0,0 +1,37 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.gametest.api.GameTestHolder
+import dan200.computercraft.gametest.api.Structures
+import dan200.computercraft.gametest.api.sequence
+import dan200.computercraft.shared.Registry
+import net.minecraft.core.BlockPos
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestHelper
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.world.level.block.entity.ChestBlockEntity
+import net.minecraft.world.level.storage.loot.BuiltInLootTables
+
+@GameTestHolder
+class Loot_Test {
+ /**
+ * Test that the loot tables will spawn in treasure disks.
+ */
+ @GameTest(template = Structures.DEFAULT)
+ fun Chest_contains_disk(context: GameTestHelper) = context.sequence {
+ thenExecute {
+ val pos = BlockPos(2, 2, 2)
+
+ context.setBlock(pos, Blocks.CHEST)
+ val chest = context.getBlockEntity(pos) as ChestBlockEntity
+ chest.setLootTable(BuiltInLootTables.SIMPLE_DUNGEON, 123)
+ chest.unpackLootTable(null)
+
+ context.assertContainerContains(pos, Registry.ModItems.TREASURE_DISK.get())
+ }
+ }
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt
new file mode 100644
index 000000000..a4b454ba3
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/Modem_Test.kt
@@ -0,0 +1,102 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.api.lua.ObjectArguments
+import dan200.computercraft.core.apis.PeripheralAPI
+import dan200.computercraft.core.computer.ComputerSide
+import dan200.computercraft.gametest.api.*
+import dan200.computercraft.shared.peripheral.modem.wired.BlockCable
+import dan200.computercraft.test.core.assertArrayEquals
+import dan200.computercraft.test.core.computer.LuaTaskContext
+import dan200.computercraft.test.core.computer.getApi
+import net.minecraft.core.BlockPos
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestHelper
+import org.junit.jupiter.api.Assertions.assertEquals
+import kotlin.time.Duration.Companion.milliseconds
+
+@GameTestHolder
+class Modem_Test {
+ @GameTest
+ fun Have_peripherals(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ assertEquals(listOf("monitor_0", "printer_0", "right"), getPeripheralNames(), "Starts with peripherals")
+ }
+ }
+
+ @GameTest
+ fun Gains_peripherals(helper: GameTestHelper) = helper.sequence {
+ val position = BlockPos(2, 2, 2)
+ thenOnComputer {
+ assertEquals(listOf("back"), getPeripheralNames(), "Starts with peripherals")
+ }
+ thenExecute {
+ helper.setBlock(
+ position,
+ BlockCable.correctConnections(
+ helper.level,
+ helper.absolutePos(position),
+ dan200.computercraft.shared.Registry.ModBlocks.CABLE.get().defaultBlockState().setValue(BlockCable.CABLE, true),
+ ),
+ )
+ }
+ thenIdle(1)
+ thenOnComputer {
+ assertEquals(listOf("back", "monitor_1", "printer_1"), getPeripheralNames(), "Gains new peripherals")
+ }
+ }
+
+ /**
+ * Sends a modem message to another computer on the same network
+ */
+ @GameTest
+ fun Transmits_messages(context: GameTestHelper) = context.sequence {
+ thenStartComputer("send") {
+ val modem = findPeripheral("modem") ?: throw IllegalStateException("Cannot find modem")
+ while (true) {
+ callPeripheral(modem, "transmit", 12, 34, "Hello")
+ sleep(50.milliseconds)
+ }
+ }
+ thenOnComputer("receive") {
+ val modem = findPeripheral("modem") ?: throw IllegalStateException("Cannot find modem")
+ callPeripheral(modem, "open", 12)
+
+ pullEvent("modem_message")
+ .assertArrayEquals("modem_message", "left", 12, 34, "Hello", 4, message = "Modem message")
+ }
+ }
+}
+
+private fun LuaTaskContext.findPeripheral(type: String): String? {
+ val peripheral = getApi()
+ for (side in ComputerSide.NAMES) {
+ val hasType = peripheral.hasType(side, type)
+ if (hasType != null && hasType[0] == true) return side
+ }
+
+ return null
+}
+
+private suspend fun LuaTaskContext.getPeripheralNames(): List {
+ val peripheral = getApi()
+ val peripherals = mutableListOf()
+ for (side in ComputerSide.NAMES) {
+ if (!peripheral.isPresent(side)) continue
+ peripherals.add(side)
+
+ val hasType = peripheral.hasType(side, "modem")
+ if (hasType == null || hasType[0] != true) continue
+
+ val names = peripheral.call(context, ObjectArguments(side, "getNamesRemote")).await() ?: continue
+ @Suppress("UNCHECKED_CAST")
+ peripherals.addAll(names[0] as Collection)
+ }
+
+ peripherals.sort()
+ return peripherals
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/Monitor_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/Monitor_Test.kt
new file mode 100644
index 000000000..e34ba16d3
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/Monitor_Test.kt
@@ -0,0 +1,50 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.gametest.api.GameTestHolder
+import dan200.computercraft.gametest.api.getBlockEntity
+import dan200.computercraft.gametest.api.sequence
+import dan200.computercraft.gametest.api.setBlock
+import dan200.computercraft.shared.Registry
+import net.minecraft.commands.arguments.blocks.BlockInput
+import net.minecraft.core.BlockPos
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestHelper
+import net.minecraft.nbt.CompoundTag
+import net.minecraft.world.level.block.Blocks
+import java.util.*
+
+@GameTestHolder
+class Monitor_Test {
+ @GameTest
+ fun Ensures_valid_on_place(context: GameTestHelper) = context.sequence {
+ val pos = BlockPos(2, 2, 2)
+
+ thenExecute {
+ val tag = CompoundTag()
+ tag.putInt("Width", 2)
+ tag.putInt("Height", 2)
+
+ val toSet = BlockInput(
+ Registry.ModBlocks.MONITOR_ADVANCED.get().defaultBlockState(),
+ Collections.emptySet(),
+ tag,
+ )
+
+ context.setBlock(pos, Blocks.AIR.defaultBlockState())
+ context.setBlock(pos, toSet)
+ }
+ thenIdle(2)
+ thenExecute {
+ val tile = context.getBlockEntity(pos, Registry.ModBlockEntities.MONITOR_ADVANCED.get())
+
+ if (tile.width != 1 || tile.height != 1) {
+ context.fail("Tile has width and height of ${tile.width}x${tile.height}, but should be 1x1", pos)
+ }
+ }
+ }
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/Recipe_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/Recipe_Test.kt
new file mode 100644
index 000000000..4e9bfdc5a
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/Recipe_Test.kt
@@ -0,0 +1,61 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.gametest.api.GameTestHolder
+import dan200.computercraft.gametest.api.Structures
+import dan200.computercraft.gametest.api.sequence
+import dan200.computercraft.shared.Registry
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestAssertException
+import net.minecraft.gametest.framework.GameTestHelper
+import net.minecraft.nbt.CompoundTag
+import net.minecraft.world.entity.player.Player
+import net.minecraft.world.inventory.AbstractContainerMenu
+import net.minecraft.world.inventory.CraftingContainer
+import net.minecraft.world.inventory.MenuType
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.world.item.crafting.CraftingRecipe
+import net.minecraft.world.item.crafting.RecipeType
+import org.junit.jupiter.api.Assertions.assertEquals
+import java.util.*
+
+@GameTestHolder
+class Recipe_Test {
+ /**
+ * Test that crafting results contain NBT data.
+ *
+ * Mostly useful for Fabric, where we need a mixin for this.
+ */
+ @GameTest(template = Structures.DEFAULT)
+ fun Craft_result_has_nbt(context: GameTestHelper) = context.sequence {
+ thenExecute {
+ val container = CraftingContainer(DummyMenu, 3, 3)
+ container.setItem(0, ItemStack(Items.SKELETON_SKULL))
+ container.setItem(1, ItemStack(Registry.ModItems.COMPUTER_ADVANCED.get()))
+
+ val recipe: Optional = context.level.server.recipeManager
+ .getRecipeFor(RecipeType.CRAFTING, container, context.level)
+ if (!recipe.isPresent) throw GameTestAssertException("No recipe matches")
+
+ val result = recipe.get().assemble(container)
+
+ val owner = CompoundTag()
+ owner.putString("Name", "dan200")
+ owner.putString("Id", "f3c8d69b-0776-4512-8434-d1b2165909eb")
+ val tag = CompoundTag()
+ tag.put("SkullOwner", owner)
+
+ assertEquals(tag, result.tag, "Expected NBT tags to be the same")
+ }
+ }
+
+ object DummyMenu : AbstractContainerMenu(MenuType.GENERIC_9x1, 0) {
+ override fun quickMoveStack(player: Player, slot: Int): ItemStack = ItemStack.EMPTY
+ override fun stillValid(p0: Player): Boolean = true
+ }
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt b/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt
new file mode 100644
index 000000000..41d378c6d
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt
@@ -0,0 +1,252 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest
+
+import dan200.computercraft.api.detail.BasicItemDetailProvider
+import dan200.computercraft.api.detail.DetailRegistries
+import dan200.computercraft.api.lua.ObjectArguments
+import dan200.computercraft.core.apis.PeripheralAPI
+import dan200.computercraft.gametest.api.*
+import dan200.computercraft.shared.Registry
+import dan200.computercraft.shared.media.items.ItemPrintout
+import dan200.computercraft.shared.peripheral.monitor.BlockMonitor
+import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState
+import dan200.computercraft.shared.turtle.apis.TurtleAPI
+import dan200.computercraft.test.core.assertArrayEquals
+import dan200.computercraft.test.core.computer.LuaTaskContext
+import dan200.computercraft.test.core.computer.getApi
+import net.minecraft.core.BlockPos
+import net.minecraft.gametest.framework.GameTest
+import net.minecraft.gametest.framework.GameTestHelper
+import net.minecraft.world.entity.EntityType
+import net.minecraft.world.entity.item.PrimedTnt
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.world.level.block.FenceBlock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.array
+import org.hamcrest.Matchers.instanceOf
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import java.util.*
+
+@GameTestHolder
+class Turtle_Test {
+ @GameTest
+ fun Unequip_refreshes_peripheral(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().getType("right").assertArrayEquals("modem", message = "Starts with a modem")
+ getApi().equipRight().await()
+ getApi().getType("right").assertArrayEquals("drive", message = "Unequipping gives a drive")
+ }
+ }
+
+ /**
+ * Checks turtles can sheer sheep (and drop items)
+ *
+ * @see [#537](https://github.com/cc-tweaked/CC-Tweaked/issues/537)
+ */
+ @GameTest
+ fun Shears_sheep(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().placeDown(ObjectArguments()).await()
+ .assertArrayEquals(true, message = "Shears the sheep")
+
+ assertEquals("minecraft:white_wool", getTurtleItemDetail(2)["name"])
+ }
+ }
+
+ /**
+ * Checks turtles can place lava.
+ *
+ * @see [#518](https://github.com/cc-tweaked/CC-Tweaked/issues/518)
+ */
+ @GameTest
+ fun Place_lava(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().placeDown(ObjectArguments()).await()
+ .assertArrayEquals(true, message = "Placed lava")
+ }
+ thenExecute { helper.assertBlockPresent(Blocks.LAVA, BlockPos(2, 2, 2)) }
+ }
+
+ /**
+ * Checks turtles can place when waterlogged.
+ *
+ * @see [#385](https://github.com/cc-tweaked/CC-Tweaked/issues/385)
+ */
+ @GameTest
+ fun Place_waterlogged(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().place(ObjectArguments()).await()
+ .assertArrayEquals(true, message = "Placed oak fence")
+ }
+ thenExecute {
+ helper.assertBlockIs(BlockPos(2, 2, 2), { it.block == Blocks.OAK_FENCE && it.getValue(FenceBlock.WATERLOGGED) })
+ }
+ }
+
+ /**
+ * Checks turtles can pick up lava
+ *
+ * @see [#297](https://github.com/cc-tweaked/CC-Tweaked/issues/297)
+ */
+ @GameTest
+ fun Gather_lava(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().placeDown(ObjectArguments()).await()
+ .assertArrayEquals(true, message = "Picked up lava")
+
+ assertEquals("minecraft:lava_bucket", getTurtleItemDetail()["name"])
+ }
+ thenExecute { helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 2)) }
+ }
+
+ /**
+ * Checks turtles can hoe dirt.
+ *
+ * @see [#258](https://github.com/cc-tweaked/CC-Tweaked/issues/258)
+ */
+ @GameTest
+ fun Hoe_dirt(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().dig(Optional.empty()).await()
+ .assertArrayEquals(true, message = "Dug with hoe")
+ }
+ thenExecute { helper.assertBlockPresent(Blocks.FARMLAND, BlockPos(1, 2, 1)) }
+ }
+
+ /**
+ * Checks turtles can place monitors
+ *
+ * @see [#691](https://github.com/cc-tweaked/CC-Tweaked/issues/691)
+ */
+ @GameTest
+ fun Place_monitor(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().place(ObjectArguments()).await()
+ .assertArrayEquals(true, message = "Block was placed")
+ }
+ thenIdle(1)
+ thenExecute { helper.assertBlockHas(BlockPos(1, 2, 3), BlockMonitor.STATE, MonitorEdgeState.LR) }
+ }
+
+ /**
+ * Checks turtles can place into compostors. These are non-typical inventories, so
+ * worth testing.
+ */
+ @GameTest
+ fun Use_compostors(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ getApi().dropDown(Optional.empty()).await()
+ .assertArrayEquals(true, message = "Item was dropped")
+ assertEquals(63, getApi().getItemCount(Optional.of(1)), "Only dropped one item")
+ }
+ }
+
+ /**
+ * Checks turtles can be cleaned in cauldrons.
+ *
+ * Currently not required as turtles can no longer right-click cauldrons.
+ */
+ @GameTest(required = false)
+ fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence {
+ thenOnComputer {
+ val details = getTurtleItemDetail(1, true)
+ getApi().place(ObjectArguments()).await()
+ .assertArrayEquals(true, message = "Used item on cauldron")
+ val newDetails = getTurtleItemDetail(1, true)
+
+ assertEquals("computercraft:turtle_normal", newDetails["name"], "Still a turtle")
+ assertNotEquals(details["nbt"], newDetails["nbt"], "Colour should have changed")
+ }
+ }
+
+ /**
+ * Checks turtles can use IDetailProviders by getting details for a printed page.
+ */
+ @GameTest
+ fun Item_detail_provider(helper: GameTestHelper) = helper.sequence {
+ // Register a dummy provider for printout items
+ thenExecute {
+ DetailRegistries.ITEM_STACK.addProvider(
+ object :
+ BasicItemDetailProvider("printout", ItemPrintout::class.java) {
+ override fun provideDetails(data: MutableMap, stack: ItemStack, item: ItemPrintout) {
+ data["type"] = item.type.toString().lowercase()
+ }
+ },
+ )
+ }
+ thenOnComputer {
+ val details = getTurtleItemDetail(detailed = true)
+ assertEquals(mapOf("type" to "page"), details["printout"]) {
+ "Printout information is returned (whole map is $details)"
+ }
+ }
+ }
+
+ /**
+ * Advanced turtles resist all explosions but normal ones don't.
+ */
+ @GameTest
+ fun Resists_explosions(helper: GameTestHelper) = helper.sequence {
+ thenExecute {
+ val pos = helper.absolutePos(BlockPos(2, 2, 2))
+ val tnt = PrimedTnt(helper.level, pos.x + 0.5, pos.y + 1.0, pos.z + 0.5, null)
+ tnt.fuse = 1
+ helper.level.addFreshEntity(tnt)
+ }
+ thenWaitUntil { helper.assertEntityNotPresent(EntityType.TNT) }
+ thenExecute {
+ helper.assertBlockPresent(Registry.ModBlocks.TURTLE_ADVANCED.get(), BlockPos(2, 2, 2))
+ helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 1))
+ }
+ }
+
+ /**
+ * Turtles resist mob explosions
+ */
+ @GameTest
+ fun Resists_entity_explosions(helper: GameTestHelper) = helper.sequence {
+ thenExecute { helper.getEntity(EntityType.CREEPER).ignite() }
+ thenWaitUntil { helper.assertEntityNotPresent(EntityType.CREEPER) }
+ thenExecute {
+ helper.assertBlockPresent(Registry.ModBlocks.TURTLE_ADVANCED.get(), BlockPos(2, 2, 2))
+ helper.assertBlockPresent(Registry.ModBlocks.TURTLE_NORMAL.get(), BlockPos(2, 2, 1))
+ }
+ }
+
+ /**
+ * Test calling `turtle.drop` into an inventory.
+ */
+ @GameTest
+ fun Drop_to_chest(helper: GameTestHelper) = helper.sequence {
+ val turtle = BlockPos(2, 2, 2)
+ val chest = BlockPos(2, 2, 3)
+
+ thenOnComputer {
+ getApi().drop(Optional.of(32)).await()
+ .assertArrayEquals(true, message = "Could not drop items")
+ }
+ thenExecute {
+ helper.assertContainerExactly(turtle, listOf(ItemStack(Blocks.DIRT, 32), ItemStack.EMPTY, ItemStack(Blocks.DIRT, 32)))
+ helper.assertContainerExactly(chest, listOf(ItemStack(Blocks.DIRT, 48)))
+ }
+ }
+
+ // TODO: Ghost peripherals?
+ // TODO: Dropping into minecarts
+ // TODO: Turtle sucking from items
+}
+
+private suspend fun LuaTaskContext.getTurtleItemDetail(slot: Int = 1, detailed: Boolean = false): Map {
+ val item = getApi().getItemDetail(context, Optional.of(slot), Optional.of(detailed)).await()
+ assertThat("Returns details", item, array(instanceOf(Map::class.java)))
+
+ @Suppress("UNCHECKED_CAST")
+ return item!![0] as Map
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt b/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt
new file mode 100644
index 000000000..e492f8fe0
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/api/TestExtensions.kt
@@ -0,0 +1,222 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest.api
+
+import dan200.computercraft.gametest.core.ManagedComputers
+import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
+import dan200.computercraft.mixin.gametest.GameTestSequenceAccessor
+import dan200.computercraft.test.core.computer.LuaTaskContext
+import net.minecraft.commands.arguments.blocks.BlockInput
+import net.minecraft.core.BlockPos
+import net.minecraft.gametest.framework.*
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.world.Container
+import net.minecraft.world.entity.Entity
+import net.minecraft.world.entity.EntityType
+import net.minecraft.world.item.ItemStack
+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.Property
+import net.minecraftforge.registries.ForgeRegistries
+
+/**
+ * Globally usable structures.
+ *
+ * @see GameTest.template
+ */
+object Structures {
+ /** The "default" structure, a 5x5 area with a polished Andesite floor */
+ const val DEFAULT = "default"
+}
+
+/** Pre-set in-game times */
+object Times {
+ const val NOON: Long = 6000
+}
+
+/**
+ * Custom timeouts for various test types.
+ *
+ * @see GameTest.timeoutTicks
+ */
+object Timeouts {
+ private const val SECOND: Int = 20
+
+ const val COMPUTER_TIMEOUT: Int = SECOND * 15
+}
+
+/**
+ * Equivalent to [GameTestSequence.thenExecute], but which won't run the next steps if the parent fails.
+ */
+fun GameTestSequence.thenExecuteFailFast(task: Runnable): GameTestSequence =
+ thenExecute(task).thenWaitUntil {
+ val failure = (this as GameTestSequenceAccessor).parent.error
+ if (failure != null) throw failure
+ }
+
+/**
+ * Wait until a computer has finished running and check it is OK.
+ */
+fun GameTestSequence.thenComputerOk(name: String? = null, marker: String = ComputerState.DONE): GameTestSequence {
+ val label = (this as GameTestSequenceAccessor).parent.testName + (if (name == null) "" else ".$name")
+
+ thenWaitUntil {
+ val computer = ComputerState.get(label)
+ if (computer == null || !computer.isDone(marker)) throw GameTestAssertException("Computer '$label' has not reached $marker yet.")
+ }
+ thenExecuteFailFast { ComputerState.get(label)!!.check(marker) }
+ return this
+}
+
+/**
+ * Run a task on a computer but don't wait for it to finish.
+ */
+fun GameTestSequence.thenStartComputer(name: String? = null, action: suspend LuaTaskContext.() -> Unit): GameTestSequence {
+ val test = (this as GameTestSequenceAccessor).parent
+ val label = test.testName + (if (name == null) "" else ".$name")
+ return thenExecuteFailFast { ManagedComputers.enqueue(test, label, action) }
+}
+
+/**
+ * Run a task on a computer and wait for it to finish.
+ */
+fun GameTestSequence.thenOnComputer(name: String? = null, action: suspend LuaTaskContext.() -> Unit): GameTestSequence {
+ val test = (this as GameTestSequenceAccessor).parent
+ val label = test.testName + (if (name == null) "" else ".$name")
+ var monitor: ManagedComputers.Monitor? = null
+ thenExecuteFailFast { monitor = ManagedComputers.enqueue(test, label, action) }
+ thenWaitUntil { if (!monitor!!.isFinished) throw GameTestAssertException("Computer '$label' has not finished yet.") }
+ thenExecuteFailFast { monitor!!.check() }
+ return this
+}
+
+/**
+ * Create a new game test sequence
+ */
+fun GameTestHelper.sequence(run: GameTestSequence.() -> Unit) {
+ val sequence = startSequence()
+ run(sequence)
+ sequence.thenSucceed()
+}
+
+/**
+ * A custom instance of [GameTestAssertPosException] which allows for longer error messages.
+ */
+private class VerboseGameTestAssertPosException(message: String, absolutePos: BlockPos, relativePos: BlockPos, tick: Long) :
+ GameTestAssertPosException(message, absolutePos, relativePos, tick) {
+ override fun getMessageToShowAtBlock(): String = message!!.lineSequence().first()
+}
+
+/**
+ * Fail this test. Unlike [GameTestHelper.fail], this trims the in-game error message to the first line.
+ */
+private fun GameTestHelper.failVerbose(message: String, pos: BlockPos): Nothing {
+ throw VerboseGameTestAssertPosException(message, absolutePos(pos), pos, tick)
+}
+
+/** Fail with an optional context message. */
+private fun GameTestHelper.fail(message: String?, detail: String, pos: BlockPos): Nothing {
+ failVerbose(if (message.isNullOrEmpty()) detail else "$message: $detail", pos)
+}
+
+/**
+ * A version of [GameTestHelper.assertBlockState] which also includes the current block state.
+ */
+fun GameTestHelper.assertBlockIs(pos: BlockPos, predicate: (BlockState) -> Boolean, message: String = "") {
+ val state = getBlockState(pos)
+ if (!predicate(state)) fail(message, state.toString(), pos)
+}
+
+/**
+ * A version of [GameTestHelper.assertBlockProperty] which includes the current block state in the error message.
+ */
+fun > GameTestHelper.assertBlockHas(pos: BlockPos, property: Property, value: T, message: String = "") {
+ val state = getBlockState(pos)
+ if (!state.hasProperty(property)) {
+ val id = ForgeRegistries.BLOCKS.getKey(state.block)
+ fail(message, "block $id does not have property ${property.name}", pos)
+ } else if (state.getValue(property) != value) {
+ fail(message, "${property.name} is ${state.getValue(property)}, expected $value", pos)
+ }
+}
+
+/**
+ * Assert a container contains exactly these items and no more.
+ *
+ * @param pos The position of the container.
+ * @param items The list of items this container must contain. This should be equal to the expected contents of the
+ * first `n` slots - the remaining are required to be empty.
+ */
+fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List) {
+ val container = getBlockEntity(pos) ?: failVerbose("Expected a container at $pos, found nothing", pos)
+ if (container !is Container) {
+ failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
+ }
+
+ val slot = (0 until container.containerSize).indexOfFirst { slot ->
+ val expected = if (slot >= items.size) ItemStack.EMPTY else items[slot]
+ !ItemStack.matches(container.getItem(slot), expected)
+ }
+
+ if (slot >= 0) {
+ failVerbose(
+ """
+ Items do not match (first mismatch at slot $slot).
+ Expected: $items
+ Container: ${(0 until container.containerSize).map { container.getItem(it) }.dropLastWhile { it.isEmpty }}
+ """.trimIndent(),
+ pos,
+ )
+ }
+}
+
+private fun getName(type: BlockEntityType<*>): ResourceLocation = ForgeRegistries.BLOCK_ENTITIES.getKey(type)!!
+
+/**
+ * Get a [BlockEntity] of a specific type.
+ */
+fun GameTestHelper.getBlockEntity(pos: BlockPos, type: BlockEntityType): T {
+ val tile = getBlockEntity(pos)
+ @Suppress("UNCHECKED_CAST")
+ return when {
+ tile == null -> failVerbose("Expected ${getName(type)}, but no tile was there", pos)
+ tile.type != type -> failVerbose("Expected ${getName(type)} but got ${getName(tile.type)}", pos)
+ else -> tile as T
+ }
+}
+
+/**
+ * Get all entities of a specific type within the test structure.
+ */
+fun GameTestHelper.getEntities(type: EntityType): List {
+ val info = (this as GameTestHelperAccessor).testInfo
+ return level.getEntities(type, info.structureBounds!!) { it.isAlive }
+}
+
+/**
+ * Get an [Entity] inside the game structure, requiring there to be a single one.
+ */
+fun GameTestHelper.getEntity(type: EntityType): T {
+ val entities = getEntities(type)
+ when (entities.size) {
+ 0 -> throw GameTestAssertException("No $type entities")
+ 1 -> return entities[0]
+ else -> throw GameTestAssertException("Multiple $type entities (${entities.size} in bounding box)")
+ }
+}
+
+/**
+ * Set a block within the test structure.
+ */
+fun GameTestHelper.setBlock(pos: BlockPos, state: BlockInput) = state.place(level, absolutePos(pos), 3)
+
+/**
+ * Modify a block state within the test.
+ */
+fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
+ setBlock(pos, modify(getBlockState(pos)))
+}
diff --git a/src/testMod/kotlin/dan200/computercraft/gametest/core/ManagedComputers.kt b/src/testMod/kotlin/dan200/computercraft/gametest/core/ManagedComputers.kt
new file mode 100644
index 000000000..624c98ea7
--- /dev/null
+++ b/src/testMod/kotlin/dan200/computercraft/gametest/core/ManagedComputers.kt
@@ -0,0 +1,137 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.gametest.core
+
+import dan200.computercraft.api.lua.ILuaAPI
+import dan200.computercraft.core.apis.OSAPI
+import dan200.computercraft.core.lua.CobaltLuaMachine
+import dan200.computercraft.core.lua.ILuaMachine
+import dan200.computercraft.core.lua.MachineEnvironment
+import dan200.computercraft.core.lua.MachineResult
+import dan200.computercraft.gametest.api.thenOnComputer
+import dan200.computercraft.mixin.gametest.GameTestInfoAccessor
+import dan200.computercraft.shared.computer.core.ServerContext
+import dan200.computercraft.test.core.computer.KotlinLuaMachine
+import dan200.computercraft.test.core.computer.LuaTaskContext
+import net.minecraft.gametest.framework.GameTestAssertException
+import net.minecraft.gametest.framework.GameTestAssertPosException
+import net.minecraft.gametest.framework.GameTestInfo
+import net.minecraft.gametest.framework.GameTestSequence
+import org.apache.logging.log4j.LogManager
+import java.io.InputStream
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedDeque
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Provides a custom [ILuaMachine] which allows computers to run Kotlin or Lua code, depending on their ID.
+ *
+ * This allows writing game tests which consume Lua APIs, without having the overhead of starting a new computer for
+ * each test.
+ *
+ * @see GameTestSequence.thenOnComputer
+ */
+object ManagedComputers : ILuaMachine.Factory {
+ private val LOGGER = LogManager.getLogger(ManagedComputers::class.java)
+ private val computers: MutableMap Unit>> = mutableMapOf()
+
+ internal fun enqueue(test: GameTestInfo, label: String, task: suspend LuaTaskContext.() -> Unit): Monitor {
+ val monitor = Monitor(test, label)
+ computers.computeIfAbsent(label) { ConcurrentLinkedDeque() }.add {
+ try {
+ LOGGER.info("Running $label")
+ task()
+ monitor.result.set(Result.success(Unit))
+ } catch (e: Throwable) {
+ if (e !is AssertionError) LOGGER.error("Computer $label failed", e)
+ monitor.result.set(Result.failure(e))
+ throw e
+ } finally {
+ LOGGER.info("Finished $label")
+ }
+ }
+
+ ServerContext.get(test.level.server).registry().computers
+ .firstOrNull { it.label == label }?.queueEvent("test_wakeup")
+
+ return monitor
+ }
+
+ override fun create(environment: MachineEnvironment): ILuaMachine = DelegateMachine(environment)
+
+ private class DelegateMachine(private val environment: MachineEnvironment) : ILuaMachine {
+ private val apis = mutableListOf()
+ private var delegate: ILuaMachine? = null
+
+ override fun addAPI(api: ILuaAPI) {
+ val delegate = this.delegate
+ if (delegate != null) return delegate.addAPI(api)
+
+ apis.add(api)
+
+ if (api is OSAPI) {
+ val newMachine = if (api.computerID != 1) {
+ CobaltLuaMachine(environment)
+ } else if (api.computerLabel != null) {
+ KotlinMachine(environment, api.computerLabel[0] as String)
+ } else {
+ LOGGER.error("Kotlin Lua machine must have a label")
+ CobaltLuaMachine(environment)
+ }
+
+ this.delegate = newMachine
+ for (api in apis) newMachine.addAPI(api)
+ }
+ }
+
+ override fun loadBios(bios: InputStream): MachineResult {
+ val delegate = this.delegate ?: return MachineResult.error("Computer not created")
+ return delegate.loadBios(bios)
+ }
+
+ override fun handleEvent(eventName: String?, arguments: Array?): MachineResult {
+ val delegate = this.delegate ?: return MachineResult.error("Computer not created")
+ return delegate.handleEvent(eventName, arguments)
+ }
+
+ override fun printExecutionState(out: StringBuilder) {
+ delegate?.printExecutionState(out)
+ }
+
+ override fun close() {
+ delegate?.close()
+ }
+ }
+
+ private class KotlinMachine(environment: MachineEnvironment, private val label: String) : KotlinLuaMachine(environment) {
+ override fun getTask(): (suspend KotlinLuaMachine.() -> Unit)? = computers[label]?.poll()
+ }
+
+ class Monitor(private val test: GameTestInfo, private val label: String) {
+ internal val result = AtomicReference>()
+
+ val isFinished
+ get() = result.get() != null
+
+ fun check() {
+ val result = result.get() ?: fail("Computer $label did not finish")
+ val error = result.exceptionOrNull()
+ if (error != null) fail(error.message ?: error.toString())
+ }
+
+ private fun fail(message: String): Nothing {
+ val computer =
+ ServerContext.get(test.level.server).registry().computers.firstOrNull { it.label == label }
+ if (computer == null) {
+ throw GameTestAssertException(message)
+ } else {
+ val pos = computer.position
+ val relativePos = pos.subtract(test.structureBlockPos)
+ throw GameTestAssertPosException(message, pos, relativePos, (test as GameTestInfoAccessor).`computercraft$getTick`())
+ }
+ }
+ }
+}
diff --git a/src/testMod/resources/META-INF/accesstransformer.cfg b/src/testMod/resources/META-INF/accesstransformer.cfg
deleted file mode 100644
index 6d0f7e462..000000000
--- a/src/testMod/resources/META-INF/accesstransformer.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-public net.minecraft.gametest.framework.TestCommand m_128010_(Lnet/minecraft/commands/CommandSourceStack;Ljava/lang/String;)I # exportTestStructure
-
-public net.minecraft.gametest.framework.GameTestHelper m_177448_()Lnet/minecraft/world/phys/AABB; # getBounds
-public net.minecraft.gametest.framework.GameTestHelper f_127595_ # testInfo
-
-public net.minecraft.gametest.framework.GameTestSequence f_127774_ # parent
diff --git a/src/testMod/resources/META-INF/mods.toml b/src/testMod/resources/META-INF/mods.toml
index 30ddf13c4..f9ba9dc8a 100644
--- a/src/testMod/resources/META-INF/mods.toml
+++ b/src/testMod/resources/META-INF/mods.toml
@@ -16,3 +16,10 @@ displayName="CC: Tweaked test framework"
description='''
A test framework for ensuring CC: Tweaked works correctly.
'''
+
+[[dependencies.cctest]]
+modId="computercraft"
+mandatory=true
+versionRange="[1.0,)"
+ordering="AFTER"
+side="BOTH"
diff --git a/src/testMod/resources/computercraft-gametest.mixins.json b/src/testMod/resources/computercraft-gametest.mixins.json
new file mode 100644
index 000000000..76ac77997
--- /dev/null
+++ b/src/testMod/resources/computercraft-gametest.mixins.json
@@ -0,0 +1,16 @@
+{
+ "required": true,
+ "package": "dan200.computercraft.mixin.gametest",
+ "minVersion": "0.8",
+ "compatibilityLevel": "JAVA_17",
+ "injectors": {
+ "defaultRequire": 1
+ },
+ "mixins": [
+ "GameTestHelperAccessor",
+ "GameTestInfoAccessor",
+ "GameTestSequenceAccessor",
+ "GameTestSequenceMixin",
+ "TestCommandAccessor"
+ ]
+}
diff --git a/src/testMod/server-files/computers/computer/0/startup.lua b/src/testMod/resources/data/cctest/computer/startup.lua
similarity index 100%
rename from src/testMod/server-files/computers/computer/0/startup.lua
rename to src/testMod/resources/data/cctest/computer/startup.lua
diff --git a/src/testMod/server-files/computers/computer/0/tests/craftos_test.sends_basic_rednet_messages.echo.lua b/src/testMod/resources/data/cctest/computer/tests/craftos_test.sends_basic_rednet_messages.echo.lua
similarity index 100%
rename from src/testMod/server-files/computers/computer/0/tests/craftos_test.sends_basic_rednet_messages.echo.lua
rename to src/testMod/resources/data/cctest/computer/tests/craftos_test.sends_basic_rednet_messages.echo.lua
diff --git a/src/testMod/server-files/computers/computer/0/tests/craftos_test.sends_basic_rednet_messages.main.lua b/src/testMod/resources/data/cctest/computer/tests/craftos_test.sends_basic_rednet_messages.main.lua
similarity index 100%
rename from src/testMod/server-files/computers/computer/0/tests/craftos_test.sends_basic_rednet_messages.main.lua
rename to src/testMod/resources/data/cctest/computer/tests/craftos_test.sends_basic_rednet_messages.main.lua
diff --git a/src/testMod/server-files/structures/computer_test.no_through_signal.snbt b/src/testMod/resources/data/cctest/structures/computer_test.no_through_signal.snbt
similarity index 100%
rename from src/testMod/server-files/structures/computer_test.no_through_signal.snbt
rename to src/testMod/resources/data/cctest/structures/computer_test.no_through_signal.snbt
diff --git a/src/testMod/resources/data/cctest/structures/computer_test.no_through_signal_reverse.snbt b/src/testMod/resources/data/cctest/structures/computer_test.no_through_signal_reverse.snbt
new file mode 100644
index 000000000..592ccefaa
--- /dev/null
+++ b/src/testMod/resources/data/cctest/structures/computer_test.no_through_signal_reverse.snbt
@@ -0,0 +1,141 @@
+{
+ DataVersion: 3120,
+ size: [5, 5, 5],
+ data: [
+ {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [0, 1, 0], state: "minecraft:air"},
+ {pos: [0, 1, 1], state: "minecraft:air"},
+ {pos: [0, 1, 2], state: "minecraft:air"},
+ {pos: [0, 1, 3], state: "minecraft:air"},
+ {pos: [0, 1, 4], state: "minecraft:air"},
+ {pos: [1, 1, 0], state: "minecraft:air"},
+ {pos: [1, 1, 1], state: "minecraft:air"},
+ {pos: [1, 1, 2], state: "minecraft:air"},
+ {pos: [1, 1, 3], state: "minecraft:air"},
+ {pos: [1, 1, 4], state: "minecraft:air"},
+ {pos: [2, 1, 0], state: "minecraft:lever{face:floor,facing:south,powered:false}"},
+ {pos: [2, 1, 1], state: "minecraft:repeater{delay:1,facing:north,locked:false,powered:false}"},
+ {pos: [2, 1, 2], state: "computercraft:computer_advanced{facing:north,state:off}", nbt: {ComputerId: 0, Label: "computer_test.no_through_signal_rev", On: 0b, id: "computercraft:computer_advanced"}},
+ {pos: [2, 1, 3], state: "minecraft:redstone_wire{east:none,north:side,power:0,south:side,west:none}"},
+ {pos: [2, 1, 4], state: "minecraft:redstone_lamp{lit:false}"},
+ {pos: [3, 1, 0], state: "minecraft:air"},
+ {pos: [3, 1, 1], state: "minecraft:air"},
+ {pos: [3, 1, 2], state: "minecraft:air"},
+ {pos: [3, 1, 3], state: "minecraft:air"},
+ {pos: [3, 1, 4], state: "minecraft:air"},
+ {pos: [4, 1, 0], state: "minecraft:air"},
+ {pos: [4, 1, 1], state: "minecraft:air"},
+ {pos: [4, 1, 2], state: "minecraft:air"},
+ {pos: [4, 1, 3], state: "minecraft:air"},
+ {pos: [4, 1, 4], state: "minecraft:air"},
+ {pos: [0, 2, 0], state: "minecraft:air"},
+ {pos: [0, 2, 1], state: "minecraft:air"},
+ {pos: [0, 2, 2], state: "minecraft:air"},
+ {pos: [0, 2, 3], state: "minecraft:air"},
+ {pos: [0, 2, 4], state: "minecraft:air"},
+ {pos: [1, 2, 0], state: "minecraft:air"},
+ {pos: [1, 2, 1], state: "minecraft:air"},
+ {pos: [1, 2, 2], state: "minecraft:air"},
+ {pos: [1, 2, 3], state: "minecraft:air"},
+ {pos: [1, 2, 4], state: "minecraft:air"},
+ {pos: [2, 2, 0], state: "minecraft:air"},
+ {pos: [2, 2, 1], state: "minecraft:air"},
+ {pos: [2, 2, 2], state: "minecraft:air"},
+ {pos: [2, 2, 3], state: "minecraft:air"},
+ {pos: [2, 2, 4], state: "minecraft:air"},
+ {pos: [3, 2, 0], state: "minecraft:air"},
+ {pos: [3, 2, 1], state: "minecraft:air"},
+ {pos: [3, 2, 2], state: "minecraft:air"},
+ {pos: [3, 2, 3], state: "minecraft:air"},
+ {pos: [3, 2, 4], state: "minecraft:air"},
+ {pos: [4, 2, 0], state: "minecraft:air"},
+ {pos: [4, 2, 1], state: "minecraft:air"},
+ {pos: [4, 2, 2], state: "minecraft:air"},
+ {pos: [4, 2, 3], state: "minecraft:air"},
+ {pos: [4, 2, 4], state: "minecraft:air"},
+ {pos: [0, 3, 0], state: "minecraft:air"},
+ {pos: [0, 3, 1], state: "minecraft:air"},
+ {pos: [0, 3, 2], state: "minecraft:air"},
+ {pos: [0, 3, 3], state: "minecraft:air"},
+ {pos: [0, 3, 4], state: "minecraft:air"},
+ {pos: [1, 3, 0], state: "minecraft:air"},
+ {pos: [1, 3, 1], state: "minecraft:air"},
+ {pos: [1, 3, 2], state: "minecraft:air"},
+ {pos: [1, 3, 3], state: "minecraft:air"},
+ {pos: [1, 3, 4], state: "minecraft:air"},
+ {pos: [2, 3, 0], state: "minecraft:air"},
+ {pos: [2, 3, 1], state: "minecraft:air"},
+ {pos: [2, 3, 2], state: "minecraft:air"},
+ {pos: [2, 3, 3], state: "minecraft:air"},
+ {pos: [2, 3, 4], state: "minecraft:air"},
+ {pos: [3, 3, 0], state: "minecraft:air"},
+ {pos: [3, 3, 1], state: "minecraft:air"},
+ {pos: [3, 3, 2], state: "minecraft:air"},
+ {pos: [3, 3, 3], state: "minecraft:air"},
+ {pos: [3, 3, 4], state: "minecraft:air"},
+ {pos: [4, 3, 0], state: "minecraft:air"},
+ {pos: [4, 3, 1], state: "minecraft:air"},
+ {pos: [4, 3, 2], state: "minecraft:air"},
+ {pos: [4, 3, 3], state: "minecraft:air"},
+ {pos: [4, 3, 4], state: "minecraft:air"},
+ {pos: [0, 4, 0], state: "minecraft:air"},
+ {pos: [0, 4, 1], state: "minecraft:air"},
+ {pos: [0, 4, 2], state: "minecraft:air"},
+ {pos: [0, 4, 3], state: "minecraft:air"},
+ {pos: [0, 4, 4], state: "minecraft:air"},
+ {pos: [1, 4, 0], state: "minecraft:air"},
+ {pos: [1, 4, 1], state: "minecraft:air"},
+ {pos: [1, 4, 2], state: "minecraft:air"},
+ {pos: [1, 4, 3], state: "minecraft:air"},
+ {pos: [1, 4, 4], state: "minecraft:air"},
+ {pos: [2, 4, 0], state: "minecraft:air"},
+ {pos: [2, 4, 1], state: "minecraft:air"},
+ {pos: [2, 4, 2], state: "minecraft:air"},
+ {pos: [2, 4, 3], state: "minecraft:air"},
+ {pos: [2, 4, 4], state: "minecraft:air"},
+ {pos: [3, 4, 0], state: "minecraft:air"},
+ {pos: [3, 4, 1], state: "minecraft:air"},
+ {pos: [3, 4, 2], state: "minecraft:air"},
+ {pos: [3, 4, 3], state: "minecraft:air"},
+ {pos: [3, 4, 4], state: "minecraft:air"},
+ {pos: [4, 4, 0], state: "minecraft:air"},
+ {pos: [4, 4, 1], state: "minecraft:air"},
+ {pos: [4, 4, 2], state: "minecraft:air"},
+ {pos: [4, 4, 3], state: "minecraft:air"},
+ {pos: [4, 4, 4], state: "minecraft:air"}
+ ],
+ entities: [],
+ palette: [
+ "minecraft:polished_andesite",
+ "minecraft:redstone_lamp{lit:false}",
+ "minecraft:air",
+ "minecraft:lever{face:floor,facing:south,powered:false}",
+ "minecraft:repeater{delay:1,facing:north,locked:false,powered:false}",
+ "minecraft:redstone_wire{east:none,north:side,power:0,south:side,west:none}",
+ "computercraft:computer_advanced{facing:north,state:off}"
+ ]
+}
diff --git a/src/testMod/resources/data/cctest/structures/computer_test.set_and_destroy.snbt b/src/testMod/resources/data/cctest/structures/computer_test.set_and_destroy.snbt
new file mode 100644
index 000000000..c9dcb29c8
--- /dev/null
+++ b/src/testMod/resources/data/cctest/structures/computer_test.set_and_destroy.snbt
@@ -0,0 +1,138 @@
+{
+ DataVersion: 3120,
+ size: [5, 5, 5],
+ data: [
+ {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [0, 1, 0], state: "minecraft:air"},
+ {pos: [0, 1, 1], state: "minecraft:air"},
+ {pos: [0, 1, 2], state: "minecraft:air"},
+ {pos: [0, 1, 3], state: "minecraft:air"},
+ {pos: [0, 1, 4], state: "minecraft:air"},
+ {pos: [1, 1, 0], state: "minecraft:air"},
+ {pos: [1, 1, 1], state: "minecraft:air"},
+ {pos: [1, 1, 2], state: "minecraft:air"},
+ {pos: [1, 1, 3], state: "minecraft:air"},
+ {pos: [1, 1, 4], state: "minecraft:air"},
+ {pos: [2, 1, 0], state: "minecraft:air"},
+ {pos: [2, 1, 1], state: "minecraft:air"},
+ {pos: [2, 1, 2], state: "computercraft:computer_advanced{facing:north,state:on}", nbt: {ComputerId: 1, Label: "computer_test.set_and_destroy", On: 1b, id: "computercraft:computer_advanced"}},
+ {pos: [2, 1, 3], state: "minecraft:redstone_lamp{lit:false}"},
+ {pos: [2, 1, 4], state: "minecraft:air"},
+ {pos: [3, 1, 0], state: "minecraft:air"},
+ {pos: [3, 1, 1], state: "minecraft:air"},
+ {pos: [3, 1, 2], state: "minecraft:air"},
+ {pos: [3, 1, 3], state: "minecraft:air"},
+ {pos: [3, 1, 4], state: "minecraft:air"},
+ {pos: [4, 1, 0], state: "minecraft:air"},
+ {pos: [4, 1, 1], state: "minecraft:air"},
+ {pos: [4, 1, 2], state: "minecraft:air"},
+ {pos: [4, 1, 3], state: "minecraft:air"},
+ {pos: [4, 1, 4], state: "minecraft:air"},
+ {pos: [0, 2, 0], state: "minecraft:air"},
+ {pos: [0, 2, 1], state: "minecraft:air"},
+ {pos: [0, 2, 2], state: "minecraft:air"},
+ {pos: [0, 2, 3], state: "minecraft:air"},
+ {pos: [0, 2, 4], state: "minecraft:air"},
+ {pos: [1, 2, 0], state: "minecraft:air"},
+ {pos: [1, 2, 1], state: "minecraft:air"},
+ {pos: [1, 2, 2], state: "minecraft:air"},
+ {pos: [1, 2, 3], state: "minecraft:air"},
+ {pos: [1, 2, 4], state: "minecraft:air"},
+ {pos: [2, 2, 0], state: "minecraft:air"},
+ {pos: [2, 2, 1], state: "minecraft:air"},
+ {pos: [2, 2, 2], state: "minecraft:air"},
+ {pos: [2, 2, 3], state: "minecraft:air"},
+ {pos: [2, 2, 4], state: "minecraft:air"},
+ {pos: [3, 2, 0], state: "minecraft:air"},
+ {pos: [3, 2, 1], state: "minecraft:air"},
+ {pos: [3, 2, 2], state: "minecraft:air"},
+ {pos: [3, 2, 3], state: "minecraft:air"},
+ {pos: [3, 2, 4], state: "minecraft:air"},
+ {pos: [4, 2, 0], state: "minecraft:air"},
+ {pos: [4, 2, 1], state: "minecraft:air"},
+ {pos: [4, 2, 2], state: "minecraft:air"},
+ {pos: [4, 2, 3], state: "minecraft:air"},
+ {pos: [4, 2, 4], state: "minecraft:air"},
+ {pos: [0, 3, 0], state: "minecraft:air"},
+ {pos: [0, 3, 1], state: "minecraft:air"},
+ {pos: [0, 3, 2], state: "minecraft:air"},
+ {pos: [0, 3, 3], state: "minecraft:air"},
+ {pos: [0, 3, 4], state: "minecraft:air"},
+ {pos: [1, 3, 0], state: "minecraft:air"},
+ {pos: [1, 3, 1], state: "minecraft:air"},
+ {pos: [1, 3, 2], state: "minecraft:air"},
+ {pos: [1, 3, 3], state: "minecraft:air"},
+ {pos: [1, 3, 4], state: "minecraft:air"},
+ {pos: [2, 3, 0], state: "minecraft:air"},
+ {pos: [2, 3, 1], state: "minecraft:air"},
+ {pos: [2, 3, 2], state: "minecraft:air"},
+ {pos: [2, 3, 3], state: "minecraft:air"},
+ {pos: [2, 3, 4], state: "minecraft:air"},
+ {pos: [3, 3, 0], state: "minecraft:air"},
+ {pos: [3, 3, 1], state: "minecraft:air"},
+ {pos: [3, 3, 2], state: "minecraft:air"},
+ {pos: [3, 3, 3], state: "minecraft:air"},
+ {pos: [3, 3, 4], state: "minecraft:air"},
+ {pos: [4, 3, 0], state: "minecraft:air"},
+ {pos: [4, 3, 1], state: "minecraft:air"},
+ {pos: [4, 3, 2], state: "minecraft:air"},
+ {pos: [4, 3, 3], state: "minecraft:air"},
+ {pos: [4, 3, 4], state: "minecraft:air"},
+ {pos: [0, 4, 0], state: "minecraft:air"},
+ {pos: [0, 4, 1], state: "minecraft:air"},
+ {pos: [0, 4, 2], state: "minecraft:air"},
+ {pos: [0, 4, 3], state: "minecraft:air"},
+ {pos: [0, 4, 4], state: "minecraft:air"},
+ {pos: [1, 4, 0], state: "minecraft:air"},
+ {pos: [1, 4, 1], state: "minecraft:air"},
+ {pos: [1, 4, 2], state: "minecraft:air"},
+ {pos: [1, 4, 3], state: "minecraft:air"},
+ {pos: [1, 4, 4], state: "minecraft:air"},
+ {pos: [2, 4, 0], state: "minecraft:air"},
+ {pos: [2, 4, 1], state: "minecraft:air"},
+ {pos: [2, 4, 2], state: "minecraft:air"},
+ {pos: [2, 4, 3], state: "minecraft:air"},
+ {pos: [2, 4, 4], state: "minecraft:air"},
+ {pos: [3, 4, 0], state: "minecraft:air"},
+ {pos: [3, 4, 1], state: "minecraft:air"},
+ {pos: [3, 4, 2], state: "minecraft:air"},
+ {pos: [3, 4, 3], state: "minecraft:air"},
+ {pos: [3, 4, 4], state: "minecraft:air"},
+ {pos: [4, 4, 0], state: "minecraft:air"},
+ {pos: [4, 4, 1], state: "minecraft:air"},
+ {pos: [4, 4, 2], state: "minecraft:air"},
+ {pos: [4, 4, 3], state: "minecraft:air"},
+ {pos: [4, 4, 4], state: "minecraft:air"}
+ ],
+ entities: [],
+ palette: [
+ "minecraft:polished_andesite",
+ "minecraft:redstone_lamp{lit:false}",
+ "minecraft:air",
+ "computercraft:computer_advanced{facing:north,state:on}"
+ ]
+}
diff --git a/src/testMod/server-files/structures/craftos_test.sends_basic_rednet_messages.snbt b/src/testMod/resources/data/cctest/structures/craftos_test.sends_basic_rednet_messages.snbt
similarity index 100%
rename from src/testMod/server-files/structures/craftos_test.sends_basic_rednet_messages.snbt
rename to src/testMod/resources/data/cctest/structures/craftos_test.sends_basic_rednet_messages.snbt
diff --git a/src/testMod/resources/data/cctest/structures/default.snbt b/src/testMod/resources/data/cctest/structures/default.snbt
new file mode 100644
index 000000000..6bbbe27a5
--- /dev/null
+++ b/src/testMod/resources/data/cctest/structures/default.snbt
@@ -0,0 +1,136 @@
+{
+ DataVersion: 3120,
+ size: [5, 5, 5],
+ data: [
+ {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [0, 1, 0], state: "minecraft:air"},
+ {pos: [0, 1, 1], state: "minecraft:air"},
+ {pos: [0, 1, 2], state: "minecraft:air"},
+ {pos: [0, 1, 3], state: "minecraft:air"},
+ {pos: [0, 1, 4], state: "minecraft:air"},
+ {pos: [1, 1, 0], state: "minecraft:air"},
+ {pos: [1, 1, 1], state: "minecraft:air"},
+ {pos: [1, 1, 2], state: "minecraft:air"},
+ {pos: [1, 1, 3], state: "minecraft:air"},
+ {pos: [1, 1, 4], state: "minecraft:air"},
+ {pos: [2, 1, 0], state: "minecraft:air"},
+ {pos: [2, 1, 1], state: "minecraft:air"},
+ {pos: [2, 1, 2], state: "minecraft:air"},
+ {pos: [2, 1, 3], state: "minecraft:air"},
+ {pos: [2, 1, 4], state: "minecraft:air"},
+ {pos: [3, 1, 0], state: "minecraft:air"},
+ {pos: [3, 1, 1], state: "minecraft:air"},
+ {pos: [3, 1, 2], state: "minecraft:air"},
+ {pos: [3, 1, 3], state: "minecraft:air"},
+ {pos: [3, 1, 4], state: "minecraft:air"},
+ {pos: [4, 1, 0], state: "minecraft:air"},
+ {pos: [4, 1, 1], state: "minecraft:air"},
+ {pos: [4, 1, 2], state: "minecraft:air"},
+ {pos: [4, 1, 3], state: "minecraft:air"},
+ {pos: [4, 1, 4], state: "minecraft:air"},
+ {pos: [0, 2, 0], state: "minecraft:air"},
+ {pos: [0, 2, 1], state: "minecraft:air"},
+ {pos: [0, 2, 2], state: "minecraft:air"},
+ {pos: [0, 2, 3], state: "minecraft:air"},
+ {pos: [0, 2, 4], state: "minecraft:air"},
+ {pos: [1, 2, 0], state: "minecraft:air"},
+ {pos: [1, 2, 1], state: "minecraft:air"},
+ {pos: [1, 2, 2], state: "minecraft:air"},
+ {pos: [1, 2, 3], state: "minecraft:air"},
+ {pos: [1, 2, 4], state: "minecraft:air"},
+ {pos: [2, 2, 0], state: "minecraft:air"},
+ {pos: [2, 2, 1], state: "minecraft:air"},
+ {pos: [2, 2, 2], state: "minecraft:air"},
+ {pos: [2, 2, 3], state: "minecraft:air"},
+ {pos: [2, 2, 4], state: "minecraft:air"},
+ {pos: [3, 2, 0], state: "minecraft:air"},
+ {pos: [3, 2, 1], state: "minecraft:air"},
+ {pos: [3, 2, 2], state: "minecraft:air"},
+ {pos: [3, 2, 3], state: "minecraft:air"},
+ {pos: [3, 2, 4], state: "minecraft:air"},
+ {pos: [4, 2, 0], state: "minecraft:air"},
+ {pos: [4, 2, 1], state: "minecraft:air"},
+ {pos: [4, 2, 2], state: "minecraft:air"},
+ {pos: [4, 2, 3], state: "minecraft:air"},
+ {pos: [4, 2, 4], state: "minecraft:air"},
+ {pos: [0, 3, 0], state: "minecraft:air"},
+ {pos: [0, 3, 1], state: "minecraft:air"},
+ {pos: [0, 3, 2], state: "minecraft:air"},
+ {pos: [0, 3, 3], state: "minecraft:air"},
+ {pos: [0, 3, 4], state: "minecraft:air"},
+ {pos: [1, 3, 0], state: "minecraft:air"},
+ {pos: [1, 3, 1], state: "minecraft:air"},
+ {pos: [1, 3, 2], state: "minecraft:air"},
+ {pos: [1, 3, 3], state: "minecraft:air"},
+ {pos: [1, 3, 4], state: "minecraft:air"},
+ {pos: [2, 3, 0], state: "minecraft:air"},
+ {pos: [2, 3, 1], state: "minecraft:air"},
+ {pos: [2, 3, 2], state: "minecraft:air"},
+ {pos: [2, 3, 3], state: "minecraft:air"},
+ {pos: [2, 3, 4], state: "minecraft:air"},
+ {pos: [3, 3, 0], state: "minecraft:air"},
+ {pos: [3, 3, 1], state: "minecraft:air"},
+ {pos: [3, 3, 2], state: "minecraft:air"},
+ {pos: [3, 3, 3], state: "minecraft:air"},
+ {pos: [3, 3, 4], state: "minecraft:air"},
+ {pos: [4, 3, 0], state: "minecraft:air"},
+ {pos: [4, 3, 1], state: "minecraft:air"},
+ {pos: [4, 3, 2], state: "minecraft:air"},
+ {pos: [4, 3, 3], state: "minecraft:air"},
+ {pos: [4, 3, 4], state: "minecraft:air"},
+ {pos: [0, 4, 0], state: "minecraft:air"},
+ {pos: [0, 4, 1], state: "minecraft:air"},
+ {pos: [0, 4, 2], state: "minecraft:air"},
+ {pos: [0, 4, 3], state: "minecraft:air"},
+ {pos: [0, 4, 4], state: "minecraft:air"},
+ {pos: [1, 4, 0], state: "minecraft:air"},
+ {pos: [1, 4, 1], state: "minecraft:air"},
+ {pos: [1, 4, 2], state: "minecraft:air"},
+ {pos: [1, 4, 3], state: "minecraft:air"},
+ {pos: [1, 4, 4], state: "minecraft:air"},
+ {pos: [2, 4, 0], state: "minecraft:air"},
+ {pos: [2, 4, 1], state: "minecraft:air"},
+ {pos: [2, 4, 2], state: "minecraft:air"},
+ {pos: [2, 4, 3], state: "minecraft:air"},
+ {pos: [2, 4, 4], state: "minecraft:air"},
+ {pos: [3, 4, 0], state: "minecraft:air"},
+ {pos: [3, 4, 1], state: "minecraft:air"},
+ {pos: [3, 4, 2], state: "minecraft:air"},
+ {pos: [3, 4, 3], state: "minecraft:air"},
+ {pos: [3, 4, 4], state: "minecraft:air"},
+ {pos: [4, 4, 0], state: "minecraft:air"},
+ {pos: [4, 4, 1], state: "minecraft:air"},
+ {pos: [4, 4, 2], state: "minecraft:air"},
+ {pos: [4, 4, 3], state: "minecraft:air"},
+ {pos: [4, 4, 4], state: "minecraft:air"}
+ ],
+ entities: [],
+ palette: [
+ "minecraft:polished_andesite",
+ "minecraft:air"
+ ]
+}
diff --git a/src/testMod/server-files/structures/disk_drive_test.audio_disk.snbt b/src/testMod/resources/data/cctest/structures/disk_drive_test.audio_disk.snbt
similarity index 96%
rename from src/testMod/server-files/structures/disk_drive_test.audio_disk.snbt
rename to src/testMod/resources/data/cctest/structures/disk_drive_test.audio_disk.snbt
index ead09c8b3..95da26f01 100644
--- a/src/testMod/server-files/structures/disk_drive_test.audio_disk.snbt
+++ b/src/testMod/resources/data/cctest/structures/disk_drive_test.audio_disk.snbt
@@ -15,7 +15,7 @@
{pos: [0, 1, 1], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {Item: {Count: 1b, id: "minecraft:music_disc_13"}, id: "computercraft:disk_drive"}},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
- {pos: [1, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 0, Label: "disk_drive_test.audio_disk", On: 1b, id: "computercraft:computer_advanced"}},
+ {pos: [1, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "disk_drive_test.audio_disk", On: 1b, id: "computercraft:computer_advanced"}},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/disk_drive_test.ejects_disk.snbt b/src/testMod/resources/data/cctest/structures/disk_drive_test.ejects_disk.snbt
similarity index 99%
rename from src/testMod/server-files/structures/disk_drive_test.ejects_disk.snbt
rename to src/testMod/resources/data/cctest/structures/disk_drive_test.ejects_disk.snbt
index 7453134b3..d3576a9c4 100644
--- a/src/testMod/server-files/structures/disk_drive_test.ejects_disk.snbt
+++ b/src/testMod/resources/data/cctest/structures/disk_drive_test.ejects_disk.snbt
@@ -43,7 +43,7 @@
{pos: [2, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
- {pos: [3, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 0, Label: "disk_drive_test.ejects_disk", On: 1b, id: "computercraft:computer_advanced"}},
+ {pos: [3, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "disk_drive_test.ejects_disk", On: 1b, id: "computercraft:computer_advanced"}},
{pos: [3, 1, 2], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [3, 1, 4], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/modem_test.gains_peripherals.snbt b/src/testMod/resources/data/cctest/structures/modem_test.gains_peripherals.snbt
similarity index 99%
rename from src/testMod/server-files/structures/modem_test.gains_peripherals.snbt
rename to src/testMod/resources/data/cctest/structures/modem_test.gains_peripherals.snbt
index 6f91041bd..63e670a58 100644
--- a/src/testMod/server-files/structures/modem_test.gains_peripherals.snbt
+++ b/src/testMod/resources/data/cctest/structures/modem_test.gains_peripherals.snbt
@@ -48,7 +48,7 @@
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
- {pos: [4, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 0, Label: "modem_test.gains_peripherals", On: 1b, id: "computercraft:computer_advanced"}},
+ {pos: [4, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "modem_test.gains_peripherals", On: 1b, id: "computercraft:computer_advanced"}},
{pos: [4, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeirpheralAccess: 0b, id: "computercraft:cable"}},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/modem_test.have_peripherals.snbt b/src/testMod/resources/data/cctest/structures/modem_test.have_peripherals.snbt
similarity index 99%
rename from src/testMod/server-files/structures/modem_test.have_peripherals.snbt
rename to src/testMod/resources/data/cctest/structures/modem_test.have_peripherals.snbt
index d4f5457a2..c16283a27 100644
--- a/src/testMod/server-files/structures/modem_test.have_peripherals.snbt
+++ b/src/testMod/resources/data/cctest/structures/modem_test.have_peripherals.snbt
@@ -49,7 +49,7 @@
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
- {pos: [4, 1, 2], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 0, Label: "modem_test.have_peripherals", On: 1b, id: "computercraft:computer_advanced"}},
+ {pos: [4, 1, 2], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "modem_test.have_peripherals", On: 1b, id: "computercraft:computer_advanced"}},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/modem_test.transmits_messages.snbt b/src/testMod/resources/data/cctest/structures/modem_test.transmits_messages.snbt
similarity index 98%
rename from src/testMod/server-files/structures/modem_test.transmits_messages.snbt
rename to src/testMod/resources/data/cctest/structures/modem_test.transmits_messages.snbt
index b6431ba77..79ed20b01 100644
--- a/src/testMod/server-files/structures/modem_test.transmits_messages.snbt
+++ b/src/testMod/resources/data/cctest/structures/modem_test.transmits_messages.snbt
@@ -35,7 +35,7 @@
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "minecraft:air"},
- {pos: [1, 1, 3], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 0, Label: "modem_test.transmits_messages.receive", On: 1b, id: "computercraft:computer_advanced"}},
+ {pos: [1, 1, 3], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "modem_test.transmits_messages.receive", On: 1b, id: "computercraft:computer_advanced"}},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "computercraft:cable{cable:true,down:false,east:true,modem:east_off,north:false,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
@@ -43,7 +43,7 @@
{pos: [2, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_off,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
- {pos: [3, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 0, Label: "modem_test.transmits_messages.send", On: 1b, id: "computercraft:computer_advanced"}},
+ {pos: [3, 1, 1], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "modem_test.transmits_messages.send", On: 1b, id: "computercraft:computer_advanced"}},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/monitor_test.ensures_valid_on_place.snbt b/src/testMod/resources/data/cctest/structures/monitor_test.ensures_valid_on_place.snbt
similarity index 100%
rename from src/testMod/server-files/structures/monitor_test.ensures_valid_on_place.snbt
rename to src/testMod/resources/data/cctest/structures/monitor_test.ensures_valid_on_place.snbt
diff --git a/src/testMod/server-files/structures/monitor_test.looks_acceptable.snbt b/src/testMod/resources/data/cctest/structures/monitor_test.looks_acceptable.snbt
similarity index 100%
rename from src/testMod/server-files/structures/monitor_test.looks_acceptable.snbt
rename to src/testMod/resources/data/cctest/structures/monitor_test.looks_acceptable.snbt
diff --git a/src/testMod/server-files/structures/monitor_test.looks_acceptable_dark.snbt b/src/testMod/resources/data/cctest/structures/monitor_test.looks_acceptable_dark.snbt
similarity index 100%
rename from src/testMod/server-files/structures/monitor_test.looks_acceptable_dark.snbt
rename to src/testMod/resources/data/cctest/structures/monitor_test.looks_acceptable_dark.snbt
diff --git a/src/testMod/server-files/structures/printouttest.in_frame_at_night.snbt b/src/testMod/resources/data/cctest/structures/printouttest.in_frame_at_night.snbt
similarity index 100%
rename from src/testMod/server-files/structures/printouttest.in_frame_at_night.snbt
rename to src/testMod/resources/data/cctest/structures/printouttest.in_frame_at_night.snbt
diff --git a/src/testMod/server-files/structures/turtle_test.cleaned_with_cauldrons.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.cleaned_with_cauldrons.snbt
similarity index 97%
rename from src/testMod/server-files/structures/turtle_test.cleaned_with_cauldrons.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.cleaned_with_cauldrons.snbt
index 7a30e1677..a208024a5 100644
--- a/src/testMod/server-files/structures/turtle_test.cleaned_with_cauldrons.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.cleaned_with_cauldrons.snbt
@@ -14,7 +14,7 @@
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
- {pos: [1, 1, 0], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "computercraft:turtle_normal", tag: {Color: 13388876, ComputerId: 0, display: {Name: '{"text":"Clean turtle"}'}}}], Label: "turtle_test.cleaned_with_cauldrons", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [1, 1, 0], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "computercraft:turtle_normal", tag: {Color: 13388876, ComputerId: 0, display: {Name: '{"text":"Clean turtle"}'}}}], Label: "turtle_test.cleaned_with_cauldrons", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [1, 1, 1], state: "minecraft:cauldron{level:3}"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
diff --git a/src/testMod/resources/data/cctest/structures/turtle_test.drop_to_chest.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.drop_to_chest.snbt
new file mode 100644
index 000000000..669efdf6b
--- /dev/null
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.drop_to_chest.snbt
@@ -0,0 +1,138 @@
+{
+ DataVersion: 3120,
+ size: [5, 5, 5],
+ data: [
+ {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [0, 1, 0], state: "minecraft:air"},
+ {pos: [0, 1, 1], state: "minecraft:air"},
+ {pos: [0, 1, 2], state: "minecraft:air"},
+ {pos: [0, 1, 3], state: "minecraft:air"},
+ {pos: [0, 1, 4], state: "minecraft:air"},
+ {pos: [1, 1, 0], state: "minecraft:air"},
+ {pos: [1, 1, 1], state: "minecraft:air"},
+ {pos: [1, 1, 2], state: "minecraft:air"},
+ {pos: [1, 1, 3], state: "minecraft:air"},
+ {pos: [1, 1, 4], state: "minecraft:air"},
+ {pos: [2, 1, 0], state: "minecraft:air"},
+ {pos: [2, 1, 1], state: "minecraft:air"},
+ {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 64b, Slot: 0b, id: "minecraft:dirt"}, {Count: 32b, Slot: 2b, id: "minecraft:dirt"}], Label: "turtle_test.drop_to_chest", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 1, 3], state: "minecraft:chest{facing:west,type:single,waterlogged:false}", nbt: {Items: [{Count: 16b, Slot: 0b, id: "minecraft:dirt"}], id: "minecraft:chest"}},
+ {pos: [2, 1, 4], state: "minecraft:air"},
+ {pos: [3, 1, 0], state: "minecraft:air"},
+ {pos: [3, 1, 1], state: "minecraft:air"},
+ {pos: [3, 1, 2], state: "minecraft:air"},
+ {pos: [3, 1, 3], state: "minecraft:air"},
+ {pos: [3, 1, 4], state: "minecraft:air"},
+ {pos: [4, 1, 0], state: "minecraft:air"},
+ {pos: [4, 1, 1], state: "minecraft:air"},
+ {pos: [4, 1, 2], state: "minecraft:air"},
+ {pos: [4, 1, 3], state: "minecraft:air"},
+ {pos: [4, 1, 4], state: "minecraft:air"},
+ {pos: [0, 2, 0], state: "minecraft:air"},
+ {pos: [0, 2, 1], state: "minecraft:air"},
+ {pos: [0, 2, 2], state: "minecraft:air"},
+ {pos: [0, 2, 3], state: "minecraft:air"},
+ {pos: [0, 2, 4], state: "minecraft:air"},
+ {pos: [1, 2, 0], state: "minecraft:air"},
+ {pos: [1, 2, 1], state: "minecraft:air"},
+ {pos: [1, 2, 2], state: "minecraft:air"},
+ {pos: [1, 2, 3], state: "minecraft:air"},
+ {pos: [1, 2, 4], state: "minecraft:air"},
+ {pos: [2, 2, 0], state: "minecraft:air"},
+ {pos: [2, 2, 1], state: "minecraft:air"},
+ {pos: [2, 2, 2], state: "minecraft:air"},
+ {pos: [2, 2, 3], state: "minecraft:air"},
+ {pos: [2, 2, 4], state: "minecraft:air"},
+ {pos: [3, 2, 0], state: "minecraft:air"},
+ {pos: [3, 2, 1], state: "minecraft:air"},
+ {pos: [3, 2, 2], state: "minecraft:air"},
+ {pos: [3, 2, 3], state: "minecraft:air"},
+ {pos: [3, 2, 4], state: "minecraft:air"},
+ {pos: [4, 2, 0], state: "minecraft:air"},
+ {pos: [4, 2, 1], state: "minecraft:air"},
+ {pos: [4, 2, 2], state: "minecraft:air"},
+ {pos: [4, 2, 3], state: "minecraft:air"},
+ {pos: [4, 2, 4], state: "minecraft:air"},
+ {pos: [0, 3, 0], state: "minecraft:air"},
+ {pos: [0, 3, 1], state: "minecraft:air"},
+ {pos: [0, 3, 2], state: "minecraft:air"},
+ {pos: [0, 3, 3], state: "minecraft:air"},
+ {pos: [0, 3, 4], state: "minecraft:air"},
+ {pos: [1, 3, 0], state: "minecraft:air"},
+ {pos: [1, 3, 1], state: "minecraft:air"},
+ {pos: [1, 3, 2], state: "minecraft:air"},
+ {pos: [1, 3, 3], state: "minecraft:air"},
+ {pos: [1, 3, 4], state: "minecraft:air"},
+ {pos: [2, 3, 0], state: "minecraft:air"},
+ {pos: [2, 3, 1], state: "minecraft:air"},
+ {pos: [2, 3, 2], state: "minecraft:air"},
+ {pos: [2, 3, 3], state: "minecraft:air"},
+ {pos: [2, 3, 4], state: "minecraft:air"},
+ {pos: [3, 3, 0], state: "minecraft:air"},
+ {pos: [3, 3, 1], state: "minecraft:air"},
+ {pos: [3, 3, 2], state: "minecraft:air"},
+ {pos: [3, 3, 3], state: "minecraft:air"},
+ {pos: [3, 3, 4], state: "minecraft:air"},
+ {pos: [4, 3, 0], state: "minecraft:air"},
+ {pos: [4, 3, 1], state: "minecraft:air"},
+ {pos: [4, 3, 2], state: "minecraft:air"},
+ {pos: [4, 3, 3], state: "minecraft:air"},
+ {pos: [4, 3, 4], state: "minecraft:air"},
+ {pos: [0, 4, 0], state: "minecraft:air"},
+ {pos: [0, 4, 1], state: "minecraft:air"},
+ {pos: [0, 4, 2], state: "minecraft:air"},
+ {pos: [0, 4, 3], state: "minecraft:air"},
+ {pos: [0, 4, 4], state: "minecraft:air"},
+ {pos: [1, 4, 0], state: "minecraft:air"},
+ {pos: [1, 4, 1], state: "minecraft:air"},
+ {pos: [1, 4, 2], state: "minecraft:air"},
+ {pos: [1, 4, 3], state: "minecraft:air"},
+ {pos: [1, 4, 4], state: "minecraft:air"},
+ {pos: [2, 4, 0], state: "minecraft:air"},
+ {pos: [2, 4, 1], state: "minecraft:air"},
+ {pos: [2, 4, 2], state: "minecraft:air"},
+ {pos: [2, 4, 3], state: "minecraft:air"},
+ {pos: [2, 4, 4], state: "minecraft:air"},
+ {pos: [3, 4, 0], state: "minecraft:air"},
+ {pos: [3, 4, 1], state: "minecraft:air"},
+ {pos: [3, 4, 2], state: "minecraft:air"},
+ {pos: [3, 4, 3], state: "minecraft:air"},
+ {pos: [3, 4, 4], state: "minecraft:air"},
+ {pos: [4, 4, 0], state: "minecraft:air"},
+ {pos: [4, 4, 1], state: "minecraft:air"},
+ {pos: [4, 4, 2], state: "minecraft:air"},
+ {pos: [4, 4, 3], state: "minecraft:air"},
+ {pos: [4, 4, 4], state: "minecraft:air"}
+ ],
+ entities: [],
+ palette: [
+ "minecraft:polished_andesite",
+ "minecraft:air",
+ "computercraft:turtle_normal{facing:south,waterlogged:false}",
+ "minecraft:chest{facing:west,type:single,waterlogged:false}"
+ ]
+}
diff --git a/src/testMod/server-files/structures/turtle_test.gather_lava.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.gather_lava.snbt
similarity index 99%
rename from src/testMod/server-files/structures/turtle_test.gather_lava.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.gather_lava.snbt
index 102071219..e2e703e88 100644
--- a/src/testMod/server-files/structures/turtle_test.gather_lava.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.gather_lava.snbt
@@ -64,7 +64,7 @@
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
- {pos: [2, 2, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:bucket"}], Label: "turtle_test.gather_lava", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 2, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:bucket"}], Label: "turtle_test.gather_lava", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/turtle_test.hoe_dirt.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.hoe_dirt.snbt
similarity index 96%
rename from src/testMod/server-files/structures/turtle_test.hoe_dirt.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.hoe_dirt.snbt
index d662d1b6c..4b43c47b3 100644
--- a/src/testMod/server-files/structures/turtle_test.hoe_dirt.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.hoe_dirt.snbt
@@ -14,7 +14,7 @@
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
- {pos: [1, 1, 0], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [], Label: "turtle_test.hoe_dirt", LeftUpgrade: "minecraft:diamond_hoe", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [1, 1, 0], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.hoe_dirt", LeftUpgrade: "minecraft:diamond_hoe", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [1, 1, 1], state: "minecraft:dirt"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/turtle_test.item_detail_provider.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.item_detail_provider.snbt
similarity index 98%
rename from src/testMod/server-files/structures/turtle_test.item_detail_provider.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.item_detail_provider.snbt
index 0c8647206..5e67a701f 100644
--- a/src/testMod/server-files/structures/turtle_test.item_detail_provider.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.item_detail_provider.snbt
@@ -14,7 +14,7 @@
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
- {pos: [1, 1, 0], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "computercraft:printed_page", tag: {Color0: "fffffffffffffffffffffffff", Color1: "fffffffffffffffffffffffff", Color10: "fffffffffffffffffffffffff", Color11: "fffffffffffffffffffffffff", Color12: "fffffffffffffffffffffffff", Color13: "fffffffffffffffffffffffff", Color14: "fffffffffffffffffffffffff", Color15: "fffffffffffffffffffffffff", Color16: "fffffffffffffffffffffffff", Color17: "fffffffffffffffffffffffff", Color18: "fffffffffffffffffffffffff", Color19: "fffffffffffffffffffffffff", Color2: "fffffffffffffffffffffffff", Color20: "fffffffffffffffffffffffff", Color3: "fffffffffffffffffffffffff", Color4: "fffffffffffffffffffffffff", Color5: "fffffffffffffffffffffffff", Color6: "fffffffffffffffffffffffff", Color7: "fffffffffffffffffffffffff", Color8: "fffffffffffffffffffffffff", Color9: "fffffffffffffffffffffffff", Pages: 1, Text0: "Example ", Text1: " ", Text10: " ", Text11: " ", Text12: " ", Text13: " ", Text14: " ", Text15: " ", Text16: " ", Text17: " ", Text18: " ", Text19: " ", Text2: " ", Text20: " ", Text3: " ", Text4: " ", Text5: " ", Text6: " ", Text7: " ", Text8: " ", Text9: " ", Title: "Example page"}}], Label: "turtle_test.item_detail_provider", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [1, 1, 0], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "computercraft:printed_page", tag: {Color0: "fffffffffffffffffffffffff", Color1: "fffffffffffffffffffffffff", Color10: "fffffffffffffffffffffffff", Color11: "fffffffffffffffffffffffff", Color12: "fffffffffffffffffffffffff", Color13: "fffffffffffffffffffffffff", Color14: "fffffffffffffffffffffffff", Color15: "fffffffffffffffffffffffff", Color16: "fffffffffffffffffffffffff", Color17: "fffffffffffffffffffffffff", Color18: "fffffffffffffffffffffffff", Color19: "fffffffffffffffffffffffff", Color2: "fffffffffffffffffffffffff", Color20: "fffffffffffffffffffffffff", Color3: "fffffffffffffffffffffffff", Color4: "fffffffffffffffffffffffff", Color5: "fffffffffffffffffffffffff", Color6: "fffffffffffffffffffffffff", Color7: "fffffffffffffffffffffffff", Color8: "fffffffffffffffffffffffff", Color9: "fffffffffffffffffffffffff", Pages: 1, Text0: "Example ", Text1: " ", Text10: " ", Text11: " ", Text12: " ", Text13: " ", Text14: " ", Text15: " ", Text16: " ", Text17: " ", Text18: " ", Text19: " ", Text2: " ", Text20: " ", Text3: " ", Text4: " ", Text5: " ", Text6: " ", Text7: " ", Text8: " ", Text9: " ", Title: "Example page"}}], Label: "turtle_test.item_detail_provider", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/turtle_test.place_lava.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.place_lava.snbt
similarity index 99%
rename from src/testMod/server-files/structures/turtle_test.place_lava.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.place_lava.snbt
index 68cd47551..c93eb769e 100644
--- a/src/testMod/server-files/structures/turtle_test.place_lava.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.place_lava.snbt
@@ -64,7 +64,7 @@
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
- {pos: [2, 2, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:lava_bucket"}], Label: "turtle_test.place_lava", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 2, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:lava_bucket"}], Label: "turtle_test.place_lava", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/turtle_test.place_monitor.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.place_monitor.snbt
similarity index 99%
rename from src/testMod/server-files/structures/turtle_test.place_monitor.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.place_monitor.snbt
index 504fe1f1e..a1621f782 100644
--- a/src/testMod/server-files/structures/turtle_test.place_monitor.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.place_monitor.snbt
@@ -34,7 +34,7 @@
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
- {pos: [1, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "computercraft:monitor_advanced"}], Label: "turtle_test.place_monitor", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [1, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "computercraft:monitor_advanced"}], Label: "turtle_test.place_monitor", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:dark_oak_planks"},
{pos: [2, 1, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/turtle_test.place_waterlogged.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.place_waterlogged.snbt
similarity index 99%
rename from src/testMod/server-files/structures/turtle_test.place_waterlogged.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.place_waterlogged.snbt
index 609bb63b8..434b3d26f 100644
--- a/src/testMod/server-files/structures/turtle_test.place_waterlogged.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.place_waterlogged.snbt
@@ -38,7 +38,7 @@
{pos: [1, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:white_stained_glass"},
- {pos: [2, 1, 1], state: "computercraft:turtle_normal{facing:south,waterlogged:true}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:oak_fence"}], Label: "turtle_test.place_waterlogged", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 1, 1], state: "computercraft:turtle_normal{facing:south,waterlogged:true}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:oak_fence"}], Label: "turtle_test.place_waterlogged", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 2], state: "minecraft:water{level:0}"},
{pos: [2, 1, 3], state: "minecraft:white_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
diff --git a/src/testMod/resources/data/cctest/structures/turtle_test.resists_entity_explosions.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.resists_entity_explosions.snbt
new file mode 100644
index 000000000..79b2f8ef6
--- /dev/null
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.resists_entity_explosions.snbt
@@ -0,0 +1,140 @@
+{
+ DataVersion: 3120,
+ size: [5, 5, 5],
+ data: [
+ {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [0, 1, 0], state: "minecraft:air"},
+ {pos: [0, 1, 1], state: "minecraft:air"},
+ {pos: [0, 1, 2], state: "minecraft:air"},
+ {pos: [0, 1, 3], state: "minecraft:air"},
+ {pos: [0, 1, 4], state: "minecraft:air"},
+ {pos: [1, 1, 0], state: "minecraft:air"},
+ {pos: [1, 1, 1], state: "minecraft:air"},
+ {pos: [1, 1, 2], state: "minecraft:air"},
+ {pos: [1, 1, 3], state: "minecraft:air"},
+ {pos: [1, 1, 4], state: "minecraft:air"},
+ {pos: [2, 1, 0], state: "minecraft:air"},
+ {pos: [2, 1, 1], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {Fuel: 0, Items: [], On: 0b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 1, 2], state: "computercraft:turtle_advanced{facing:south,waterlogged:false}", nbt: {Fuel: 0, Items: [], On: 0b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_advanced"}},
+ {pos: [2, 1, 3], state: "minecraft:air"},
+ {pos: [2, 1, 4], state: "minecraft:air"},
+ {pos: [3, 1, 0], state: "minecraft:air"},
+ {pos: [3, 1, 1], state: "minecraft:air"},
+ {pos: [3, 1, 2], state: "minecraft:air"},
+ {pos: [3, 1, 3], state: "minecraft:air"},
+ {pos: [3, 1, 4], state: "minecraft:air"},
+ {pos: [4, 1, 0], state: "minecraft:air"},
+ {pos: [4, 1, 1], state: "minecraft:air"},
+ {pos: [4, 1, 2], state: "minecraft:air"},
+ {pos: [4, 1, 3], state: "minecraft:air"},
+ {pos: [4, 1, 4], state: "minecraft:air"},
+ {pos: [0, 2, 0], state: "minecraft:air"},
+ {pos: [0, 2, 1], state: "minecraft:air"},
+ {pos: [0, 2, 2], state: "minecraft:air"},
+ {pos: [0, 2, 3], state: "minecraft:air"},
+ {pos: [0, 2, 4], state: "minecraft:air"},
+ {pos: [1, 2, 0], state: "minecraft:air"},
+ {pos: [1, 2, 1], state: "minecraft:air"},
+ {pos: [1, 2, 2], state: "minecraft:air"},
+ {pos: [1, 2, 3], state: "minecraft:air"},
+ {pos: [1, 2, 4], state: "minecraft:air"},
+ {pos: [2, 2, 0], state: "minecraft:air"},
+ {pos: [2, 2, 1], state: "minecraft:air"},
+ {pos: [2, 2, 2], state: "minecraft:air"},
+ {pos: [2, 2, 3], state: "minecraft:air"},
+ {pos: [2, 2, 4], state: "minecraft:air"},
+ {pos: [3, 2, 0], state: "minecraft:air"},
+ {pos: [3, 2, 1], state: "minecraft:air"},
+ {pos: [3, 2, 2], state: "minecraft:air"},
+ {pos: [3, 2, 3], state: "minecraft:air"},
+ {pos: [3, 2, 4], state: "minecraft:air"},
+ {pos: [4, 2, 0], state: "minecraft:air"},
+ {pos: [4, 2, 1], state: "minecraft:air"},
+ {pos: [4, 2, 2], state: "minecraft:air"},
+ {pos: [4, 2, 3], state: "minecraft:air"},
+ {pos: [4, 2, 4], state: "minecraft:air"},
+ {pos: [0, 3, 0], state: "minecraft:air"},
+ {pos: [0, 3, 1], state: "minecraft:air"},
+ {pos: [0, 3, 2], state: "minecraft:air"},
+ {pos: [0, 3, 3], state: "minecraft:air"},
+ {pos: [0, 3, 4], state: "minecraft:air"},
+ {pos: [1, 3, 0], state: "minecraft:air"},
+ {pos: [1, 3, 1], state: "minecraft:air"},
+ {pos: [1, 3, 2], state: "minecraft:air"},
+ {pos: [1, 3, 3], state: "minecraft:air"},
+ {pos: [1, 3, 4], state: "minecraft:air"},
+ {pos: [2, 3, 0], state: "minecraft:air"},
+ {pos: [2, 3, 1], state: "minecraft:air"},
+ {pos: [2, 3, 2], state: "minecraft:air"},
+ {pos: [2, 3, 3], state: "minecraft:air"},
+ {pos: [2, 3, 4], state: "minecraft:air"},
+ {pos: [3, 3, 0], state: "minecraft:air"},
+ {pos: [3, 3, 1], state: "minecraft:air"},
+ {pos: [3, 3, 2], state: "minecraft:air"},
+ {pos: [3, 3, 3], state: "minecraft:air"},
+ {pos: [3, 3, 4], state: "minecraft:air"},
+ {pos: [4, 3, 0], state: "minecraft:air"},
+ {pos: [4, 3, 1], state: "minecraft:air"},
+ {pos: [4, 3, 2], state: "minecraft:air"},
+ {pos: [4, 3, 3], state: "minecraft:air"},
+ {pos: [4, 3, 4], state: "minecraft:air"},
+ {pos: [0, 4, 0], state: "minecraft:air"},
+ {pos: [0, 4, 1], state: "minecraft:air"},
+ {pos: [0, 4, 2], state: "minecraft:air"},
+ {pos: [0, 4, 3], state: "minecraft:air"},
+ {pos: [0, 4, 4], state: "minecraft:air"},
+ {pos: [1, 4, 0], state: "minecraft:air"},
+ {pos: [1, 4, 1], state: "minecraft:air"},
+ {pos: [1, 4, 2], state: "minecraft:air"},
+ {pos: [1, 4, 3], state: "minecraft:air"},
+ {pos: [1, 4, 4], state: "minecraft:air"},
+ {pos: [2, 4, 0], state: "minecraft:air"},
+ {pos: [2, 4, 1], state: "minecraft:air"},
+ {pos: [2, 4, 2], state: "minecraft:air"},
+ {pos: [2, 4, 3], state: "minecraft:air"},
+ {pos: [2, 4, 4], state: "minecraft:air"},
+ {pos: [3, 4, 0], state: "minecraft:air"},
+ {pos: [3, 4, 1], state: "minecraft:air"},
+ {pos: [3, 4, 2], state: "minecraft:air"},
+ {pos: [3, 4, 3], state: "minecraft:air"},
+ {pos: [3, 4, 4], state: "minecraft:air"},
+ {pos: [4, 4, 0], state: "minecraft:air"},
+ {pos: [4, 4, 1], state: "minecraft:air"},
+ {pos: [4, 4, 2], state: "minecraft:air"},
+ {pos: [4, 4, 3], state: "minecraft:air"},
+ {pos: [4, 4, 4], state: "minecraft:air"}
+ ],
+ entities: [
+ {blockPos: [1, 1, 2], pos: [1.5d, 1.0d, 2.5d], nbt: {AbsorptionAmount: 0.0f, Air: 300s, ArmorDropChances: [0.085f, 0.085f, 0.085f, 0.085f], ArmorItems: [{}, {}, {}, {}], Attributes: [{Base: 0.0d, Name: "forge:step_height_addition"}, {Base: 0.25d, Name: "minecraft:generic.movement_speed"}, {Base: 0.08d, Name: "forge:entity_gravity"}, {Base: 16.0d, Modifiers: [{Amount: -0.0871987524284032d, Name: "Random spawn bonus", Operation: 1, UUID: [I; -1592956383, -599506679, -1812844190, 1076877318]}], Name: "minecraft:generic.follow_range"}], Brain: {memories: {}}, CanPickUpLoot: 0b, CanUpdate: 1b, DeathTime: 0s, ExplosionRadius: 3b, FallDistance: 0.0f, FallFlying: 0b, Fire: -1s, Fuse: 30s, HandDropChances: [0.085f, 0.085f], HandItems: [{}, {}], Health: 20.0f, HurtByTimestamp: 0, HurtTime: 0s, Invulnerable: 0b, LeftHanded: 0b, Motion: [0.0d, -0.0784000015258789d, 0.0d], OnGround: 1b, PersistenceRequired: 0b, PortalCooldown: 0, Pos: [2.5d, -58.0d, 3.5d], Rotation: [143.85756f, 0.0f], UUID: [I; 1463798444, -662876850, -1423329658, 948503391], id: "minecraft:creeper", ignited: 0b}}
+ ],
+ palette: [
+ "minecraft:polished_andesite",
+ "minecraft:air",
+ "computercraft:turtle_normal{facing:south,waterlogged:false}",
+ "computercraft:turtle_advanced{facing:south,waterlogged:false}"
+ ]
+}
diff --git a/src/testMod/resources/data/cctest/structures/turtle_test.resists_explosions.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.resists_explosions.snbt
new file mode 100644
index 000000000..89ae4bc14
--- /dev/null
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.resists_explosions.snbt
@@ -0,0 +1,139 @@
+{
+ DataVersion: 3120,
+ size: [5, 5, 5],
+ data: [
+ {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [0, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [1, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [2, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [3, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 0], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 1], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 2], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 3], state: "minecraft:polished_andesite"},
+ {pos: [4, 0, 4], state: "minecraft:polished_andesite"},
+ {pos: [0, 1, 0], state: "minecraft:barrier"},
+ {pos: [0, 1, 1], state: "minecraft:barrier"},
+ {pos: [0, 1, 2], state: "minecraft:barrier"},
+ {pos: [0, 1, 3], state: "minecraft:barrier"},
+ {pos: [0, 1, 4], state: "minecraft:barrier"},
+ {pos: [1, 1, 0], state: "minecraft:barrier"},
+ {pos: [1, 1, 1], state: "minecraft:air"},
+ {pos: [1, 1, 2], state: "minecraft:air"},
+ {pos: [1, 1, 3], state: "minecraft:air"},
+ {pos: [1, 1, 4], state: "minecraft:barrier"},
+ {pos: [2, 1, 0], state: "minecraft:barrier"},
+ {pos: [2, 1, 1], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {Fuel: 0, Items: [], On: 0b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 1, 2], state: "computercraft:turtle_advanced{facing:south,waterlogged:false}", nbt: {Fuel: 0, Items: [], On: 0b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_advanced"}},
+ {pos: [2, 1, 3], state: "minecraft:air"},
+ {pos: [2, 1, 4], state: "minecraft:barrier"},
+ {pos: [3, 1, 0], state: "minecraft:barrier"},
+ {pos: [3, 1, 1], state: "minecraft:air"},
+ {pos: [3, 1, 2], state: "minecraft:air"},
+ {pos: [3, 1, 3], state: "minecraft:air"},
+ {pos: [3, 1, 4], state: "minecraft:barrier"},
+ {pos: [4, 1, 0], state: "minecraft:barrier"},
+ {pos: [4, 1, 1], state: "minecraft:barrier"},
+ {pos: [4, 1, 2], state: "minecraft:barrier"},
+ {pos: [4, 1, 3], state: "minecraft:barrier"},
+ {pos: [4, 1, 4], state: "minecraft:barrier"},
+ {pos: [0, 2, 0], state: "minecraft:barrier"},
+ {pos: [0, 2, 1], state: "minecraft:barrier"},
+ {pos: [0, 2, 2], state: "minecraft:barrier"},
+ {pos: [0, 2, 3], state: "minecraft:barrier"},
+ {pos: [0, 2, 4], state: "minecraft:barrier"},
+ {pos: [1, 2, 0], state: "minecraft:barrier"},
+ {pos: [1, 2, 1], state: "minecraft:air"},
+ {pos: [1, 2, 2], state: "minecraft:air"},
+ {pos: [1, 2, 3], state: "minecraft:air"},
+ {pos: [1, 2, 4], state: "minecraft:barrier"},
+ {pos: [2, 2, 0], state: "minecraft:barrier"},
+ {pos: [2, 2, 1], state: "minecraft:air"},
+ {pos: [2, 2, 2], state: "minecraft:air"},
+ {pos: [2, 2, 3], state: "minecraft:air"},
+ {pos: [2, 2, 4], state: "minecraft:barrier"},
+ {pos: [3, 2, 0], state: "minecraft:barrier"},
+ {pos: [3, 2, 1], state: "minecraft:air"},
+ {pos: [3, 2, 2], state: "minecraft:air"},
+ {pos: [3, 2, 3], state: "minecraft:air"},
+ {pos: [3, 2, 4], state: "minecraft:barrier"},
+ {pos: [4, 2, 0], state: "minecraft:barrier"},
+ {pos: [4, 2, 1], state: "minecraft:barrier"},
+ {pos: [4, 2, 2], state: "minecraft:barrier"},
+ {pos: [4, 2, 3], state: "minecraft:barrier"},
+ {pos: [4, 2, 4], state: "minecraft:barrier"},
+ {pos: [0, 3, 0], state: "minecraft:air"},
+ {pos: [0, 3, 1], state: "minecraft:air"},
+ {pos: [0, 3, 2], state: "minecraft:air"},
+ {pos: [0, 3, 3], state: "minecraft:air"},
+ {pos: [0, 3, 4], state: "minecraft:air"},
+ {pos: [1, 3, 0], state: "minecraft:air"},
+ {pos: [1, 3, 1], state: "minecraft:air"},
+ {pos: [1, 3, 2], state: "minecraft:air"},
+ {pos: [1, 3, 3], state: "minecraft:air"},
+ {pos: [1, 3, 4], state: "minecraft:air"},
+ {pos: [2, 3, 0], state: "minecraft:air"},
+ {pos: [2, 3, 1], state: "minecraft:air"},
+ {pos: [2, 3, 2], state: "minecraft:air"},
+ {pos: [2, 3, 3], state: "minecraft:air"},
+ {pos: [2, 3, 4], state: "minecraft:air"},
+ {pos: [3, 3, 0], state: "minecraft:air"},
+ {pos: [3, 3, 1], state: "minecraft:air"},
+ {pos: [3, 3, 2], state: "minecraft:air"},
+ {pos: [3, 3, 3], state: "minecraft:air"},
+ {pos: [3, 3, 4], state: "minecraft:air"},
+ {pos: [4, 3, 0], state: "minecraft:air"},
+ {pos: [4, 3, 1], state: "minecraft:air"},
+ {pos: [4, 3, 2], state: "minecraft:air"},
+ {pos: [4, 3, 3], state: "minecraft:air"},
+ {pos: [4, 3, 4], state: "minecraft:air"},
+ {pos: [0, 4, 0], state: "minecraft:air"},
+ {pos: [0, 4, 1], state: "minecraft:air"},
+ {pos: [0, 4, 2], state: "minecraft:air"},
+ {pos: [0, 4, 3], state: "minecraft:air"},
+ {pos: [0, 4, 4], state: "minecraft:air"},
+ {pos: [1, 4, 0], state: "minecraft:air"},
+ {pos: [1, 4, 1], state: "minecraft:air"},
+ {pos: [1, 4, 2], state: "minecraft:air"},
+ {pos: [1, 4, 3], state: "minecraft:air"},
+ {pos: [1, 4, 4], state: "minecraft:air"},
+ {pos: [2, 4, 0], state: "minecraft:air"},
+ {pos: [2, 4, 1], state: "minecraft:air"},
+ {pos: [2, 4, 2], state: "minecraft:air"},
+ {pos: [2, 4, 3], state: "minecraft:air"},
+ {pos: [2, 4, 4], state: "minecraft:air"},
+ {pos: [3, 4, 0], state: "minecraft:air"},
+ {pos: [3, 4, 1], state: "minecraft:air"},
+ {pos: [3, 4, 2], state: "minecraft:air"},
+ {pos: [3, 4, 3], state: "minecraft:air"},
+ {pos: [3, 4, 4], state: "minecraft:air"},
+ {pos: [4, 4, 0], state: "minecraft:air"},
+ {pos: [4, 4, 1], state: "minecraft:air"},
+ {pos: [4, 4, 2], state: "minecraft:air"},
+ {pos: [4, 4, 3], state: "minecraft:air"},
+ {pos: [4, 4, 4], state: "minecraft:air"}
+ ],
+ entities: [],
+ palette: [
+ "minecraft:polished_andesite",
+ "minecraft:barrier",
+ "minecraft:air",
+ "computercraft:turtle_normal{facing:south,waterlogged:false}",
+ "computercraft:turtle_advanced{facing:south,waterlogged:false}"
+ ]
+}
diff --git a/src/testMod/server-files/structures/turtle_test.shears_sheep.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.shears_sheep.snbt
similarity index 99%
rename from src/testMod/server-files/structures/turtle_test.shears_sheep.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.shears_sheep.snbt
index 52fb223ae..4f6afef2c 100644
--- a/src/testMod/server-files/structures/turtle_test.shears_sheep.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.shears_sheep.snbt
@@ -89,7 +89,7 @@
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
- {pos: [2, 3, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:shears", tag: {Damage: 0}}], Label: "turtle_test.shears_sheep", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 3, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:shears", tag: {Damage: 0}}], Label: "turtle_test.shears_sheep", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/turtle_test.unequip_refreshes_peripheral.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.unequip_refreshes_peripheral.snbt
similarity index 99%
rename from src/testMod/server-files/structures/turtle_test.unequip_refreshes_peripheral.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.unequip_refreshes_peripheral.snbt
index 068c40c83..de5d48438 100644
--- a/src/testMod/server-files/structures/turtle_test.unequip_refreshes_peripheral.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.unequip_refreshes_peripheral.snbt
@@ -39,7 +39,7 @@
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
- {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [], Label: "turtle_test.unequip_refreshes_peripheral", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, RightUpgrade: "computercraft:wireless_modem_normal", RightUpgradeNbt: {active: 0b}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.unequip_refreshes_peripheral", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, RightUpgrade: "computercraft:wireless_modem_normal", RightUpgradeNbt: {active: 0b}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
diff --git a/src/testMod/server-files/structures/turtle_test.use_compostors.snbt b/src/testMod/resources/data/cctest/structures/turtle_test.use_compostors.snbt
similarity index 99%
rename from src/testMod/server-files/structures/turtle_test.use_compostors.snbt
rename to src/testMod/resources/data/cctest/structures/turtle_test.use_compostors.snbt
index 8c70e0090..7bf966662 100644
--- a/src/testMod/server-files/structures/turtle_test.use_compostors.snbt
+++ b/src/testMod/resources/data/cctest/structures/turtle_test.use_compostors.snbt
@@ -64,7 +64,7 @@
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
- {pos: [2, 2, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 0, Fuel: 0, Items: [{Count: 64b, Slot: 0b, id: "minecraft:spruce_sapling"}], Label: "turtle_test.use_compostors", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
+ {pos: [2, 2, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 64b, Slot: 0b, id: "minecraft:spruce_sapling"}], Label: "turtle_test.use_compostors", On: 1b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_normal"}},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
diff --git a/src/testMod/resources/data/computercraft/loot_tables/treasure_disk.json b/src/testMod/resources/data/computercraft/loot_tables/treasure_disk.json
new file mode 100644
index 000000000..0e238db06
--- /dev/null
+++ b/src/testMod/resources/data/computercraft/loot_tables/treasure_disk.json
@@ -0,0 +1,20 @@
+{
+ "pools": [
+ {
+ "name": "main",
+ "rolls": 1,
+ "entries": [
+ {
+ "type": "minecraft:item",
+ "name": "computercraft:treasure_disk",
+ "functions": [
+ {
+ "function": "minecraft:set_nbt",
+ "tag": "{\"Title\": \"Demo disk\", \"SubPath\": \"demo\", \"Colour\": 15905331}"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/testMod/server-files/computers/computer/0/tests/disk_drive_test.audio_disk.lua b/src/testMod/server-files/computers/computer/0/tests/disk_drive_test.audio_disk.lua
deleted file mode 100644
index d48ce5fda..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/disk_drive_test.audio_disk.lua
+++ /dev/null
@@ -1,2 +0,0 @@
-test.eq(true, disk.hasAudio("right"), "Has audio")
-test.eq("C418 - 13", disk.getAudioTitle("right"), "Audio title")
diff --git a/src/testMod/server-files/computers/computer/0/tests/disk_drive_test.ejects_disk.lua b/src/testMod/server-files/computers/computer/0/tests/disk_drive_test.ejects_disk.lua
deleted file mode 100644
index 510af2a81..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/disk_drive_test.ejects_disk.lua
+++ /dev/null
@@ -1 +0,0 @@
-disk.eject("right")
diff --git a/src/testMod/server-files/computers/computer/0/tests/modem_test.gains_peripherals.lua b/src/testMod/server-files/computers/computer/0/tests/modem_test.gains_peripherals.lua
deleted file mode 100644
index d7f8559ab..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/modem_test.gains_peripherals.lua
+++ /dev/null
@@ -1,18 +0,0 @@
-local function check_peripherals(expected, msg)
- local peripherals = peripheral.getNames()
- table.sort(peripherals)
-
- test.eq(table.concat(expected, ", "), table.concat(peripherals, ", "), msg)
-end
-
-check_peripherals({"back"}, "Has no peripherals on startup")
-test.ok("initial")
-
-os.pullEvent("peripheral")
-sleep(0)
-
-check_peripherals({
- "back",
- "monitor_1",
- "printer_1",
-}, "Gains new peripherals")
diff --git a/src/testMod/server-files/computers/computer/0/tests/modem_test.have_peripherals.lua b/src/testMod/server-files/computers/computer/0/tests/modem_test.have_peripherals.lua
deleted file mode 100644
index 4752250d2..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/modem_test.have_peripherals.lua
+++ /dev/null
@@ -1,12 +0,0 @@
-local function check_peripherals(expected, msg)
- local peripherals = peripheral.getNames()
- table.sort(peripherals)
-
- test.eq(table.concat(expected, ", "), table.concat(peripherals, ", "), msg)
-end
-
-check_peripherals({
- "monitor_0",
- "printer_0",
- "right",
-}, "Starts with peripherals")
diff --git a/src/testMod/server-files/computers/computer/0/tests/modem_test.transmits_messages.receive.lua b/src/testMod/server-files/computers/computer/0/tests/modem_test.transmits_messages.receive.lua
deleted file mode 100644
index 7a2daa82b..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/modem_test.transmits_messages.receive.lua
+++ /dev/null
@@ -1,10 +0,0 @@
-local modem = peripheral.find("modem")
-modem.open(12)
-
-
-local _, name, chan, reply, payload, distance = os.pullEvent("modem_message")
-test.eq("left", name, "Modem name")
-test.eq(12, chan, "Channel")
-test.eq(34, reply, "Reply channel")
-test.eq("Hello!", payload, "Payload")
-test.eq(4, distance, "Distance") -- Why 4?!
diff --git a/src/testMod/server-files/computers/computer/0/tests/modem_test.transmits_messages.send.lua b/src/testMod/server-files/computers/computer/0/tests/modem_test.transmits_messages.send.lua
deleted file mode 100644
index 0d36bb1a0..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/modem_test.transmits_messages.send.lua
+++ /dev/null
@@ -1,5 +0,0 @@
-local modem = peripheral.find("modem")
-while true do
- modem.transmit(12, 34, "Hello!")
- sleep(1)
-end
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.cleaned_with_cauldrons.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.cleaned_with_cauldrons.lua
deleted file mode 100644
index 7041fec9b..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.cleaned_with_cauldrons.lua
+++ /dev/null
@@ -1,7 +0,0 @@
-local old_details = turtle.getItemDetail(1, true)
-
-test.assert(turtle.place(), "Dyed turtle")
-
-local new_details = turtle.getItemDetail(1, true)
-test.eq("computercraft:turtle_normal", new_details.name, "Still a turtle")
-test.neq(old_details.nbt, new_details.nbt, "Colour has changed")
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.gather_lava.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.gather_lava.lua
deleted file mode 100644
index 928989f45..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.gather_lava.lua
+++ /dev/null
@@ -1,7 +0,0 @@
-turtle.placeDown()
-
-local item = turtle.getItemDetail()
-test.eq("minecraft:lava_bucket", item.name)
-
-local has_down, down = turtle.inspectDown()
-test.eq(false, has_down, "Air below")
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.hoe_dirt.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.hoe_dirt.lua
deleted file mode 100644
index 51dc70576..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.hoe_dirt.lua
+++ /dev/null
@@ -1,5 +0,0 @@
-test.assert(turtle.dig())
-
-local has_block, block = turtle.inspect()
-test.assert(has_block, "Has block")
-test.eq("minecraft:farmland", block.name)
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.item_detail_provider.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.item_detail_provider.lua
deleted file mode 100644
index 76f4749f1..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.item_detail_provider.lua
+++ /dev/null
@@ -1,7 +0,0 @@
-test.ok("initial")
-
-local details = turtle.getItemDetail(1, true)
-
-test.assert(details, "Has details")
-test.assert(details.printout, "Has printout meta")
-test.eq("PAGE", details.printout.type)
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_lava.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_lava.lua
deleted file mode 100644
index 2ad6f9ee1..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_lava.lua
+++ /dev/null
@@ -1,5 +0,0 @@
-test.assert(turtle.placeDown())
-
-local ok, down = turtle.inspectDown()
-test.assert(ok, "Has below")
-test.eq("minecraft:lava", down.name, "Is lava")
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_monitor.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_monitor.lua
deleted file mode 100644
index e5ad6f0e6..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_monitor.lua
+++ /dev/null
@@ -1,6 +0,0 @@
-test.assert(turtle.place())
-
-local has_block, block = turtle.inspect()
-test.assert(has_block, "Has block")
-test.eq("computercraft:monitor_advanced", block.name)
-test.eq("lr", block.state.state)
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_waterlogged.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_waterlogged.lua
deleted file mode 100644
index e55dadfbc..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.place_waterlogged.lua
+++ /dev/null
@@ -1,6 +0,0 @@
-test.assert(turtle.place())
-
-local has_block, block = turtle.inspect()
-test.eq(true, has_block, "Has block")
-test.eq("minecraft:oak_fence", block.name)
-test.eq(true, block.state.waterlogged)
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.shears_sheep.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.shears_sheep.lua
deleted file mode 100644
index 8efbb30ab..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.shears_sheep.lua
+++ /dev/null
@@ -1,9 +0,0 @@
--- TurtleTest.`Shears sheep`
-
-turtle.placeDown()
-
-local item = turtle.getItemDetail(2)
-if item == nil then test.fail("Got no item") end
-test.eq("minecraft:white_wool", item.name)
-
-test.ok()
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.unequip_refreshes_peripheral.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.unequip_refreshes_peripheral.lua
deleted file mode 100644
index 2814b1a44..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.unequip_refreshes_peripheral.lua
+++ /dev/null
@@ -1,3 +0,0 @@
-test.eq("modem", peripheral.getType("right"), "Starts with a modem")
-turtle.equipRight()
-test.eq("drive", peripheral.getType("right"), "Unequipping gives a drive")
diff --git a/src/testMod/server-files/computers/computer/0/tests/turtle_test.use_compostors.lua b/src/testMod/server-files/computers/computer/0/tests/turtle_test.use_compostors.lua
deleted file mode 100644
index 7a5f4b52b..000000000
--- a/src/testMod/server-files/computers/computer/0/tests/turtle_test.use_compostors.lua
+++ /dev/null
@@ -1 +0,0 @@
-test.eq(true, turtle.dropDown(), "Drop items into compostor")
diff --git a/src/testMod/server-files/computers/ids.json b/src/testMod/server-files/computers/ids.json
deleted file mode 100644
index 9c8f668d7..000000000
--- a/src/testMod/server-files/computers/ids.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "computer": 0,
- "peripheral.monitor": 1,
- "peripheral.printer": 1
-}
diff --git a/src/testMod/server-files/eula.txt b/src/testMod/server-files/eula.txt
deleted file mode 100644
index e6765d6c9..000000000
--- a/src/testMod/server-files/eula.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-# Automatically generated EULA. Please don't use this for a real server.
-eula=true
diff --git a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable.png b/src/testMod/server-files/screenshots/monitor_test.looks_acceptable.png
deleted file mode 100644
index 0639191d2..000000000
Binary files a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable.png and /dev/null differ
diff --git a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_dark.png b/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_dark.png
deleted file mode 100644
index 1ce71f27f..000000000
Binary files a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_dark.png and /dev/null differ
diff --git a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_dark_vbo.png b/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_dark_vbo.png
deleted file mode 100644
index ba57924ab..000000000
Binary files a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_dark_vbo.png and /dev/null differ
diff --git a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_vbo.png b/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_vbo.png
deleted file mode 100644
index 9c7014984..000000000
Binary files a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable_vbo.png and /dev/null differ
diff --git a/src/testMod/server-files/screenshots/printouttest.in_frame_at_night.png b/src/testMod/server-files/screenshots/printouttest.in_frame_at_night.png
deleted file mode 100644
index 6b6f5ad3b..000000000
Binary files a/src/testMod/server-files/screenshots/printouttest.in_frame_at_night.png and /dev/null differ
diff --git a/src/testMod/server-files/server.properties b/src/testMod/server-files/server.properties
deleted file mode 100644
index 60384302f..000000000
--- a/src/testMod/server-files/server.properties
+++ /dev/null
@@ -1,45 +0,0 @@
-# Minecraft server properties
-allow-flight=false
-allow-nether=true
-broadcast-console-to-ops=true
-broadcast-rcon-to-ops=true
-difficulty=easy
-enable-command-block=true
-enable-query=false
-enable-rcon=false
-enforce-whitelist=false
-force-gamemode=false
-function-permission-level=2
-gamemode=creative
-generate-structures=false
-generator-settings=
-hardcore=false
-level-name=world
-level-seed=
-level-type=flat
-max-build-height=256
-max-players=20
-max-tick-time=60000
-max-world-size=29999984
-motd=A testing server
-network-compression-threshold=256
-online-mode=false
-op-permission-level=4
-player-idle-timeout=0
-prevent-proxy-connections=false
-pvp=true
-query.port=25565
-rcon.password=
-rcon.port=25575
-resource-pack=
-resource-pack-sha1=
-server-ip=
-server-port=25565
-snooper-enabled=true
-spawn-animals=true
-spawn-monsters=true
-spawn-npcs=true
-spawn-protection=16
-use-native-transport=true
-view-distance=10
-white-list=false