1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-25 22:53:22 +00:00

Merge branch 'mc-1.18.x' into mc-1.19.x

This commit is contained in:
Jonathan Coates 2022-11-01 20:11:36 +00:00
commit 629abb65e3
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
151 changed files with 2976 additions and 1285 deletions

View File

@ -86,11 +86,17 @@ minecraft {
val testMod = configurations["testModRuntimeClasspath"].resolve()
val implementation = configurations.runtimeClasspath.get().resolve()
val new = (testMod - implementation)
.asSequence().filter { it.isFile }.map { it.absolutePath }
.asSequence()
.filter { it.isFile && !it.name.endsWith("-test-fixtures.jar") }
.map { it.absolutePath }
.joinToString(File.pathSeparator)
if (old == null) new else old.get() + File.pathSeparator + new
}
property("cctest.sources", file("src/testMod/resources/data/cctest").absolutePath)
arg("--mixin.config=computercraft-gametest.mixins.json")
mods.register("cctest") {
source(sourceSets["testMod"])
source(sourceSets["testFixtures"])
@ -114,7 +120,6 @@ minecraft {
mappings("parchment", "${libs.versions.parchmentMc.get()}-${libs.versions.parchment.get()}-$mcVersion")
accessTransformer(file("src/main/resources/META-INF/accesstransformer.cfg"))
accessTransformer(file("src/testMod/resources/META-INF/accesstransformer.cfg"))
}
mixin {

View File

@ -78,7 +78,9 @@
<!-- Javadoc -->
<!-- TODO: Missing* checks for the dan200.computercraft.api package? -->
<module name="AtclauseOrder" />
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
</module>
<module name="InvalidJavadocPosition" />
<module name="JavadocBlockTagLocation" />
<module name="JavadocMethod"/>
@ -107,7 +109,9 @@
<module name="LocalFinalVariableName" />
<module name="LocalVariableName" />
<module name="MemberName" />
<module name="MethodName" />
<module name="MethodName">
<property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />
</module>
<module name="MethodTypeParameterName" />
<module name="PackageName">
<property name="format" value="^dan200\.computercraft(\.[a-z][a-z0-9]*)*" />
@ -115,11 +119,6 @@
<module name="ParameterName" />
<module name="StaticVariableName">
<property name="format" value="^[a-z][a-zA-Z0-9]*|CAPABILITY(_[A-Z_]+)?$" />
<property name="applyToPrivate" value="false" />
</module>
<module name="StaticVariableName">
<property name="format" value="^(s_)?[a-z][a-zA-Z0-9]*|CAPABILITY(_[A-Z_]+)?$" />
<property name="applyToPrivate" value="true" />
</module>
<module name="TypeName" />

View File

@ -22,13 +22,43 @@ directory exist) and one without it (meaning this entry is an immediate
completion candidate). `include_dirs` can be set to @{false} to only include
those with a trailing slash.
@tparam string path The path to complete.
@tparam string location The location where paths are resolved from.
@tparam[opt] boolean include_files When @{false}, only directories will be
included in the returned list.
@tparam[opt] boolean include_dirs When @{false}, "raw" directories will not be
included in the returned list.
@tparam[1] string path The path to complete.
@tparam[1] string location The location where paths are resolved from.
@tparam[1,opt=true] boolean include_files When @{false}, only directories will
be included in the returned list.
@tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will
not be included in the returned list.
@tparam[2] string path The path to complete.
@tparam[2] string location The location where paths are resolved from.
@tparam[2] {
include_dirs? = boolean, include_files? = boolean,
include_hidden? = boolean
} options
This table form is an expanded version of the previous syntax. The
`include_files` and `include_dirs` arguments from above are passed in as fields.
This table also accepts the following options:
- `include_hidden`: Whether to include hidden files (those starting with `.`)
by default. They will still be shown when typing a `.`.
@treturn { string... } A list of possible completion candidates.
@since 1.74
@changed 1.101.0
@usage Complete files in the root directory.
read(nil, nil, function(str)
return fs.complete(str, "", true, false)
end)
@usage Complete files in the root directory, hiding hidden files by default.
read(nil, nil, function(str)
return fs.complete(str, "", {
include_files = true,
include_dirs = false,
included_hidden = false,
})
end)
]]
function complete(path, location, include_files, include_dirs) end

View File

@ -5,7 +5,7 @@ kotlin.stdlib.default.dependency=false
kotlin.jvm.target.validation.mode=error
# Mod properties
modVersion=1.100.10
modVersion=1.101.0
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.19.2

View File

@ -7,7 +7,7 @@ parchment = "2022.10.16"
parchmentMc = "1.19.2"
autoService = "1.0.1"
cobalt = { strictly = "[0.5.7,0.6.0)", prefer = "0.5.7" }
cobalt = { strictly = "[0.5.8,0.6.0)", prefer = "0.5.8" }
jetbrainsAnnotations = "23.0.0"
kotlin = "1.7.10"
kotlin-coroutines = "1.6.0"

View File

@ -15,6 +15,7 @@
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.OptionalInt;
import java.util.concurrent.TimeUnit;
@Mod( ComputerCraft.MOD_ID )
@ -37,8 +38,8 @@ public final class ComputerCraft
public static boolean httpEnabled = true;
public static boolean httpWebsocketEnabled = true;
public static List<AddressRule> httpRules = List.of(
AddressRule.parse( "$private", null, Action.DENY.toPartial() ),
AddressRule.parse( "*", null, Action.ALLOW.toPartial() )
AddressRule.parse( "$private", OptionalInt.empty(), Action.DENY.toPartial() ),
AddressRule.parse( "*", OptionalInt.empty(), Action.ALLOW.toPartial() )
);
public static int httpMaxRequests = 16;

View File

@ -22,7 +22,7 @@
import dan200.computercraft.core.apis.ApiFactories;
import dan200.computercraft.core.asm.GenericMethod;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.ResourceMount;
import dan200.computercraft.shared.computer.core.ResourceMount;
import dan200.computercraft.impl.ComputerCraftAPIService;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.shared.BundledRedstone;

View File

@ -16,6 +16,7 @@
import net.minecraft.resources.ResourceLocation;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
@ -56,9 +57,8 @@ public static void addButtons( Screen screen, BooleanSupplier isOn, InputHandler
() -> isOn.getAsBoolean() ? Arrays.asList(
Component.translatable( "gui.computercraft.tooltip.turn_off" ),
Component.translatable( "gui.computercraft.tooltip.turn_off.key" ).withStyle( ChatFormatting.GRAY )
) : Arrays.asList(
Component.translatable( "gui.computercraft.tooltip.turn_on" ),
Component.translatable( "gui.computercraft.tooltip.turn_off.key" ).withStyle( ChatFormatting.GRAY )
) : Collections.singletonList(
Component.translatable( "gui.computercraft.tooltip.turn_on" )
)
) );

View File

@ -8,7 +8,8 @@
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerState;
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.pocket.core.PocketServerComputer;
import javax.annotation.Nonnull;
@ -25,13 +26,13 @@
*/
public class PocketComputerData
{
private final Terminal terminal;
private final NetworkedTerminal terminal;
private ComputerState state = ComputerState.OFF;
private int lightColour = -1;
public PocketComputerData( boolean colour )
{
terminal = new Terminal( ComputerCraft.pocketTermWidth, ComputerCraft.pocketTermHeight, colour );
terminal = new NetworkedTerminal( ComputerCraft.pocketTermWidth, ComputerCraft.pocketTermHeight, colour );
}
public int getLightState()

View File

@ -6,13 +6,17 @@
package dan200.computercraft.core.apis.http.options;
import javax.annotation.Nonnull;
import java.util.OptionalInt;
import java.util.OptionalLong;
public enum Action
{
ALLOW,
DENY;
private final PartialOptions partial = new PartialOptions( this, null, null, null, null );
private final PartialOptions partial = new PartialOptions(
this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty()
);
@Nonnull
public PartialOptions toPartial()

View File

@ -16,6 +16,7 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.OptionalInt;
import java.util.regex.Pattern;
/**
@ -29,10 +30,10 @@ public final class AddressRule
public static final int WEBSOCKET_MESSAGE = 128 * 1024;
private final AddressPredicate predicate;
private final Integer port;
private final OptionalInt port;
private final PartialOptions partial;
private AddressRule( @Nonnull AddressPredicate predicate, @Nullable Integer port, @Nonnull PartialOptions partial )
private AddressRule( @Nonnull AddressPredicate predicate, OptionalInt port, @Nonnull PartialOptions partial )
{
this.predicate = predicate;
this.partial = partial;
@ -40,7 +41,7 @@ private AddressRule( @Nonnull AddressPredicate predicate, @Nullable Integer port
}
@Nullable
public static AddressRule parse( String filter, @Nullable Integer port, @Nonnull PartialOptions partial )
public static AddressRule parse( String filter, OptionalInt port, @Nonnull PartialOptions partial )
{
int cidr = filter.indexOf( '/' );
if( cidr >= 0 )
@ -72,7 +73,7 @@ else if( filter.equalsIgnoreCase( "$private" ) )
*/
private boolean matches( String domain, int port, InetAddress address, Inet4Address ipv4Address )
{
if( this.port != null && this.port != port ) return false;
if( this.port.isPresent() && this.port.getAsInt() != port ) return false;
return predicate.matches( domain )
|| predicate.matches( address )
|| (ipv4Address != null && predicate.matches( ipv4Address ));
@ -80,8 +81,7 @@ private boolean matches( String domain, int port, InetAddress address, Inet4Addr
public static Options apply( Iterable<? extends AddressRule> rules, String domain, InetSocketAddress socketAddress )
{
PartialOptions options = null;
boolean hasMany = false;
PartialOptions options = PartialOptions.DEFAULT;
int port = socketAddress.getPort();
InetAddress address = socketAddress.getAddress();
@ -91,24 +91,9 @@ public static Options apply( Iterable<? extends AddressRule> rules, String domai
for( AddressRule rule : rules )
{
if( !rule.matches( domain, port, address, ipv4Address ) ) continue;
if( options == null )
{
options = rule.partial;
}
else
{
if( !hasMany )
{
options = options.copy();
hasMany = true;
}
options.merge( rule.partial );
}
options = options.merge( rule.partial );
}
return (options == null ? PartialOptions.DEFAULT : options).toOptions();
return options.toOptions();
}
}

View File

@ -14,6 +14,8 @@
import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.concurrent.ConcurrentHashMap;
/**
@ -48,7 +50,7 @@ public static UnmodifiableConfig makeRule( String host, Action action )
public static boolean checkRule( UnmodifiableConfig builder )
{
String hostObj = get( builder, "host", String.class ).orElse( null );
Integer port = get( builder, "port", Number.class ).map( Number::intValue ).orElse( null );
OptionalInt port = unboxOptInt( get( builder, "port", Number.class ) );
return hostObj != null && checkEnum( builder, "action", Action.class )
&& check( builder, "port", Number.class )
&& check( builder, "timeout", Number.class )
@ -65,11 +67,11 @@ public static AddressRule parseRule( UnmodifiableConfig builder )
if( hostObj == null ) return null;
Action action = getEnum( builder, "action", Action.class ).orElse( null );
Integer port = get( builder, "port", Number.class ).map( Number::intValue ).orElse( null );
Integer timeout = get( builder, "timeout", Number.class ).map( Number::intValue ).orElse( null );
Long maxUpload = get( builder, "max_upload", Number.class ).map( Number::longValue ).orElse( null );
Long maxDownload = get( builder, "max_download", Number.class ).map( Number::longValue ).orElse( null );
Integer websocketMessage = get( builder, "websocket_message", Number.class ).map( Number::intValue ).orElse( null );
OptionalInt port = unboxOptInt( get( builder, "port", Number.class ) );
OptionalInt timeout = unboxOptInt( get( builder, "timeout", Number.class ) );
OptionalLong maxUpload = unboxOptLong( get( builder, "max_upload", Number.class ).map( Number::longValue ) );
OptionalLong maxDownload = unboxOptLong( get( builder, "max_download", Number.class ).map( Number::longValue ) );
OptionalInt websocketMessage = unboxOptInt( get( builder, "websocket_message", Number.class ).map( Number::intValue ) );
PartialOptions options = new PartialOptions(
action,
@ -122,6 +124,16 @@ private static <T extends Enum<T>> Optional<T> getEnum( UnmodifiableConfig confi
return get( config, field, String.class ).map( x -> parseEnum( klass, x ) );
}
private static OptionalLong unboxOptLong( Optional<? extends Number> value )
{
return value.map( Number::intValue ).map( OptionalLong::of ).orElse( OptionalLong.empty() );
}
private static OptionalInt unboxOptInt( Optional<? extends Number> value )
{
return value.map( Number::intValue ).map( OptionalInt::of ).orElse( OptionalInt.empty() );
}
@Nullable
private static <T extends Enum<T>> T parseEnum( Class<T> klass, String x )
{

View File

@ -5,21 +5,25 @@
*/
package dan200.computercraft.core.apis.http.options;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.OptionalInt;
import java.util.OptionalLong;
public final class PartialOptions
{
static final PartialOptions DEFAULT = new PartialOptions( null, null, null, null, null );
public static final PartialOptions DEFAULT = new PartialOptions(
null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty()
);
Action action;
Long maxUpload;
Long maxDownload;
Integer timeout;
Integer websocketMessage;
private final @Nullable Action action;
private final OptionalLong maxUpload;
private final OptionalLong maxDownload;
private final OptionalInt timeout;
private final OptionalInt websocketMessage;
Options options;
private @Nullable Options options;
PartialOptions( Action action, Long maxUpload, Long maxDownload, Integer timeout, Integer websocketMessage )
public PartialOptions( @Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt timeout, OptionalInt websocketMessage )
{
this.action = action;
this.maxUpload = maxUpload;
@ -28,31 +32,36 @@ public final class PartialOptions
this.websocketMessage = websocketMessage;
}
@Nonnull
Options toOptions()
{
if( options != null ) return options;
return options = new Options(
action == null ? Action.DENY : action,
maxUpload == null ? AddressRule.MAX_UPLOAD : maxUpload,
maxDownload == null ? AddressRule.MAX_DOWNLOAD : maxDownload,
timeout == null ? AddressRule.TIMEOUT : timeout,
websocketMessage == null ? AddressRule.WEBSOCKET_MESSAGE : websocketMessage
maxUpload.orElse( AddressRule.MAX_UPLOAD ),
maxDownload.orElse( AddressRule.MAX_DOWNLOAD ),
timeout.orElse( AddressRule.TIMEOUT ),
websocketMessage.orElse( AddressRule.WEBSOCKET_MESSAGE )
);
}
void merge( @Nonnull PartialOptions other )
/**
* Perform a left-biased union of two {@link PartialOptions}.
*
* @param other The other partial options to combine with.
* @return The merged options map.
*/
PartialOptions merge( PartialOptions other )
{
if( action == null && other.action != null ) action = other.action;
if( maxUpload == null && other.maxUpload != null ) maxUpload = other.maxUpload;
if( maxDownload == null && other.maxDownload != null ) maxDownload = other.maxDownload;
if( timeout == null && other.timeout != null ) timeout = other.timeout;
if( websocketMessage == null && other.websocketMessage != null ) websocketMessage = other.websocketMessage;
}
// Short circuit for DEFAULT. This has no effect on the outcome, but avoids an allocation.
if( this == DEFAULT ) return other;
PartialOptions copy()
{
return new PartialOptions( action, maxUpload, maxDownload, timeout, websocketMessage );
return new PartialOptions(
action == null && other.action != null ? other.action : action,
maxUpload.isPresent() ? maxUpload : other.maxUpload,
maxDownload.isPresent() ? maxDownload : other.maxDownload,
timeout.isPresent() ? timeout : other.timeout,
websocketMessage.isPresent() ? websocketMessage : other.websocketMessage
);
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.core.apis.http.websocket;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import java.net.URI;
/**
* A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the
* original HTTP request.
*/
public class NoOriginWebSocketHanshakder extends WebSocketClientHandshaker13
{
public NoOriginWebSocketHanshakder( URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength )
{
super( webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength );
}
@Override
protected FullHttpRequest newHandshakeRequest()
{
FullHttpRequest request = super.newHandshakeRequest();
HttpHeaders headers = request.headers();
if( !customHeaders.contains( HttpHeaderNames.ORIGIN ) ) headers.remove( HttpHeaderNames.ORIGIN );
return request;
}
}

View File

@ -26,7 +26,6 @@
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.ssl.SslContext;
@ -152,7 +151,7 @@ protected void initChannel( SocketChannel ch )
}
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
);

View File

@ -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
{

View File

@ -10,7 +10,6 @@
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 @@
* 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.
* <p>
* 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)}.
* <p>
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as

View File

@ -7,8 +7,6 @@
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 @@
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 final void setChanged()
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';

View File

@ -7,7 +7,7 @@
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;

View File

@ -365,8 +365,7 @@ private static BasicComputerMetricsObserver getMetricsInstance( CommandSourceSta
private static final List<AggregatedMetric> 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<AggregatedMetric> fields ) throws CommandSyntaxException

View File

@ -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 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;

View File

@ -15,12 +15,12 @@
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 ServerComputer( ServerLevel level, int computerID, String label, Computer
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 );

View File

@ -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 @@ private ServerContext( MinecraftServer server )
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" ) );
}

View File

@ -11,7 +11,8 @@
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<ContainerComputerBase> input;
private final @Nullable Terminal terminal;
private final @Nullable NetworkedTerminal terminal;
private final ItemStack displayStack;

View File

@ -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;
/**

View File

@ -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();
}
}

View File

@ -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 int size()
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;
}

View File

@ -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" );

View File

@ -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;

View File

@ -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;

View File

@ -7,8 +7,9 @@
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 PocketComputerDataMessage( PocketServerComputer computer, boolean sendTer
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 )

View File

@ -7,7 +7,7 @@
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;

View File

@ -25,6 +25,7 @@
* :::
*
* @cc.module energy_storage
* @cc.since 1.94.0
*/
public class EnergyMethods implements GenericPeripheral
{

View File

@ -35,6 +35,7 @@
* Methods for interacting with tanks and other fluid storage blocks.
*
* @cc.module fluid_storage
* @cc.since 1.94.0
*/
public class FluidMethods implements GenericPeripheral
{

View File

@ -37,6 +37,7 @@
* Methods for interacting with inventories.
*
* @cc.module inventory
* @cc.since 1.94.0
*/
public class InventoryMethods implements GenericPeripheral
{
@ -170,6 +171,7 @@ public static int size( IItemHandler inventory )
* end
* print(total)
* }</pre>
* @cc.since 1.96.0
*/
@LuaFunction( mainThread = true )
public static int getItemLimit( IItemHandler inventory, int slot ) throws LuaException

View File

@ -9,7 +9,8 @@
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 @@ void read( TerminalState state )
{
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;
}

View File

@ -8,7 +8,7 @@
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;

View File

@ -7,6 +7,7 @@
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 @@ synchronized void rebuild()
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 @@ boolean pollTerminalChanged()
@Nullable
@VisibleForTesting
public Terminal getTerminal()
public NetworkedTerminal getTerminal()
{
return terminal;
}

View File

@ -11,7 +11,7 @@
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;

View File

@ -8,6 +8,7 @@
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<IPeripheral> 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;

View File

@ -17,6 +17,7 @@
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 static String getNBTHash( @Nullable CompoundTag tag )
{
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 static String getNBTHash( @Nullable CompoundTag tag )
}
}
/**
* 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;

View File

@ -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.",

View File

@ -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", {

View File

@ -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 @@ # New features in CC: Tweaked 1.97.0
# 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).

View File

@ -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.

View File

@ -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")

View File

@ -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]

View File

@ -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')
]]

View File

@ -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

View File

@ -12,6 +12,7 @@
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<AddressRule> 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 );

View File

@ -9,9 +9,6 @@
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 @@ void testClearLineOutOfBounds()
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()
{

View File

@ -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;

View File

@ -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 ) );
}
}

View File

@ -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 void testCompressed()
@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 void testUncompressed()
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 @@ private static void checkEqual( Terminal expected, Terminal actual )
}
}
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;
}

View File

@ -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()

View File

@ -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")

View File

@ -5,11 +5,9 @@
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 @@ override fun handleEvent(eventName: String?, arguments: Array<out Any>?): Machin
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 @@ override fun printExecutionState(out: StringBuilder) {}
* 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()
}
}
}

View File

@ -4,9 +4,11 @@
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 @@ suspend fun pullEvent(event: String? = null): Array<out Any?>
/** Call a peripheral method. */
suspend fun LuaTaskContext.callPeripheral(name: String, method: String, vararg args: Any?): Array<out Any?>? =
getApi<PeripheralAPI>().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<OSAPI>().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. */

View File

@ -10,8 +10,11 @@
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.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 @@
/**
* Provides a {@literal /ccexport <path>} 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 <S> void register( CommandDispatcher<S> dispatcher )
{
String prefix = "/ccexport";
if( !event.getMessage().startsWith( prefix ) ) return;
event.setCanceled( true );
dispatcher.register(
LiteralArgumentBuilder.<S>literal( "ccexport" )
.then( RequiredArgumentBuilder.<S, String>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" ) );

View File

@ -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;

View File

@ -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}.
* <p>
* This is used by Forge to automatically load and test classes.
*/
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface GameTestHolder
{
}

View File

@ -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.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 @@
*/
class CCTestCommand
{
public static final LevelResource LOCATION = new LevelResource( ComputerCraft.MOD_ID );
public static void register( CommandDispatcher<CommandSourceStack> dispatcher )
{
dispatcher.register( choice( "cctest" )
@ -49,7 +50,7 @@ public static void register( CommandDispatcher<CommandSourceStack> dispatcher )
for( TestFunction function : GameTestRegistry.getAllTestFunctions() )
{
TestCommand.exportTestStructure( context.getSource(), function.getStructureName() );
TestCommandAccessor.callExportTestStructure( context.getSource(), function.getStructureName() );
}
return 0;
} ) )
@ -57,17 +58,11 @@ public static void register( CommandDispatcher<CommandSourceStack> dispatcher )
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 @@ public static void importFiles( MinecraftServer server )
{
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 @@ static void exportFiles( MinecraftServer server )
{
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 @@ static void exportFiles( MinecraftServer server )
}
}
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 )

View File

@ -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 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 );

View File

@ -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;

View File

@ -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.
*
* <p>
* 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.

View File

@ -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 );
}
}

View File

@ -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<Method> 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.
* <p>
* 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 <T> Consumer<T> 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 );
}
};
}
}

View File

@ -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" },
)
}
}
}

View File

@ -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") }
}

View File

@ -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) }
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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()
}
}

View File

@ -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<ItemPrintout>("printout", ItemPrintout::class.java) {
override fun provideDetails(data: MutableMap<in String, Any>, stack: ItemStack, item: ItemPrintout) {
data["type"] = item.type.toString()
}
},
)
}
.thenComputerOk()
}
}

View File

@ -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<Unit>? = 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 <T : BlockEntity> GameTestHelper.getBlockEntity(pos: BlockPos, type: BlockEntityType<T>): 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()
}
}
}

View File

@ -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) );
}
}

View File

@ -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 );
}
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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 )
{
}
}

View File

@ -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;
}
}

View File

@ -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<RedstoneAPI>().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)
}

View File

@ -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") }
}

View File

@ -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
}

View File

@ -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())
}
}
}

View File

@ -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<PeripheralAPI>()
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<String> {
val peripheral = getApi<PeripheralAPI>()
val peripherals = mutableListOf<String>()
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<String>)
}
peripherals.sort()
return peripherals
}

View File

@ -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)
}
}
}
}

View File

@ -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<CraftingRecipe> = 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
}
}

View File

@ -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<PeripheralAPI>().getType("right").assertArrayEquals("modem", message = "Starts with a modem")
getApi<TurtleAPI>().equipRight().await()
getApi<PeripheralAPI>().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<TurtleAPI>().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<TurtleAPI>().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<TurtleAPI>().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<TurtleAPI>().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<TurtleAPI>().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<TurtleAPI>().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<TurtleAPI>().dropDown(Optional.empty()).await()
.assertArrayEquals(true, message = "Item was dropped")
assertEquals(63, getApi<TurtleAPI>().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<TurtleAPI>().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<ItemPrintout>("printout", ItemPrintout::class.java) {
override fun provideDetails(data: MutableMap<in String, Any>, 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<TurtleAPI>().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<String, *> {
val item = getApi<TurtleAPI>().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<String, *>
}

View File

@ -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 <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: Property<T>, 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<ItemStack>) {
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 <T : BlockEntity> GameTestHelper.getBlockEntity(pos: BlockPos, type: BlockEntityType<T>): 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 <T : Entity> GameTestHelper.getEntities(type: EntityType<T>): List<T> {
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 <T : Entity> GameTestHelper.getEntity(type: EntityType<T>): 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)))
}

View File

@ -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<String, Queue<suspend LuaTaskContext.() -> 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<ILuaAPI>()
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<out Any>?): 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<Result<Unit>>()
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`())
}
}
}
}

View File

@ -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

View File

@ -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"

View File

@ -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"
]
}

Some files were not shown because too many files have changed in this diff Show More